分类 默认分类 下的文章

算法设计与分析报告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)$。
  • 剪枝要用二进制优化,稍微有点复杂,这里不再详细描述啦。

虚拟化和容器7——实验7 Docker网络管理应用

实验要求

了解Docker常用网络模式,掌握Docker常用网络模式的使用。本实验主要任务是利用busybox镜像建立容器,容器名称为test_busybox1和test_busybox2,将网络模式设置为none,并为容器配置IP地址,容器test_busybox1的IP设置为172.17.0.100,容器test_busybox2的IP设置为172.17.0.200,要求实现两容器互通。

前置准备

要求实验主机能够连接外网,已经正确安装Docker,并关闭防火墙和selinux。

实验过程

步骤1-3

# 步骤1.1: 创建容器test_busybox1,设置网络模式为none
docker run -dit --name=test_busybox1 --net=none busybox:latest

# 进入容器test_busybox1
docker exec -it test_busybox1 /bin/sh

# 步骤1.2: 查看IP地址(容器test_busybox1)
ip addr
# 从现象可以得知容器test_busybox1没有IP地址。

# 退出容器test_busybox1
exit

# 步骤2.1: 创建容器test_busybox2,设置网络模式为none
docker run -dit --name test_busybox2 --net=none busybox:latest
# 进入容器test_busybox2
docker exec -it test_busybox2 /bin/sh
# 步骤2.2: 查看IP地址(容器test_busybox2)
ip address
# 退出容器test_busybox2
exit

从现象可以得知容器test_busybox1,test_busybox2都没有IP地址。

步骤4

# 步骤3: 为容器test_busybox1设置IP地址为172.17.0.100
# 安装bridge-utils软件包
yum -y install bridge-utils

# 创建veth对,并将veth0加入docker0网桥
ip link add veth0 type veth peer name veth1
#           虚拟网桥           peer的name 
brctl addif docker0 veth0
# 桥接管理器 添加 管理桥接
brctl show


# 启动veth0,原神启动 (另外一个veth1也会自动启动)
ip link set veth0 up

# 获取容器test_busybox1的PID
pid1=$(docker inspect -f '{{.State.Pid}}' test_busybox1)
echo "容器test_busybox1的PID是:$pid1"


#有两种途径索引network namespace:名字(例如netns1)或者属于该namespace的进程PID。
#使用命名(Name):为网络命名空间分配可读的名称,然后使用该名称来引用和操作命名空间。这使得管理网络命名空间更加方便和直观。
#使用进程PID:每个网络命名空间都与一个进程相关联,通常是一个子进程。可以使用该进程的PID来访问和管理与之关联的网络命名空间。

# 创建network namespace软连接
mkdir -p /var/run/netns
ln -s /proc/$pid1/ns/net /var/run/netns/$pid1
ip netns ls

# 将veth1连接到容器test_busybox1的network namespace,并重命名为eth0
ip link set veth1 netns $pid1
ip netns exec $pid1 ip link set dev veth1 name eth0



# 启用eth0
ip netns exec $pid1 ip link set eth0 up

# 分配IP地址和设置网关
ip netns exec $pid1 ip addr add 172.17.0.100/24 dev eth0
ip netns exec $pid1 ip route add default via 172.17.0.1

安装包:

网桥:

veth0启动:

PID:


Docker State信息

  1. .Id: 容器的唯一标识符,通常是一个长字符串,也被称为容器ID。
  2. .Name: 容器的名称,通常是用户定义的名称,可以用来引用容器。
  3. .State.Status: 容器的状态,如运行中、停止等。
  4. .State.Running: 表示容器是否正在运行(布尔值)。
  5. .State.Pid: 容器内部主进程的PID。
  6. .Config.Image: 使用的容器镜像的名称。
  7. .Config.Cmd: 启动容器时使用的命令。
  8. .Config.Env: 容器的环境变量。
  9. .NetworkSettings.IPAddress: 容器的IP地址(如果有网络配置)。
  10. .HostConfig.Binds: 挂载到容器内部的卷或目录。
  11. .Mounts: 容器的挂载点信息。
  12. .Created: 容器创建的时间戳。
  13. .Ports: 容器的端口映射信息。
  14. .Labels: 用户定义的容器标签。
  15. .LogPath: 容器的日志文件路径。
  16. .HostConfig.NetworkMode: 容器的网络模式。

