2023年11月

算法设计与分析报告5 贪心算法

本文发布地址(方便阅读):

  1. https://cmd.dayi.ink/WfnxTsYRQ4OdwI587BGDRQ
  2. https://blog.dayi.ink/?p=89
  3. https://www.cnblogs.com/rabbit-dayi/p/17817387.html
  4. https://type.dayiyi.top

1. 硬币找零问题

贪心

就是假设我们是收银员,需要找零,然后需要选取最少的硬币数量给他人。

我们直接从最大的依次给,给不了就给再小的,很爽。

但贪心的原理是子问题最优解推出整体问题最优解,也就是最优子结构,而对于子问题,只考虑当前怎么做最优,而不考虑整体情况,对于这个我们就只考虑现在需要的硬币而不考虑整体需要多少硬币。

实际的贪心问题需要证明,但是都贪心了,就贪心一点,不要那么多,直接做贪!

n = eval(input()) #要贪心的数额
arr = [10,5,1] # 面额
ans = 0
for i in arr:
  if n>=i:
    k = n//i #整除
    n -= i*k
    ans += k
    print(f"[info]{i} * {k}")

if ans ==0:
  print(-1)
else:print(ans)

  1. 最大面额的硬币比任何其他可能的硬币组合能表示的最大金额还要大。 这意味着任何较大金额的找零都应该首先尽可能多地使用最大面额的硬币
  2. 较小的硬币面额能被较大的硬币面额整除。
  3. 组合属性。 对于任意两个硬币面额ab,如果a > b,那么对于任何金额x,如果x能够用a来找零,那么x - b也能用a来找零。移除一个较小面额的硬币并不会改变剩余金额能否用较大面额的硬币找零。也就是说,x可以被a整除或者x大于a,那么从x中减去b(得到的金额是x-b)仍然能够使用面额为a的硬币找零。

比如这道题:

https://www.luogu.com.cn/problem/B3635

如果我们需要15元,我们的贪心算法会这样找:

15
15 - 1*11
4 - 4*1

这样就需要5枚硬币,而这里,我们则只需要3枚硬币(3*5)。

一般这两种情况会导致:

  1. 硬币的面额不能互相组合出更小的面额中的任何一个。
  2. 较小面额的硬币不能组合成较大面额的硬币。

对于本题中的硬币系统,因为不能保证使用最大面额的硬币后,剩下的金额还能用较小的面额凑出,所以这里不能用贪心算法。

那咋办嘛?

动态规划(Oh no)

  1. 定义状态:

    • dp[i] 凑成总金额 i 所需的最少硬币数量。

      假设我们想要凑成金额为6元,那么 dp[6] 将包含以下集合:

      • {1, 1, 1, 1, 1, 1}:使用六个1元硬币 1个硬币
      • {1, 5}:使用一个1元和一个5元硬币 2个硬币
  2. 状态初始化:

    • dp[0] = 0,因为凑成总金额0不需要任何硬币。
    • 其他状态初值可以设为无限,表示还没有找到凑成这个金额的方法。
  3. 状态转移方程:

    • 要凑成总金额 i,可以通过以下三种方式:

      • 用一个1元硬币加上金额为 i-1 的最优解
      • 用一个5元硬币加上金额为 i-5 的最优解
      • 用一个11元硬币加上金额为 i-11 的最优解
    • 因此,状态转移方程为 dp[i] = min(dp[i-1], dp[i-5], dp[i-11]) + 1
  4. 计算顺序:

    • 由小到大计算 dp[i],计算到 dp[n]
  5. 结果:

    • dp[n]
n = eval(input())
dp = [0x3f3f3f3f for i in range(n+1)]
dp[0]=0
for i in range(1,n+1):
  dp[i] = min(dp[i-1],dp[i-5],dp[i-11])+1
print(dp[n])

再来一道一样的题目题:

https://vjudge.net/problem/%E8%AE%A1%E8%92%9C%E5%AE%A2-T1781

n,m = map(int,input().split())
arr = list(map(int,input().split()))
dp = [0x3f3f3f3f for i in range(m+10)]
dp[0]=0
for i in range(1,m+1):
  for j in arr:
    if i>=j:
      dp[i] = min(dp[i-j]+1,dp[i])

if dp[m]==0x3f3f3f3f:
  print(-1)
else:
  print(dp[m])

..

AC:

2.活动安排问题

设有 n 个活动的集合 E={1,2,..,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活动 i 都有一个要求使用该资源的起始时间 si 和一个结束时间 fi,且 si<fi。如果选择了活动 i ,则它在时间区间 [si,fi) 内占用资源。若区间[si,fi) 与区间 [sj,fj) 不相交,则称活动 i 与活动 j 是相容的。也就是说,当 fi≤sj 或 fj≤si 时,活动 i 与活动 j 相容。选择出由互相兼容的活动组成的最大集合。

这道题是纯贪心啦(DP也能做):

总是选择结束时间最早的活动,这样留给其它活动的时间就最多了,选择后使得对其他活动影响最小。(证明就不证了)

  1. 将所有活动按照结束时间进行排序。
  2. 选择结束时间最早的活动。
  3. 剔除所有与已选择活动时间上有冲突的活动。
  4. 重复步骤 2 和 3,直到没有剩余的活动为止。
# 时间复杂度 O(nlogn)(排序)
n = eval(input())
ls = [ ]
for i in range(n):
  st,end = map(int,input().split())
  ls.append((st,end))
ls.sort(key=lambda x:x[1])
# [(1, 3), (2, 5), (4, 6), (1, 7)]
# print(ls)
# vis = [0 for i in range(ls[-1][1]+10)]
now = 0
ans = 1 # 选了排序后的第一个
for i,v in enumerate(ls):
  print(v)
  # 如果起始时间大于5等于现在的终止时间(注意集合开闭)
  if v[0]>=ls[now][1]:
    now = i
    ans+=1
print(ans)

3.背包问题

  • 因为完全背包01背包都在之前的实验里有啦
  • 这里就拿这两种问题下的和课本样例输出下fi数组吧!

01 背包

n,c= 4,8
w = [0,2,4,5,3]
v = [0,5,4,6,2]
# 01背包
dp=[[0 for _ in range(c+10)]for i in range(n+10)]
for i in range(n+1):
  dp[i][0]=0

for i in range(1,n+1):
  for j in range(0,c+1):
    if j-w[i]<0:
      dp[i][j] = dp[i-1][j]
    else:
      dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i])

for i in range(0,n+1):
  for j in range(0,c+1):
    print(f"{dp[i][j]}",end=" ")
  print()
0 0 0 0 0 0 0 0 0 
0 0 5 5 5 5 5 5 5 
0 0 5 5 5 5 9 9 9 
0 0 5 5 5 6 9 11 11 
0 0 5 5 5 7 9 11 11 

回溯选择了什么:

# 回溯找出选中的物品
selected = []
i, j = n,c
while i>0 and j > 0:
  if dp[i][j] != dp[i-1][j]:
    selected.append(i)
    j -= w[i]
  i -= 1
selected.reverse()  # 因为是逆向查找的
for item in selected:
    print(f"Sel:{item} weight:{w[item]} value:{v[item]}")

贪心?

我觉得是不对的啦

至少不一定可以求出最优解,贪心算法在每一步都选取当前最优的选择,但是对于01背包问题,当前最优的选择不一定能导致全局最优解。

就比如这样,假设背包的容量为10,有以下三件物品:

  • 物品A:重量3,价值5
  • 物品B:重量4,价值6
  • 物品C:重量7,价值10

如果使用贪心算法根据单位重量价值(价值除以重量)来选择物品,我们将会选择物品A(单位重量价值为1.67)和物品B(单位重量价值为1.5),总重量为7,总价值为11。
但是,最优解是选择物品B和物品C,总重量为11(如果背包可以装11的话),总价值为16。

但是课本例题的数据如果用这种贪心似乎还能跟DP选的一样,但这不能全部情况啦

n,c= 4,8

w = [0,2,4,5,3]
v = [0,5,4,6,2]
# 贪心

v_old = -1.00
ans_val = 0
left_c =  c
for i in range(1,n+1):
  v_now = v[i]/w[i]
  if v_now>v_old and left_c>=w[i]:
    ans_val+=v[i]
    left_c-=w[i]
    print(f"Sel:{i} weight:{w[i]} value:{v[i]} attempt:{v_now}")
  v_old = v_now

print(ans_val)

输出结果,虽然跟01 DP一样,但是这个题目正确不能说明对于全部正确。

Sel:1 weight:2 value:5 attempt:2.5
Sel:3 weight:5 value:6 attempt:1.2
11

比如这样一组数据就没法正常贪心成功:

所以贪心不能代表全部,也经常不能得出正确解(不如说得出正确解是巧合)

完全背包

假设背包容量还是这个情况(C=8),但是我们的物品有无限个。

这里数据比较小,没有用递推优化DP

# 完全背包
dp=[[0 for _ in range(c+10)]for i in range(n+10)]
for i in range(n+1):
  dp[i][0]=0

for i in range(1,n+1):
  for j in range(0,c+1):
    for k in range(0,int(j/w[i])+1,1):
      dp[i][j] = max(dp[i-1][j-k*w[i]]+v[i]*k,dp[i][j])

for i in range(0,n+1):
  for j in range(0,c+1):
    print(f"{dp[i][j]}",end=" ")
  print()

0 0 0 0 0 0 0 0 0 
0 0 5 5 10 10 15 15 20 
0 0 5 5 10 10 15 15 20 
0 0 5 5 10 10 15 15 20 
0 0 5 5 10 10 15 15 20 

算法实验报告1

发布地址(方便阅读):

  1. https://cmd.dayi.ink/3VqGmm4dRamR85T2ptXCsQ
  2. https://blog.dayi.ink/?p=91

T1

N = eval(input())
nums = [i for i in range(1, N + 1)]
res = []
def fn(n, st, cur):
  if n < 0:
      return
  if n == 0:
      res.append(cur)
      return
  for i in range(st, len(nums)):
      fn(n - nums[i], i, cur + [nums[i]])
fn(N, 0, [])
print(res[1:len(res)-1])

T2

ls,k = eval(input())
st = sorted(ls)
print(st[k-1])

T3

ls = eval(input())

def calc(len_):
  res = []
  cmax = -0x3f3f3f3f
  for i in range(1,len(ls)):
    l = i-1
    r = l+len_
    if r>len(ls):
      break
    tmp_ls =ls[l:r]
    print(tmp_ls)
    tmp_sum = sum(tmp_ls)
    if tmp_sum>=cmax:
      res = tmp_ls
      cmax = tmp_sum
  return res

print(calc(2))

T4

有n名选手参加一个进行n-1天的比赛。每一名选手都需要和其他n-1名选手进行一场比赛,且每位选手每天只进行一场比赛。请为比赛安排日程。输入n,输出一个二维数组,令第i行、第j列的值代表第个选手在第j天的比赛。
#include<bits/stdc++.h>
int N;

struct node{
  int whoami;
};
int mp[1024][1024];

inline void debug_print(){
  for(int i=1;i<=16;++i){
    for(int j=1;j<=16;++j){
      std::cout<<mp[i][j]<<" ";
    }
    std::cout<<"\n";
  }std::cout<<"\n";
}
inline void solve(int sz){
  if(sz==2){
    mp[1][1]=1;
    mp[1][2]=2;
    mp[2][1]=2;
    mp[2][2]=1;
    return;
  } 
  solve(sz/2);
  //复制当前数据到右下角
  for(int i=1;i<=sz/2;++i){
    for(int j=1;j<=sz/2;++j){
      mp[i+sz/2][j+sz/2]=mp[i][j];
    }
  }
  //扩展左上角和左下角
  for(int i=1;i<=sz/2;++i){
    for(int j=1;j<=sz/2;++j){
      mp[i][j+sz/2]=mp[i][j]+sz/2;
      mp[i+sz/2][j]=mp[i][j]+sz/2;
    }
  }
} 

