关于DP那些事

· · 算法·理论

DP入门

关于动态规划(DP),有一个简单的小故事

A * "1+1+1+1+1+1+1+1 =?" *

A : "上面等式的值是多少"
B : *计算* "8!"

A *在上面等式的左边写上 "1+" *
A : "此时等式的值为多少"
B : *quickly* "9!"
A : "你怎么这么快就知道答案了"
A : "只要在8的基础上加1就行了"
A : "所以你不用重新计算因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"

可以知道动态规划算法的核心是将大问题分解为小问题,记住已经解决过的子问题的解,从而解决主问题

那么我们应该怎样记住主问题呢,有两种方法:

①自顶向下的备忘录法(递归记忆)

②自底向上(递推)

来看一道很经典的DP题:

斐波那契数列是指这样的数列:数列的第一个和第二个数都为 1,接下来每个数都等于前面 2 个数之和。

给出一个正整数 a,要求斐波那契数列中第 a 个数是多少。

这是一个很基础的递推题,甚至递推式都有 : f[ i ]=f[ i-1 ]+f[ i-2 ]

直接上伪代码:

int n,a[31]=[0,1,1],i;//首先定义一个循环变量
cin>>n;// 再定义一个数组记录斐波那契数列的第n项,并且初始化第0项,第1项和第二项。
for (i=3;i<=n;i++){然后一个 for 循环,从第3项开始;
   a[i]=a[i-1]+a[i-2]; //利用递推公式逐步计算每一项的值;
}
cout<<a[n]<<endl;

当然,这只是最简单的递推,但别忘了我们还有递归

int a[31];//记忆化
void fib(int i){
    if(i==1 || i==2) return a[i]=1;//第1项和第2项返回1
    if(i==0) return a[i]=0;//第0项返回0
   if(a[i]!=0) return a[i];//子问题解决过直接返回
    return a[i]=fib(i-1)+fib(i-2);//没有解决过
}

好了,你已经学会DP了,快去刷黑题吧

接下来,是另一道很基础的DP题

楼梯有 N 阶,上楼可以一步上一阶,也可以一步上二阶。
编一个程序,计算共有多少种不同的走法。

第一眼看到这题的童鞋可能会被吓到,但实际上只要简单地想一下:

想要上到底 i 个台阶,我们只能从第 i - 1 和第 i - 2 个台阶上来。也就是说,我们只需要求出上到第 i - 1 层和第 i - 2 层台阶的方案总数,就可以出答案,然后就得出了递推式:

数楼梯 (太水了甚至橙题,代码不贴了) 接下来,又是一道题: 给定一个 n,再给定一个 n 个整数的数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦支付此费用,即可选择向上爬一个或者两个台阶。 可以选择从下标为 i-1 或下标为 i-2 的台阶开始爬楼梯,请计算并返回达到楼梯顶部的最低花费。 由于每次只能爬 1 个或者 2 个台阶,所以 i 这个状态只能从 i-1 或者 i-2 转移过来: 1)如果从 i-1 爬上来,需要的花费就是 cost[i-1] + f[i-1]; 2)如果从 i-2 爬上来,需要的花费就是 cost[i-2] + f[i-2]; 没有其他情况了,而我们要求的是最小花费,所以 i 就应该是这两者的小者,得出状态转移方程: _f [ i ] = min ( f [ i - 1] + cost [ i - 1 ] , f [ i - 2] + cost [ i - 2 ] )_ 最后打出代码就完了 如果你看到了这里,那么恭喜你,这3道题没什么卵用,是DP中最简单的3道题,但我们可以从中总结出一些做动态规划的规律: 1.设计状态 2.写出状态转移方程 3.初始化 4.执行状态转移 5.求解 接下来,就让我们做一道进阶题来找找感觉 ``` 给定一个整数 n ,再给一个长度为 n 的数组 num ,请找到一个连续的最大子树和,并输出最大和 ``` 现在,请你收起你的贪心,并用DP来解决这道题 首先,让我们来理一下状态,而这种状态只有两种可能: 1.第 i 个与第 i - 1 个相连 2.第 i 个与第 i - 1 个不相连(第 i 个为开头和结尾,单独为一个子串) 得到状态转移方程: _dp[i]=max(dp[i-1]+num[i],num[i]);_ 然后代码(自己打吧) 当你看到这里,DP就已经入门啦。