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步的话,就只有一种方法,假如说我走371步,在走一个2步,这也可以走完这39级台阶,但是这个2步每放一个位置,那就是一种不同的方法这就有38中方法了,然后再想着我可以我可以走351步,再走两个2步,假如这两步在一起的话那就有36种走法,还有就是这2步不在一起,这一共有多少种走法呢?36*35/2种,因为这两个没有差别,所以得考虑到对称性,再看看这个是什么问题,排列组合呀,我像发现新大陆一样就那样稀里糊涂的写着,越到后面越头疼,咋个式子这么长啊,还需要我打上去,最后也是没搞出结果来。

现在想来也是觉得好笑的很。如今仅仅用递归解决这道题也很有局限性了。因为再递归的时候,会重复计算很多子问题,这就大大影响了运算速度,得用一个中间数组保存计算过程中已经算出了子问题的结果,在后面需要用到的时候,直接拿出来用就可以了。

现在有很多问题,不优化就很难解决了,这也是头疼的问题,但是得治,下面弄两个题。

# 题目描述

# 台阶问题

N级的台阶,你一开始在底部,每次可以向上迈最多K级台阶(最少1级),问到达第N级台阶有多少种不同方式。

# 输入格式

两个正整数NK

# 输出格式

一个正整数,为不同方式数,由于答案可能很大,你需要输出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->11->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;
}