DP
动态规划(DP)是对一类最优化问题的解法。动态规划问题每次决策时依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,这种多阶段最优化决策解决问题的过程称为动态规划。
动态规划的使用前提:
(1)最优化原理。(搬CCF的) 在作出一个决策之后,剩下的问题是与原问题性质相同的子问题,只要规模或参数发生变化,要使原问题最优,子问题必须最优。
(2)无后效性:这个非常好理解,其实就是把一个问题分成两个问题子问题和原问题,我们在子问题里面求最优值,跟我原问题有什么关系呢?(抓鲁迅跟我周树人有什么关系? 揍张大威跟我张大帅有什么关系? )
子问题的决策序列不会影响原问题的决策。但是你不能从后面推到前面,比如斐波那契数列,怎么可能从fibo[3]推出fibo[1]呢?这是不科学的!
动态规划的步骤:
(1)确定状态:(搬CCF的) 状态是一种数学形式,能刻画一个最优解的解的结构特征,能够把题目里的必不可少的要素包含进来。
状态到底是什么呢?我们通过下面的例题看吧。。↓
(2)确定边界条件:跟递归一样,我们写DP的时候也要确定状态的最优解。如果不写边界条件那写DP干什么。。和DFS一样,有些情况也要写边界条件。
(3)使用各种骚操作优化,比如记忆化搜索。
[IOI1994]数字三角形 Number Triangles
观察下面的数字金字塔。
写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
在上面的样例中,从7 到 3 到 8 到 7 到 5 的路径产生了最大
输入格式
第一个行包含 R(1<= R<=1000) ,表示行的数目。
后面每行为这个数字金字塔特定行包含的整数。
所有的被供应的整数是非负的且不大于100。
输出格式
单独的一行,包含那个可能得到的最大的和。
样例数据
input
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
output
30
分析:
这题有Ao多的做法。但是我们第一个想到的估计是贪心,这题能不能用贪心做呢?答案很明显,不可以。为什么?因为贪心在每行找一个最大值并不一定是真的最优解。至于为什么,自己走一遍就知道了。
(1)用D大F法S师 ,问题要求的是从最高点走到最低点,走法规则很明确,起点终点固定,可以考虑用搜索解决
#include<bits/stdc++.h>
using namespace std;
int a[1000][1000],f[1000][1000],n,ans;
void dfs(long long x,long long y,long long curr){
if(x==n){
//边界条件
if(curr>ans) ans=curr;
return;
}
dfs(x+1,y,curr+a[x+1][y]); //向左走
dfs(x+1,y+1,curr+a[x+1][y+1]); //向右走
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
cin>>a[i][j];
ans=0;
dfs(1,1,a[1][1]);
cout<<ans<<endl;
return 0;
}
这种垃圾做法的复杂度是O(2^(n-1)),用军哥的话来说,“很拉胯”。
N稍稍一大,瞬间爆炸。
(2)可以用记忆化搜索给他调到O(N²)。之所以会超时是因为重复搜索,比如从(1,1)走到(3,2)有“左右”和“右左”两种路径,被搜索了两次。我们完全可以在第一次搜索(3,2)的时候就记录下(3,2)到终点的最大权值和。下次再来到(3,2)的时候就跳过了。
定义dfs(x,y)表示从(x,y)出发到终点的最大权值和,答案就是dfs(1,1)。
然后我们定义第一步向左和第一步向右的操作。
第一步向左:就先从(x,y)出发到终点的这类路径就被分成两个部分,先从(x,y)到(x+1,y),再从(x+1,y)到终点,这一部分权值固定为a[x][y],要使得这种情况路径权值和最大,那么第二部分从(x+1,y)到终点的权值和也要最大,这一部分与前面的dfs(x,y)的定义类似,仅仅是参数不同,
综上所述,第一步向左的路径的最大权值和为a[x][y]+dfs(x+1,y)。
那么第一步向右:也一样,a[x][y]+dfs(x+1,y+1)。
为了避免重复搜索,我们开f[x][y]记录从(x,y)出发到终点路径的最大权值和,一开始全部初始化为-1表明没有被计算过。在计算dfs(x,y)时,首先查询f[x][y],如果不等于-1,那么就被计算过,直接返回f[x][y]即可。否则计算出dfs(x,y)的值并存在f[x][y]中。
完全能过。
#include<bits/stdc++.h>
using namespace std;
const int maxn=505;
int a[maxn][maxn],f[maxn][maxn],n;
int dfs(int x,int y){
if(f[x][y]==-1){
if(x==n) f[x][y]=a[x][y];
else f[x][y]=a[x][y]+max(dfs(x+1,y),dfs(x+1,y+1));
}
return f[x][y];
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
cin>>a[i][j];
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
f[i][j]=-1;
}
}
dfs(1,1);
cout<<f[1][1]<<endl;
return 0;
}
(3)动态规划
诶?我们今天学的是动态规划吗?
DFS完全能A了啊!
所以我懒得写了。
好吧,今天学的是动态规划,还是用动态规划做一波吧。
其实记忆化搜索本质上已经是动态规划了。
我们用普通的动态规划写一遍。
1.确定状态:题目要求从(1,1)出发到最底层路径最大权值和,路径是由各个点串联而成,起点固定,终点和中间点相对不固定,因此定义f[x][y]表示从(1,1)出发到(x,y)的路径最大权值和。
则ans=max(f[n][1],f[n][2]…f[n][n])
2.确定状态转移方程和边界条件:不去考虑(1,1)到(x,y)的每一步是如何走的,只考虑最后一步是怎么走的,根据最后一步是向左和向右分成如下情况:
(1)向左:最后一步是从(x-1,y)走到(x,y),此类路径被分割成两部分,第一部分是计算从(1,1)走到(x-1,y),第二部分是从(x-1,y),要计算此类路径的最大权值和,就必须用到第一部分的最大权值和,就是f[x-1][y]+a[x][y]。
(2)向右,同上,f[x-1][y-1]+a[x][y]。
总结,状态转移方程就是
f [ x ] [ y ] = m a x ( f [ x − 1 ] [ y − 1 ] , f [ x − 1 ] [ y ] ) + a [ x ] [ y ] f[x][y]=max(f[x-1][y-1],f[x-1][y])+a[x][y] f[x][y]=max(f[x−