TIP
动态规划是一门艺术,可是我感觉我连跑龙套的资格都不够,总是不解其中的奥秘。越是不会就要越迎难而上,我知道,只有攀上一座又一座的山丘,才能看到更美的风景。
# 写在前面
动态规划的关键就是要找到状态转移方程,像汉诺塔问题,斐波那契数列问题,都可以看作是一个动态规划类型的问题。汉诺塔问题,当盘子很少的时候,也许自己脑子想一想,眼睛转一转,也许就能够算出答案,但是当盘子数超过一个手指头也许就并不那么好想了,这个时候我们就可以这样想了,假如盘子数有n
个,当有人帮助我借助C
,把A
上面的n-1
个盘子移到了B
,然后再把A
上面的一个盘子移到C
,再把B
上的n-1
个盘子借助A
移动到C
,这样就解决了这个问题。至于中间这些盘子是怎么样移动的,那就可以交给电脑了。
还有就是斐波那契数列,这个状态转移方程是f[n]=f[n-1]+f[n-2](n>=3)
,初始化f[1]=1,f[2]=1
。这个问题比较容易发现规律,一个数等于前边的两个数之和,那么这个问题就变得很简单了,不管你要求那个数,我只需要求两个数之和就可以了,然后直到求出那个数这个问题就可以解决了。
这两个问题都有一个共同点,那就是把大的问题化解成小的问题,有许多的步骤都是重复的,那么就可以只考虑走一步的时候该怎么走,然后就按照这样的方法一直走就可以解决这个问题了。
现在我还记得当初在做39
级台阶的时候是怎么做的,可以走一步,也可以走两步,我就在想,如果这39
步我都走1
步的话,就只有一种方法,假如说我走37
个1
步,在走一个2
步,这也可以走完这39
级台阶,但是这个2
步每放一个位置,那就是一种不同的方法这就有38
中方法了,然后再想着我可以我可以走35
个1
步,再走两个2
步,假如这两步在一起的话那就有36
种走法,还有就是这2
步不在一起,这一共有多少种走法呢?36*35/2
种,因为这两个没有差别,所以得考虑到对称性,再看看这个是什么问题,排列组合呀,我像发现新大陆一样就那样稀里糊涂的写着,越到后面越头疼,咋个式子这么长啊,还需要我打上去,最后也是没搞出结果来。
现在想来也是觉得好笑的很。如今仅仅用递归解决这道题也很有局限性了。因为再递归的时候,会重复计算很多子问题,这就大大影响了运算速度,得用一个中间数组保存计算过程中已经算出了子问题的结果,在后面需要用到的时候,直接拿出来用就可以了。
现在有很多问题,不优化就很难解决了,这也是头疼的问题,但是得治,下面弄两个题。
# 题目描述
# 台阶问题
有N
级的台阶,你一开始在底部,每次可以向上迈最多K
级台阶(最少1
级),问到达第N
级台阶有多少种不同方式。
# 输入格式
两个正整数N
,K
。
# 输出格式
一个正整数,为不同方式数,由于答案可能很大,你需要输出ans mod 100003
后的结果。
# 输入输出样例
5 2
8
Z
# 解题思路
这个问题,以前我还写过了,就是一个n叉树的问题,跟39
级台阶也是一样,只不过,这个有了更多的走法,然后我就用帝国的方式写了,然后就LTE
了,所以最后又学习了怎么用记忆化搜索做,怎么用动态规划的思想做。
# 代码
记忆化搜索
#include<iostream>
#include<algorithm>
using namespace std;
int n,k;
const int Max=1e6+3;//最大值
long long f[Max];//保存中间结果
int dfs(int c)//c代表还剩几步没有走
{
if(c==0){
return 1;
}
if(f[c]!=-1){//假如之前已经计算过了,就不用再计算了
return f[c];
}
long long p=0;
for(int i=1;i<=k;i++){
if(c-i>=0){
p=(p+dfs(c-i))%100003;
}
}
f[c]=p;//当还剩c步的时候,有p种走法
return p;
}
int main()
{
cin >> n >> k;
fill(f,f+Max,-1);
f[0]=1;
dfs(n);
cout << f[n];
return 0;
}
动态规划的写法
#include<iostream>
using namespace std;
int n,k;
const int Max=1e6+4;
int f[Max];//状态转移数组,下表代表还剩几步,数组值代表走这几部有几种方法。
int main()
{
cin >> n >> k;
fill(f,f+Max,0);
f[0]=1;
f[1]=1;
for(int i=2;i<=n;i++){
for(int j=min(i,k);j>=1;j--){
f[i]+=f[i-j];//走j步后还剩下几步,因为方法数已经算出来所以直接一个嵌套一个。
f[i]%=100003;
}
}
cout << f[n];
return 0;
}
# 传球游戏
上体育课的时候,小蛮的老师经常带着同学们一起做游戏。这次,老师带着同学们一起做传球游戏。
游戏规则是这样的:n
个同学站成一个圆圈,其中的一个同学手里拿着一个球,当老师吹哨子时开始传球,每个同学可以把球传给自己左右的两个同学中的一个(左右任意),当老师再次吹哨子时,传球停止,此时,拿着球没有传出去的那个同学就是败者,要给大家表演一个节目。
聪明的小蛮提出一个有趣的问题:有多少种不同的传球方法可以使得从小蛮手里开始传的球,传了m
次以后,又回到小蛮手里。两种传球方法被视作不同的方法,当且仅当这两种方法中,接到球的同学按接球顺序组成的序列是不同的。比如有三个同学1
号、2
号、3
号,并假设小蛮为1
号,球传了3
次回到小蛮手里的方式有1->2->3->1
和1->3->2->1
,共2
种。
# 输入格式
一行,有两个用空格隔开的整数n,m(3<=n<=30,1<=m<=30)
。
# 输出格式
1个整数,表示符合题意的方法数。
# 输入输出样例
3 3
2
# 解题思路
这道题最开始我用递归的方式写的,因为每一个人都有向左走和向右走的权利,所以用递归写比较的简单,果断提交,LTE
。一看n
最大为30
,那么递归一共要走多少个地方,30!
,很明显,递归这道题并不能很好的解决。
然后就看到有人写个这样的状态转移方程f[i][j]=f[i-1][j-1]+f[i+1][j-1]
,这个状态转移方程我看了好久也不明白该怎么使用,最后还是想到了只走一步该怎么走,才想明白。i
代表的第i
个人,j
代表的是还剩下几步可以走,那么这第i
个人,可以由他的左边的那个人把球传给他,也可以由右边的那个人把球传给他,那么就只需要考虑一步就好了,(假设小蛮在1
位置),不管怎么走,一定是由小蛮左边的同学把球传给他,或者是右边的同学把球传给他。
其实这也是个局部最优推广到全局最优,先假设只有1
步,只有2
步,直到推广到只有m
步。
# 代码
#include <iostream>
#include <cstring>
using namespace std;
int f[32][32];
int main()
{
int n, m;
cin >> n >> m;
memset(f, 0, sizeof(f));
f[1][0] = 1;
for (int i = 1; i <= m; i++)
{
f[1][i] = f[2][i - 1] + f[n][i - 1];
for (int j = 2; j <= n - 1; j++)
{
f[j][i] = f[j - 1][i - 1] + f[j + 1][i - 1];
}
f[n][i] = f[1][i - 1] + f[n - 1][i - 1];
}
cout << f[1][m];
return 0;
}