动态规划难度相对较大。它的基础是最优原理。有很多问题,用贪婪法或者分而治之无法简洁高效的解决,但是动态规划就可以。
在动态规划中,我们要考察一系列抉择,以确定一个最优抉择序列是否包含最优抉择子序列。当最优抉择包含最优抉择子序列时,可建立动态规划递归方程以帮助我们高效解决问题。
动态规划常常基于一个递推式或一个或多个初始状态。当前子问题的解将由上一个子问题的解推出。一般是多项式复杂度。
1.0/1背包问题
有n个物品和一个容量为c的背包,从n个物品中选取装包的物品。物品i的重量为wi,价值为pi。一个可行的背包装载是指,背包的物品总重量不超过背包的容量。一个最佳的背包装载是物品总价值最高的可行的背包装载。
问题的公式描述是 maxΣpixi ; 约束条件是 Σwixi <= c且 xi∈{0,1}.
1)递归求解,F(1,c)返回f(1,c)的值。
int F(int i, int y)
{//Return f(i,y).
if(i == n) return (y < w[n]) ? 0 : p[n];
if(y < w[i]) return F(i+1,y);
return max(F(i+1,y),F(i+1,y-w[i])+p[i]);
}
2)迭代计算 f 和 x template<class T>
void Knapsack(T p[], int w[], int c, int n, T** f){
//Compute f[i][y] for all i and y.
//initialize f[n][]
for(int y = 0; y < w[n]; y++)
f[n][y] = 0;
for(int y = w[n]; y <= c; y++)
f[n][y] = p[n];
//compute remaining f's
for(int i = n - 1; i > 1; i--){
for(int y = 0; y < w[i]; y++)
f[i][y] = f[i+1][y];
for (int y = w[i]; y <= c; y++)
f[i][y] = max(f[i+1][y],f[i+1][y-w[i]] + p[i]);
}
f[1][c] = f[2][c];
if(c >= w[1])
f[1][c] = max(f[1][c], f[2][c-w[1]] + p[1]);
}
template<class T>
void Traceback(T **f, int w[], int c, int n, int x[])
{//Compute x for optimal filling.
for(int i = 1; i < n; i++)
if(f[i][c] == f[i+1][c]) x[i] = 0;
else {
x[i] = 1;
c -= w[i];
}
x[n] = (f[n][c]) ? 1 : 0;
}
2.所有顶点对之间的最短路径
i)问题描述
在有向图中,寻找每一对顶点之间的最短路径。即对于每对顶点( i , j ),要寻找从顶点i到顶点j以及从顶点j到顶点i的最短路径。
在无向图中,这两条路径等于一条。
当边上的权都不是负值的时候,所有顶点对之间的最短路径可以用Dijkstra单元算法n次来求解,每一次都选择n个顶点中的一个顶点作为源点。这个过程的时间复杂为O(n^3).
我们这里用动态规划算法——Floy算法,来求解,时间复杂度为Θ(n^3).Floy算法即使在边长为负值时也适用(不过环路的带权长度不能是负值)。与Floy算法相关的常数因子比Dijkstra算法的要小。因此Floy算法比最坏情况下的Dijkstra算法用时要少。
ii)动态规划公式
假定图可以有权值为负的边,但不能有带权长度为负值的环路。因此,每一对顶点(i,j)总有一条不含环路的最短路径。
图G 有n个顶点,1~n。c(i,j,k)表示从顶点i到顶点j的一条最短路径的长度,其中间顶点的编号不都大于k。
对任意k>0,如何确定c(i,j,k)呢? 从i到j,中间顶点不超过k的最短路径有两种可能:该路径包含或不包含中间顶点k。若不包含,则该路径长度应为c(i,j,k-1),否则长度为c(i,k,k-1)+c(k,j,k-1). c(i,j,k)可取两者中的较小值。由此可得如下递归式:
c(i,j,k)=min{c(i,j,k-1), c(i,k,k-1)+c(k,j,k-1)} , k>0
以上递归公式将一个k级运算转化为多个 k-1 级运算,而多个 k-1 级运算应比一个k级运算简单。
1)递归求解时间复杂度极大。(Θ(n23n))
2)迭代求解:
通过多次计算c(i,j,k-1)的值,可以得到c(i,j,n)的值,这是一种非常高效的方法。如果c(i,j,k)的值不用重复计算,那么c值的计算用时可以减少到Θ(n3).
伪代码:
Initial shortest-paths algorithm
//Find the lengths of the shortest paths.
//initialize c(i,j,0)
for(int i = 1; i <= n; i++)
for (int j = i; j <= n; j++)
c(i,j,0) = a(i,j); //a is the cost-agjacency matrix
//compute c(i,j,k) for 0 < k <= n
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(c(i,k,k-1)+c(k,j,k-1)<c(i,j,k-1))
c(i,j,k)=c(i,k,k-1)+c(k,j,k-1);
else c(i,j,k)=c(i,j,k-1);
注意到对于任意 i,c(i,k,k)=c(i,k,k-1)且c(k,i,k)=c(k,i,k-1),因而,若用c(i,j)代替上述伪代码中的c(i,j,*),c(i,j)的最后值将等于c(i,j,n)的值。
代码实现:
allPairs函数在c中返回最短路径的长度, 还计算kay[i][j],它是从i到j的最短路径中最大的k值。kay值可以用来构建从一个顶点到另一个顶点的最短路径(见函数outputPath)。程序的时间复杂度为Θ(n3).程序用时O(n)来输出一条最短路径。
template<class T>
void AdjacencyWDigraph<T>::AllPairs(T **c, int **kay)
{//All pairs shortest paths.
//Compute c[i][j] and kay[i][j] for all i and j.
//initialize c[i][j] = c(i,j,0)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++){
c[i][j] = a[i][j];
kay[i][j] = 0;
}
for(int i = 1; j <= n; i++)
c[i][i] = 0;
//compute c[i][j] = c(i,j,k)
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++){
T t1 = c[i][k];
T t2 = c[k][j];
T t3 = c[i][j];
if (t1 != NoEdge && t2 != NoEdge &&(t3 == NoEdge || t1 + t2 < t3)){
c[i][j] = t1 + t2;
kay[i][j] = k;
}
}
}
//Output a shortest path
void outputPath(int **kay, int i, int j){//Actual
code to output i to j path. if(i == j) return; if(kay[i][j] == 0) cout << j <<' '; else{ outputPath(kay, i, kay[i][j]); outputPath(kay, kay[i][j], j); } }template<class T>void OutputPath(T **c, int **kay, T NoEdge, int i, int j){//OutputPath shortest path
from i to j if(c[i][j] == NoEdge){ cout <<"There is no path from " << i << "to"<< j << endl; return; } cout <<"The path is" << endl; cout << i <<' '; outputPath(kay,i,j); cout << endl;}