百度百科说:动态规划是运筹学的一个分支,是求解决策过程最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程的优化问题时,提出了著名的最优化原理,把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。
适用条件
任何思想方法都有一定的局限性,超出了特定条件,它就失去了作用。同样,动态规划也并不是万能的。适用动态规划的问题必须满足最优化原理和无后效性。
1.最优化原理(最优子结构性质) 最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
2.无后效性将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
3.子问题的重叠性 动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。
状态转移方程,是动态规划中本阶段的状态往往是上一阶段状态和上一阶段决策的结果。如果给定了第K阶段的状态Sk以及决策uk(Sk),则第K+1阶段的状态Sk+1也就完全确定。
通常情况下,在确定一个题需要用到动规时,我们要先确定出他的状态转移方程。根据状态转移方程写出代码。
一定要注意边界条件。状态转移方程要从第二层开始用。
来看几个栗子;
小浣熊松松特别喜欢交朋友,今年松松生日,就有N个朋友给他送礼物。可是要把这些礼物搬回家是一件很困难的事,具体来说,如果松松一次搬运x件礼物,就要花费w[x]的体力(显而易见,有w[x]<=w[x+1],搬得越多耗费体力越多)。松松并不在意他会搬多少次,但是他想知道,自己最少花费多少体力,就可以把礼物全部搬回家。
#include<iostream> #include<cstdio> using namespace std; int main() { int a[15111],n; cin>>n; for(int i=1;i<=n;++i) cin>>a[i]; for(int i=1;i<=n;++i) for(int j=1;j<i;++j) a[i]=min(a[i-j]+a[j],a[i]); cout<<a[n]; return 0; }
a[i]记录运送i件礼物最小体力花费,初值为输入值,通过递推计算出N件礼物最小体力花费。
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
#include<iostream> #include<algorithm> using namespace std; int n=1; int a[1005]; int dp[1005]; int xulie1() { for(int i=1;i<=n;i++) for(int j=1;j<i;j++) if(a[j]>a[i]) dp[i]=max(dp[j]+1,dp[i]); int maxl=-1; for(int i=1;i<=n;++i) maxl=max(maxl,dp[i]); return maxl; } int xulie2() { for(int i=1;i<=n;i++) dp[i]=1; for(int i=1;i<=n;i++) for(int j=1;j<i;j++) if(a[j]<=a[i]) dp[i]=max(dp[j]+1,dp[i]); int maxl=-1; for(int i=1;i<=n;++i) maxl=max(maxl,dp[i]); return maxl; } int main() { while(cin>>a[n]) n++; cout<<xulie1()<<endl; cout<<xulie2(); }
例3:codevs 5294 挖地雷传送门
在一个地图上有N个地窖(N<=20),每个地窖中埋有一定数量的地雷。同时,给出地窖之间的连接路径。当地窖及其连接的数据给出之后,某人可以从第一个地窖开始挖地雷,然后可以沿着指出的连接往下挖(仅能选择一条路径),当无连接时挖地雷工作结束。设计一个挖地雷的方案,使某人能挖到最多的地雷。
思路:由于可以从任何一个地窖开始挖,所以要从每个点开始递推;
#include<iostream> using namespace std; int s[15111],num[15111]; int n; bool sum[15111][15111]; int path[15111]; int main() { cin>>n; for(int i=1;i<=n;++i) cin>>s[n]; for(int i=1;i<=n;++i) for(int j=i+1;j<=n;++j) cin>>sum[i][j]; for(int i=n;i>=1;--i) for(int j=i;j<=n;++j) if(sum[i][j]) { if(num[i]<num[j]+s[i]) { num[i]=num[j]+s[i]; path[i]=j; } } else num[i]=max(num[i],s[i]); int maxl=0,k=0; for(int i=1;i<=n;++i) if(num[i]>maxl) { maxl=num[i]; k=i; } int qu=k; while(k!=0) { cout<<k<<' '; k=path[k]; } cout<<endl; cout<<maxl; }
有n堆石子排成一列,每堆石子有一个重量w[i], 每次合并可以合并相邻的两堆石子,一次合并的代价为两堆石子的重量和w[i]+w[i+1]。问安排怎样的合并顺序,能够使得总合并代价达到最小。
思路:递推和记忆化搜索
#include<iostream> #include<cstring> using namespace std; int dp[1005][1005]; int s[1005]; int n; int main() { s[1]=0; cin>>n; for(int i=1;i<=n;++i) { int sum; cin>>sum; s[i]=s[i-1]+sum; } memset(dp,0x3f,sizeof(dp)); for(int i=1;i<=n;++i)dp[i][i]=0; for(int i=n;i>0;--i) for(int j=i;j<=n;++j) for(int k=i;k<j;++k) dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]); cout<<dp[1][n]; return 0; }