这里用状态维数对动态规划进行了分类:
1.状态是一维的
1.1下降/非降子序列问题:
问题描述: {挖掘题目的本质,一但抽象成这样的描述就可以用这个方法解}
在一个无序的序列a1,a2,a3,a4…an里,找到一个最长的序列满足:ai<=aj<=ak…<=am,且i<j<k…<m.(最长非降子序列)或ai>aj>ak…>am,且i>j>k…>m.(最长下降子序列)。
问题分析:
如果前i-1个数中用到a[k] (a[k]>a[i]或a[k]<=a[i])构成了一个的最长的序列加上第i个数a[i]就是前i个数中用到a[i]的最长的序列了。那么求用到a[k]构成的最长的序列有要求前k-1个数中……
从上面的分析可以看出这样划分问题满足最优子结构,那满足无后效性么?显然对于第i个数时只考虑前i-1个数,显然满足无后效性,可以用动态规划解。
以最长子非降序列为例,我们举例说明,如:
23,54,16,43,67,23,79分别储存在数组input[i]中;
另外再定义一数组flag[i]储存下标从0到i这些元素中存在的最长递增子序列的元素个数。如flag[5]表示从input[0]到input[5]之间存在的最长递增子序列长度。由于flag[0]标识区间中只有input[0],故flag[0]=1;接下来进入循环,再次发觉数学归纳对算法编写起着不可言喻的精妙作用!我们假设flag[0]到flag[i-1]已经求出,接下来求flag[i]。那么flag[i]与flag[0]到flag[i-1]又有什么关联呢?接下来看例子:
input[0]=23——>flag[0]=1;
input[1]=54——>flag[1]=flag[0]+1,这里的前提是input[0]<=input[1];
input[2]=16——>flag[2]=1;
input[3]=43——>flag[3]=flag[0]+1;
input[4]=67——>flag[4]=flag[1]+1,前提是input[1]<=input[4];
...
由以上部分模拟可以归纳出flag[i]的求法:即在input[k]<=input[i]的前提下,其中k<i,找到最大的flag[k],置flag[i]=flag[k]+1即可,代码如下:
flag[0]=1;
for(int j=1;j<n;j++)
for(int k=0;k<j;k++)
{
if(input[k]<=input[j]&&flag[j]<flag[k]+1)
flag[j]=flag[k]+1;
}
然后循环遍历flag[]找到最大的值即是我们要找到的那个数了。本算法的时间复杂度为O(n^2)。
以下为某例代码,重点了解思想与流程:
#include<iostream>
#include<string>
using namespace std;
#define MAX_N 100
int dp[MAX_N],s[MAX_N],path[MAX_N];
void output(int k){ //保存要寻找的序列
if(k==-1) return;
output(path[k]);
cout<<s[k]<<" ";
}
int main(){
int n;
while(cin>>n){
fill_n(dp,n+1,1);
memset(path,-1,sizeof(path)); //掌握这两个函数用法
int i=0,j,k;
while(i<n)
cin>>s[i++];
k=0;
for(i=1;i<n;i++){
for(j=0;j<i;j++){
if(s[i]>s[j] && dp[j]+1>dp[i]){
dp[i]=dp[j]+1;
path[i]=j;
if(dp[k]<dp[i])
k=i;
}
}
}
cout<<"最长公共子序列长度为:"<<dp[k]<<endl<<endl;
cout<<"最长公共子序列为(其中一个):"<<endl;
output(k);
cout<<endl;
}
return 0;
}
类似的题目还有另外一种不容易想到的算法。这种算法本人认为其精巧度与KMP算法有的一比。
参考http://hi.baidu.com/zongwobuwang/blog/item/e48745c479072bc6d10060c5.html
若给出的数据中无相等数据,附上一种STL算法的set容器:
#include<iostream>
#include<set>
using namespace std;
int main()
{
size_t n;
while (cin>>n)
{
set<long> Last;
for(size_t i=0;i<n;i++){
long Temp;
set<long>::iterator Index;
cin>>Temp;
Last.insert(Temp);
Index=Last.find(Temp);
if (++Index!=Last.end()) Last.erase(Index);
}
cout<<Last.size()<<endl;
}
return 0;
}
例题:
拦截导弹
来源:NOIP1999(提高组) 第一题
【问题描述】
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
【输入文件】missile.in
单独一行列出导弹依次飞来的高度。
【输出文件】missile.out
两行,分别是最多能拦截的导弹数,要拦截所有导弹最少要配备的系统数
【输入样例】
389 207 155 300 299 170 158 65
【输出样例】
6
2
【问题分析】
有经验的选手不难看出这是一个求最长下降序列问题,显然标准算法是动态规划。
以导弹依次飞来的顺序为阶段,设计状态opt[i]表示前i个导弹中拦截了导弹i可以拦截最多能拦截到的导弹的个数。
状态转移方程:
flag[0]=1;
flag[i]=flag[k]+1; (input[i]<input[k],0<=k<i)
最大的flag[i]就是最终的解。
这只解决了第一问,对于第二问最直观的方法就是求完一次opt[i]后把刚才要打的导弹去掉,在求一次opt[i]直到打完所有的导弹,但这样做就错了。
不难举出反例: 6 1 7 3 2
错解: 6 3 2/1/7 正解:6 1/7 3 2
其实认真分析一下题就回发现:每一个导弹最终的结果都是要被打的,如果它后面有一个比它高的导弹,那打它的这个装置无论如何也不能打那个导弹了,经过这么一分析,这个问题便抽象成在已知序列里找最长上升序列的问题。
求最长上升序列和上面说的求最长非升序列是一样的,这里就不多说了。
复杂度:时间复杂度为O(N^2),空间复杂度为O(N)。