算法设计与分析(陈慧南)Ch7

算法设计与分析(陈慧南)Ch7

创建于 2025-03-03·更改于 2025-03-03

第七章 动态规划

参考资料:动态规划法 AOE网络(最长路径) 弗洛伊德算法 最长公共子序列 01背包问题 https://www.bilibili.com/video/BV1L54y1U7zP

7.2 Floyd

7.2.1 递推关系

D(k)[i][j]D^{(k)}[i][j] 表示从节点 ii 到节点 jj 且中间节点编号不超过 kk 的最短路径长度。

递推关系:对于每个中间节点 kk,有两种情况:

  1. 不经过 kk:路径保持原值,即 D(k)[i][j]=D(k1)[i][j]D^{(k)}[i][j] = D^{(k-1)}[i][j]
  2. 经过 kk:路径拆分为 iki \to kkjk \to j,即 D(k)[i][j]=D(k1)[i][k]+D(k1)[k][j]D^{(k)}[i][j] = D^{(k-1)}[i][k] + D^{(k-1)}[k][j]
    实际实现中,我们可以将 kk 从 0 到 n 枚举,覆盖更新,就不再需要 k 这一维了。

简化版的状态转移:

D[i][j]=min(D[i][j],D[i][k]+D[k][j])D[i][j] = \min\left(D[i][j], D[i][k] + D[k][j]\right)

伪代码:
枚举顺序 kijk \to i \to j

for k: 0->n
    for i: 0->n
        for j: 0->n
            if(i~k~j 小于 i~j)
                d[i][j] = d[i][k] + d[k][j]

7.2.2 记录路径

引入一个额外的二维数组 next,其中 next[i][j] 表示从节点 ii 到节点 jj 的最短路径中,ii 的下一个节点。

  • 初始化

    • 如果 iijj 之间有边,则 next[i][j] = j
    • 否则,next[i][j] = -1
  • 更新规则

    • 当发现更短的路径时,更新 dist[i][j]next[i][j]
    • next[i][j] 被设置为 next[i][k],因为路径从 ii 经过 kkjj
  • 路径打印

    • 使用 next 数组回溯路径,从 ii 开始,沿着 next 数组直到 jj

假设图的邻接矩阵如下:

dist=[03780250120]\text{dist} = \begin{bmatrix} 0 & 3 & \infty & 7 \\ 8 & 0 & 2 & \infty \\ 5 & \infty & 0 & 1 \\ 2 & \infty & \infty & 0 \\ \end{bmatrix}

调用 floyd 算法后,distnext 数组如下:

dist=[0356502336012570],next=[1222313344141111]\text{dist} = \begin{bmatrix} 0 & 3 & 5 & 6 \\ 5 & 0 & 2 & 3 \\ 3 & 6 & 0 & 1 \\ 2 & 5 & 7 & 0 \\ \end{bmatrix}, \quad \text{next} = \begin{bmatrix} -1 & 2 & 2 & 2 \\ 3 & -1 & 3 & 3 \\ 4 & 4 & -1 & 4 \\ 1 & 1 & 1 & -1 \\ \end{bmatrix}

代码演示(gpt-4o-mini生成,可以丢给AI解释)

#include <iostream>
#include <vector>
#include <limits>

using namespace std;

const int INF = numeric_limits<int>::max();

void floydWarshall(int n, vector<vector<int>>& graph, vector<vector<int>>& dist, vector<vector<int>>& next) {
    // Initialize distance and next matrices
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            dist[i][j] = graph[i][j];
            if (graph[i][j] != INF && i != j) {
                next[i][j] = j;
            } else {
                next[i][j] = -1; // No path
            }
        }
    }

    // Floyd-Warshall algorithm
    for (int k = 1; k <= n; ++k) {
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (dist[i][k] != INF && dist[k][j] != INF && dist[i][j] > dist[i][k] + dist[k][j]) {
                    dist[i][j] = dist[i][k] + dist[k][j];
                    next[i][j] = next[i][k];
                }
            }
        }
    }
}

