算法导论中讲解动态规划的一个很经典的例子,详细分析了朴素的递归、自顶向下动态规划的递归和自底向上的非递归动态规划,这三种思路之间的区别,并重构解。
联系第22章:
自顶向下动态规划的递归:就是子问题图的深度优先搜索
自底向上的非递归动态规划:就是子问题图的逆拓扑排序
代码实现:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
//动态规划,钢条切割问题
public class Cut {
public static void main(String[] args) {
//长度对应价格表price,下标从0开始,每(i+1)长的钢条价值为price[i],n为给的钢条长度
int[] price = {1,5,8,9,10,17,17,20,24,30};
int n=7;
System.out.println("给定钢条长度价格表为:");
System.out.println(Arrays.toString(price));
System.out.println("给定钢条长度为:");
System.out.println(n);
//1.使用朴素的自顶向下递归算法求解最大切割收益q1
int q1 = Cut_Rod(price, n);
System.out.println("使用朴素的自顶向下递归算法求解最大切割收益为:"+q1);
//2.使用带备忘的自顶向下递归的动态规划求解最大切割收益q2
int q2 = Memoized_Cut_Rod(price, n);
System.out.println("使用带备忘的自顶向下递归的动态规划求解最大切割收益为:"+q2);
//3.使用自底向上的非递归动态规划求解最大切割收益q3
int q3 = Bottom_Up_Cut_Rod(price, n);
System.out.println("使用自底向上的非递归动态规划求解最大切割收益为:"+q3);
//4.重构解,获得每个子问题长度的最大切割收益和最大切割收益的切割轨迹
List<int[]> result = Extended_Bottom_Up_Cut_Rod(price, n);
int[] r = result.get(0);//最大切割收益记录
int[] s = result.get(1);//最大切割收益的切割轨迹记录
System.out.println("最大切割收益记录为"+Arrays.toString(r));
System.out.println("最大切割收益的切割轨迹记录为"+Arrays.toString(s));
System.out.print("当前长度最大切割收益为:"+r[n]+",当前长度最大切割收益方案为:");
Print_Cut_Rod_Solution(price, n);
}
//1.朴素的自顶向下的递归实现,对于同一个子问题多次展开计算,效率太低
//数组p为价格数组,下标从0开始,每(i+1)长的钢条价值为p[i],n为给的钢条长度
public static int Cut_Rod(int[] p,int n) {
//递归的出口,如果钢条长度为0,那么没有价值
if(n==0) {
return 0;
}
//q用来保存当前的最大价格收益,初始化为无穷小
int q = -99999;
//从1到n依次求从左向右切割i长的钢板能获得的最大收益值
for(int i=1;i<=n;i++) {
q=Math.max(q, p[i-1]+Cut_Rod(p, n-i));
}
return q;
}
//2.带备忘的自顶向下的动态规划实现,是递归版的动态规划,用了一个r数组来保存记忆住以往的重叠子问题(长度为i的钢条切割的最大收益)
//自顶向下在子问题图上就是深度优先搜索
//主过程
public static int Memoized_Cut_Rod(int[] p,int n) {
//初始化一个记忆数组r
int[] r = new int[n+1];
//给r数组初始化,每个值设为未知的小值
for(int i=0;i<r.length;i++) {
r[i]=-99999;
}
//调用基本过程
return Memoized_Cut_Rod_Aux(p, n, r);
}
//基本过程
private static int Memoized_Cut_Rod_Aux(int[] p,int n,int[] r) {
int q;//用于保存当前最大收益
if(r[n]>=0) {
return r[n];//递归,动态规划的出口,即长度为n的钢条最大切割收益找到了(r[n]由负更新为正了)
}
//q的初始化,如果钢条长为0,无法切割,收益也为0,否则初始化为一个未知的小值
if(n==0) {
q=0;
}else {
q=-99999;
}
for(int i=1;i<=n;i++) {
q=Math.max(q, p[i-1]+Memoized_Cut_Rod_Aux(p, n-i, r));
}
//每轮递归后更新相应长度i的最大切割收益值,即r[i]
r[n]=q;
return q;
}
//3.自底向上的动态规划实现,不再是是递归,求解每个子问题的前提是它所依赖的更小子问题求解完毕了
//自底向上在子问题图上就是逆拓扑排序
public static int Bottom_Up_Cut_Rod(int[] p,int n) {
int r[] = new int[n+1];
//初始化与递归不同,自底向上要从基本的子问题开始,所以基本的子问题要确定,r[0]=0,不再是-99999
r[0]=0;
for(int j=1;j<=n;j++) {
//即j从1到n一个个找,每个j是个和n级别相同的层面问题了,但长度j又分为相同的基本问题i与j-i两段
int q=-99999;
for(int i=1;i<=j;i++) {
q=Math.max(q, p[i-1]+r[j-i]);
}
//每次外轮循环后求出长度i的最大切割收益,更新r[i]
r[j]=q;
}
return r[n];
}
//4.自底向上动态规划的重构解,用来跟踪解的轨迹
public static List<int[]> Extended_Bottom_Up_Cut_Rod(int[] p,int n) {
//r来保存长度j的钢条最大切割收益
int[] r = new int[n+1];
//s来保存长度j的最优切割方案中的第一段切割下来的钢条长度
int[] s = new int[n+1];
r[0]=0;
for(int j=1;j<=n;j++) {
int q=-99999;
for(int i=1;i<=j;i++) {
if(q<p[i-1]+r[j-i]) {
q=p[i-1]+r[j-i];
s[j]=i;//保存长度j的切割方案中第一段钢条的长度,那么剩下长度就是j-s[j]
r[j]=q;
}
}
}
//由于要返回一个int[]型最大收益记录数组r和一个int[]型切割轨迹数组s,Java中不支持元组,所以用一个List集合保存这两个
List<int[]> temp = new ArrayList<int[]>();
temp.add(r);
temp.add(s);
return temp;
}
//对最大收益和切割方案轨迹打印
public static void Print_Cut_Rod_Solution(int[] p,int n) {
List<int[]> result = Extended_Bottom_Up_Cut_Rod(p, n);
int[] r = result.get(0);
int[] s = result.get(1);
while(n>0) {
System.out.print(s[n]+" ");
n=n-s[n];
}
}
}
程序结果: