考研复试 算法设计与分析 笔试笔记

第一~五章 算法基础、时间复杂度、分治法

1.x 算法概述

算法五个特征 详见名词解释

2.1 渐进表示法

大O记号

nn \to \infty 时,f(n)f(n) 不会快于 g(n)g(n)

如果存在正数ccn0n_0,使得当nn0n \geq n_0时,有f(n)cg(n)f(n) \leq cg(n),则称f(n)f(n)O(g(n))O(g(n))的。
ccn0n_0怎么找?系数加一
简单地说,如果f(n)f(n)最高项为一次项(例如3n+23n+2),那么就把常数项放掉,看nn等于几时f(n)f(n)的值开始小于4n4n
如果f(n)f(n)既有一次项又有二次项(例如3n2+2n+13n^2+2n+1),那么首先把常数项放掉,看nn等于几时f(n)f(n)的值开始小于3n2+3n3n^2+3n再把一次项放掉,看nn等于几时f(n)f(n)的值开始小于4n24n^2

p19 例2-2 证明f(n)=10n2+4n+2=O(n2)f(n) = 10n^2 + 4n + 2 = O(n^2)
解:
n2n\geq 2时,有10n2+4n+210n2+5n10n^2 + 4n + 2 \leq 10n^2 + 5n
n5n\geq 5时,有5nn25n \leq n^2
n0=5n_0 = 5c=11c = 11,则当nn0n\geq n_0时,有f(n)11n2f(n) \leq 11n^2,所以f(n)=O(n2)f(n) = O(n^2).

大Omega记号

nn \to \infty 时,f(n)f(n) 不会慢于 g(n)g(n)

如果存在正数ccn0n_0,使得当nn0n \geq n_0时,有f(n)cg(n)f(n) \geq cg(n),则称f(n)f(n)Ω(g(n))\Omega(g(n))的。
ccn0n_0怎么找?最高次系数不动,小的全放掉

p20 例2-6 证明f(n)=10n2+4n+2=Ω(n2)f(n) = 10n^2 + 4n + 2 = \Omega(n^2)
解:
对所有nn,有10n2+4n+210n210n^2 + 4n + 2 \geq 10n^2
n0=1n_0 = 1c=10c = 10,则当nn0n\geq n_0时,有f(n)10n2f(n) \geq 10n^2,所以f(n)=Ω(n2)f(n) = \Omega(n^2).

综合比较两个函数的时间复杂度

基本思路: 如果f(n)f(n)Ω(某个函数)\Omega(\text{某个函数})的,同时g(n)g(n)又是O(某个函数)O(\text{某个函数})的,那么f(n)f(n)就是Ω(g(n))\Omega(g(n))的。

p29 习题2-11(2) f(n)=n2/lognf(n)=n^2/\log ng(n)=nlog2ng(n)=n\log^2 n
解:

  1. n0=2,c=12n_0 = 2, c=\frac{1}{2},对于所有nn0n\geq n_0时,有logn1\log n \geq 1
    因此 f(n)12n2f(n) \geq \frac{1}{2}n^2,所以f(n)=Ω(n2)f(n) = \Omega(n^2).
  2. n0=1,c=1n_0 = 1, c = 1,对于所有nn0n\geq n_0时,有log2nn\log^2 n \leq n
    因此 g(n)nn=n2g(n) \leq n*n = n^2,所以g(n)=O(n2)g(n) = O(n^2).
  3. 由(1)和(2)可得f(n)=Ω(g(n))f(n) = \Omega(g(n))

2.2 迭代法求解时间复杂度

递归时间复杂度求解-迭代法

2.3 主定理求解时间复杂度*

若:

T(n)={cn=1aT(nb)+cnkn>1T(n) = \left \{ \begin{array}{ll} c & n = 1 \\ aT(\frac{n}{b}) + c*n^k & n > 1 \end{array} \right.

则:

T(n)={Θ(nlogba)logba>kΘ(nklogn)logba=kΘ(nk)logba<kT(n) = \left \{ \begin{array}{ll} \Theta(n^{\log_b a}) & \log_b a > k \\ \Theta(n^k \log n) & \log_b a = k \\ \Theta(n^k) & \log_b a < k \end{array} \right.

即,谁大要谁(n的指数部分),相等时再乘一个logn

2.4 给一段代码,求时间复杂度

类似408选择题第一题,中间过程写的言之有理即可 25考研?那408第一题你已经拿下了。408时间复杂度的几个经典考法

2.4.1 单循环

int i=0;
while (i*i*i <= n)
    i++;

T=O(n3)T = O(\sqrt[3]{n})

int i=1;
while (i<=n)
    i = i * 2;

T=O(logn) T = O(\log n)

x=0;
while (n >= (x+1)*(x+1))
    x++;

T=O(n) T = O(\sqrt{n})

i = n*n;
while (i != 1)
    i = i / 2;

T=O(logn)T = O(\log n)

2.4.2 两重循环

int m=0, i, j;
for (i=1; i<=n; i++)
    for (j=1; j<=2*i; j++)
        m++;

T=O(n2)T = O(n^2)

for (i=0; i<n; i++)
    for (j=0; j<m; j++)
        A[i][j] = 0;

T=O(mn)T = O(mn)

count = 0;
for (k=1; k<=n; k*=2)
    for (j=1; j<=n; j++)
        count++;

T=O(nlogn)T = O(n \log n)

for (i=n-1; i>=1; i--)
    for (j=1; j<i; j++)
        if (A[i] > A[j+1])
            swap(A[i], A[j]);

T=O(n2)T = O(n^2)

5.1 基本思想

什么是分治法 详见名词解释

5.2 ~ 5.4 分治法实例

会按照2.1的方式分析时间复杂度,能解释什么是二分搜索、快速排序、合并排序即可。

第六章 贪心

6.2 可分割物品的背包问题

描述:略

按性价比从大到小排序:pi/wip_i/w_i

6.3 带时限的作业排序问题[贪心版]

描述:作业 ii 必须在期限 did_i 之前完成才能获取收益 pip_i,且完成每个作业只需1个单位时间(贪心法的前提)

分支限界版 => 9.3

6.3.1 朴素算法

思路:优先安排收益最高的作业,尽可能将其安排在最晚的可用时间点,从而为其他作业预留更多的早期时间段。

  1. 按收益 pip_i 从大到小排序
  2. 按顺序遍历所有作业
  3. 从其截止时间向前查找可用时间 分配给该作业,若无空闲时间则跳过该作业。
作业j期限d收益p
12100
2110
3215
4127

首先按收益排序:

  • 作业1:(2, 100)
  • 作业4:(1, 27)
  • 作业3:(2, 15)
  • 作业2:(1, 10)

依次处理:

  • 作业1(100):可以安排在时间2
  • 作业4(27):可以安排在时间1
  • 作业3(15):没有可用时间,跳过
  • 作业2(10):没有可用时间,跳过
  • 最终获得的最大收益为:100 + 27 = 127

习题6-3

最优解 5 6 3 2
最大收益 74

6.3.2* 改进算法

待补充

6.4 最佳合并模式

6.4.1 最佳两路合并模式(哈夫曼编码)

就是哈夫曼树求最小 WPL(带权外路径长度)

(20,30,30,10,5)

WPL = (5+10)*3 + (20+30+30)*2 = 205

6.4.2* 三(k)路合并 书p105/习题6-8

https://www.bilibili.com/video/BV1tucQeQECb

补:虚节点,数量为 (k1)(n1)%(k1)(k-1) - (n-1) \% (k-1)
(3, 7, 8, 9, 15, 16, 18, 20, 23, 25, 28) => n=11,k=3, (n-1)%(k-1)=0,不用补;
(1, 2, 3, 3, 5, 6, 7 ,9) => n=8,k=3,(n-1)%(k-1)=1,补1个虚节点;WPL=66

6.5 最小代价生成树(最小生成树)

对于图 G=(V,E)G=(V,E)

6.5.1 Prim

适用于稠密图 每次从已有点集合里扩展出一条最短的边

  • 使用邻接矩阵时,时间复杂度 O(V2)O(V^2)
  • 使用邻接表 和优先队列 时,时间复杂度 O(ElogV)O(E*logV)

6.5.2 Kruskal

适用于稀疏图 对边从小到大排序,依次加入边,每次加入都需要判环路

  • 时间复杂度 O(ElogE)O(E*logE)

习题6-9

明显是稠密图,选 Prim

6.6 单源最短路径

Dijkstra算法 边权非负

手写步骤: 又快又准做对考研真题,从考试的角度出发【Dijkstra】【单源最短路径】https://www.bilibili.com/video/BV1nh411Y7vj/

6.7 磁带最优存储

6.7.1 单带最优存储

说人话:
nn 个程序(线段)按某种顺序排列在一个磁带(数轴)上,程序不能重叠,保证磁带能放得下所有程序,要求平均检索时间(MRT)最小。 MRT = 所有程序的检索时间之和 ÷\div 程序个数
由于程序个数固定,所以 MRT 最小等价于检索时间之和最小。
检索时间 = 程序起点到磁带起点距离 ++ 程序长度
每次检索都从磁带起点开始,所以离起点越远的程序检索时间越长。
各种排列方式参考:书p117 表6-5

贪心策略:按程序长度从小到大排序,依次放入磁带。

6.7.2 多带最优存储

现在磁带变成了 mm 个,其实也很简单:将程序00放在第一个磁带上,程序11放在第二个磁带上,程序22放在第三个磁带上…当 mm 个磁带都排完一遍,回到第一个磁带,继续顺次放程序。

#include <iostream.h>
void Store(int n, int m)     
{
     int j = 0; 
     for (int i=0; i<n; i++) { 
        cout << "将程序"<<i<<" 加到磁带" <<j<<endl; 
        j=(j+1)% m; 
     } 
     cout<<endl;
}

EX1: 最佳装载问题(习题6-17)

nn 个集装箱和一艘载重为 CC 的船。每个集装箱的重量为wiw_i
求装载方案,使得装载的集装箱数量最大化,同时集装箱的总重量不超过船的载重限制 CC

贪心策略:每次都选择最轻的集装箱

(5) 最优方案:装 [2,3,4,5,6]

EX2: 活动安排问题(线段覆盖)

一个数轴上有n条线段,现要选取其中k条线段,使得这些线段两两没有重合部分,问最大的k为多少。线段的起点为活动的开始时间,终点为活动的结束时间。

贪心策略:每次选择结束时间最早的活动,以留下最多的时间给后续活动。

例:设待安排的11个活动的开始时间和结束时间按结束时间的非递减序排列如下:

ii1234567891011
s[i]s[i]130535688212
t[i]t[i]4567891011121314

贪心算法步骤

  1. 首先将活动按结束时间从小到大排列。
  2. 选择第一个活动。
  3. 之后遍历剩下活动,若[活动B]的 开始时间 晚于 当前选择的[活动A]的结束时间,则选择[活动B]。

答案 1,4,8,11{1,4,8,11}

EX3: 多机调度问题

注意不要与动态规划的流水作业调度问题混淆。

作业数 nn,机器数 mm,如何分配作业让完成时间最短。

贪心策略:按 当前最短完成时间 分配作业到 最空闲的机器优先分配处理时间较长的任务 以填满机器。

  • nmn \leq m 时,直接为每个作业分配一台机器完成作业
  • n>mn > m 时,首先将作业按其执行时间从大到小排序,然后依次将作业分配给当前最空闲的机器

例:7个独立作业 {1,2,3,4,5,6,7}\{1,2,3,4,5,6,7\} 由3台机器 M1,M2,M3M1, M2, M3 加工处理。

作业编号1234567
处理时间214416653

答案 M1{4}M1 \{4\} M2{2,7}M2 \{2,7\} M3{5,6,3,1}M3 \{5,6,3,1\} 最优完成时间:17

第七章 动态规划

参考资料:动态规划法 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) ...\text{其中} 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(\text{选第i件}, \text{不选第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* 阶跃点法

https://www.bilibili.com/video/BV1ZNcUeoEuk

阶跃点(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}


学习笔记