void printPath(int u, int v, const vector<vector<int>>& next) {
    if (next[u][v] == -1) {
        cout << "No path from " << u << " to " << v << endl;
        return;
    }
    cout << "Path from " << u << " to " << v << ": ";
    while (u != v) {
        cout << u << " ";
        u = next[u][v];
    }
    cout << v << endl;
}

int main() {
    int n = 4; // Number of vertices
    vector<vector<int>> graph(n + 1, vector<int>(n + 1, INF));
    vector<vector<int>> dist(n + 1, vector<int>(n + 1));
    vector<vector<int>> next(n + 1, vector<int>(n + 1));

    // Example graph initialization (0: no edge, INF: no path)
    graph[1][1] = 0;
    graph[1][2] = 3;
    graph[1][3] = INF;
    graph[1][4] = 7;
    graph[2][1] = 8;
    graph[2][2] = 0;
    graph[2][3] = 2;
    graph[2][4] = INF;
    graph[3][1] = 5;
    graph[3][2] = INF;
    graph[3][3] = 0;
    graph[3][4] = 1;
    graph[4][1] = 2;
    graph[4][2] = INF;
    graph[4][3] = INF;
    graph[4][4] = 0;

    floydWarshall(n, graph, dist, next);

    // Print final path and distance matrices
    cout << "Distance Matrix:" << endl;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            if (dist[i][j] == INF) {
                cout << "INF ";
            } else {
                cout << dist[i][j] << " ";
            }
        }
        cout << endl;
    }

    cout << "Path Matrix:" << endl;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            cout << next[i][j] << " ";
        }
        cout << endl;
    }

    return 0;
}

7.3 矩阵连乘

AiAi+1...AjA_{i}A_{i+1}...A{j}

矩阵 AiA_i 行数 pip_i,列数 qiq_i
后一个矩阵的行数必等于前一个矩阵的列数,矩阵是这样用一维数组存储的:{p1, q1(p2), q2(p3), ...}
例如,{10,30,5,60}\{ 10, 30, 5, 60 \} 表示 A1(1030),A2(305),A3(560)A_1(10*30), A_2(30*5), A_3(5*60) (ps. 本例答案为4500)

7.3.1 普通动态规划

自底向上:通过 迭代 预先计算所有可能的子问题,按子链长度从小到大的顺序填充表格。

m[i][j]m[i][j] 为从 i 到 j 最少乘法次数,那么:

  • ij i \sim j 中任意一个数 kkm[i][k],m[k+1][j]m[i][k], m[k+1][j] 也都是最少的 [最优子结构性质]
  • m[i][i]=0m[i][i]=0 [一个矩阵不用做乘法]
  • i<ji < j 时,m[i][j]=min{m[i][k]+m[k+1][j]+(一次乘法操作)}m[i][j]= min \{ m[i][k] + m[k+1][j] + \text{(一次乘法操作)} \},其中 ik<ji \leq k < j

(一次乘法操作)= piqkqjp_i*q_k*q_j
根据本节开头提到的性质,(一次乘法操作)可以改写为 qi1qkqjq_{i-1}*q_k*q_j
这样改写的好处是,矩阵可以简化为用一维数组保存。

7.3.2 备忘录方法

自顶向下:通过 递归 分解问题,遇到子问题时先查表,若未计算则递归求解并保存结果,按需填充表格。

m[i][j]m[i][j] 为从 i 到 j 最少乘法次数,Memo(i,j)Memo(i, j) 从 i 到 j 最少乘法次数的递归函数,那么:

  • m[i][i]=0m[i][i]=0 [一个矩阵不用做乘法]
  • 其他的 m[i][j]m[i][j] 均初始化为 -1
  • 如果 m[i][j]1m[i][j] \neq -1 说明子问题已经计算过,直接返回
  • i<ji < j 时,m[i][j]=Memo(i,k)+Memo(k+1,j)+(一次乘法操作)m[i][j]= Memo(i, k) + Memo(k+1, j) + \text{(一次乘法操作)}

