教你入门动态规划
动态规划VS搜索
动态规划,相信几乎所有人都听说过,但有些人不知道动态规划的好处,特别是没学过的人,经常把本该用动态规划做的题用搜索做,比如P1508和 P2607,导致 TLE,我当初也是这样。
为什么呢?因为搜索需要从一个状态开始,推出若干个决策,这若干个决策又会推出更多的决策,假设一个决策会推出2个决策:
| 次数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | n |
|---|---|---|---|---|---|---|---|---|---|---|
| 状态数 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 2的n次方 |
假设一个决策会推出10个决策时就更可怕了:
| 次数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | n |
|---|---|---|---|---|---|---|---|---|---|---|
| 状态数 | 10 | 100 | 1000 | 10000 | 100000 | 1000000 | 10000000 | 100000000 | 1000000000 | 10的n次方 |
所以搜索是一种时空消耗极大的算法,数据稍大时就有可能TLE或MLE,搜索优化?有一些效果,但还是会在超大数据时TLE或MLE。
相比之下,动态规划复杂度低,一般只有O(n的2次方或3次方),用我的实际举例子,搜索和动态规划算法整整因为时间原因整整差了60分。
动态规划入门
那,怎样学这种复杂度简单的算法呢?
追根溯源这个成语,我想大家一定听说过,没错,这也是动态规划的思想,从反方向找,往上赋最优值,直到赋到答案,输出它,与搜索的顺序完全相反。
现在,我建议先做P1002 P1216 P1508 这三道题。
P1002:
一道动规题,也是一道奥数题。
如上图,把在第一行和第一列的点都标上1(控制点及之后的不标记),之后的点是该交叉点上面和左面的和,转移方程:
dp[i][j]=dp[i-1][j]+dp[i][j-1];
代码:
(抄了对你没好处)
#include<bits/stdc++.h>
using namespace std;
long long fi_x,fi_y,h_x,h_y,a[100][100];
bool b[100][100];
int main(){
cin>>fi_x>>fi_y>>h_x>>h_y;
b[h_x][h_y]=1;
if(h_x-2>=0&&h_y-1>=0)b[h_x-2][h_y-1]=1;
if(h_x-2>=0&&h_y+1<=fi_y)b[h_x-2][h_y+1]=1;
if(h_x-1>=0&&h_y-2>=0)b[h_x-1][h_y-2]=1;
if(h_x+1<=fi_x&&h_y-2>=0)b[h_x+1][h_y-2]=1;
if(h_x-1>=0&&h_y+2<=fi_y)b[h_x-1][h_y+2]=1;
if(h_x+1<=fi_x&&h_y+2<=fi_y)b[h_x+1][h_y+2]=1;
if(h_x+2<=fi_x&&h_y-1>=0)b[h_x+2][h_y-1]=1;
if(h_x+2<=fi_x&&h_y+1<=fi_y)b[h_x+2][h_y+1]=1;
for(int i=0;i<=fi_y;i++){
if(b[0][i]==0)a[0][i]=1;
else break;
}
for(int i=0;i<=fi_x;i++){
if(b[i][0]==0)a[i][0]=1;
else break;
}
for(int i=1;i<=fi_x;i++){
for(int j=1;j<=fi_y;j++){
if(b[i][j]==0)a[i][j]=a[i-1][j]+a[i][j-1];
}
}
cout<<a[fi_x][fi_y];
return 0;
}
P1216: 当初我也找不出这道题的转移方程,此时我想出了我曾经在小学六年级时与同学玩的一种占卜游戏,可求得两者成为伴侣的关系: (一切真名用拼音首字母大写表示)
C R Z W H M
10 10 8 4 15 8
0 2 4 11 7
2 2 7 4
0 5 3
5 2
3---------100%-3%=97%
C R Z Z K R
10 10 8 11 9 13
0 2 3 2 4
2 1 1 2
1 0 1
1 1
0---------100%-0%=100%
我们先求出两个人名字每个字的笔画数,写在所对应字的下面,再在每两个数的下面的中间写上面两个数字差的的绝对值(就是大减小的差),得到最底下的一个数后,假设它为x吧,成为伴侣的概率便是100%-x%。
这种方法肯定是荒谬的,但如果你能把它理解成动态规划的转移图,那用处就大了,你的理解至少会减少几个小时。
但这道题的赋值方式与这种游戏有所不同,它不是求绝对值的差,而是求总和,要求下面两个值的最大值,并加上当前位置的值,理解也不难,附上转移方程和代码:
转移方程:
dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+a[i][j];
代码:(抄了对你没好处)
#include<bits/stdc++.h>
using namespace std;
int a[10000][10000],dp[10000][10000],n;
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++)dp[n][i]=a[n][i];
for(int i=n-1;i>=1;i--){
for(int j=1;j<=i;j++)dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+a[i][j];
}
cout<<dp[1][1];
return 0;
}
P1508:与上一题类似,但有几个注意点:
- 它要求从最低下一行下方的中间一直走到上方
- 那个人可以往左上、正上、右上的方向走
- 食物能量可能为负,你至少不能开unsigned数组了
转移方程有所改动,尤其注意max函数只能输入两个数,三个数及以上就会编译错误
(当时我被卡了半天,以为编译器坏了)
:
dp[i][j]=max(dp[i-1][j-1],max(dp[i-1][j],dp[i-1][j+1]))+a[i][j];
代码:(抄了对你没好处)
#include<bits/stdc++.h>
using namespace std;
int n,m,a[205][205],dp[205][205];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++)cin>>a[i][j];
}
for(int i=1;i<=n;i++)dp[1][i]=a[1][i];
for(int i=2;i<=n+1;i++){
for(int j=1;j<=m;j++)dp[i][j]=max(dp[i-1][j-1],max(dp[i-1][j],dp[i-1][j+1]))+a[i][j];
}
cout<<dp[n+1][m/2+1];
return 0;
}
这次的内容就结束了,运气好的话我还会出版背包dp的思考方法,祝大家过关斩将,AKIOI!