netns:

执行完之后,可以看到已经分配到网卡:

目前这个namespace叫2133

然后再命名空间里执行了一些命令。

步骤5

配置容器test_busybox2的网络

# 创建一对虚拟以太网设备veth2和veth3,这两个设备是成对出现的,数据可以在两个设备之间传送
ip link add veth2 type veth peer name veth3

# 将veth2这端加入到docker0桥接器中,这样veth2就能和docker0桥接器上的其他网络设备进行通信了
brctl addif docker0 veth2

# 显示当前桥接器的信息,可以看到docker0桥接器及其所连接的网络接口
brctl show

# 启用veth2网络接口,使其能够进行数据传输
ip link set veth2 up

# 使用docker命令检查名为test_busybox2的容器,提取容器的进程ID
docker inspect test_busybox2 | grep Pid

# 用docker inspect命令获取名为test_busybox2的容器的PID,并将其存储在变量pid2中
pid2=$(docker inspect -f '{{.State.Pid}}' test_busybox2)

# 输出容器test_busybox2的PID
echo "容器test_busybox2的PID是:$pid2"

# 为容器的网络命名空间创建一个软链接,方便后续的操作。/var/run/netns/目录通常用于存放网络命名空间
ln -s /proc/$pid2/ns/net /var/run/netns/$pid2

# 将veth3这端的网络接口移到容器test_busybox1的网络命名空间中
ip link set veth3 netns $pid2

# 在test_busybox1容器的网络命名空间内,将网络接口veth3重命名为eth0
ip netns exec $pid2 ip link set dev veth3 name eth0

# 启用容器内的eth0网络接口
ip netns exec $pid2 ip link set eth0 up

# 为容器内的eth0接口分配IP地址172.17.0.200,并设置子网掩码为24位
ip netns exec $pid2 ip addr add 172.17.0.200/24 dev eth0

# 设置容器内的网络路由,使其默认网关为172.17.0.1,即docker0桥的IP地址
ip netns exec $pid2 ip route add default via 172.17.0.1

[root@node-a docker]# ip link add veth2 type veth peer name veth3
[root@node-a docker]# brctl addif docker0 veth2
[root@node-a docker]# brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.0242398240fb       no              veth0
                                                        veth2
                                                        veth271d838
                                                        veth588fc94
[root@node-a docker]# ip link set veth2 up
[root@node-a docker]# docker inspect test_busybox2 | grep Pid
            "Pid": 2212,
            "PidMode": "",
            "PidsLimit": null,
[root@node-a docker]# pid2=$(docker inspect -f '{{.State.Pid}}' test_busybox2)
[root@node-a docker]# echo "容器test_busybox2的PID是:$pid2"
容器test_busybox2的PID是:2212
[root@node-a docker]# ln -s /proc/$pid2/ns/net /var/run/netns/$pid2
[root@node-a docker]# ip link set veth3 netns $pid2
[root@node-a docker]# ip netns exec $pid2 ip link set dev veth3 name eth0
[root@node-a docker]# ip netns exec $pid2 ip link set eth0 up
[root@node-a docker]# ip netns exec $pid2 ip addr add 172.17.0.200/24 dev eth0
[root@node-a docker]# ip netns exec $pid2 ip route add default via 172.17.0.1
[root@node-a docker]# docker exec -it test_busybox2 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
12: eth0@if13: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
    link/ether 92:7f:5d:85:1e:69 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.200/24 scope global eth0
       valid_lft forever preferred_lft forever
[root@node-a docker]#

步骤6:测试

docker exec -it test_busybox2 ip addr
docker exec -it test_busybox2 ping -c 4 172.17.0.100
docker exec -it test_busybox1 ip addr
docker exec -it test_busybox1 ping -c 4 172.17.0.200

虚拟化和容器——6 Docker资源控制

基于cgroups

https://tech.meituan.com/2015/03/31/cgroups.html

1. 对CPU的控制

步骤1:限制CPU命令速率。

(1)利用centos:7镜像分别生成容器名为centos1和centos2的容器,其中centos1容器不限制cpu的使用率,centos2容器将cpu的使用率限制为20%。