inline void print_res(int n){
  using namespace std;
  cout<<"   ";
  for(int i=1;i<=n-1;++i){
    cout<<"D"<<i<<"  ";
  }
  cout<<"\n";
  for(int i=1;i<=N;++i){
    for(int j=1;j<=n;++j){
      if(mp[i][j] > N) cout<<"R   "; // 只有当值大于N时才打印'R'
      else cout<<mp[i][j]<<"   ";
    }
    cout<<"\n";
  }
}

int main(){
  using namespace std;
  // N=8;
  std::cin>>N;
  int n = N;
  if((n & (n - 1))!=0){
    n = pow(2, ceil(log2(n)));
    // printf("xxx:%d",n);
  }
  solve(n);
  // debug_print();
  print_res(n);
}

输出结果:

=========2==========
   D1  
1   2   
2   1   
=====================

=========3==========
   D1  D2  D3  
1   2   3   R   
2   1   R   3   
3   R   1   2   
=====================

=========4==========
   D1  D2  D3  
1   2   3   4   
2   1   4   3   
3   4   1   2   
4   3   2   1   
=====================

=========5==========
   D1  D2  D3  D4  D5  D6  D7  
1   2   3   4   5   R   R   R   
2   1   4   3   R   5   R   R   
3   4   1   2   R   R   5   R   
4   3   2   1   R   R   R   5   
5   R   R   R   1   2   3   4   
=====================

=========6==========
   D1  D2  D3  D4  D5  D6  D7  
1   2   3   4   5   6   R   R   
2   1   4   3   6   5   R   R   
3   4   1   2   R   R   5   6   
4   3   2   1   R   R   6   5   
5   6   R   R   1   2   3   4   
6   5   R   R   2   1   4   3   
=====================

=========7==========
   D1  D2  D3  D4  D5  D6  D7  
1   2   3   4   5   6   7   R   
2   1   4   3   6   5   R   7   
3   4   1   2   7   R   5   6   
4   3   2   1   R   7   6   5   
5   6   7   R   1   2   3   4   
6   5   R   7   2   1   4   3   
7   R   5   6   3   4   1   2   
=====================

=========8==========
   D1  D2  D3  D4  D5  D6  D7  
1   2   3   4   5   6   7   8   
2   1   4   3   6   5   8   7   
3   4   1   2   7   8   5   6   
4   3   2   1   8   7   6   5   
5   6   7   8   1   2   3   4   
6   5   8   7   2   1   4   3   
7   8   5   6   3   4   1   2   
8   7   6   5   4   3   2   1   
=====================

=========9==========
   D1  D2  D3  D4  D5  D6  D7  D8  D9  D10  D11  D12  D13  D14  D15  
1   2   3   4   5   6   7   8   9   R   R   R   R   R   R   R   
2   1   4   3   6   5   8   7   R   9   R   R   R   R   R   R   
3   4   1   2   7   8   5   6   R   R   9   R   R   R   R   R   
4   3   2   1   8   7   6   5   R   R   R   9   R   R   R   R   
5   6   7   8   1   2   3   4   R   R   R   R   9   R   R   R   
6   5   8   7   2   1   4   3   R   R   R   R   R   9   R   R   
7   8   5   6   3   4   1   2   R   R   R   R   R   R   9   R   
8   7   6   5   4   3   2   1   R   R   R   R   R   R   R   9   
9   R   R   R   R   R   R   R   1   2   3   4   5   6   7   8   
=====================

=========10==========
   D1  D2  D3  D4  D5  D6  D7  D8  D9  D10  D11  D12  D13  D14  D15  
1   2   3   4   5   6   7   8   9   10   R   R   R   R   R   R   
2   1   4   3   6   5   8   7   10   9   R   R   R   R   R   R   
3   4   1   2   7   8   5   6   R   R   9   10   R   R   R   R   
4   3   2   1   8   7   6   5   R   R   10   9   R   R   R   R   
5   6   7   8   1   2   3   4   R   R   R   R   9   10   R   R   
6   5   8   7   2   1   4   3   R   R   R   R   10   9   R   R   
7   8   5   6   3   4   1   2   R   R   R   R   R   R   9   10   
8   7   6   5   4   3   2   1   R   R   R   R   R   R   10   9   
9   10   R   R   R   R   R   R   1   2   3   4   5   6   7   8   
10   9   R   R   R   R   R   R   2   1   4   3   6   5   8   7   
=====================

=========11==========
   D1  D2  D3  D4  D5  D6  D7  D8  D9  D10  D11  D12  D13  D14  D15  
1   2   3   4   5   6   7   8   9   10   11   R   R   R   R   R   
2   1   4   3   6   5   8   7   10   9   R   11   R   R   R   R   
3   4   1   2   7   8   5   6   11   R   9   10   R   R   R   R   
4   3   2   1   8   7   6   5   R   11   10   9   R   R   R   R   
5   6   7   8   1   2   3   4   R   R   R   R   9   10   11   R   
6   5   8   7   2   1   4   3   R   R   R   R   10   9   R   11   
7   8   5   6   3   4   1   2   R   R   R   R   11   R   9   10   
8   7   6   5   4   3   2   1   R   R   R   R   R   11   10   9   
9   10   11   R   R   R   R   R   1   2   3   4   5   6   7   8   
10   9   R   11   R   R   R   R   2   1   4   3   6   5   8   7   
11   R   9   10   R   R   R   R   3   4   1   2   7   8   5   6   
=====================

=========12==========
   D1  D2  D3  D4  D5  D6  D7  D8  D9  D10  D11  D12  D13  D14  D15  
1   2   3   4   5   6   7   8   9   10   11   12   R   R   R   R   
2   1   4   3   6   5   8   7   10   9   12   11   R   R   R   R   
3   4   1   2   7   8   5   6   11   12   9   10   R   R   R   R   
4   3   2   1   8   7   6   5   12   11   10   9   R   R   R   R   
5   6   7   8   1   2   3   4   R   R   R   R   9   10   11   12   
6   5   8   7   2   1   4   3   R   R   R   R   10   9   12   11   
7   8   5   6   3   4   1   2   R   R   R   R   11   12   9   10   
8   7   6   5   4   3   2   1   R   R   R   R   12   11   10   9   
9   10   11   12   R   R   R   R   1   2   3   4   5   6   7   8   
10   9   12   11   R   R   R   R   2   1   4   3   6   5   8   7   
11   12   9   10   R   R   R   R   3   4   1   2   7   8   5   6   
12   11   10   9   R   R   R   R   4   3   2   1   8   7   6   5   
=====================

=========13==========
   D1  D2  D3  D4  D5  D6  D7  D8  D9  D10  D11  D12  D13  D14  D15  
1   2   3   4   5   6   7   8   9   10   11   12   13   R   R   R   
2   1   4   3   6   5   8   7   10   9   12   11   R   13   R   R   
3   4   1   2   7   8   5   6   11   12   9   10   R   R   13   R   
4   3   2   1   8   7   6   5   12   11   10   9   R   R   R   13   
5   6   7   8   1   2   3   4   13   R   R   R   9   10   11   12   
6   5   8   7   2   1   4   3   R   13   R   R   10   9   12   11   
7   8   5   6   3   4   1   2   R   R   13   R   11   12   9   10   
8   7   6   5   4   3   2   1   R   R   R   13   12   11   10   9   
9   10   11   12   13   R   R   R   1   2   3   4   5   6   7   8   
10   9   12   11   R   13   R   R   2   1   4   3   6   5   8   7   
11   12   9   10   R   R   13   R   3   4   1   2   7   8   5   6   
12   11   10   9   R   R   R   13   4   3   2   1   8   7   6   5   
13   R   R   R   9   10   11   12   5   6   7   8   1   2   3   4   
=====================

=========14==========
   D1  D2  D3  D4  D5  D6  D7  D8  D9  D10  D11  D12  D13  D14  D15  
1   2   3   4   5   6   7   8   9   10   11   12   13   14   R   R   
2   1   4   3   6   5   8   7   10   9   12   11   14   13   R   R   
3   4   1   2   7   8   5   6   11   12   9   10   R   R   13   14   
4   3   2   1   8   7   6   5   12   11   10   9   R   R   14   13   
5   6   7   8   1   2   3   4   13   14   R   R   9   10   11   12   
6   5   8   7   2   1   4   3   14   13   R   R   10   9   12   11   
7   8   5   6   3   4   1   2   R   R   13   14   11   12   9   10   
8   7   6   5   4   3   2   1   R   R   14   13   12   11   10   9   
9   10   11   12   13   14   R   R   1   2   3   4   5   6   7   8   
10   9   12   11   14   13   R   R   2   1   4   3   6   5   8   7   
11   12   9   10   R   R   13   14   3   4   1   2   7   8   5   6   
12   11   10   9   R   R   14   13   4   3   2   1   8   7   6   5   
13   14   R   R   9   10   11   12   5   6   7   8   1   2   3   4   
14   13   R   R   10   9   12   11   6   5   8   7   2   1   4   3   
=====================

=========15==========
   D1  D2  D3  D4  D5  D6  D7  D8  D9  D10  D11  D12  D13  D14  D15  
1   2   3   4   5   6   7   8   9   10   11   12   13   14   15   R   
2   1   4   3   6   5   8   7   10   9   12   11   14   13   R   15   
3   4   1   2   7   8   5   6   11   12   9   10   15   R   13   14   
4   3   2   1   8   7   6   5   12   11   10   9   R   15   14   13   
5   6   7   8   1   2   3   4   13   14   15   R   9   10   11   12   
6   5   8   7   2   1   4   3   14   13   R   15   10   9   12   11   
7   8   5   6   3   4   1   2   15   R   13   14   11   12   9   10   
8   7   6   5   4   3   2   1   R   15   14   13   12   11   10   9   
9   10   11   12   13   14   15   R   1   2   3   4   5   6   7   8   
10   9   12   11   14   13   R   15   2   1   4   3   6   5   8   7   
11   12   9   10   15   R   13   14   3   4   1   2   7   8   5   6   
12   11   10   9   R   15   14   13   4   3   2   1   8   7   6   5   
13   14   15   R   9   10   11   12   5   6   7   8   1   2   3   4   
14   13   R   15   10   9   12   11   6   5   8   7   2   1   4   3   
15   R   13   14   11   12   9   10   7   8   5   6   3   4   1   2   
=====================

=========16==========
   D1  D2  D3  D4  D5  D6  D7  D8  D9  D10  D11  D12  D13  D14  D15  
1   2   3   4   5   6   7   8   9   10   11   12   13   14   15   16   
2   1   4   3   6   5   8   7   10   9   12   11   14   13   16   15   
3   4   1   2   7   8   5   6   11   12   9   10   15   16   13   14   
4   3   2   1   8   7   6   5   12   11   10   9   16   15   14   13   
5   6   7   8   1   2   3   4   13   14   15   16   9   10   11   12   
6   5   8   7   2   1   4   3   14   13   16   15   10   9   12   11   
7   8   5   6   3   4   1   2   15   16   13   14   11   12   9   10   
8   7   6   5   4   3   2   1   16   15   14   13   12   11   10   9   
9   10   11   12   13   14   15   16   1   2   3   4   5   6   7   8   
10   9   12   11   14   13   16   15   2   1   4   3   6   5   8   7   
11   12   9   10   15   16   13   14   3   4   1   2   7   8   5   6   
12   11   10   9   16   15   14   13   4   3   2   1   8   7   6   5   
13   14   15   16   9   10   11   12   5   6   7   8   1   2   3   4   
14   13   16   15   10   9   12   11   6   5   8   7   2   1   4   3   
15   16   13   14   11   12   9   10   7   8   5   6   3   4   1   2   
16   15   14   13   12   11   10   9   8   7   6   5   4   3   2   1   
=====================

直接照着抄为python

import math

N = None
mp = [[0 for _ in range(1024)] for _ in range(1024)]

def debug_print():
    for i in range(1, 17):
        for j in range(1, 17):
            print(f"{mp[i][j]:2d}", end=" ")
        print()
    print()

