前言
点击查看算法介绍
五大算法
WX搜素"Java长征记"对这些算法也有详细介绍。
动态规划
一、算法概述
动态规划即就是通过将一个问题拆分为多阶段决策过程的若干子问题.每步求解的
问题是后面阶段求解问题的子问题. 每步决策将依赖于以前步骤的决策结果.动态规划
的本质是问题状态之间的关系,它的求解过程是一个多阶段决策过程。
多阶段决策
经常会有一些问题根据其时间或空间划分为若干个联系的阶段,针对于每个阶段,
我们都要选择方案去解决,称其为决策。每一阶段的决策后往往会影响下一个阶段的
决策,通常也会影响整个解决过程,对于每一阶段确定好的决策构成一个序列,称其
为策略。因为每个阶段有好多决策,所以也就构成了一个策略集合。不同的策略往往
会有不同的结果,所以大家都关注最好的那个结果,称之为最优策略,这类问题即成
为多阶段决策问题。
二、动态规划的设计思想
- 划分子问题,确定子问题边界,将问题求解转变成多步判断的过程.
- 定义优化函数,以该函数极大(或极小) 值作为依据,确定是否满足优化原则.
- 列优化函数的递推方程(子问题间的依赖关系)和边界条件
- 自底向上计算,设计备忘录 (表格)
- 考虑是否需要设立标记函数
- 时间复杂度估计
【其实动态规划和分治算法的类似,但最大区别就是动态规划的子问题(阶段)相互依赖,最后一个阶段的解也就是原问题的结果】
注意:
动态规划子问题要满足优化原则或最优子结构性质,即:一个最优决策序列的任何子序列本身一定是相对于子序列的初始和结束状态的最优决策序列
三、适用条件
- 满足优化原则或最优子结构性质
- 无后效性:某阶段状态一旦确定就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响曾经的状态,仅与当前状态有关;
- 有重叠子问题:子问题之间不独立的,一个子问题在下一阶段决策中可能被多次使用到
四动态规划递归与迭代实现的对比
【看下面这个例子理解起来更容易】
矩阵链相乘
●问题介绍
设 A1, A2 , … , An 为矩阵序列,Ai 为 Pi-1 * Pi 阶矩阵,i = 1, 2, … , n. 试确定矩阵的乘法顺序,使得元素相乘的总次数最少.
输入:向量 P = < P0, P1, … , Pn >,其中 P0, P1, …, Pn为 n 个矩阵的行数与列数(第一个矩阵即为P0行P1列,第二个为P1行P2列,依次类推…)
输出:矩阵链乘法加括号的位置.
【Aij * Bjk=Cik,每个元素要乘j次,总计乘 i * j * k 次】
问题分析及动态规划
- 子问题划分:Ai … j :矩阵链 Ai Ai+1 … Aj,边界i, j
- 输入向量:< Pi-1, Pi, … , Pj >其最好划分的运算次数:m[i, j]
- 子问题的依赖关系最优划分最后一次相乘发生在矩阵k 的位置,即 Ai …j = Ai …k Ak+1…j Ai…j最优运算次数依赖于 Ai…k与Ak+1…j 的最优运算次数
- 递推方程:m[i,j] :得到 Ai…j 的最少的相乘次数
一一递归实现
部分伪码:
算法1 RecurMatrixChain (P, i, j)
1.m[i,j]←∞
2.s[i,j]←i
3.for k←i to j−1 do
4.q←RecurMatrixChain(P,i,k)+RecurMatrixChain(P,k+1,j)+pi−1 pk pj
5.if q< m[i,j]
6.then m[i,j]←q
7. s[i,j]←k
8.return m[i,j]
时间复杂度的递推方程
图解
【上图中颜色相同的一直在被多次计算】
子问题计数
- 边界不同的子问题:15 个
- 递归计算的子问题:81 个
递归的弊端及改进
- 动态规划算法的递归实现效率不高,原因在于同一子问题多次重复出现,每次出现都需要重新计算一遍.
- 采用空间换时间策略,记录每个子问题首次计算结果,后面再用时就直接取值,每个子问题只算一次(迭代)。
一一迭代实现
迭代计算的关键
- 每个子问题只计算一次
- 迭代过程
1.从最小的子问题算起
2.考虑计算顺序,以保证后面用到的值前面已经计算好
3.存储结构保存计算结果——备忘录- 解的追踪
1.设计标记函数标记每步的决策
2.考虑根据标记函数追踪解的算法
计算顺序
伪码
算法 MatrixChain (P, n)
1.令所有的 m[i,i]初值为0
2.for r←2 to n do // r为链长
3.for i←1 to n−r+1 do // 左边界i
4.j←i+r−1 // 右边界j
5.m[i,j]←m[i+1,j]+pi−1pi pj //k=i
6.s[i,j]←i //记录k
7.for k←i+1 to j−1 do // 遍历k
8.t←m[i,k]+m[k+1,j]+pi−1 pk pj
9.if t<m[i,j]
10.then m[i,j]←t //更新解
11.s[i,j]←k
【二维数组m与s为备忘录】
时间复杂度分析
- 根据伪码:行 2, 3, 7 都是O(n),循环执行O(n3)次,内部为O(1)W(n) = O(n3)
- 根据备忘录:估计每项工作量, 求和. 子问题有O(n2)个,确定每个子问题的最少乘法次数需要对不同划分位置比较,需要O(n)时间. W(n) = O(n3)
- 追踪解工作量 O(n),总工作量O(n3).
递归与迭代的比较
- 递归实现:时间复杂性高,空间较小
- 迭代实现:时间复杂性低,空间消耗多
五、经典例子
- 矩阵链相乘(见上面)
- 投资问题
- 背包问题
- 最大子段和
●投资问题
问题介绍
m 元钱,n个投资项目, fi (x): 将 x 元投入第 i 个项目的效益. 求使得总效益最大的投资方案.
建模
问题的解是向量 < x1, x2, …, xn >, xi 是投给项目i 的钱数,i =1, 2, … , n.目标函数 max{f1(x1)+f2(x2)+…+fn(xn)}约束条件x1+x2+…+xn=m,xi∈N
问题分析及动态规划
- 子问题界定:由参数 k 和 x 界定
k:考虑对项目1, 2, …, k 的投资
x:投资总钱数不超过 x- 优化函数的递推方程
Fk(x): b万元钱投给前k个项目最大效益多步判断:若知道 x元钱 ( x <= b) 投给前 k-1个项目的最大效益Fk-1(x), 确定b元钱投给前k个项目的方案
递推方程和边界条件
例:5万元钱,4个项目效益函数如下表所示
程序:
//投资问题
public class investment {
static int b=5;
static int value[][] = {{0,0,0,0,0},
{1,11,0,2,20},
{2,12,5,10,21},
{3,13,10,30,22},
{4,14,15,32,23},
{5,15,20,40,24}};//投资k项目x万元的收益
static int F[][]=new int [6][5];//备忘录储存x万元投资前K个项目所得最大总收益
static int num[][]=new int[6][5];//备忘录储存各阶段的解
public static void main(String[] args){
maxValue();//求最大收益,并标记
trackPrint();//追踪各阶段的解
}
public static void maxValue() {
for(int x=1;x<=5;x++) {
for(int k=1;k<=4;k++) {
for(int i=x;i>=0;i--)
if(F[x][k]<value[i][k]+F[x-i][k-1])
{
F[x][k]=value[i][k]+F[x-i][k-1];//更新最大收益
num[x][k]=i;//标记
}
/*打印x万元投资4个项目的最大收益*/
System.out.print(F[x][k]+" ");
if(k==4)
System.out.println();
}
}
}
public static void trackPrint() {
for(int i=1;i<6;i++)
for(int j=1;j<5;j++)
{
System.out.print(num[i][j]+" ");
if(j==4)
System.out.println();
}
for(int i=4;i>=1;i--) {
System.out.println(i+"号项目投资"+num[b][i]+"万元");
b-=num[b][i];
}
}
}
时间复杂度分析
备忘录表中有 m 行 n 列, 共计 mn 项 xk有 x +1 种可能的取值,计算Fk(x)
项 (2<= k <=n, 1<=x <= m)需要:x+1次加法,x 次比较
对备忘录中所有的项求和:
- 加法次数
- 比较次数
推出W(n)=O(nm ^2)
●背包问题
问题介绍
一个旅行者随身携带一个背包. 可以放入背包的物品有n 种, 每种物品的重量和价值分别为 wi , vi . 如果背包的最大重量限制是 b, 每种物品可以放多个. 怎样选择放入背包的物品以使得背包的价值最大 ? 设上述 wi , vi , b 都是正整数
建模
解是<x1, x2,…, xn>,其中xi 是装入背包的第 i 种物品个数
问题分析及动态规划
子问题的界定:由参数 k(物品选择)和 y(背包中所放物品总质量)界定
k:考虑对物品1, 2, … , k 的选择
y:背包总重量不超过b优化函数的递推方程
Fk(y): 装前 k 种物品, 总重不超过 y, 背包达到的最大价值
Fk(y)=max{Fk-1(y),Fk(y-wk)+vk}
F0(y)=0,0<=y<=b,Fk(0)=0,0<=k<=n
F1(y)=⌊y/w1⌋ *v1
标记函数
ik(y): 装前 k 种物品, 总重不超 y, 背包达到最大价值时装入物品的最大标号
例:输入:v1 =1, v2=3, v3=5, v4=9,n=4. w1=2, w2=3, w3=4, w4=7,b=10
Fk (y) 的计算表如下:
标记函数ik(y)(追踪解)
追踪算法伪码:
算法 Track Solution
输入: ik (y)表, k=1,2,…,n, y=1,2,…,b
输出: x1, x2,…,xn,n种物品的装入量
1.for k<-1 to n do Xk<-0
2.y<-b, k<-n
3.j<-ik (y)
4.Xk<-1
5.y<-y-Wk
6.while ik(y)=k do
7.y<-y-wk
8.xk<-xk+ 1
9 if ik(y)≠0 then goto 4
程序:
//背包问题
public class Test {
static int n=4,b=10;//n代表物品数,b代表背包最大容量
static int F[][]=new int[5][11];//存放对应最大价值
static int i[][]=new int[5][11];//备忘录,标记背包达到最大价值时装入物品的最大标号
static int num[]=new int[5];//对应物品的存放的数量
static int w[]= {0,2,3,4,7};//存放物品重量
static int v[]= {0,1,3,5,9};//存放物品价值
public static void main(String[] args) {
maxValue();
packageTrack();
System.out.println("最大总价值为"+i[n][b]);
for(int k=1;k<=n;k++)
System.out.println(k+"号物品放"+num[k]+"个");
}
public static void maxValue() {
//System.out.println("最大价值表");
for(int k=1;k<=n;k++) {
for(int y=1;y<=10;y++) {
if(y<w[k])
{
F[k][y]=F[k-1][y];
i[k][y]=i[k-1][y];
}
else
{
F[k][y]=Math.max(F[k-1][y],F[k][y-w[k]]+v[k]);
if(F[k-1][y]<=F[k][y-w[k]]+v[k])
{
//F[k][y]=F[k][y-w[k]]+v[k]
i[k][y]=k;
}
else
{
//F[k][y]=F[k-1][y];
i[k][y]=i[k-1][y];
}
}
/*打印对应最大价值
* System.out.print(F[k][y]+" ");
if(y==10)
System.out.println();*/
}
}
//打印对应标记备忘录
/*System.out.println("标记备忘录");
for(int k=1;k<=n;k++)
for(int y=1;y<=b;y++) {
System.out.print(i[k][y]);
if(y==10)
System.out.println();
}*/
}
public static void packageTrack() {
int y=b;
for(;;) {
int j=i[n][y];
num[j]++;
y-=w[j];
if(y==0)
break;
}
}
}
时间复杂度分析
根据公式
Fk(y)=max{Fk-1(y),Fk(y-wk)+vk}
备忘录需计算 nb 项,每项常数时间,计算时间为 O(nb) .
背包问题推广
- 0-1背包问题(物品数受限背包):第 i 种物品最多用 ni 个1. 0-1背包问题:xi = 0, 1,i = 1, 2, … , n
- 多背包问题:m个背包,背包 j 装入最大重量 Bj , j =1,2, … , m. 在满足所有背包重量约束条件下使装入物品价值最大.
- 二维背包问题:每件物品有重量 wi 和体积 ti, i =1, 2, … , n,背包总重不超过 b,体积不超过V, 如何选择物品以得到最大价值
【推广背包问题的代码WX搜索"Java长征记"回复"算法代码即可获得"】
●最大子段和
问题介绍
给定n个数(可以为负数)的序列 (a1, a2, … , an),求其中连续的子序列和的最大值,即如图
子问题的分析及动态规划
- 子问题界定:前边界为 1,后边界 i, C[i] 是 A[1… i]中必须包含元素 A[i] 的向前连续延伸的最大子段和
- 优化函数的递推方程
C[i]= max{C[i-1]+A[i], A[i]}
若A[1]>0 C[1]=A[1]否则C[1]=0
(i =2, …, n)
伪码
算法 MaxSum (A, n)
输入:数组A
输出:最大子段和sum, 子段最后位置c
1.sum<-0
2.b<-0
3.for i<-1 to n do
4.if b > 0
5.then b<-b + A[i]
6.else b<-A[i]
7.if b > sum
8.then sum<-b
9.c<-i
10.return sum , c
程序
//最大子段和
public static int maxsum(int a[]) {
if(a.length==0 || a==null)
return 0;
int c[]=new int[a.length];
int max=0;
for(int i=1;i<a.length;i++) {
c[i]=Math.max(c[i-1]+a[i],a[i]);
if(max<c[i]) //更新最大值
max=c[i];
}
return max;
}
时间复杂度分析
时间复杂度:O(n), 空间复杂度:O(n)
干货分享
想学习更多的关于Java基础、算法、数据结构等编程知识的WX搜索"Java长征记"。上面的这些在里面也有详细介绍。