做的第一道DP,是HDU上面的1003
题意是给你个数列,让你求出第一个指定序列的长度和它的起点和终点。(对于数列来说,第一个明显是1)
这个指定序列满足它是母序列的的所有子序列中和最大。
网上主要有三种解法:一:分治;二:DP(插头DP);三:贪心(disgussl里面有人说可用贪心做)
我最近在学习DP,就用DP来解决这一题。
出入DP这道大门,我先到油管上看了MIT的两个关于dynamic programming的公开课,里面的教授讲的还不错,可惜到了后面就听不太懂。
那个高高的男教授一开始是用斐波那契递归引入了DP这个个概念。
对于求解任意地点的斐波那契数,我们首先想的是递归算法;
#include <cstdio>
#include <iostream>
using namespace std;
long long fib(int n){
if(n<3)
return 1;
else{
return fib(n-1)+fib(n-2);
}
}
int main(){
int n;
cin>>n;
cout<<fib(n)<<endl;
return 0;
}
没错,就是上面那个丑的要死版本。
但是,我相信一定有人这么干过。
后面,想到的是,这个程序如果要交到判题机上面的话,铁定爆栈,所以有人用下面这个方法(滚动数组)
#include <stdio.h>
using namespace std;
int main(){
int n;
long long fib[3];
while(~scanf("%d",&n)){
fib[1]=1,fib[2]=1;
for(int i=3;i<=n;++i)
fib[i%3]=fib[(i-1)%3]+fib[(i-2)%3];
printf("%lld\n",fib[n%3]);
}
return 0;
}
用了滚动数组以后,就节省了空间和时间。
油管上的那位教授采用了全新的方法(HUA JI)DP来做了:
准确的来说应该是用记忆化来算:
#include <stdio.h>
#include <cstring>
using namespace std;
long long memo[1000000];
long long k;
long long fib(int n){
if(n<3)
return 1;
if(memo[n]!=0)
return memo[n];
else{
k=fib(n-1)+fib(n-2);
memo[n]=k;
}
return k;
}
int main(){
int n;
while(~scanf("%d",&n)){
memset(memo,0,sizeof(memo));
long long ans=fib(n);
printf("%lld\n",ans);
}
return 0;
}
这个就和他写的差不多。这段代码采用了memo数组保存了在递归过程中已经计算过了的值。这样做的好处就在于免去了大量的重复计算,从而节约了时间。
在《算法导论》的动态规划这一章中,作者用切钢条的例子,向我们展示了DP这个算法的产生和它的两种解法。
但在这之前,我们要明白什么样的问题适用于DP呢?
有两个原则:
一:最优性原理
不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略”。(百度百科)
就是说,原问题可以分解为多个子问题,子问题的最优解一定是原问题的最优解。
二:无后效性
就是说原问题的最优解只取决于子问题的值(最优解),而与子问题的状态无关。
简单来说,无论子问题是那条路走过来的,原问题都毫不在乎,他只关心子问题的结果(无情)。
DP两种思路:
一:TOP->DOWN
即自顶向下,从原问题出发,利用递归和记忆化保存中间变量,向下求解子问题,最后回溯,合并,得到答案。
二:DOWN->TOP
自底向上,从子问题出发,一般会用到一个for 循环,牵涉到一个叫做“状态转移方程”的东东,这个东东不简单啊,你要去推导它。
假设我有一个函数叫 f(P),P是关于n维变量的一个集(x,y,z...),在这个集中,我把这些元素叫做一个状态,对应一个值(最优解,要计算)。
我还有一个决策函数Ф(P),P的定义同上,这个函数对应在P的状态下,我做的决策(选择的分支),那么一般的状态转移方程是{f(Pn)=max/min(f(Pn+1),f(Pn)),Ф(P)<-f(P)}