要做网站到哪里做/网站设计规划
《算法竞赛入门》中动态规划的第一个例子就是经典的数字三角形问题。
数字三角形问题:
问题描述:
有一个由非负整数组成的三角形,每一行只有一个数,除了最下行之外每个数的左下外和右下方各有一个数。
13 24 10 1 4 3 2 20 最大值为1+2+1+20 = 24
从第一行的数开始,每次可以往左下或右下走一格,直到走到最下行,把沿途经过的数全部加起来,和最大的是多少?
分析:
这一个动态的决策问题。从第一行开始,要么向左,要么向右。如果把所有路线全部暴力出来再取其中最大这个方法是不是可行的呢,然而一共有2^n-1条路,显然这是非常慢的。
还有其他的方法吗? 我们抽象的分析一下这个题目,把当前的位置(i,j)看成一个状态,并定义dp[i][j]为从(i,j)出发走到最后一行最大和。
而从dp[i][j]开始走有两个选择:
1.向左走,则走到(i+1,j) ,那么需要求dp[i+1][j]
2.向右走,则走到(i+1,j+1),那么需要求dp[i+1][j+1]
所以就得出了这样的一个状态转移方程:dp[i][j] = a[i][j] + max(dp[i+1][j] , dp[i+1][j+1] )
通过这个状态转移方程我们可以写出它的递归函数
代码如下:
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 10000+10;
int n,a[maxn][maxn];int solve(int i,int j)
{return a[i][j]+(i==n?0:max(solve(i+1,j),solve(i+1,j+1)));
}//通过状态转移方程写出的递归函数
int main()
{cin>>n;for(int i=n;i>=1;i--){for(int j=1;j<=i;j++){cin>>a[i][j];}}cout<<solve(1,1)<<endl;//即解出从位置(1,1)走到最后一行的最大和return 0;
}
但是我们分析一下这个过程
我们可以看到从(2,1)向右走到达(3,2),而从(2,2)向左走也到达(3,2),所以如果按照这个递归,有些结点多走了,从这张图中可以看出有许多结点是重叠的。
记忆化:
那么这时需要一个数组来保存走过的结点,这样以后再走到这个结点就直接使用它已经计算过的值,不用再多计算一遍了。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 10000+10;
int dp[maxn][maxn],a[maxn][maxn];
int n;int d(int i,int j)
{if(dp[i][j]>=0) return dp[i][j];//采用记忆化的方式,减小时间复杂度 return dp[i][j]=a[i][j]+(i==n?0:max(d(i+1,j),d(i+1,j+1)));
}
int main()
{memset(dp,-1,sizeof(dp));scanf("%d",&n);for(int i=n;i>=1;i--){for(int j=1;j<=i;j++){scanf("%d",&a[i][j]);}}printf("%d\n",d(1,1));return 0;
}
这种用递归的方式来解决动态规划问题在《算法导论》中被称为“自顶而下”的方式。那么相反的就会有“自下而上”的方式,那就是用表来记录,通过递推进行动态规划。
自下而上,进行递推:
#include<cstdio>
#include<iostream>
using namespace std;
const int maxn = 10000+10;
int d[maxn][maxn],a[maxn][maxn];
int n;int main()
{scanf("%d",&n);for(int i=1;i<=n;i++){for(int j=1;j<=i;j++){scanf("%d",&a[i][j]);}}for(int i=1;i<=n;i++){d[n][i] = a[n][i];//从边界出发的最大值就是其本身 }for(int i=n-1;i>=1;i--){for(int j=1;j<=i;j++){d[i][j] = a[i][j]+max(d[i+1][j],d[i+1][j+1]);}}printf("%d\n",d[1][1]);return 0; }
注意:用递推的方式记录需要有一个边界值,否则就无法进行递推。