动态规划算法1. 动态规划算法介绍基本思想是将待求解问题分解成若干子问题,先求解子问题,最后用这些子问题带到原问题,与分治算法的不同是,经分解得到的子问题往往是不是相互独立,若用分治则子问题太多。
2. 适用动态规划算法问题的特征(1)最优子结构设计动态规划算法的第一步骤通常是要刻画最优解的结构。
当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。
问题的最优子结构性质提供了该问题可用动态规划算法求解的重要线索。
在动态规划算法中,问题的最优子结构性质使我们能够以自底向下的方式递归地从子问题的最优解逐步构造出整个问题的最优解。
同时,它也使我们能在相对小的子问题空间中考虑问题。
(2)重叠子问题可用动态规划算法求解的问题应具备的另一基本要素是子问题的重叠性质。
在用递归算法自顶向下解此问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。
动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只解一次,而后将其解保存在一个表格中,当再次需要解此子问题时,只有简单地用常数时间查看一下结果。
通常,不同的子问题个数随输入问题的大小呈多项式增长。
因此,用动态规划算法通常只需要多项式时间,从而获得较高的解题效率。
(3)备忘录方法动态规划算法的一个变形是备忘录方法。
备忘录方法也是一个表格来保存已解决的子问题的答案,在下次需要解此子问题时,只要简单地查看该子问题的解答,而不必重新计算。
与动态规划算法不同的是,备忘录方法的递归方式是自顶向下的,而动态规划算法则是自底向上递归的。
因此,备忘录方法的控制结构与直接递归方法的控制结构相同,区别在于备忘录方法为每个解过的子问题建立了备忘录以备需要时查看,避免了相同子问题的重复求解。
备忘录方法为每个子问题建立一个记录项,初始化时,该记录项存入一个特殊的值,表示该子问题尚未求解。
在求解过程中,对每个待求的子问题,首先查看其相应的记录项。
若记录项中存储的是初始化时存入的特殊值,则表示该子问题是第一次遇到,则此时计算出该子问题的解,并保存在其相应的记录项中。
若记录项中存储的已不是初始化时存入的特殊值,则表示该子问题已被计算过,其相应的记录项中存储的是该子问题的解答。
此时,只要从记录项中取出该子问题的解答即可。
3. 基本步骤a 、找出最优解的性质,并刻画其结构特征。
b 、递归地定义最优值。
c 、以自底向上的方式计算出最优值。
d 、根据计算最优值时得到的信息构造一个最优解。
(可省) 例1-1 [0/1背包问题] [问题描述]用贪心算法不能保证求出最优解。
在0/1背包问题中,需要对容量为c 的背包进行装载。
从n 个物品中选取装入背包的物品,每件物品i 的重量为iw ,价值为iv 。
对于可行的背包装载,背包中物品的总重量不能超过背包的容量,最佳装载是指所装入的物品价值最高,即∑=ni iix v1取得最大值。
约束条件为cx wni i i≤∑=1,{}()n i x i ≤≤∈11,0。
在这个表达式中,需要求i x的值。
i x=1表示物品i装入背包中,i x=0表示物品i不装入背包。
1.找出最优解性质考察上述0 / 1背包问题。
如前所述,在该问题中需要决定x1,…,xn的值。
假设按i = 1,2,…,n 的次序来确定xi 的值。
如果置x1 = 0,则问题转变为相对于其余物品(即物品2,3,…,n),背包容量仍为c 的背包问题。
若置x1 = 1,问题就变为关于最大背包容量为c-w1 的问题。
现设r∈{c,c-w1 } 为剩余的背包容量。
在第一次决策之后,剩下的问题便是考虑背包容量为r 时的决策。
不管x1 是0或是1,[x2 ,…,xn ] 必须是第一次决策之后的一个最优方案,如果不是,则会有一个更好的方案[y2,…,yn ],因而[x1,y2,…,yn ]是一个更好的方案。
因此最优解符合条件,cxwywxwniiiniii≤+≤∑∑==2111。
例:假设n=3, w=[100,14,10], p=[20,18,15], c= 116。
若设x1 = 1,则在本次决策之后,可用的背包容量为r= 116-100=16 。
[x2,x3 ]=[0,1] 符合容量限制的条件,所得值为1 5,但因为[x2,x3 ]= [1,0] 同样符合容量条件且所得值为1 8,因此[x2,x3 ] = [ 0,1] 并非最优策略。
即x= [ 1,0,1] 可改进为x= [ 1,1,0 ]。
若设x1 = 0,则对于剩下的两种物品而言,容量限制条件为11 6。
总之,如果子问题的结果[x2,x3 ]不是剩余情况下的一个最优解,则[x1,x2,x3 ]也不会是总体的最优解。
2.递归定义最优解在上述例题的0 / 1背包问题中,最优决策序列由最优决策子序列组成。
假设m(i,v) 表示例子中剩余容量为j,剩余物品为i,i+1,…,n 时的最优解的值,即:nn nw j w j v j n m ≤≤≥⎩⎨⎧=00),( (1)和{}ii i i w j w j j i m v w j i m j i m j i m ≤≥⎩⎨⎧++-++=0),1(),1(),,1(max ),((2)因此,m (1,c )是初始时最优问题的最优解。
现在计算xi 值,步骤如下:若m(1 ,c) =m( 2 ,c),则x1 = 0,否则x1 = 1。
接下来需从剩余容量c-w1中寻求最优解,用m(2, c-w1) 表示最优解。
依此类推,可得到所有的xi (i= 1,2,…,n) 值。
[0/1背包问题的C 语言实现算法]#define N 12void Knapsack(int v[],int w[],int c,int n,int m[6][N]) {int i,j,jMax,k;jMax=(w[n]-1<c?w[n]-1:c); for(i=0;i<=jMax;i++) m[n][i]=0;for(i=w[n];i<=c;i++) m[n][i]=v[n];for(i=n-1;i>1;i--){jMax=(w[i]-1<c?w[i]-1:c); for(j=0;j<=jMax;j++) m[i][j]=m[i+1][j]; for(j=w[i];j<=c;j++){k=j-w[i];if(m[i+1][j]<m[i+1][k]+v[i]) m[i][j]=m[i+1][k]+v[i]; elsem[i][j]=m[i+1][j]; } }m[1][c]=m[2][c]; if(c>=w[1]) {k=c-w[1];m[1][c]=(m[2][c]>m[2][k]+v[1])?m[2][c]:m[2][k]+v[1]; }}void Traceback(int m[6][N],int w[],int c,int n,int x[]){int i;for(i=1;i<N;i++){if(m[i][c]==m[i+1][c])x[i]=0;else{x[i]=1;c-=w[i];}}x[n]=(m[n][c])?1:0;}main(){int i,c=10,n=5,w[]={0,2,2,6,5,4},v[]={0,6,3,5,4,6}; int m[6][N]={0};int x[6]={0};int j;Knapsack(v,w,c,n,m);for(i=1;i<=n;i++){for(j=1;j<=c;j++)printf("%3d",m[i][j]);printf("\n");}Traceback(m,w,c,n,x);for(i=1;i<=n;i++){if(x[i])printf("%4d:%4d",i,v[i]);}printf("\n");}例2-1 [最短路径问题][问题描述]现有一张地图,各结点代表城市,两结点间连线代表道路,线上数字表示城市间的距离。
如图2所示,试找出从结点1到结点7的最短路径。
① ② ⑤③ ④ ⑦⑥图2 有向图1. 找出最优解性质在第一次决策到达了某个节点i v之后,不管iv 怎样确定,剩下的问题就是选择从iv 到jv 的最短路径。
在所有点对最短路径问题( a l l - p a i r sshorest-paths problem )中,要寻找有向图G 中每对顶点之间的最短路径。
也就是说,对于每对顶点(i, j),需要寻找从i 到j 的最短路径及从j 到i 的最短路径。
因此对于一个n 个顶点的图来说,需寻找p =n(n-1) 条最短路径。
假定图G 中不含有长度为负数的环路,只有在这种假设下才可保证G 中每对顶点(i, j) 之间总有一条不含环路的最短路径。
2. 递归定义最优值上述问题已经转化为寻找从i 到j 的最短路径。
若记最短路径的长度为ee[j].逊县将其初始值赋值为 。
设k 为从i 到j 中节点编号中最大的数,则用C (i ,j ,k )表示经过节点k 的<i ,j>通路的路径长度。
若C (i ,j ,k )< ee[j],则ee[j] =C (i ,j ,k )。
69 9454 5865 78如此寻遍<i,j>中每条通路,则得到的ee[j]为<i,j>的最短路径长度。
最后递归得到从节点1到节点7的最短路径长度。
[最短路径问题的C语言算法实现]#include <stdio.h>#define N 7 /* 顶点数目 */ #define I 999 /* 表示无穷大 */int graph[N][N] = { /* 图的邻接矩阵 */ {I, 4, 5, 8, I, I, I},{I, I, I, 6,6, I, I},{I, I, I, 5, I, 7, I},{I, I, I, I, 8, 9, 9},{I, I, I, I, I, I, 5},{I, I, I, I, I, I, 4},{I, I, I, I, I, I, I}};int List[N]; /* 存放拓扑序列 */ int TopologicalOrder(); /* 拓扑排序函数 */void main() /* 主函数 */ { int i, j, k, l;int ee[N]; /* 最短距离 */int path_e[N][N], n_e[N]; /* 记录路径数据 *//* 初始化数据 */for (i = 0; i < N; i++) {n_e[i] = 0; /* 到 i 的最短路线的结点数 */ee[i] = I; }ee[0] = 0; /* 初始化头结点 */path_e[0][0] = 0;n_e[0] = 1;/* 拓扑排序 */if (!TopologicalOrder())return;/* 对于拓扑序列,运用动态规划步步算出最短路线 */for (i = 0; i < N; i++) {k = List[i]; /* 提取拓扑序列的元素 */ for (j = 0; j < N; j++) /* 更新它所指向顶点的所有数据 */{ if (graph[k][j] != I) /* 寻找指向的顶点 */if (graph[k][j] + ee[k] < ee[j]) /* 如果新路径更短 */{ee[j] = graph[k][j] + ee[k]; /* 更新最短路径长度 */ for (l = 0; l < n_e[k]; l++) /* 更新最短路线 */ { path_e[j][l] = path_e[k][l];}path_e[j][l] = j;n_e[j] = l + 1;}}}}/* 输出结果到屏幕 */for (i = 0; i < N; i++) {printf("shortest(%d): %2d Path: ", i + 1, ee[i]);for (j = 0; j < n_e[i]; j++) {printf("%d ", path_e[i][j] + 1);}printf("\n");}}int TopologicalOrder(){int i, j, top, count;int indegree[N], Stack[N];top = 0; /* 栈顶标志 */for (i = 0; i < N; i++) {indegree[i] = 0; /* 初始化入度 */for (j = 0; j < N; j++) {if (graph[j][i] != I) { /* 如连通 */indegree[i]++; /* 入度自增1 */}}if (!indegree[i]){ /* 如入度为零 */Stack[top++] = i; /* 入栈 */ }}count = 0;while (top != 0) /* 输出顶点数 */{ i = Stack[--top];List[count++] = i;for (j = 0; j < N; j++) {if (graph[i][j] != I) { /* 如连通 */if (!(--indegree[j])) { /* 而且入度为零 */Stack[top++] = j; /* 入栈 */}}}/* for */}/* while */return (count < N) ? 0 : 1;}例3 [航费问题]某航线价格表为:从亚特兰大到纽约或芝加哥,或从洛杉矶到亚特兰大的费用为$ 100;从芝加哥到纽约票价$20;而对于路经亚特兰大的旅客,从亚特兰大到芝加哥的费用仅为$20。