动态规划基础
动态规划(Dynamic Program)的基本要素
- 最优子结构->保证子结构计算正确
- 无后效性->保证孩子不会对父亲产生影响
- 状态转移方程->由子状态转移到父状态
如何设置一个合理的状态是动态规划最难的一步,也只能靠题目的积累。
例题1 :爬楼梯
有100级楼梯,每次只能上1级或2级,一共有多少种不同的方法上楼?
d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] dp[i]=dp[i-1]+dp[i-2] dp[i]=dp[i−1]+dp[i−2]
例题2: 摘花生
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) + s [ i ] [ j ] dp[i][j]=max(dp[i-1] [j],dp[i][j-1])+s[i][j] dp[i][j]=max(dp[i−1][j],dp[i][j−1])+s[i][j]
例题3:鸡蛋的硬度
- 题目简述:给出n,m分别代表楼层高度以及鸡蛋个数,问最小的尝试次数来找到鸡蛋的硬度,考虑最坏情况。
- 解释样例:对于100 2的解释: n + ( n − 1 ) + ( n − 2 ) + . . . + 1 ≥ 100 n+(n-1)+(n-2)+...+1\ge100 n+(n−1)+(n−2)+...+1≥100 显然有 n × ( n + 1 ) ÷ 2 ≥ 100 n\times(n+1)\div2\ge100 n×(n+1)÷2≥100 ,解得 n ≥ 14 n\ge14 n≥14
- 思考更展开的问题:我们发现第n颗鸡蛋的作用无非就是帮助第n-1颗鸡蛋缩小范围,不妨根据上面的式子换个角度考虑问题,这个方法的精髓在于始终保持扔了的和没扔的次数指和相同,这样做的好处是充分考虑了最坏情况,(不会透支使用次数来赌)始终把总投掷次数控制在14.再宽泛一点说,对于第n颗鸡蛋使用的次数与留给第n-1颗鸡蛋的次数之和永远相同,相当于平均分。换一个角度理解上面的例子,2颗鸡蛋,14次投掷,最多能测多少层楼,显然是105层(我们已经有了2颗鸡蛋,n次投掷测出的最大楼层数公式 n × ( n + 1 ) ÷ 2 n\times(n+1)\div2 n×(n+1)÷2 ,那么照猫画虎,我们可以推测3颗鸡蛋扔n次,最多能测多少层楼 1 + ( n − 1 ) × n ÷ 2 + 1 + ( n − 2 ) × ( n − 1 ) ÷ 2 + . . . + 1 + 2 × 3 ÷ 2 + 1 + 1 × 2 ÷ 2 + 1 1+(n-1)\times n\div2+1+(n-2)\times (n-1)\div 2+...+1+2\times3\div2+1+1\times2\div2+1 1+(n−1)×n÷2+1+(n−2)×(n−1)÷2+...+1+2×3÷2+1+1×2÷2+1
- 推广!推广:现在这个问题已经基本明了了,我们展示了从2颗鸡蛋推到3颗鸡蛋的过程,换句话说,没有2颗鸡蛋的计算就不会有3颗鸡蛋的计算(最优子结构),同时,3颗鸡蛋的计算不会干扰2颗鸡蛋的计算(无后效性),剩下的事情就是来找递推公式了,我们假设我们已经找到了n-1颗鸡蛋投掷k次的答案 p n − 1 k p^{k}_{n-1} pn−1k ,我们尝试用n-1颗鸡蛋来扩展到n颗鸡蛋,其实已经相当明了了 p n k = 1 + p n − 1 k − 1 + 1 + p n − 1 k − 2 + . . . + 1 + p n − 1 2 + 1 + p n − 1 1 + 1 p^{k}_{n } = 1+p^{k-1}_{n-1}+1+p^{k-2}_{n-1}+...+1+p^{2}_{n-1}+1+p^{1}_{n-1}+1 pnk=1+pn−1k−1+1+pn−1k−2+...+1+pn−12+1+pn−11+1
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
#include <queue>
int dp[102][102];
int main()
{
int n,m;
while (scanf("%d%d",&n,&m)==2) {
memset (dp,0,sizeof(dp));
for (int i=1;i<=n;i++) dp[1][i]=i;
for (int i=2;i<=m;i++) { // i颗鸡蛋,扔j次,能测量的最大楼层
for (int j=1;j<=n;j++) {
dp[i][j]=dp[i-1][j-1]+dp[i][j-1]+1;
}
}
for (int i=1;i<=n;i++) {
if (dp[m][i]>=n) {
std::cout<<i<<std::endl;
break;
}
}
}
return 0;
}
例题4 :舔狗舔到最后一无所有
这道题目便是利用辅助数列的思想来进行求解,由于以前讲过多次,不再过多赘述。
总结
动态规划类似于高中数列,对于一个复杂的数列(称之为答案数列),写出通项公式所需要的数学能力是非常强大的,但是我们可以写出递推公式,如果递推公式仍然难写,那我们便可以构造一些辅助数列来解决,辅助数列可以帮助我们很好的写出答案数列的递推公式,在探索辅助数列的过程中我们也可能会发现有些数列不是那么必须,那么便可以省去。可以泛泛地理解,辅助数列越多,答案数列越好写,但是维护辅助数列的过程就越发地冗余。