本文内容整理自中国大学MOOC郭炜老师的程序设计与算法(二)
首先由数字三角形问题出发( ̄︶ ̄)↗ ,题目描述如下:
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
在上面的数字三角形中寻找一条从顶部到底部的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或右下走。只需要求出这个最大和即可,不必给出具体路径。(三角形的行数大于1小于100,数字为0~99)
输入:
第一行是数字三角形的行数,接下来 n 行是数字三角形中的数字。
比如:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出:
最大和
分析:
1)首先最容易想到的是递归算法,第n行第n列到底部的路径最大和为:自身+第n+1行最大的;临界条件:最后一行的路径最大和即为自身。
递归参考代码
///递归 #include <iostream> using namespace std; int n; int MaxSum(int i,int j,int a[][101]); int main() { int a[101][101]; cin>>n; for(int i=0;i<n;i++){ for(int j=0;j<i+1;j++){ cin>>a[i][j]; } } cout<<MaxSum(0,0,a); return 0; } int MaxSum(int i,int j,int a[][101]){ if(i==n-1){ return a[i][j]; }else if(i<n-1){ int x=MaxSum(i+1,j,a); int y=MaxSum(i+1,j+1,a); return max(x,y)+a[i][j]; } }
2)可是该递归方法有很多很多重复的计算,所以我们想到可以记下已经算过的,当用到时直接拿过来用,这样就避免了大量的重复运算。
///记忆递归型动规 #include <iostream> using namespace std; int n; int b[101][101];//记忆数组用来存已经计算出来的结果 int MaxSum(int i,int j,int a[][101]); int main() { int a[101][101]; cin>>n; for(int i=0;i<n;i++){ for(int j=0;j<i+1;j++){ cin>>a[i][j]; b[i][j]=-1;//初始化为-1 } } cout<<MaxSum(0,0,a); return 0; } int MaxSum(int i,int j,int a[][101]){ if(b[i][j]!=-1){//如果不等于-1说明已经计算过,所以直接返回 return b[i][j]; } else { if(i==n-1){ return b[i][j]=a[i][j]; } else if(i<n-1) { int x=MaxSum(i+1,j,a); int y=MaxSum(i+1,j+1,a); return b[i][j]=max(x,y)+a[i][j]; } } }
3)其实呢,一般都是用递推的方式来推出动态规划算法的,用循环递推不但比递归快,而且还能节省空间,写起来也比较方便,但是还是基于递归的思想才能递推出递推式。
第一步:在草稿纸或者脑子里想出解决该题的递归函数。
第二步:递归函数有n个参数,就定义一个n维的数组,数组的下标是,递归函数参数的取值范围,数组元素的值是递归函数的返回值。
第三步:根据边界条件,初始化n维数组。
第四步:得到递推式,根据递推式逐步填充数组,相当于递归的逆过程。
如本题:
///第一步写递归函数已经在上面写完了 #include <iostream> #include <algorithm> using namespace std; int main() { int i,j; int n; int D[101][101]; ///第二步,因为递归函数有行i和列j两个参数所以定义一个二维数组 int maxSum[101][101]; cin>>n; for(i=1;i<=n;i++){ for(j=1;j<=i;j++){ cin>>D[i][j]; } } ///第三步根据边界条件初始化二维数组 for(int i=1;i<=n;i++){ maxSum[n][i]=D[n][i]; } /**第四步,根据递归函数推出递推式 a[i][j],i==n-1; dp[i][j]= max(dp[i+1][j],dp[i+1][j+1])+a[i][j]; */ ///得到递推式,把递推式写出来 for(int i=n-1;i>=1;i--){ for(int j=1;j<=i;j++){ maxSum[i][j]=max(maxSum[i+1][j],maxSum[i+1][j+1])+D[i][j]; } } cout<<maxSum[1][1]<<endl; return 0; }
4)往往由递推式写出的程序可以进行空间优化,如样例输入
①用一维记忆数组代替多维记忆数组
其一维记忆数组为maxSum={4,5,2,6,5}
那么算n-1行经过2的路径的最大和,在4和5中选出最大数,将结果存在4的位置,即maxSum={7,5,2,6,5}。这样并不会影响其他的路径和,比如第n-1行经过7的路径的最大和,是在5和2中选出最大数。
#include <iostream> #include <algorithm> using namespace std; int main() { int i,j; int n; int D[101][101]; int maxSum[101];///此处由二维数组变成了一维数组 cin>>n; for(i=1;i<=n;i++){ for(j=1;j<=i;j++){ cin>>D[i][j]; } } for(int i=1;i<=n;i++){ maxSum[i]=D[n][i]; } for(int i=n-1;i>=1;i--){ for(int j=1;j<=i;j++){ maxSum[j]=max(maxSum[j],maxSum[j+1])+D[i][j];///取i下面和右面的最大值+本身,存在i下面数的位置 } } cout<<maxSum[1]<<endl; return 0; }
②删去一维记忆数组,用保存输入数据的数组的最后一行当作记忆数组保存计算结果
#include <iostream> #include <algorithm> using namespace std; int main() { int i,j; int n; int D[101][101]; cin>>n; for(i=1;i<=n;i++){ for(j=1;j<=i;j++){ cin>>D[i][j]; } } //因为一维记忆数组初始化是和D数组第n行一样,而且D数组第n行只在n-1才会用到 for(int i=n-1;i>=1;i--){ for(int j=1;j<=i;j++){ D[n][j]=max(D[n][j],D[n][j+1])+D[i][j];///取i下面和右面的最大值+本身,存在第n行 } } cout<<D[n][1]<<endl; return 0; }
4)那么什么样的问题适合用动态规划呢?
①问题具有最优子结构性质。
如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质。
②无后效性。
当前的若干状态的值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取哪种手段或经过哪条路径演变到当前的这若干个状态,没有关系。