【动态规划32讲】第二节 动态规划分类讨论

来源:互联网 发布:抽烟喝酒 知乎 编辑:程序博客网 时间:2024/06/01 07:38

这里用状态维数对动态规划进行了分类:

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)。

原创粉丝点击