上述两种方法时间复杂度都是 O(n3)O(n^3),空间复杂度都是 O(n2)O(n^2)

7.4 最长公共子序列LCS

S1=ABCBDAB(len=7)S2=BDCABC(len=6)LCS=BCABS1=ABCBDAB (len=7) \\ S2=BDCABC (len=6) \\ LCS=BCAB

dp[i][j]dp[i][j] 为 S1 前 i 个字符、S2 前 j 个字符的 LCS,则例题所要求的就是 dp[7][6]dp[7][6].

状态如何转移?

  1. dp[i][0]=dp[0][j]=0 dp[i][0] = dp[0][j] = 0,长度为0哪来的子序列
  2. S1[i]==S2[j]S1[i] == S2[j] 匹配上了,皆大欢喜,dp[i][j]=dp[i1][j1]+1dp[i][j] = dp[i-1][j-1] + 1
  3. S1[i]S2[j]S1[i] \neq S2[j] 没匹配上,只能选前面最大的,dp[i][j]=max{dp[i1][j],dp[i][j1]}dp[i][j] = max\{ dp[i-1][j], dp[i][j-1]\}

时间复杂度:O(n2)O(n^2)

7.5** 最优二叉搜索树

p143 例7.7
p159 习题7.13

Optimal Binary Search Tree = OBST 书上讲的啥玩意,看不懂
如何做题:https://www.bilibili.com/video/BV1oTcUexE8T

建议看视频时对照看下面的内容:

pp 矩阵是查找成功的概率, qq 矩阵是查找失败的概率
(w,c,r)(w,c,r) 矩阵:我们目的是,让 c[0][n]c[0][n] 最小

w(i,j)w(i,j): Weight, 表示 aia_iaja_j 的总概率,既包括成功也包括失败
例:w(1,2)w(1,2) 为什么等于 p1+p2+q0+q1+q2p1+p2+q0+q1+q2 请看下图,星号表示查找失败

         a₂(p2)
        /     \
      a₁(p1)   *(q2)
     /    \ 
   *(q0)  *(q1)

对于其他的树形,仍然是这几个概率相加不变

c(i,j)c(i,j): Cost, 表示 aia_iaja_j 内构造OBST的最小平均搜索代价
由最优子结构,我们把从 i 到 j 挨个作为根,看谁作为根的时候,(左子树代价+右子树代价) 最小,此时 c(i,j)c(i,j) 就是最小。
c(i,j)=min{c(i,k1)+c(k,j)}+w(i,j)...其中i<kjc(i,j) = min\{ c(i,k-1) + c(k,j)\} + w(i,j) ...其中 i < k \leq j
联想一下矩阵连乘,递推式几乎就是同一个意思,只是把(一次乘法操作)改成了w(i,j)即aia_iaja_j 的总概率。

r(i,j)r(i,j): Root, 当 aia_iaja_j 代价最小时,把谁作为根?

ps. 《算法导论》(原书第三版) 15.5 最优二叉搜索树
注意算法导论解此问题,i是从1开始的,j是从0开始的。而陈的《算法设计与分析》中i和j都是从0开始。算法导论p230的推导,答案为 e(1,5)e(1,5)。如果想按照陈的书来理解,把i下标整体减1就能看懂了。

7.6 01背包

7.6.1 一般方法

设背包容量6,物品如下:

物品 ii12345
重量 wiw_i32145
价值 viv_i2520154050

dp[i][j]dp[i][j] 表示仅能在前 i 件物品里面选、放入大小为 j 的背包中,得到的最大价值。例题要求的就是 dp[5][6]dp[5][6].

