Lecture 8: Dynamic Programming¶
约 726 个字 28 行代码 1 张图片 预计阅读时间 3 分钟
动态规划。
Solve sub-problems just once and save the answers in a table.
1 Example: Fibonacci Number¶
使用传统的递归方法:
对此进行性能分析:\(T(N) \ge T(N-1) + T(N-2)\),推导出了 \(T(N) \ge F(N)\)。
问题出在,我们在递归过程中,重复计算了很多次相同的子问题。如果我们能将这些重复的子问题的解放在一张表中(或者就是简单地保存下来),那么我们就可以避免重复计算。
int fib(int n)
{
int f[n + 1];
f[0] = 0;
f[1] = 1;
for (int i = 2; i <= n; i++)
f[i] = f[i - 1] + f[i - 2];
return f[n];
}
显然这种算法的时间复杂度低很多,为 \(O(N)\)。
2 Example: Ordering Matrix Multiplication¶
要找到一种使得开销最小的乘法顺序,我们使用这种动态规划的思想。计算任意一个矩阵链的最小乘法次数,我们可以将其分解为两个子链的最小乘法次数,但是这个分解方式就有很多种,我们需要找到最优的那种。
3 Example: Optimal Binary Search Tree¶
需要找到一种使得平均查询时间最小的树结构,也即需要使
最小。一种很 Greedy 的想法就是把 freq 最大的放在根节点。这种虽然做出来不一定是最优解,但是却给我们以启示:我们根据“谁来当根节点”作为这种遍历的指标。
具体推导可以看 PPT,最后解决问题的方式是自底向上,从只有一个结点的树开始一步一步构造。
4 Example: All-Pairs Shortest Path¶
Floyd 算法(动态规划)。
与前面的例子类似,我们还是维护一个动态规划的数组。
这代表从 i 到 j 的,中间经过 k 个结点的最短路径。很容易知道当我们从 k 为 0 的时候开始遍历,最后 k 到 N-1 的时候一定得到的是最优解。
如何进行“状态转移”呢?当我们已经知道了 \(D^{k-1}\) 的信息之后:
- 如果我们不想将 k 加入最短路径,直接让 \(D^k\) 继承上一个值即可;
- 如果想,那么就是把 i 到 k 的最短路径和 k 到 j 的最短路径加起来就行了。
所以:
给出代码:
void AllPairs(A[][], D[][], int n)
{
int i,j,k;
for(i=0;i<n;i++)
for(j=0;j<n;j++)
D[i][j]=A[i][j];
for(k=0;k<n;k++)
for(i=0;i<n;i++)
for(j=0;j<n;j++)
if(D[i][k]+D[k][j]<D[i][j])
D[i][j]=D[i][k]+D[k][j];
}
\(T(N) = O(N)\).
5 Example: Product Assembly¶
第 i 个 stage 来源于上一个 stage,它可以是由别的生产线转移而来,也可以是自己这条生产线。
6 Summary¶
总结一下。
DP 的特征:
- 最优子结构
- 重叠子问题
设计 DP 方法:
- 描述最优解的样子
- 递归地定义最优值
- 以某一顺序来计算
- 最后重构解法(选)