基本思想
动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题.但是经分解得到的子问题往往不是互相独立的。
不同子问题的数目常常只有多项式量级。在用分治法求解时,有些子问题被重复计算了许多次。
如果能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,就可以避免大量重复计算,从而得到多项式时间算法。
基本思想: 将待求解的问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
基本要素: 最优子结构性质和重叠子问题性质
与分治法的区别:
分治算法是把原问题分解为若干个子问题,自顶向下求解子问题,合并子问题的解,从而得到原问题的解。动态规划也是把原始问题分解为若干个子问题,然后自底向上,先求解最小的子问题,把结果存在表格中,在求解大的子问题时,直接从表格中查询小的子问题的解,避免重复计算,从而提高算法效率。
矩阵连乘
问题:给定n个矩阵{A1,A2,…,An},其中,Ai与Ai+1是可乘的,(i=1,2 ,…,n-1)。用加括号的方法表示矩阵连乘的次序,不同的计算次序计算量(乘法次数)是不同的,找出一种加括号的方法,使得矩阵连乘的次数最小
解决思路:
A[i:j] 表示矩阵 i 到 j 的连乘
假设在第 k 位置上找到最优解,则问题变成了两个子问题:A[i:k] , A[k+1,j]
用 m[i][j] 表示矩阵连乘的最优值,那么两个子问题对应的最优值变成 m[i][k], m[k+1][j],且原问题的最优值为 m[1][n]。
- i=j,m[i][j] = 0
- i<j,m[i][j] = min{m[i][k] + m[k+1][j] + pi-1 * pk * pj}(i<=k<j,加上合并的计算量)
伪代码
public static void matrixChain(int[] p,int[][] m,int[][] s){
int n = p.length-1;
for(int i=1;i<=n;i++)
m[i][i] = 0;
for(int r=2;r<=n;r++){//矩阵连乘的规模为r
for(int i=1;i<=n;i++){
int j=i+r-1;
m[i][j] = m[i+1][j] + p[i-1]*p[i]p[j];
for(int k=i+1;k<j;k++){
int t = m[i][k]+m[k+1][j] + p[i-1]*p[i]*p[j];
if(t<m[i][j]){
m[i][j] = t;
s[i][j] = k;
}
}
}
}
}
最长公共子序列
问题:字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序列<i0,i1,…,ik-1>,使得对所有的j=0,1,…,k-1,有xij=yj。例如,X=“ABCBDAB”,Y=“BCDB”是X的一个子序列。
解决思路:
对于 X={x1,x2,x3,…,xm},Y = {y1,y2,y3,…,yn}
- xm=yn,则 zk = xm = yn -> Zk-1 是 Xm-1 和 Yn-1 的最长公共子序列
- xm =/= yn,则 Z 是 Xm-1 和 Y ,X 和 Yn-1 中较长的子序列。
由上面的分析可以得到如下递归关系,c[i][j] 表示 Xi 和 Yi 的最长公共子序列长度
结果如下:
伪代码:
public static int lcsLength(char[] x,char[] y,int[][] b){//b用来记录路径
int m=x.length-1'
int n = y.length-1;
int[][] c = new int[m+1][n+1];
for(int i=1;i<=m;i++){
c[i][0]=0;
}
for(int i=1;i<=n;i++){
c[0][i]=0;
}
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(x[i]==y[j]){
c[i][j] = c[i-1][j-1]+1;
b[i][j] = 1;
}else if(c[i-1][j]>=c[i][j-1]){
c[i][j] = c[i-1][j];
b[i][j] = 2;
}else{
c[i][j] = c[i][j-1];
b[i][j] = 3;
}
}
return c[m][n];
}
}
凸多边形三角剖分
问题:给定凸多边形P,以及定义在由多边形的边和弦组成的三角形上的权函数w。要求确定该凸多边形的三角剖分,使得该三角剖分中诸三角形上权之和为最小。
解决思路:
和矩阵连乘问题是类似的,若凸(n+1)边形 P = {v0,v1,v2,…,vn} 的最优三角剖分 T 包含三角形 v0vkvn(1<=k<=n-1),则 T 的权分为三个部分权的和:三角形v0vkvn的权、子多边形{v0,v1,…,vk} 和{vk+1,…,vn}。设 t[i][j] 表示凸多边形{vi-1,vi,…,vj}的最优三角剖分所对应的权和,则原问题的最优权值为t[1][n]。
- i=j,t[i][j] = 0
- i<j,t[i][j] = min{t[i][k]+t[k+1][j]+w(vi-1vkvj)}(i<=k<j)
伪代码
和上面的矩阵连乘一样,这里就不写了。
电路布线
问题:确定导线集Nets = {i,π(i),1 ≤ i ≤ n}的最大不相交子集
解决思路:
记N(i,j) = {t|(t, π(t)) ∈ Nets,t ≤ i, π(t) ≤ j }. N(i,j)的最大不相交子集为MNS(i,j)Size(i,j)=|MNS(i,j)|,即最大不相交子集的个数。
- 当 i=1 时
- 当 i>1 时
伪代码
public static void mnset(int[] c,int[][] size){
int n=c.length-1;
for(int j=0;j<c[1];j++){
size[1][j] = 0;
}
for(int j=c[1];j<=n;j++){
size[1][j] = 1;
}
for(int i=2;i<n;i++){
for(int j=0;j<c[i];j++){
size[][] = size[i-1][j];
}
for(int j=c[i];j<=n;j++){
size[][] = Math.max(size[i-1][j],size[i-1][c[i]-1]+1);
}
}
size[n][m] = Math.max(size[n-1][n],size[n-1][c[n]-1]+1);
}
流水作业调度
问题:n个作业 N={1,2,…,n}要在2台机器M1和M2组成的流水线上完成加工。每个作业须先在M1上加工,然后在M2上加工。M1和M2加工作业 i 所需的时间分别为 ai 和bi,每台机器同一时间最多只能执行一个作业。
流水作业调度问题要求确定这n个作业的最优加工顺序,使得所有作业在两台机器上都加工完成所需最少时间
解决思路:
- N = {1,2,3,…,n} 表示 n 个作业,ai 和 bi 表示分别在 M1 和 M2 上加工所需要的时间
T(S,t) 表示 M1 在加工 S 中的作业时,M2 在加工别的作业还需等待 t 才可利用,原问题的最优解可以表示为 T(N,0),且 T(N,0) = {ai + T(N-{i},bi) } -> T(S,t) = {ai + T(S-{i},bi + max{t-ai,0}) }(bi + max{t,ai}-ai = bi+max{t-ai,0})
由上面的递归式就可以设计出我们的动态规划方法,但是还可以使用 johnson 法则进行简化:
- Johnson 法则:对于流水作业调度问题,必定存在一个最优调度π, 使得任意 i < j 满足Johnson不等式 min{bπ(i), aπ(j)} >= min{bπ(j), aπ(i)},所以流水作业调度问题的Johnson算法:
(1) 令N1 = { i | ai < bi}, N2 = { i | ai >= bi };
(2) 将N1中作业依ai的非减序排序;将N2中作业依bi的非增序排序;
(3) N1中作业接N2中作业构成满足Johnson法则的最优调度。
代码:
public static int flowShop(int a[],int b[],int c[]){//c为最优调度顺序
int n=a.length-1;
Element d[]=new Element[n];
for(int i=0;i<n;i++){
int key=a[i]>b[i]? b[i]:a[i];//找作业在两台机器上处理时间最小的那个作业处理时间
boolean job = a[i]<=b[i];
d[i]=new Element(key,i,job);
}
Arrays.sort(d);//将所有作业的key进行从小到大排序
int j=0,int k = n-1;
//将作业按照Johnson法则排序放入c中
for(int i=0;i<n;i++){
if(d[i].job) c[j++]=d[i].index;//如果ai<=bi,将其作业序号放入c数组中(从头开始放)
else c[k--]=d[i].index;//否则
}
//真正的动态规划部分!
j=a[c[0]];//第一个作业在M1上的处理时间
k=j+b[c[0]];//第一个作业处理完所需时间
for(int i=1;i<n;i++){
j+=a[c[i]];//第i个作业在机器上加工完成所需时间
k=j<k? k+b[c[i]]:j+b[c[i]];
/*如果此作业在M1上加工完成时间
(包含前面作业在M1上的所用时间和)小于上一个作业全部完成时间(还得等M2做完),则此作业所需时间
为k+b[c[i]],否则为j+b[c[i]]*/
}
return k;
}
public static class Element{
public int key;
public int index;
public boolean job;
private Element(int kk,int ii,boolean jj){
key=kk;
index=ii;
job=jj;
}
}
01-背包
问题:给定 n 种物品和一背包,物品 i 的重量是 wi,其价值为 vi,背包的容量为 C,问应该如何选择装入背包的物品,使得装入背包中物品的总价值最大?
解决思路:
- 常规解法:用 m(i,j) 表示背包容量为 j,可选择物品为 i,i+1,…,n 的最优值,则原问题的解为m(1,C)。
- 跳跃点解法:对于每一个确定的 i 值,都有一个对应的跳跃点集Pi={ ( j, m(i,j) ),……}
(1) 开始求解时,先求Pi,初始时Pn+1={(0,0)},i=n+1,由此按下列步骤计算Pi-1,Pi-2……P1,即Pn,Pn-1,……P1
(2) Qi+1 = pi+1 ⊕ (wi,vi) = {(j+wi,m(i,j)+vi) | (j,m(i,j))∈pi+1}
(3) Pi-1 = Pi∪Qi 然后再去掉受控跳跃点后的点集。
受控跳跃点: 若点(a,b),(c,d)∈Pi∪Qi,且a<=c,b>d,则(c,d)受控于(a,b),所以(c,d)∉Pi-1。
由此计算得出Pn,Pn-1,……,P1。求得P1的最后那个跳跃点即为所求的最优值m(1,C)。
代码: 待补充