状态如何转移?

  1. dp[i][0]=dp[0][j]=0 dp[i][0] = dp[0][j] = 0,没有物品/背包容量为0,肯定没有价值
  2. j<w[i]j < w[i],背包的容量装不下第 i 件物品,选不了第 i 件物品。 dp[i][j]=dp[i1][j]dp[i][j] = dp[i-1][j]
  3. j>=w[i]j >= w[i],背包的容量能装下第 i 件物品,此时考虑选不选第 i 件。 dp[i][j]=max(选第i,不选第i)dp[i][j] = max(选第i件, 不选第i件)
  4. 若选第 i 件,价值为
    (第 i 件物品的价值) + (前 i-1 件物品、放入去掉第 i 件物品重量的背包、得到的最大价值)
    dp[i1][jw[i]]+v[i]dp[i-1][j-w[i]] + v[i]
  5. 若不选第 i 件,价值就还是 dp[i1][j]dp[i-1][j]

时间复杂度:O(nm)O(nm),其中n表示物品的数量,m表示背包的容量

7.6.2* 阶跃点法

阶跃点(Step Points) 表示在不同背包容量下,能够达到的最优(重量,价值)组合。
每个阶跃点集合 SiS_i 表示处理前 i+1i+1 个物品后的状态。

剔除被支配点:两个阶跃点A(W1, V1)和B(W2, V2),如果W1 ≤ W2且V1 ≥ V2,那么B点被支配,应该剔除。
即,A点花费了更少的容量,得到了更大的价值,必定比B点更优。就不要B点了。

设背包容量6,物品如下:

物品 ii012
重量 wiw_i1.83.22.5
价值 viv_i235

(1) 初始状态 S1S^{-1}(未放入任何物品)

  • 初始状态为空背包,仅包含原点:

    S1={(0,0)}S^{-1} = \{(0, 0)\}

(2) 处理第0个物品(重量1.8,价值2) → 生成 S0S^{0}

  • 不放入物品0:保留 S1S^{-1} 的所有点 {(0,0)}\{(0, 0)\}
  • 放入物品0(记为 S10S^0_1):生成新点 (1.8,2)(1.8, 2)
  • 合并并剔除被支配的点

    • 比较 (0,0)(0, 0)(1.8,2)(1.8, 2),均未被支配。
    • 最终集合:

      S0={(0,0),(1.8,2)}S^{0} = \{(0, 0), (1.8, 2)\}

(3) 处理第1个物品(重量3.2,价值3) → 生成 S1S^{1}

  • 不放入物品1:保留 S0S^{0} 的所有点 {(0,0),(1.8,2)}\{(0, 0), (1.8, 2)\}
  • 放入物品1(记为 S11S^1_1):对 S0S^{0} 中的每个点 (w,v)(w, v),生成新点 (w+3.2,v+3)(w+3.2, v+3)

    • (0+3.2,0+3)=(3.2,3)(0+3.2, 0+3) = (3.2, 3)
    • (1.8+3.2,2+3)=(5.0,5)(1.8+3.2, 2+3) = (5.0, 5)
  • 合并并剔除被支配的点

    • 原集合 S0S^{0} {(0,0),(1.8,2)}\{(0,0), (1.8,2)\} 与新集合 S11S^1_1 {(3.2,3),(5.0,5)}\{(3.2,3), (5.0,5)\} 合并。
    • 剔除被支配的点(例如,(3.2,3)(3.2,3) 未被任何点支配)。
    • 最终集合:

      S1={(0,0),(1.8,2),(3.2,3),(5.0,5)}S^{1} = \{(0, 0), (1.8, 2), (3.2, 3), (5.0, 5)\}