docker pull centos:7
docker run -dit --name centos1 centos:7 /bin/bash
# 限制CPU百分之20000
docker run -dit --name centos2 --cpu-quota 20000 centos:7 /bin/bash

100000微秒
020000只能使用百分之20

--cpu-quota 20000: --cpu-quota定义了容器每--cpu-period(默认为100000微秒,即100ms)可以获得的最大CPU时间,单位为微秒。--cpu-quota 20000意味着在默认的100ms周期中,容器最多只能使用20ms的CPU时间。

(2)通过查看对应的cgroup配置文件 /sys/fs/cgroup/cpu/docker/容器编号/cpu.cfs_quota_us来查看各容器cpu的使用率。

#这里是你的容器名
cat /sys/fs/cgroup/cpu/docker/86ff33795a71f86236011598c9cbaea55c4a319bd142e6ffb845e055d4db4fd9/cpu.cfs_quota_us

没限制的(-1

(3)如需修改对应容器的cpu使用率,可以直接修改cgroup配置文件 /sys/fs/cgroup/cpu/docker/容器编号/cpu.cfs_quota_us的值来实现。

修改占用为百分之40

echo 40000 >> /sys/fs/cgroup/cpu/docker/9b19d5677dd03ad0f0f4bd3eee7659b7845fb34e78e42e819f21b6338fdc6342/cpu.cfs_quota_us

cat /sys/fs/cgroup/cpu/docker/9b19d5677dd03ad0f0f4bd3eee7659b7845fb34e78e42e819f21b6338fdc6342/cpu.cfs_quota_us

这里虽然是追加写,但是并不会追加,linux都是文件,估计是直接内核交互了。?

删掉这俩实验容器

docker rm -f centos1 centos2

步骤2:多任务按比例分享CPU

(1)利用centos:7镜像创建centos3和centos4容器。设置容器权重,使用centos3和centos4的cpu占比为33.3%和66.7% 。

docker run -dit --name=centos3 --cpu-shares 512 centos:7 /bin/bash
docker run -dit --name=centos4 --cpu-shares 1024 centos:7 /bin/bash

docker ps -a

(2)打开另一个终端,使用docker stats命令查看状态。

docker stats

(3)分别打开两个终端,利用两个终端分别进行centos1和centos2容器,安装压力测试包stress。(4)分别在两个容器内启用4个线程。

容器3

#进入容器
docker exec -it centos3 /bin/bash
yum -y install epel-release
yum -y install stress
stress -c 4

容器4

docker exec -it centos4 /bin/bash
yum -y install epel-release
yum -y install stress
stress -c 4

(5)再次利用docker stats命令查看。

还是挺准的
132/(66+132)=0.666666666667

步骤3: 查看cpu内核使用

通过查看Cgroup配置文件为/sys/fs/cgroup/cpuset/docker/容器编号/cpuset.cpus。

1.CPU限制

这个没有文件诶。

应该是默认没有启用

docker run -dit --cpuset-cpus="0,1" --name my_container nginx

这样就有啦

[root@node-a cpuset]# cd docker/8aae2d1e6621d076570cb4e9fc10d099a46475f25add3440848ec1bd243a1f16/
[root@node-a 8aae2d1e6621d076570cb4e9fc10d099a46475f25add3440848ec1bd243a1f16]# cat cpuset.cpus
0-1

删除容器

docker rm -f my_container

2. 对内存使用的限制

docker run -dit --name=centos5 -m 512m centos:7 /bin/bash
docker run -dit --name=centos6 centos:7 /bin/bash

步骤2:打开一个终端,使用docker stats命令查看状态

docker stats

CONTAINER ID   NAME      CPU %     MEM USAGE / LIMIT   MEM %     NET I/O     BLOCK I/O     PIDS
8df1885fca97   centos6   0.00%     400KiB / 1.777GiB   0.02%     656B / 0B   0B / 0B       1
501410e22049   centos5   0.00%     1.965MiB / 512MiB   0.38%     656B / 0B   9.61MB / 0B   1

步骤3:查看容器对应的Cgroup配置文件可查看容器内存限制。

cat /sys/fs/cgroup/memory/docker/501410e22049929b244235423c19239b50cc61a082834dba85cb18c59e2f7470/memory.limit_in_bytes

536870912 // 536870912=512×1024×1024