def solve(sz):
    if sz == 2:
        mp[1][1] = 1
        mp[1][2] = 2
        mp[2][1] = 2
        mp[2][2] = 1
        return

    solve(sz // 2)
    # 复制当前数据到右下角
    for i in range(1, sz // 2 + 1):
        for j in range(1, sz // 2 + 1):
            mp[i + sz // 2][j + sz // 2] = mp[i][j]
    # 扩展左上角和左下角
    for i in range(1, sz // 2 + 1):
        for j in range(1, sz // 2 + 1):
            mp[i][j + sz // 2] = mp[i][j] + sz // 2
            mp[i + sz // 2][j] = mp[i][j] + sz // 2

def print_res(n):
    print("   ", end="")
    for i in range(1, n):
        print(f"D{i}  ", end="")
    print()
    for i in range(1, N + 1):
        for j in range(1, n + 1):
            if mp[i][j] > N:
                print("R   ", end="")
            else:
                print(f"{mp[i][j]:<4}", end="")
        print()

def init():
    global N
    n = N
    if (n & (n - 1)) != 0:
        n = 2 ** math.ceil(math.log2(n))

    solve(n)
    # debug_print()
    print_res(n)

if __name__ == "__main__":
    for i in range(2, 17):
        N = i
        print(f"========={i}==========")
        init()
        print("=====================")

算法设计与分析报告4 实验四 动态规划

本文发布地址:

  1. https://cmd.dayi.ink/WNa3RGNaShmf_vPpUemWxA?both
  2. https://type.dayiyi.top/index.php/archives/237/
  3. https://blog.dayi.ink/?p=82
  4. https://www.cnblogs.com/rabbit-dayi/p/17816598.html

1.爬楼梯问题

  • 小明住在 N 层楼梯之上,然后他可以一次 一脚 1 个台阶,一脚 2 个台阶,问总共多少种方法。
  • 一个楼梯共有 n 级台阶,每次可以走一级或者两级,问从第 0 级台阶走到第 n 级台阶一共有多少种方案。
  • 状态表示:
    f[i]表示 0->i 个台阶有多少种方案
    有限集合的值
    属性:数值
  • 状态计算:
    对于 f[i] 的计算:

    • 对于第 i 层台阶

      • 我可以从 i-1 上一层到 i
      • 我可以从 i-2 上两层到 i
    • 由于表示的是方案数:

      • f[i-1] 表示从 0->i-1 层的方案数
      • f[i-2] 表示从 0->i-2 层的方案数
    • f[i] 的方案数是:

      • 上一层的方案数(也就是从 f[i-1] 过来)
      • 上两层的方案数(也就是从 f[i-2] 过来)
  • 由状态可以推出状态转移方程
    $f[i] = f[i-1]+f[i-2]$

可以写代码(对于 N 范围比较小

def main():
  N = eval(input())
  dp = [0 for i in range(N+1)]
  dp[0] = 0
  dp[1] = 1
  dp[2] = 2
  for i in range(3,N+1):
    dp[i]=dp[i-1]+dp[i-2]
  print(dp[N])
  return

if __name__ =="__main__":
  main()

$N = 1000$

这个题还可以用矩阵快速幂来优化,数组也不需要开这么多。

斐波那契数列的递推关系可以用矩阵形式表示,并且通过矩阵乘法可以得到一个和斐波那契数列递推公式相同的关系。使用矩阵快速幂可以将时间复杂度从线性降低到对数级别。这样,我们就可以在非常快的时间内计算出f[N]的值,即到达第N层楼梯的方法数。

时间复杂度

  • 矩阵快速幂的时间复杂度为$O( \log N)$
  • 普通递推为$O(N)$

2.整数拆分问题

爆搜:同实验1

计数DP

设置状态 f[i][j]

表示:

2.整数拆分问题

对于整数拆分问题,我们需要计算将一个正整数 n 拆分成若干个正整数之和的方法数。我们可以使用动态规划来解决这个问题。

  • 状态表示
    f[i][j] 表示将整数 i 拆分为最大加数不超过 j 的所有不同拆分方式的数量
  • 状态计算
    要计算 f[i][j],我们可以考虑两种情况:

    1. 不使用数字 j,即所有拆分方式中的最大加数小于 j,这意味着我们只看 f[i][j-1] 的拆分方式。
    2. 至少使用一个数字 j,这时我们需要从 i 中减去 j,并且新的拆分问题变成了拆分 i-j 为最大加数不超过 j 的方式,即 f[i-j][j]

    因此,状态转移方程可以表示为:
    $$ f[i][j] = f[i][j-1] + f[i-j][j] $$

    其中 f[0][j] 应初始化为 1,因为对于任何 j,有且只有一种方式来拆分 0(即不使用任何数)。

  • 边界情况
    f[i][0] 应该为 0,因为没有办法只用 0 来拆分任何正整数 i

其实,这就是背包问题对bia

def dp(n):
    dp = [[0 for _ in range(n+1)] for _ in range(n+1)]
    for i in range(n+1):
        dp[i][0] = 0
    for j in range(n+1):
        dp[0][j] = 1
    for i in range(1, n+1):
      for j in range(1, n+1):
        dp[i][j] = dp[i][j-1]
        if j <= i:
          dp[i][j] += dp[i-j][j]
    return dp[n][n]

n = 5
print(dp(n))

如果对空间进行压缩:

  • dp[i] 来保存中间结果, dp[j] 在迭代开始前代表的是 dp[i-1][j] 的值,在迭代结束后代表的是 dp[i][j] 的值。
  • j从大到小进行枚举,dp[j],这样就能确保计算 dp[j]dp[j-1] 还没被更新,其代表的仍是 dp[i][j-1],而 dp[j] 表示的是上一轮的dp[i-1][j],即 dp[i-j][j]

转移方程:

$$ f[i][j] = f[i][j-1] + f[i-j][j] $$

在压缩为一维数组之后,状态转移方程的形式并不变,但是实现方式发生了变化。

更新dp[j] 时,等号右边的 dp[j] 表示更新前的 dp[i-1][j],也就是未加入j 之前的整数 i 的拆分数;

dp[j-1] 则表示 dp[i][j-1],即加入 j 后整数 i拆分数

压缩后的一维转移方程实现过程如下:

  1. 不使用数字 j 时,对应于 f[i][j-1] 的是当前 dp[j-1]
  2. 使用至少一个数字 j 时,对应于 f[i-j][j] 的是更新前的 dp[j]

方程如下:

$$ dp[j] = dp[j] + dp[j-1] $$

代码:

def dp_op(n):
  dp = [0] * (n+1)
  dp[0] = 1 
  for i in range(1, n+1):  
    for j in range(i, n+1):  
      dp[j] += dp[j - i] 
  return dp[n]

n = 1234
print(dp_op(n))

时间复杂度

  1. 外层循环从 i = 1 到 n,共执行 n 次。
  2. 内层循环从 j = i 到 n,平均情况下执行 n/2 次。

总的时间复杂度为 $O(n * n/2)$,即 $O(n^2)$。

3. 0-1背包问题

0-1背包已经有了

完全背包

之前的实验里有啦,这里写下完全背包。

在 01 背包的基础上,小兔子有 $n$ 种胡萝卜和一个容量为 $V$ 的背包,每种胡萝卜都有无限件可用。第 $i$ 种物品的体积是 $c_i$,价值是 $w_i$。
问:应该如何选择装入背包的物品,使得背包中的总价值最大,同时不超过背包的容量。

假设现在的胡萝卜有这几种:

号码体积/重量 (w)价值 (v)
122
235
335
447

防止表格解析不出来

集合表示

  • g[i][j] 表示从前i种胡萝卜中进行选择,选择的胡萝卜的总体积不超过j的各种选法的集合(这里是集合,不是最大值也不是方案数)

    • 那么对于 g[2][5] 来说,我可以选:

      • 1号胡萝卜0个 2号胡萝卜0个 价值 $0$
      • 1号胡萝卜1个 2号胡萝卜0个 价值 $2+0 =2$
      • 1号胡萝卜2个 2号胡萝北0个 价值 $2*2+0 =2$
      • 1号胡萝卜1个 2号胡萝北1个 价值 $2+5 =7$
      • 1号胡萝北0个 2号胡萝北1个 价值 $0+5 =5$
    • 因此对于 g[2][5] 来说就这些情况{0,2,2,7,5}
  • 不同的 g[i][j] 表示不同的集合,比如g2来说,就是上述的集合
  • f[i][j] 来表示 g[i][j] 这个集合中可以获得的最大值,比如 $f[2][5]=\max\{g[i][j]\}=\max\{0,2,2,7,5\}=7$
  • 对于每个 f[i][j],只需要保存 g[i][j] 的最大值,具体的集合元素不需要保存。对于末状态 f[N][W] 则为最后的状态,也就是 $N$ 种胡萝卜,体积为 $V$ 可以获得的最大值。

状态计算

  • 如何把 f[i][j] 计算出来?
  • f[i][j] = max{g[i][j]}
  • g[i][j] 要怎么求?
  • 可以把 g[i][j] 分为这几个部分:

    • i选 0 件
    • i选 1 件
    • i选 2 件
    • ...
    • i选 n 件

  • 虽然胡萝卜的数量是无限的,但是我们的背包的容量大小是有限的。
  • 第i种胡萝北进行分情况讨论:

    • 第i种胡萝北:选0件, 不会消耗空间,可以放心的直接转移过来f[i][j]= f[i-1][j]
    • 选1件:g[i-1][j] 会消耗w[i]*1的空间,得到 v[i] 的价值。

      • g[i][j] = g[i-1][j-w[i]] 中集合的全部元素再加上v[i]
      • g[i-1][j-w[i]] 中的全部元素的最大值为:$f[i-1][j-w[i]]
      • 于是,跟01背包一样,$f[i][j]=f[i-1][j-v[i]]+w[i]$
    • 选2件:

      • g[i-1][j] 下会消耗 w[i]*2 的空间,得到 v[i]*2 的价值
      • g[i-1][j-2*w[i]] 全部元素,再加上+2*w[i]
        然后求这个集合的最大值,就是 f[i][j] 选两件的情况。
      • $f[i][j]=f[i-1][j-2*v[i]]+w[i]$
    • 选 n 件:

      • 虽然我们的胡萝北是无限的,但是我们的背包是有限的,我们背包不是四次元,重量不能是负的。
      • 这样n实际上就会有上限 int(j/w[i]) 就是我们可以求得的上限值。
  • 可以得到转移方程:

$$ f[i][j] = MAX\{f[i-1][j-w[i]*k]+k*v[i]\} 0\le k<=int(j/w[i]) $$

防止方程解析不出来:

初始状态

f[i][0] = 0 : 什么都不选获得的价值 $0$

最终状态

f[N][W]

题目

https://www.acwing.com/problem/content/description/3/

代码

N,W = map(int,input().split())
dp= [[0 for i in range(W+10)]for i in range(N+10)]
v = [0 for i in range(N+10)]
w = [0 for i in range(N+10)]
for i in range(1,N+1):
  ww,vv = map(int,input().split())
  v[i]=vv
  w[i]=ww

for i in range(1,N+1):
  for j in range(0,W+1):
    for k in range(0,int(j/w[i])+1,1):
      dp[i][j] = max(dp[i-1][j-k*w[i]]+v[i]*k,dp[i][j])
print(dp[N][W])

恭喜,在最大点上,TLE:

其实这个状态方程是可以优化的:

$$ dp[i][j] = max(dp[i-1][j-k*w[i]]+v[i]*k,dp[i][j]) $$

对于每个物品 $i$,我们可以选择它 $0$ 次、$1$ 次、$2$ 次,一直到 $s_i$ 次(其中 $s_i$ 是物品 $i$ 的数量)。因此,对于 $f[i][j]$,我们有以下的选择:

  • 不选择当前物品 $i$,那么 $f[i][j] = f[i-1][j]$ 。
  • 选择一次物品 $i$,那么 $f[i][j] = f[i-1][j - w_i] + v_i$ 。
  • 选择两次物品 $i$,那么 $f[i][j] = f[i-1][j - 2 \cdot w_i] + 2 \cdot v_i$ 。
  • 以此类推,直到物品的数量或背包容量的限制。

$f[i][j]$ 的递推式为:

$$ f[i][j] = \max(f[i-1][j], f[i-1][j - w_i] + v_i, f[i-1][j - 2 \cdot w_i] + 2 \cdot v_i, \ldots) $$

$f[i][j-w_i]$的递推式:

$$ f[i][j - w_i] = \max(f[i-1][j - w_i], f[i-1][j - 2 \cdot w_i] + v_i, f[i-1][j - 3 \cdot w_i] + 2 \cdot v_i, \ldots) $$

如果我们将 $f[i][j - w_i]$的每一项都加上 $v_i$,我们得到:

$$ f[i][j - w_i] + v_i = \max(f[i-1][j - w_i] + v_i, f[i-1][j - 2 \cdot w_i] + 2 \cdot v_i, f[i-1][j - 3 \cdot w_i] + 3 \cdot v_i, \ldots) $$

比较 $f[i][j]$ 和 $f[i][j - w_i] + v_i$,我们可以看到 $f[i][j]$的每一项都在 $f[i][j - w_i] + v_i$中有对应的项,除了 $f[i-1][j]$。

可得:

$$ f[i][j] = \max(f[i][j - w_i] + v_i, f[i-1][j]) $$

$f[i][j]$的最优解要么包含了物品 $i$,要么不包含(即它是 $f[i-1][j]$的最优解)。如果它包含了物品 $i$,那么 $f[i][j]$的最优解可以通过在 $f[i][j - w_i]$的最优解的基础上加上物品 $i$的价值来得到。

写代码:

N,W = map(int,input().split())
dp= [[0 for i in range(W+10)]for i in range(N+10)]
v = [0 for i in range(N+10)]
w = [0 for i in range(N+10)]
for i in range(1,N+1):
  ww,vv = map(int,input().split())
  v[i]=vv
  w[i]=ww

for i in range(1,N+1):
  for j in range(0,W+1):
    # for k in range(0,int(j/w[i])+1,1):
    if j>=w[i]:
      dp[i][j]=max(dp[i-1][j], dp[i][j-w[i]] + v[i])
    else:
      dp[i][j]=dp[i-1][j]
print(dp[N][W])

时间复杂度

三层循环的嵌套层数。假设有 $N$ 种物品和背包容量为 $V$,则算法的时间复杂度可以表示为 $O(N * V^2)$。

  1. 外层循环从 i = 1N,共执行 N 次。
  2. 第二层循环从 j = 0V,共执行 V 次。
  3. 第三层循环从 k = 0int(j / w[i]),平均情况下执行 int(j / w[i]) / 2 次(因为 k 的最大值为 int(j / w[i]),但平均情况下是一半左右)。

总的时间复杂度为 $O(N * V * int(j / w[i]) / 2)$。

递推公式优化后的时间复杂度:

$O(NW)$

4.最长公共子序列问题

给定两个长度分别为 N 和 M 的字符串 A 和 B ,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少(寻找一个最长的序列,该序列同时是字符串 AB 的子序列。)

yi:
(1)子串:字符串中任意连续字符组成的序列。
(2)子序列:字符串中任意顺序保持一致的字符序列,可以非连续。
(3)公共子序列:两个序列中都出现的且顺序一致的子序列。

集合表示

  • dp[i][j] 表示字符串 A 的前 i 个字符和字符串 B 的前 j 个字符的最长公共子序列的长度。

    • 例如,如果 A 是 "ABCD" 且 B 是 "AEBD",dp[2][2] 将代表 "AB" 和 "AE" 的最长公共子序列的长度。

集合的角度去描述 dp[i][j],可以看作是所有可能的公共子序列长度值的集合,其中包括了所有对于 A[1...i]B[1...j](字符串 A 的前 i 个字符和字符串 B 的前 j 个字符)可能形成的公共子序列的长度。dp[i][j] 则是这个集合中的最大值。

假设 A = "ABCBDAB"B = "BDCAB"

  • dp[2][3] 代表 "AB" 和 "BDC" 的所有可能公共子序列长度的集合。这个集合包括长度为 0 的序列(如果没有公共元素),长度为 1 的序列(如果某个字符在两者中都出现),可能还有长度为 2 的序列(如果两个字符都按顺序出现在两者中)。但是在这个例子中,"AB" 和 "BDC" 的最长公共子序列的长度是 1,即集合中的最大值。
  • 形象一点dp[i][j] 看作一个容器,其中包含所有从字符串 A 的前 i 个字符和字符串 B 的前 j 个字符中抽取字符(保持各自的顺序不变)能得到的公共子序列长度。dp[i][j] 就是这个容器中的最大值。

状态转移

  • 为了求得 dp[i][j],考虑 A[i]B[j] 这两个字符的关系:

    • 如果 A[i] 等于 B[j],那么这个字符一定在 AB 的最长公共子序列中。我们可以在不包含 A[i]B[j] 的子序列的基础上增加这个字符,即 dp[i][j] = dp[i-1][j-1] + 1
    • 如果 A[i] 不等于 B[j],那么 A[i]B[j] 不可能同时出现在 AB 的公共子序列中。此时,需要在两个选择中取最长的那个:

      • 不包含 A[i] 的子序列,即 dp[i][j] = dp[i-1][j]
      • 不包含 B[j] 的子序列,即 dp[i][j] = dp[i][j-1]

因此,状态转移方程为:

dp[i][j] = dp[i-1][j-1] + 1,如果 A[i] 等于 B[j]
dp[i][j] = max(dp[i-1][j], dp[i][j-1]),如果 A[i] 不等于 B[j]

初始状态

  • dp[0][j]dp[i][0] 代表其中一个字符串的长度为 0,因此最长公共子序列长度为 0。

最终状态

  • dp[N][M] 是字符串 AB 的最长公共子序列的长度。

不是很“难”bia,稍微画画理解一下就可以啦

代码就简单了

# len1,len2 = 4,5
N = input()
str1 = "".join(input().split())
str2 = "".join(input().split())
len1 = len(str1)
len2 = len(str2)

dp = [[0 for _ in range(len2+10)]for _ in range(len1+10)]

for i in range(1,len1+1):
  for j in range(1,len2+1):
    if str2[j-1]==str1[i-1]:
      dp[i][j]=dp[i-1][j-1]+1
    else:
      dp[i][j]=max(dp[i-1][j],dp[i][j-1])

print(dp[len1][len2])

例题:排序的LCS
https://codeforces.com/gym/102951/problem/C

N = int(input().strip())
a = list(map(int, input().split()))
b = list(map(int, input().split()))

index_in_b = {b[i]: i for i in range(N)}
dp = [[0 for i in range(N+1)] for _ in range(N+1)]
for i in range(1, N + 1):
    for j in range(1, N + 1):
        # 加速索引
        if index_in_b[a[i-1]] == j-1:
            dp[i][j]=dp[i-1][j-1]+1
        else:
            dp[i][j]=max(dp[i-1][j],dp[i][j-1])
print(dp[N][N])

上述代码

PyPy 3-64 Memory limit exceeded on test 52 530 ms 262100 KB

优化下内存:

N = int(input().strip())
a = list(map(int, input().split()))
b = list(map(int, input().split()))
index_in_b = {b[i]: i for i in range(N)}
dp = [0 for i in range(N+1)]
for i in range(1, N + 1):
    # 持有上一行的值
    pre = 0
    for j in range(1, N + 1):
        temp = dp[j]
        if index_in_b[a[i-1]] == j-1:
            dp[j] = pre + 1
        else:
            dp[j] = max(dp[j], dp[j-1])
        pre = temp
print(dp[N])

最后改成C语言也还是不行,这个题目估计没这么简单hhh,不过已经算完成了。(能过52个点PVP)

找到另外一个题:

https://vjudge.net/problem/51Nod-1006

时间复杂度

假设字符串A的长度为N,字符串B的长度为M,则算法的时间复杂度为$O(N*M)$。

  1. 外层循环从i=1到N,共执行N次。
  2. 内层循环从j=1到M,共执行M次。

因此,总的时间复杂度为$O(N*M)$

5.最长递增子序列问题

给定长度为 N 的数组 arr,找到最长的子序列,使得这个子序列中的元素单调递增。

集合表示

  • 定义 dp[i] 表示以 arr[i] 结尾的最长递增子序列的长度。

    • 对于数组 [3, 4, 5, 1]dp[3] 表示以元素 5 结尾的最长递增子序列的长度。

再者:
dp[i] 表示的是以下集合中最大的长度:

  • arr[i] 结尾的所有可能递增子序列的长度的集合。
    数组 arr 中的第 i 个元素 arr[i],集合可以表述为所有下标 jj < i)的子序列,使得 arr[j] < arr[i] 并且 ji 之间没有比 arr[i] 更大的元素。

例如,给定数组(这里数组下标从1开始) arr = [10, 9, 2, 5, 3, 7, 101, 18],考虑计算 dp[5]arr[5](值为 3)结尾的最长递增子序列的长度。集合中包括的是:

  • [2, 3],以 2 结尾的递增子序列再加上 3,递增子序列长度为2
  • [3],只包含 3 本身的子序列,递增子序列长度为1
    dp[5] 将是上述集合中长度最大的值,即 2

状态转移

  • 为了计算 dp[i],需要检查在 arr[i] 之前的所有元素,并找出以这些元素结尾的最长递增子序列,在满足递增条件的前提下,将 arr[i] 添加到这些子序列中。

    • 如果 arr[j] < arr[i],其中 j < i,则可以将 arr[i] 添加到以 arr[j] 结尾的递增子序列中,形成一个新的递增子序列。这意味着 dp[i] 至少可以是 dp[j] + 1
  • 因此,状态转移方程为:

dp[i] = max(dp[j] + 1)0 <= j < iarr[j] < arr[i]

初始状态

  • 对于任何数组,长度至少为 1 的递增子序列显然包含它自己,因此 dp[i] 的初始值都为 1。

最终状态

  • dp[N] 为最大值。

https://www.luogu.com.cn/problem/B3637

N = eval(input())
ls = [0 for i in range(N+10)]
ls = [0]+list(map(int,input().split()))

dp = [0 for i in range(N+10)]
# 默认都是1
for i in range(N):
  dp[i+1]=1

ans = -0x3f3f3f3f
for i in range(1,N+1):
  for j in range(1,i+1):
    if ls[j]<ls[i]:
      dp[i]=max(dp[j]+1,dp[i])
      ans = max(ans,dp[i])
print(ans)

时间复杂度 $O(n^2)$

这题还可以贪心,能到 $O(nlog_n)$

二、总结

1. 爬楼梯问题

问题本质上是斐波那契数列的变种,通过简单的状态转移方程即可求解。对于大数的情况,矩阵快速幂可以有效降低时间复杂度。

2. 整数拆分问题

计数DP,通过二维动态规划表来跟踪每个拆分的情况。

3. 0-1 背包问题

太经典了,这里写了多重背包。

4. 最长公共子序列问题

二维数组来跟踪两个字符串的每个子序列的匹配情况,利用状态转移方程来更新最长公共子序列的长度。

5. 最长递增子序列问题

一维数组来存储以每个元素结尾的最长递增子序列的长度,并通过比较和更新这些长度来找到最长的递增子序列。

DP

  • 动态规划的特点:动态规划适合解决具有重叠子问题和最优子结构特性的问题。通过存储中间结果来避免重复计算,可以大幅提高效率。
  • 状态定义和转移:在动态规划中,合理定义状态并找到正确的状态转移方程是解决问题的关键。
  • 时间复杂度:动态规划通常能将时间复杂度从指数级别降低到多项式级别

都是一些简单滴DP入门题目,很经典但是很有意义啦!

算法实验报告3——分支限界法

可访问链接:

  1. https://type.dayiyi.top/index.php/archives/234/
  2. https://www.cnblogs.com/rabbit-dayi/articles/17865610.html

1.艰难旅行问题

现已知一个大小为 N · M 的地图,地图中只有可能出现两个数字:0 或 1,
规定如果位于数字为 0 的格子上,则下一步只能往相邻四个格子中数字为 1 的格子走,如果位于数字为 1 的格子上,则下一步只能往相邻四个格子中数字为 0 的格子走。
如果给定起点格子,则可以向上下左右四个方向移动,且同一个格子不能重复走,求能在地图中到达多少格子?
  • 兔兔
  • 兔兔

我觉得这个题深搜广搜都可以。

DFS

#include<bits/stdc++.h>


const int maxn=2333;
const int maxm=2333;
int mp[maxn][maxm];
int vis[maxn][maxm];
int N,M;

const int dx[] = {0,1,-1,0,0};
const int dy[] = {0,0,0,1,-1};
int dfs(int x,int y){
//  printf("de:%d %d\n",x,y);
  int now = mp[x][y];
  vis[x][y]=1;
  int res = 0;
  res++;
  for(int i =1;i<=4;++i){
    int nx = x+dx[i];
    int ny = y+dy[i];
    if((nx>N)||(ny>M)||(nx<=0)||(ny<=0))continue;
    if((mp[nx][ny]^now )&& (!vis[nx][ny])){
      vis[nx][ny]=1;
      res+=dfs(nx,ny);
    }
  }
  return res;
}

int main(){
  using namespace std;
  cin>>N>>M;
  for(int i=1;i<=N;++i){
    for(int j=1;j<=M;++j){
      cin>>mp[i][j];
    }
  }  
  int st,sy;
  cin>>st>>sy;
  int ans=dfs(st,sy);
  cout<<ans<<"\n";
}
  • T1_1.in
5 5 
1 0 1 0 1
0 1 0 1 0
1 0 1 0 1
0 1 0 1 0
1 0 1 0 1
1 1
  • T1_2.in
5 5
1 1 0 1 0
1 1 0 1 0
0 0 0 1 0
1 1 1 1 0
0 0 0 0 0
1 1
  • T1_3.in
4 4
1 0 1 1
1 0 1 0
0 1 0 1
0 0 1 1
3 3
  • 样例:

样例1:

样例2:

样例3:

BFS

#include<bits/stdc++.h>

const int maxn=2333;
const int maxm=2333;
int mp[maxn][maxm];
int vis[maxn][maxm];
int N,M;

const int dx[] = {0,1,-1,0,0};
const int dy[] = {0,0,0,1,-1};


int dfs(int x,int y){
//  printf("de:%d %d\n",x,y);
  int now = mp[x][y];
  vis[x][y]=1;
  int res = 0;
  res++;
  for(int i =1;i<=4;++i){
    int nx = x+dx[i];
    int ny = y+dy[i];
    if((nx>N)||(ny>M)||(nx<=0)||(ny<=0))continue;
    if((mp[nx][ny]^now )&& (!vis[nx][ny])){
      vis[nx][ny]=1;
      res+=dfs(nx,ny);
    }
  }
  return res;
}


int bfs(int x, int y){
  using namespace std;
  queue< pair<int,int> > q;
  q.push(make_pair(x,y));
  int ans = 1;//开始的st sy一定能到达
  vis[x][y] = 1;
  while(!q.empty()){
    pair<int,int> tmp;
    tmp = q.front();
    q.pop();
   int nowx = tmp.first;
    int nowy = tmp.second;
    int now = mp[nowx][nowy];
    for(int i=1;i<=4;++i){
      int nx = nowx+dx[i];
      int ny = nowy+dy[i];
      // printf("deee:%d %d\n",nx,ny);
      if((nx>N)||(ny>M)||(nx<=0)||(ny<=0))continue;
      if((mp[nx][ny]^now )&& (!vis[nx][ny])){
        vis[nx][ny]=1;
        q.push(make_pair(nx,ny));
        ans++;
      }
    }
  }
  return ans;
}

int main(){
  using namespace std;
  cin>>N>>M;
  for(int i=1;i<=N;++i){
    for(int j=1;j<=M;++j){
      cin>>mp[i][j];
    }
  }  
  int st,sy;
  cin>>st>>sy;
  int ans=bfs(st,sy);
  cout<<ans<<"\n";
}

Python 版本:

from collections import deque

maxn = 2333
maxm = 2333
mp = [[0] * maxm for _ in range(maxn)]
vis = [[0] * maxm for _ in range(maxn)]
N, M = 0, 0

dx = [0, 1, -1, 0, 0]
dy = [0, 0, 0, 1, -1]


def dfs(x, y):
    now = mp[x][y]
    vis[x][y] = 1
    res = 1
    for i in range(1, 5):
        nx = x + dx[i]
        ny = y + dy[i]
        if nx > N or ny > M or nx <= 0 or ny <= 0:
            continue
        if mp[nx][ny] != now and not vis[nx][ny]:
            res += dfs(nx, ny)
    return res


def bfs(x, y):
    q = deque()
    q.append((x, y))
    ans = 1 
    vis[x][y] = 1
    while q:
        nowx, nowy = q.popleft()
        now = mp[nowx][nowy]
        for i in range(1, 5):
            nx = nowx + dx[i]
            ny = nowy + dy[i]
            if nx > N or ny > M or nx <= 0 or ny <= 0:
                continue
            if mp[nx][ny] != now and not vis[nx][ny]:
                vis[nx][ny] = 1
                q.append((nx, ny))
                ans += 1
    return ans


def main():
    global N, M
    N, M = map(int, input().split())
    for i in range(1, N + 1):
        mp[i][1:M+1] = list(map(int, input().split()))

    st, sy = map(int, input().split())
    ans = bfs(st, sy)
    print(ans)


if __name__ == "__main__":
    main()

思路和分析

在艰难旅行问题中,我们使用了深度优先搜索(DFS)和广度优先搜索(BFS)两种算法。这两种算法都是通过搜索算法来探索从起点出发可以到达的所有格子。

1. 深度优先搜索(DFS)

  • DFS 从起点开始,沿着可能的路径深入探索,直到无法继续为止。然后回溯到上一个分岔点,再沿着另一条路径探索,直到探索所有可能的路径。
  • 时间复杂度:DFS 的时间复杂度通常为 $O(V + E)$,其中 $V$ 是顶点数(本问题中为格子数),$E$ 是边数(本问题中为可能的移动次数)。由于这个问题中,每个格子最多有四个方向可以探索,所以时间复杂度大约是 $O(4V)$。

2. 广度优先搜索(BFS)

  • BFS 从起点开始,探索所有相邻的格子,然后再探索这些格子的相邻格子,以此类推,直到探索完所有可以到达的格子。
  • 时间复杂度:BFS 的时间复杂度也是 $O(V + E)$。同样地,由于每个格子最多有四个相邻的格子,所以时间复杂度大约是 $O(4V)$。
    BFS 的空间复杂度由队列的最大长度决定,最坏情况下为 $O(V)$。

但是在本题中

广度优先搜索速度可能会更快,因为一直递归也是很占用资源的。(虽然占点资源,但我感觉这个题都差不多,没什么区别,队列进出也要消耗资源呀)

2.混乱地铁问题

在某一个城市中地铁网极度混乱。一条地铁线路上有 n 个地铁站,分别编号为 1 到 n。地铁线路上的每一个站都会停靠地铁,每一个地铁站上都有一个数字 m,代表从此站出发乘客必须乘坐的站数。
每个地铁站都有通往两个方向的地铁。因此既可以向编号大的方向前进 m 站,也可以向编号小的方向前进 m 站。但如果前进后超出了地铁站的范围,则该地铁不可被乘坐。例如编号为 1 的地铁上的数字为 3,那么在该地铁站上车,可以向正方向坐车到 4 号地铁站。但不能反方向坐车到 -2 号地铁站,因为 -2 号地铁站不存在。现在乘客从 A 号地铁站出发,想要到达 B 号地铁站,求他能否到达,最少要搭乘多少次地铁?
  • 每次换乘作为一层深度,需要求出最小的深度,如果在当前层(当前换层次数下)可以有解,那么后面一层的解一定不如当前的好。
  • 根据这个性质 BFS 会比 DFS 更先搜索到解,而在这个问题下,我们不需要遍历一整棵搜索树,因此,当搜索到任意一个解的时候,任务完成。
  • 可以用邻接表来进行加边。对于 $i$ 节点可以到达的节点为 $i-w$ 和 $i+w$

    int ww ;cin>>ww;
    if(i-ww>0)addedge(i,i-ww,1);//权值在此题没有意义
    if(i+ww<=N)addedge(i,i+ww,1);

C++(c with STL)代码:

#include<bits/stdc++.h>

const int maxn=114514;
const int maxm=12345;
struct edge{
  int u,v,w,nxt;
}edge[maxm<<1];
int fst[maxn];
int cnt;
inline void addedge(int u,int v,int w){
  edge[++cnt].u=u,edge[cnt].v=v,edge[cnt].w=w;
  edge[cnt].nxt=fst[u];
  fst[u]=cnt;
}
int N,M;

int vis[maxn],dis[maxn];
int bfs(int u,int dst){
  using namespace std;
  queue<int> q;
  q.push(u);
  vis[u]=1;//当有最优解的时候一个节点最多走一次
  dis[u]=0;//进站是0
  while(!q.empty()){
    int now = q.front();//当前所在的地铁now
    q.pop();
    if(now==dst)return dis[now];//如果当前到达了目的地就可以退出了。
    for(int i = fst[now];i;i=edge[i].nxt){//遍历当前地铁可以去的全部节点
      int v = edge[i].v;//即将要去的节点
      if(!vis[v]){//如果没有访问到过
        // printf("debug___%d->%d\n",now,v);
        q.push(v);
        vis[v]=1;
        dis[v]=dis[now]+1;//更新距离
        if(v==dst)return dis[v];//没什么用的优化,提前退出返回答案
      }
    }
  }
  return -1;//没有搜索到结果
}

int main(){
  using namespace std;
  cin>>N>>M;
  for(int i=1;i<=N;++i){
    int ww ;cin>>ww;
    if(i-ww>0)addedge(i,i-ww,1);//权值在此题没有意义
    if(i+ww<=N)addedge(i,i+ww,1);
  }
  int st,dt;cin>>st>>dt;
  cout<<bfs(st,dt);
}

Python 版本:

from collections import deque
maxn = 114514
maxm = 12345
class Edge:
  def __init__(self, u=0, v=0, w=0, nxt=0):
    self.u = u
    self.v = v
    self.w = w
    self.nxt = nxt
edges = [Edge() for _ in range(maxm << 1)]
fst = [0] * maxn
cnt = 0
def addedge(u, v, w):
  global cnt, edges, fst
  cnt += 1
  edges[cnt].u = u
  edges[cnt].v = v
  edges[cnt].w = w
  edges[cnt].nxt = fst[u]
  fst[u] = cnt
N, M = 0, 0
vis = [0] * maxn
dis = [0] * maxn
def bfs(u, dst):
  q = deque()
  q.append(u)
  vis[u] = 1
  dis[u] = 0 
  while q:
    now = q.popleft() 
    if now == dst:
        return dis[now] 
    i = fst[now]
    while i:
      v = edges[i].v  
      if not vis[v]:  
        q.append(v)
        vis[v] = 1
        dis[v] = dis[now] + 1 
        if v == dst:
          return dis[v]
      i = edges[i].nxt
  return -1 
def main():
  global N, M
  N, M = map(int, input().split())
  for i in range(1, N + 1):
    ww = int(input())
    if i - ww > 0:
      addedge(i, i - ww, 1)  # The weight has no meaning in this problem
    if i + ww <= N:
      addedge(i, i + ww, 1)
  st, dt = map(int, input().split())
  print(bfs(st, dt))

if __name__ == "__main__":
    main()

混乱地铁问题算法分析

在混乱地铁问题中,我们使用了图的遍历算法(特别是广度优先搜索,BFS)来解决问题。本质上是一个图的遍历问题,其中节点表示地铁站,边表示可行的地铁路线。

广度优先搜索(BFS)

  • 从给定的起始节点开始,逐步访问所有可达的节点。在每个节点,算法检查所有相邻的节点,并将那些尚未访问过的节点加入队列中。这个过程持续进行,直到队列为空,即所有可达节点都被访问。
  • 时间复杂度:BFS 的时间复杂度通常为 $O(V + E)$,其中 $V$ 是顶点数(本问题中为地铁站数量),$E$ 是边数(本问题中为可能的地铁线路数量)。每个节点最多被访问一次,每条边最多被检查一次。
  • 空间复杂度:最坏情况下为 $O(V)$,此题比较固定,最坏情况下队列满载情况。
  • BFS 适合用于找到最短路径或者层次遍历的情况,特别是在图的遍历和搜索问题中非常有效。
  • 在这个问题中,BFS 通过逐层搜索,能够保证第一次到达终点站的路径是需要最少换乘次数的路径。

3.0-1背包问题

在非空间压缩的基础上(二维 dp 数组),如何反求出我选了什么?

其实挺简单:

  • dp[N][M] 开始,逆向查找哪些物品被放入了背包
  • 比较 dp[i][j]dp[i-1][j]` 是否相等:
  • 如果相等,说明第 i 件物品没有被放入背包。
  • 如果不相等,说明第 i 件物品被放入了背包,将这个物品记录下来,并将 j 更新为 j-w[i]
  • 将 i 减 1,继续比较。
  • 重复这个过程,直到 i 为 0 或者 j 为 0。

具体代码实现:

//反求选了哪几个
int i = N;
int j = W;
for(;i>0&&j>0;i--){
  if(dp[i][j] != dp[i-1][j]){
    res[++cnt]=i;
    j = j-w[i];
  }
}
for(int i=1;i<=cnt;++i){
  printf("sel: %d\n",res[i]);
}

完整 C 语言实现:

#include<iostream>
const int maxn = 1e4+102;
int dp[maxn][maxn];
int w[maxn],v[maxn];
int N,W;


int res[maxn];
int cnt;

int main(){
  using namespace std;
  cin>>N>>W;
  for(int i=1;i<=N;++i){
    cin>>w[i]>>v[i];
  }
  //dp 不装东西的时候假设价值是0
  for(int i=1;i<=N;++i){
    dp[i][0]=0;
  }
  for(int i=1;i<=N;++i){
    for(int j=0;j<=W;++j){
      if(j-w[i]<0)dp[i][j]=dp[i-1][j];
      else dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
    }
  }
  cout<<dp[N][W]<<"\n";

  //反求选了哪几个
  int i = N;
  int j = W;
  for(;i>0&&j>0;i--){
    if(dp[i][j] != dp[i-1][j]){
      res[++cnt]=i;
      j = j-w[i];
    }
  }
  for(int i=1;i<=cnt;++i){
    printf("sel: %d\n",res[i]);
  }
} 

具体的 Python 实现:

def main():
  N, W = map(int, input().split())
  w = [0] * (N + 1)
  v = [0] * (N + 1)
  for i in range(1, N + 1):
    w[i], v[i] = map(int, input().split())

  dp = [[0] * (W + 1) for _ in range(N + 1)]

  for i in range(1, N + 1):
    for j in range(W + 1):
      if j - w[i] < 0:
        dp[i][j] = dp[i - 1][j]
      else:
        dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i])
  print(dp[N][W])
  
  res = []
  i, j = N, W
  while i > 0 and j > 0:
    if dp[i][j] != dp[i - 1][j]:
      res.append(i)
      j -= w[i]
    i -= 1
  for i in range(len(res)):
      print(f"sel: {res[i]}")

if __name__ == "__main__":
    main()

时间复杂度分析

$O(NW)$ 要dp转移一次的时间复杂度如此。

  1. 初始化:从 dp[N][W] 开始,也就是01背包转移完成的DP数组,即考虑所有物品且背包容量为W的情况。
  2. 反向追踪

    • 比较 dp[i][j]dp[i-1][j]
    • 如果 dp[i][j] != dp[i-1][j],说明第i件物品被选取,记录该物品,并更新 jj - w[i](减去该物品的重量)。
    • 重复这个过程,直到 ij 为0。
  • 追踪过程的时间复杂度为 $O(NW+N)$,其中 $N$ 是物品数量。因为最多需要追踪 $N$ 次,每次追踪的操作是常数级别的。
  • 反求$O(N)$ 转移:$O(NW)$ 合并:$O(NW+N)$
  • 最坏情况下为 $O(WN)$

4.P120 第2题

已知一个 N·M 的国际象棋棋盘,现给定一个起始点、终点。若在起始点上有一个马,
问:从起始点出发,马能否到达终点,到终点最少需要走几步?
输入中包含棋盘的大小、起始点坐标、终点坐标。(马走“日”字。)

这个 比较神奇,它可以走日字:

于是找到了个差不多的题目。

https://www.luogu.com.cn/problem/P1443

  • 因为求的是最少走多少步,我们在当前的状态里可以求得答案的话,那么下层状态求的一定是更多的数量
  • 这个性质直接用 BFS 就可以啦

N , M ,sx, sy = map(int , input().split())
mp = [[0 for j in range(1,M+1) ]for i in range(1,N+1)]
from collections import deque
# 我的马可以走这些地方
dx = [0,-1,-2,-2,-1,1,2,2,1]
dy = [0,2,1,-1,-2,2,1,-1,-2]
def bfs(stx,sty,dstx,dsty):
  # 一个点可以走三次
  vis = [[0 for j in range(M+1)]for i in range(N+1)]
  dis = [[0 for j in range(M+1)]for i in range(N+1)]
  q = deque()
  q.append((stx,sty))
  dis[stx][sty]=0
  vis[stx][sty]=1
  while q:
    x,y = q.popleft()
    if x == dstx and y == dsty:
      return dis[x][y]
    for i in range(1,9):
      # print(i)
      nx = x+dx[i]
      ny = y+dy[i]
      if 1 <= nx <= N and 1 <= ny <= M and vis[nx][ny] == 0:
        vis[nx][ny]=1
        q.append((nx,ny))
        dis[nx][ny]= dis[x][y]+1
  return -1

for i in range(N):
  for j in range(M):
    print(bfs(sx,sy,i+1,j+1),end=" ")
  print()

虽然没过,试一试 C++

用c++试一试:

#include<bits/stdc++.h>

const int maxn = 2333;
const int maxm = 2333;
int dx[] = {0,-1,-2,-2,-1,1,2,2,1};
int dy[] = {0,2,1,-1,-2,2,1,-1,-2};
int N,M;
int vis[maxn][maxm];
int dis[maxn][maxm];
#define rep(i,x,y) for(int i=x;i<=y;++i)

inline void clear(){
  rep(i,1,N){
    rep(j,1,M){
      vis[i][j]=0;
      dis[i][j]=0;
    }
  }
}

inline int bfs(int stx,int sty,int dstx,int dsty){
  using namespace std;
  queue<pair<int, int>> q;
  clear();
  q.push({stx, sty});
  vis[stx][sty] = 1;
  while (!q.empty()) {
    int x = q.front().first;
    int y = q.front().second;
    q.pop();
    if(x==dstx&&y==dsty){
      return dis[dstx][dsty];
    }
    for(int i=1;i<=8;++i){
      int nx = x+dx[i];
      int ny = y+dy[i];
      if (nx >= 1 && nx <= N && ny >= 1 && ny <= M && vis[nx][ny] == 0) {
        vis[nx][ny] = 1;
        q.push({nx, ny});
        dis[nx][ny] = dis[x][y] + 1;
      }
    }
  }
  return -1;
}
int main(){
  int sx,sy;
  std::cin>>N>>M>>sx>>sy;
  rep(i,1,N){
    rep(j,1,M){
      std::cout<<bfs(sx,sy,i,j)<<" ";
    }
    std::cout<<"\n";
  }
}

想了想,对于这个题,目标其实不变。所以不用 BFS 多次,BFS 一次就行

C++

Python

错怪 Python 了

最后的代码

C++

#include<bits/stdc++.h>

const int maxn = 800;
const int maxm = 800;
int dx[] = {0,-1,-2,-2,-1,1,2,2,1};
int dy[] = {0,2,1,-1,-2,2,1,-1,-2};
int N,M;
int vis[maxn][maxm];
int dis[maxn][maxm];
#define rep(i,x,y) for(int i=x;i<=y;++i)

inline void clear(){
  rep(i,1,N){
    rep(j,1,M){
      vis[i][j]=0;
      dis[i][j]=-1;
    }
  }
}

inline int bfs(int stx,int sty){
  using namespace std;
  queue<pair<int, int>> q;
  clear();
  q.push({stx, sty});
  vis[stx][sty] =1;
  dis[stx][sty] =0;
  while (!q.empty()) {
    int x = q.front().first;
    int y = q.front().second;
    q.pop();
    for(int i=1;i<=8;++i){
      int nx = x+dx[i];
      int ny = y+dy[i];
      if (nx >= 1 && nx <= N && ny >= 1 && ny <= M && vis[nx][ny] == 0) {
        vis[nx][ny] = 1;
        q.push({nx, ny});
        dis[nx][ny] = dis[x][y] + 1;
      }
    }
  }
  return -1;
}
int main(){
  int sx,sy;
  std::ios::sync_with_stdio(0);
  std::cin.tie(0);
  std::cin>>N>>M>>sx>>sy;
  bfs(sx,sy);
  rep(i,1,N){
    rep(j,1,M){
      std::cout<<dis[i][j]<<" ";
    }
    std::cout<<"\n";
  }
}

python:

N , M ,sx, sy = map(int , input().split())
mp = [[0 for j in range(1,M+1) ]for i in range(1,N+1)]
from collections import deque
# 我的马可以走这些地方
dx = [0,-1,-2,-2,-1,1,2,2,1]
dy = [0,2,1,-1,-2,2,1,-1,-2]
dis = [[-1 for j in range(M+1)]for i in range(N+1)]
def bfs(stx,sty):
  # 一个点可以走三次
  global dis
  vis = [[0 for j in range(M+1)]for i in range(N+1)]
  q = deque()
  q.append((stx,sty))
  dis[stx][sty]=0
  vis[stx][sty]=1
  while q:
    x,y = q.popleft()
    # if x == dstx and y == dsty:
    #   return dis[x][y]
    for i in range(1,9):
      # print(i)
      nx = x+dx[i]
      ny = y+dy[i]
      if 1 <= nx <= N and 1 <= ny <= M and vis[nx][ny] == 0:
        vis[nx][ny]=1
        q.append((nx,ny))
        dis[nx][ny]= dis[x][y]+1
  return -1

bfs(sx,sy)
for i in range(N):
  for j in range(M):
    print(dis[i+1][j+1],end=" ")
  print()

算法分析

分析:马在国际象棋棋盘上的移动问题

通过广度优先搜索(BFS)算法来解决。BFS 适用于在图中找到从起点到终点的最短路径的问题。在这个特定的问题中,我们需要找到马从起点到终点的最少步数。

算法步骤

  1. 初始化

    • 创建一个队列用于存储每一步马可以到达的位置。
    • 定义一个二维数组 dis 用于存储从起点到每个点的距离,初始值为 -1。
    • 将起始点放入队列,并将该点的 dis 值设置为0。
  2. BFS遍历

    • 当队列不为空时,取出队列的首个元素(当前位置)。
    • 遍历马可能的下一步移动(8个可能的方向)。
    • 如果下一步的位置在棋盘上并且尚未访问过(dis 值为 -1),则:

      • 将该位置加入队列。
      • 更新该位置的 dis 值为当前位置的 dis 值加1。
  3. 输出结果

    • dis 数组就是从起点到每个点的最短距离,即马到达每个位置所需的最少步数。

时空复杂度

  • BFS 的时间复杂度为 $O(V + E)$,其中 $V$ 是顶点数(在这个问题中是棋盘上的格子数),$E$ 是边数(在这个问题中是马可能的移动步数)。
  • 在这个问题中,棋盘的大小是 $N \times M$,所以顶点数 $V = NM$,每个顶点有最多8个可能的移动方向,所以边数 $E \approx 8NM$。
  • 因此,时间复杂度大致为 $O(NM)$。
  • 主要的空间消耗来自于存储棋盘上每个点的 dis 值的二维数组和队列,因此空间复杂度为 $O(NM)$。

二、总结

都是简单的搜索题啦OVO。

1. 艰难旅行问题

  • 问题描述:在一个 N×M 的地图上,根据格子上的数字决定移动方向,求能在地图中到达多少格子。
  • 使用了 DFS 和 BFS 算法。
  • 分析:DFS 和 BFS 都能有效解决问题。BFS 更适合于找到最大可达区域,因为它按层次遍历,不会因为深度太大而受限。

2. 混乱地铁问题

  • 问题描述:在一个混乱的地铁系统中,求从一站到另一站是否可达,如果可达,求最少需要搭乘多少次地铁。
  • 分析:BFS 适合此问题,因为它能保证找到的是最短路径。当搜索到解的时候就是答案啦。

3. 0-1 背包问题

  • 经典的DP例题

4. 马在国际象棋棋盘上的移动问题

  • 问题描述:在一个 N×M 的国际象棋棋盘上,求马从一个点到另一个点的最少移动步数。
  • 分析:BFS 适合此问题,因为它能确保找到的路径是最短的。通过一层层的搜索,我们可以找到马到达每个位置所需的最少步数。当搜索到解的时候就是答案啦。

总体分析

  • BFS 和 DFS:这两种搜索算法在探索路径和找到特定目标方面非常有效。BFS 通常用于找到最短路径或最大可达区域,而 DFS 更适用于需要深入探索的问题。
  • 动态规划:DP 是解决优化问题的强大工具,特别是在处理具有重叠子问题和最优子结构的问题时。

每种算法都有其适用场景和优势。在解决实际问题时,选择合适的算法至关重要。

算法实验报告2

本文链接:

  1. https://type.dayiyi.top/index.php/archives/231/
  2. https://www.cnblogs.com/rabbit-dayi/articles/17864571.html
  3. https://cmd.dayi.ink/cbNEvd5MTn-YQFw0J7IuKw?edit
  4. https://blog.dayi.ink/?p=155

1.求幂集问题

也就是求全部的组合

DFS

  • 把全排列 DFS 树给记录下来就可以
  • DFS 到每个节点的时候,记录当前状态加入到结果集即可。
  • 复杂度$O(2^{N})$

Python 代码:

def dfs(nums, path, index, res):
  res.append(path[:])
  for i in range(index, len(nums)):
    path.append(nums[i])
    dfs(nums, path, i+1, res)
    # 回溯
    path.pop()

N = 5
nums = [i for i in range(1,N+1)]
res = []
dfs(nums, [], 0, res)
print(res)

位运算

位运算

  • 复杂度$O(N*2^n)$

  • 假设有 5 个物品,每个物品可以选或者不选。
  • 0 0 0 0 0 : 5个二进制,1表示选,0表示不选
  • 这样下来,2^5次方就可以把全部的情况枚举出来。
  • 整数 0 的二进制表示00000对应空集。
  • 整数 1 的二进制表示00001对应只含有第一个元素的子集。
  • 整数 2 的二进制表示00010对应只含有第二个元素的子集。
  • ...以此类推...
  • 整数 31 的二进制表示11111对应包含所有五个元素的集合本身。
  • 这样下来,每个数都是一个子集,求完即可。
  • 每一个从 $0$ 到 $2^n - 1$ 的整数都对应一个唯一的子集。对于每个整数,检查其二进制表示中的每一位,如果当前位是 1,表示选中,当前位是 0,便不选。

落实到具体操作上:

  • 位操作符&
  • i是当前数
  • (1 << j) 把1左移j
  • i = 01000
  • j = 1<<4 = 0100
  • i & j = 1

如果 i & j = 1 则说明当前位是1,选中当前的元素。

N = 15
nums = [i for i in range(1,N+1)]
res = []
def bits(nums):
  # N的所有子集的个数为 2^n
  n = len(nums)
  for i in range(2**n):
    subset = []
  # 检测每一位
    for j in range(n):
      # i的2进制的j位是1,把1左移j位,然后跟i进行AND运算,如果运算结果是1,则说明i的2进制当前为是1
      if i & (1 << j):
        subset.append(nums[j])
    res.append(subset)
bits(nums)
print(res)

时间复杂度

深度优先搜索(DFS)

  • 在DFS方法中,我们从空集开始,逐步添加元素,直到遍历完所有元素。
  • 每到达一个新节点时,当前路径的一个副本被添加到结果集中。
  • 对于每个元素,都有选择和不选择两种可能,这导致了总共有 $2^N$ 种可能的组合(幂集),其中 $N$ 是数组中的元素数量
  • 虽然这里是递归,但递归深度和每层递归的工作量都是有限的,因此时间复杂度不是阶乘级别的。

位运算

  • 对于每个整数,我们检查其二进制表示的每一位,以确定是否包括数组中对应位置的元素。
  • 对于每个子集,我们需要检查 $N$ 位以确定哪些元素被包括在内。因此,总的时间复杂度是 $O(N \times 2^N)$,其中每个子集需要 $O(N)$ 来构建。

OVO

  • DFS 方法的时间复杂度是 $O(2^N)$,而位运算方法的时间复杂度是 $O(N \times 2^N)$。
  • 由于 $N \times 2^N$ 和 $2^N$ 之间的差异通常不是非常大(尤其是对于小型到中型的数据集),在实践中的性能差异可能不会很明显。

2.N皇后问题 实现回溯法求解皇后问题递归和非递归框架

N 皇后,在 N×N 的棋盘上放置 N 个皇后,使得它们不能相互攻击,即任何两个皇后都不能处在同一行、同一列或同一对角线上。
  • 枚举每个点
  • 检查当前的点是否可以放置皇后

检查函数

  • 斜行

落实到具体操作上:

因为一个行 一个列 一个斜行 只能放置一个皇后

直接标记当前行列是否可以放置皇后。

  • 对于每一行 一个数组 row[N]
  • 对于每一列 一个数组 col[N]
  • 对于每一斜行

    • 对角线
    • 反对角线

对于对角线来说

  • 每条直线可以表示为:$y = -x+b$
  • 截距:$b = y+x$
  • 每个截距可以表示一条对角线
  • 根据取值,于是,对于每个单元$(x,y)$的对角线,就可以 dg[y+x]来进行表示。

而对于反对角线来说

同样的:

  • 每条直线可以表示为:$y = x+b$
  • 截距:$b = y-x$
  • 每个截距可以表示一条对角线
  • 根据取值,于是,对于每个单元 $(x,y)$ 的对角线,就可以 adg[y-x] 来进行表示。

但是根据定义域来说,$x+y$ 可能为负数,对于计算机来说,可能会导致数组越界。而我们只需要表示当前行是否被占,直接对于每个数 +N 即可。

也就是用adg[y-x+N]来表示 $(x,y)$ 的对角线有没有被占。

综上

  • row[x] (如果DFS按照这个顺序枚举,其实不需要添加)
  • col[y]
  • 对角线 adg[y+x]
  • 反对角线 adg[y-x+N]

检查函数可以这样写:

row = [0 for i in range(n+10)]
col = [0 for i in range(n+10)]
dg = [0 for i in range(n*n+10)]
adg = [0 for i in range(n*n+n+10)]

def check(x,y):
  if row[x] == 1:
    return 0
  if col[y] == 1:
    return 0
  if dg[x+y]==1:
    return 0
  if adg[y-x+n]==1:
    return 0
  return 1

对于放置函数,其实就是标注一下,但是这样可以提升写代码的幸福感。

def place(x,y):
  row[x]=1
  col[y]=1
  dg[x+y]=1
  adg[y-x+n]=1

def unplace(x,y):
  row[x]=0
  col[y]=0
  dg[x+y]=0
  adg[y-x+n]=0

DFS 搜

  • 时间复杂度$O(n!)$

简单写一下:

# 检查当前位置是否可以放置
n = 8 

# +10 防止数组越界
row = [0 for i in range(n+10)]
col = [0 for i in range(n+10)]
dg = [0 for i in range(n*n+10)]
adg = [0 for i in range(n*n+n+10)]

def check(x,y):
  if row[x] == 1:
    return 0
  if col[y] == 1:
    return 0
  if dg[x+y]==1:
    return 0
  if adg[y-x+n]==1:
    return 0
  return 1

def place(x,y):
  row[x]=1
  col[y]=1
  dg[x+y]=1
  adg[y-x+n]=1

def unplace(x,y):
  row[x]=0
  col[y]=0
  dg[x+y]=0
  adg[y-x+n]=0


ans = 0
def dfs(t):
  res = 0
  if t==n+1:
    return 1
  for i in range(1,n+1):
    x = t
    y = i
    if(check(x,y)):
      place(x,y)
      res += dfs(t+1)
      unplace(x,y)
  return res

n = 8
print(dfs(1))

带结果打印:

# 检查当前位置是否可以放置
n = 8 
n = 5

# +10 防止数组越界
row = [0 for i in range(n+10)]
col = [0 for i in range(n+10)]
dg = [0 for i in range(n*n+10)]
adg = [0 for i in range(n*n+n+10)]

# 打印
board = [['.' for _ in range(n)] for _ in range(n)]

def print_res():
  for i in range(n):
        print(' '.join(board[i]))
  print('\n')

def check(x,y):
  if row[x] == 1:
    return 0
  if col[y] == 1:
    return 0
  if dg[x+y]==1:
    return 0
  if adg[y-x+n]==1:
    return 0
  return 1

def place(x,y):
  row[x]=1
  col[y]=1
  dg[x+y]=1
  adg[y-x+n]=1
  # 打印结果用
  board[x-1][y-1] = 'Q'

def unplace(x,y):
  row[x]=0
  col[y]=0
  dg[x+y]=0
  adg[y-x+n]=0
  # 打印结果用
  board[x-1][y-1] = '.'


ans = 0
def dfs(t):
  res = 0
  if t==n+1:
    print_res()
    return 1
  
  for i in range(1,n+1):
    # 把 xy标记一下
    x = t
    y = i
    if(check(x,y)):
      place(x,y)
      res += dfs(t+1)
      unplace(x,y)
  return res
print(dfs(1))

# 检查当前位置是否可以放置
n = 8 
n = eval(input())

# +10 防止数组越界
row = [0 for i in range(n+10)]
col = [0 for i in range(n+10)]
dg = [0 for i in range(n*n+10)]
adg = [0 for i in range(n*n+n+10)]

# 打印
board = [['.' for _ in range(n)] for _ in range(n)]

res_cnt = 0

def print_res():
  global res_cnt
  if res_cnt==3:
    return
  for i in range(1,n+1):
    for idx,j in enumerate(board[i-1]):
      if(j=='Q'):
        print(idx+1,end=" ")
      # print(idx,j)
  print("")
  res_cnt+=1
    # if board[i] =='Q':
    #   print(i,end=" ")
    
  
  return
  # for i in range(n):
  #       print(' '.join(board[i]))
  # print('\n')

def check(x,y):
  if row[x] == 1:
    return 0
  if col[y] == 1:
    return 0
  if dg[x+y]==1:
    return 0
  if adg[y-x+n]==1:
    return 0
  return 1

def place(x,y):
  row[x]=1
  col[y]=1
  dg[x+y]=1
  adg[y-x+n]=1
  # 打印结果用
  board[x-1][y-1] = 'Q'

def unplace(x,y):
  row[x]=0
  col[y]=0
  dg[x+y]=0
  adg[y-x+n]=0
  # 打印结果用
  board[x-1][y-1] = '.'


ans = 0
def dfs(t):
  res = 0
  if t==n+1:
    print_res()
    return 1
  for i in range(1,n+1):
    # 把 xy标记一下
    x = t
    y = i
    if(check(x,y)):
      place(x,y)
      res += dfs(t+1)
      unplace(x,y)
  return res


print(dfs(1))

TLE了,但是我觉得python已经很快了。

非递归

用栈模拟的,我实在没想到怎么用

# 初始化棋盘参数
n = 8  # 皇后数量和棋盘大小

# +10 是为了防止数组越界
row = [0 for _ in range(n+10)]
col = [0 for _ in range(n+10)]
dg = [0 for _ in range(n*n+10)]
adg = [0 for _ in range(n*n+n+10)]

# 打印棋盘
def print_board(board):
    for i in range(n):
        print(' '.join(board[i]))
    print('\n')

# 检查位置是否可以放置皇后
def check(x, y):
    if row[x] == 1 or col[y] == 1 or dg[x+y] == 1 or adg[y-x+n] == 1:
        return False
    return True

# 放置皇后
def place(x, y):
    row[x] = 1
    col[y] = 1
    dg[x+y] = 1
    adg[y-x+n] = 1

# 移除皇后
def unplace(x, y):
    row[x] = 0
    col[y] = 0
    dg[x+y] = 0
    adg[y-x+n] = 0

# 非递归解决N皇后问题
def solve_stack(n):
    board = [['.' for _ in range(n)] for _ in range(n)]  # 初始化棋盘
    ans = 0  # 解的数量
    stack = [(0, list(range(n)))]  # 使用栈进行深度优先搜索,包含行索引和列候选
    place_pos = []  # 用于回溯的放置皇后位置
    while stack:
        row, col = stack[-1]
        if not col:  # 如果当前行没有列可尝试,进行回溯
            stack.pop()
            if place_pos:
                last_row, last_col = place_pos.pop()
                board[last_row][last_col] = '.'
                unplace(last_row + 1, last_col + 1)
            continue

        col = col.pop()
        if check(row + 1, col + 1):
            place(row + 1, col + 1)
            board[row][col] = 'Q'
            place_pos.append((row, col))
            if row == n - 1:  # 找到一个解
                ans += 1
                print_board(board)
                board[row][col] = '.'
                unplace(row + 1, col + 1)
                place_pos.pop()
            else:
                stack.append((row + 1, list(range(n))))  # 移动到下一行
        # 继续尝试当前行的下一个列
    return ans
print(solve_stack(n))

时间复杂度

尽管看起来像是 $O(N!)$ 的复杂度(每行选择一个位置,共有 $N$ 行),实际上由于皇后的约束条件(不能在同行、同列、同对角线上),并不是每行都有 N 个可选位置。事实上,随着递归的深入,可选位置的数量迅速减少。平均情况下的时间复杂度近似于 $O(N!)$,但实际上通常会更低。

非递归回溯的时间复杂度与递归方法相似,也近似于 $O(N!)$。在非递归方法中,栈的使用取代了递归调用的栈帧,但算法的基本操作和约束条件检查相同,因此时间复杂度保持不变。

两种方法的时间复杂度都近似于 $O(N!)$,但实际执行效率通常会更高,因为皇后的约束条件大幅减少了实际的搜索空间。

3. 01包问题

这个,讲真如果第一次学 DP 的话,肯定会有点难理解。

问题

有 $N$ 件物品和一个容量是 $V$ 的背包。每件物品只能使用一次。

第 $i$ 件物品的体积是 $v_i$,价值是 $w_i$。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

爆搜,对于每个物品进行枚举一次

这个方案如 T1 的枚举,时间复杂度是 $O(N!)$ 或者 $O(2^N*N)$

当 $N > 20$ 的时候,肯定是搜不动了。

因此当 $N = 1000$ 的时候,该方案应该是不可取的。

DP

状态属性

  • 状态表示:f[i][j]
  • 集合:装了前 i 个物品,总体积不超过 j 的选法的集合
  • 属性:$max$ 值

状态转移

对于第 $i$ 个物品,可以选择装或者不装。

  • 我不打算装这个物品

    我的体积不需要被消耗。
    我的最大的值没有变,可以直接对 f[i-1][j] 进行状态转移。
    得到 $f[i][j] = f[i-1][j]$ ,结束。

  • 我打算装这个物品:

    • 我需要 w[i] 的空间
    • 我能获得的价值是 v[i]
    • 当前我一定要装这个物品,一定要花费 w[i] 的空间(重量)
    • 我装完之后的背包重量不能大于j
      于是我的状态转移方程:
      $f[i][j] = f[i-1][j-w[i]]+v[i]$
  • 两种情况都可能会影响到后续的结果,因此,需要将两个值合并为一个状态(f[i][j]`)

    • $f[i][j]$的属性是MAX,直到最后,我只要出现过的最大值,如果当前的值小于$f[i-1][j]$的值

      • 也就是如果
      • $f[i-1][j]=10$
      • $f[i-1][j-w[i]]+v[i]= 9$ (假设)
      • 我$f[i][j]$ 应该等于10

最后的状态转移方程为:

$f[i][j] = \max\{f[i-1][j],f[i-1][j-w[i]]+v[i]\}$

(截个图防止公式坏掉)

对于最终的结果显然就是:

$dp[N][W]$ 这个值是前 N 个物品,重量小于等于 W 的背包可以获得的最大值

对于处理的过程,要注意:

  • $j-w[i]$ 应该大于 0,背包空间如果小于 0 肯定不太合理,也会数组越界

然后直接去写代码就好啦

#include<iostream>
const int maxn = 1e4+102;
int dp[maxn][maxn];
int w[maxn],v[maxn];
int N,W;
int main(){
  using namespace std;
  cin>>N>>W;
  for(int i=1;i<=N;++i){
    cin>>w[i]>>v[i];
  }

  //dp 不装东西的时候假设价值是0
  for(int i=1;i<=N;++i){
    dp[i][0]=0;
  }
  
  for(int i=1;i<=N;++i){
    for(int j=0;j<=W;++j){
      if(j-w[i]<0)dp[i][j]=dp[i-1][j];
      else dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
    }
  }
  cout<<dp[N][W];
}

通过测试:

压缩下数组

  • 你会发现,我们的状态 dp[i][j] 只会从:

    • dp[i-1][j]
    • dp[i-1][j-w[i]]
  • 这两个状态转移过来
  • 也就是说之前的状态与最终答案和当前计算的状态没有关系
  • 即 $dp[i][j]$ 只依赖于 $dp[i-1][j]$ 和 $dp[i-1][j-w[i]]$

动态规划状态压缩的核心在于识别出状态转移仅依赖于有限的几个状态。

但是注意:

  1. 我们只关心 "当前" 和 "上一个" 两个状态,这样就可以只用一个一维数组啦。
  2. 但是为了确保 "当前状态" 是基于 "上一个状态" 计算的,需要须反向遍历背包容量(重量)。
  3. 因为如果正向遍历,当计算到 $dp[j]$ 时,$dp[j-w[i]]$ 可能已经在这一轮循环中更新过了,使用这一轮的信息而不是上一轮的信息。

压缩后的状态转移方程为:

$$ dp[j] = \max\{dp[j], dp[j-w[i]] + v[i]\} $$

  • dp[j] 表示在不超过重量 j 的前提下,目前为止能得到的最大价值。
  • dp[j-w[i]] + v[i] 表示如果你选择放入第 i 个物品,那么背包中剩余的重量就是 j-w[i],对应的最大价值就是 dp[j-w[i]],加上第 i 个物品的价值 v[i] = dp[j-w[i]] + v[i]

对于状态转移:

  • 不放第 i 个物品时,背包的最大价值(即 dp[j])。
  • 放入第 i 个物品时,背包的最大价值(即 dp[j-w[i]] + v[i])。

然后取这两种情况的 $max$ 作为新的 dp[j] 的值。

最后:dp[W] 就是小于W能获得的最大价值。

每次计算 dp[j] 时,dp[j-w[i]] 保持的是上一个物品状态的值。

dp[j-w[i]] 是基于第 i-1 个物品时的最大价值

如果正序遍历,那么在计算较大的 j 值时,可能会重复计算某个物品的价值。

OK

这样代码如下:

#include<iostream>
const int maxn = 1e4+102;
int dp[maxn]; 
int w[maxn],v[maxn];
int N,W;
int main(){
  using namespace std;
  cin>>N>>W;
  for(int i=1;i<=N;++i){
    cin>>w[i]>>v[i];
  }
  //清零(可以不用)
  for(int i=0;i<=W;++i)dp[i]=0;
  for(int i=1;i<=N;++i){
    for(int j=W;j>=0;--j){ //逆序遍历
      if(j-w[i]>=0){
        // 当 j<w[i] 时,dp[j] = dp[j]
        dp[j]=max(dp[j], dp[j-w[i]] + v[i]);
      }
    }
  }
  cout<<dp[W]; //dp[W]就是结果啦
}

空间复杂度从 $O(NW)$ 降低到了 $O(W)$,其实这里读入的数组的时候可以直接算dp方程,还能再省,但是一般空间不会不够用。

然后写个python的版本

maxn = 1000+101
dp = [0 for i in range(1,maxn)]
w = [0 for i in range(1,maxn)]
v = [0 for i in range(1,maxn)]

N,W = map(int,input().split())

for i in range(1,N+1):
  w[i],v[i] = map(int,input().split())
  for j in range(W,0,-1):
    # print(j)
    if j-w[i]>=0:
      dp[j]=max(dp[j],dp[j-w[i]]+v[i])
print(dp[W])

时间复杂度

其实很简单啦,这里遍历$N*W$,所以时间复杂度$O(N*W)$

所以这里就很明显了。

4.数独问题

你需要把一个 9×9 的数独补充完整,使得数独中每行、每列、每个 3×3 的九宫格内数字 1∼9均恰好出现一次。

可以直接拿一道题过来:

https://vjudge.net/problem/POJ-3074#author=GPT_zh
https://www.acwing.com/problem/content/description/168/

这里的输入:

.2738..1..1...6735.......293.5692.8...........6.1745.364.......9518...7..8..6534.
......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3.
end

其实还是跟八皇后类似

  • 检查函数
  • 枚举状态
  • 剪枝(新增)

对于检查函数

  • 检查当前行是否合法
  • 检查当前列是否合法
  • 斜行不需要检测,但是需要检查是否在当前九宫格内
# 暴力检查,没有优化
def check(x, y, val):
    # 检查行
    for i in range(9):
        if mp[x][i] == val:
            return False
    # 检查列
    for i in range(9):
        if mp[i][y] == val:
            return False
    # 检查3x3的格子
    startRow = x - x % 3
    startCol = y - y % 3
    for i in range(3):
        for j in range(3):
            if mp[startRow + i][startCol + j] == val:
                return False
    return True

然后是 DFS

# 拆开
mp =[inp[i:i+9] for i in range(0, 81, 9)]
mp = [[int(char) for char in row] for row in mp]
def dfs(mp):
  # 寻找空的单元格
  for x in range(9):
    for y in range(9):
      if mp[x][y] == 0:
        # 尝试所有可能的数字
        for val in range(1, 10):
          if check(x, y, val,mp):
            mp[x][y] = val
            if dfs(mp):
              return True
            # 回溯
            mp[x][y] = 0
        return False
  return True
if dfs(mp):
    ans = mp
else:
    ans = "No solution exists"

print(ans)

结果如图:

结果也正确:

试试提交

很遗憾,Vjudge 不支持 Python

在 ACWing 上,虽然结果可以正确,但是样例都会 TLE

剪枝

  • 选择下一个要填充的单元格时,优先选择候选数字最少的单元格。
  • check 函数可以优化

有点难,这里就不再叙述啦。

时间复杂度

算法遍历数独的每一个空格,尝试填入数字(1-9),然后检查当前的填充是否合法。如果合法,则递归地继续填充下一个空格。如果不合法或者没有更多的空格可以填充,算法回溯并尝试下一个数字。最坏情况下的时间复杂度为 $O(9^m)$,其中 $m$ 是空白格子的数量。这是因为每个空格最多有9种可能的数字,而每次填充都需要递归地处理剩余的空格。

剪枝优化

剪枝操作不能改变算法的最坏情况时间复杂度(仍然是$O(9^m)$),但可以显著减少搜索空间和搜索步骤。

  • 数独问题的解决是一个典型的搜索问题,其基本方法是回溯搜索。
  • 最坏情况下的时间复杂度为 $O(9^m)$。
  • 剪枝要用二进制优化,稍微有点复杂,这里不再详细描述啦。