(4) 处理第2个物品(重量2.5,价值5) → 生成 S2S^{2}

  • 不放入物品2:保留 S1S^{1} 的所有点。
  • 放入物品2(记为 S12S^2_1):对 S1S^{1} 中的每个点 (w,v)(w, v),生成新点 (w+2.5,v+5)(w+2.5, v+5)

    • (0+2.5,0+5)=(2.5,5)(0+2.5, 0+5) = (2.5, 5)
    • (1.8+2.5,2+5)=(4.3,7)(1.8+2.5, 2+5) = (4.3, 7)
    • (3.2+2.5,3+5)=(5.7,8)(3.2+2.5, 3+5) = (5.7, 8)
    • (5.0+2.5,5+5)=(7.5,10)(5.0+2.5, 5+5) = (7.5, 10)(超出容量6,舍去)
  • 合并并剔除被支配的点

    • 原集合 S1S^{1} {(0,0),(1.8,2),(3.2,3),(5.0,5)}\{(0,0), (1.8,2), (3.2,3), (5.0,5)\} 与新集合 S12S^2_1 {(2.5,5),(4.3,7),(5.7,8)}\{(2.5,5), (4.3,7), (5.7,8)\} 合并。
    • 剔除被支配的点:(2.5,5)(2.5,5)(3.2,3),(5,5)(3.2,3), (5,5) 更优。
    • (5.7,8)(5.7,8) 是最大价值点。
    • 最终集合:

      S2={(0,0),(1.8,2),(2.5,5),(4.3,7),(5.7,8)}S^{2} = \{(0, 0), (1.8, 2), (2.5, 5), (4.3, 7), (5.7, 8)\}

(5) 逆序求解选了哪些物品

如果上面的步骤看懂了,直接看视频 5:09

本例答案:

(5.7,8)S12还是S1?==>S12x2=1(5.7,8) \in S^2_1 \text{还是} S^1? ==> S^2_1 \therefore x2=1
(3.2,3)S11还是S0?==>S11x1=1(3.2,3) \in S^1_1 \text{还是} S^0? ==> S^1_1 \therefore x1=1
(0,0)S10还是S1?==>S1x0=0(0,0) \in S^0_1 \text{还是} S^{-1}? ==> S^{-1} \therefore x0=0

x=(x0,x1,x2)={0,1,1}x=(x0,x1,x2)= \{ 0,1,1 \}

7.7 (双机)流水作业调度

这和动态规划有什么关系?

nn个作业 (1,2,...,n)(1,2,...,n) 要在由两台机器 M1M1M2M2 组成的流水线上完成加工。
每个作业加工的顺序都是先在 M1M1 上加工,然后在 M2M2 上加工。
M1M1M2M2 加工作业 ii 所需的时间分别为 aia_ibib_i
求最优加工顺序,使得用时最少。

Johnson 算法:

  1. 所有 ai<bia_i \lt b_i 的作业,划归为集合 N1N_1;所有 aibia_i \geq b_i 的作业,划归为集合 N2N_2.
  2. N1N_1 中的作业按照 aia_i 递增排序,N2N_2 中的作业按照 bib_i 递减排序.
  3. N1N_1 中作业接 N2N_2 中作业,构成满足 Johnson 法则的最优调度。
  4. 最优加工顺序:N1N_1 中的作业、N2N_2 中作业
作业 JiJ_i123456
aia_i518534
bib_i722474

N1:{J1,J2,J5}N_1: \{ J_1, J_2, J_5 \} 排序后 {2, 5, 1}
N2:{J3,J4,J6}N_2: \{ J_3, J_4, J_6 \} 排序后 {6, 4, 3}
最优加工顺序: {2, 5, 1, 6, 4, 3}

fin

版权声明

① 本博客所有原创文章,除非另有特别声明,均采用CC BY-NC-SA 4.0/知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行授权。

② 本博客中,在文章内容之外使用的原创图片、动画、音频等多媒体素材的知识产权归属如下:
(i) 由博客作者独立创作的素材,其知识产权由博客作者单独所有;
(ii) 与他人合作创作的素材,其知识产权由博客作者与原创作者共同所有;
(iii) 对于特定素材,如有不同的版权归属,将在相应位置特别注明。
未经原作者或博客作者的明确授权,任何个人或组织不得在其他场合使用、复制、修改或传播这些素材。