动态规划之基础DP专题

来源:互联网 发布:java 射击游戏 编辑:程序博客网 时间:2024/06/07 01:31

动态规划(英语:Dynamicprogramming,DP)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。

动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。 通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。

动态规划问题满足三大重要性质:

最优子结构性质:

如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。

子问题重叠性质:

子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。

无后效性:

将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。


【SSOI543】最长上升子序列 LIS

【在线测试提交传送门】

【问题描述】

一个数的序列bi,当b1 < b2 <... <bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, ..., aN),我们可以得到一些上升的子序列(ai1, ai2, ..., aiK),这里1 ≤ i1 < i2 < ... < iK ≤ N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8).你的任务,就是对于给定的序列,求出最长上升子序列的长度。

【输入格式】

输入的第一行是序列的长度N (1≤N≤1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到10000。

【输出格式】

最长上升子序列的长度。

【输入样例1】

71 7 3 5 9 4 8

【输出样例1】

4

【解题思路1】

dp[i]表示以ai为末尾的最长上升子序列的长度,而以ai结尾的最长上升子序列有两种:1.只包含ai的子序列;  2.在满足j<i且aj<ai的以aj为结尾的上升子序列末尾,追加上ai得到的子序列。所以有如下递推关系:dp[i]=max{1,dp[j]+1|j<i且aj<ai} 算法的时间复杂度为O(n^2)
#include <bits/stdc++.h>using namespace std;#define maxn 1000+10int a[maxn],f[maxn];int main(){  int n;  cin>>n;  for (int i=1;i<=n;i++) cin>>a[i];  for (int i=1;i<=n;i++) f[i]=1;  for (int i=2;i<=n;i++)    for (int j=1;j<i;j++)      if (a[j]<a[i]) f[i]=max(f[i],f[j]+1);  int ans=1;  for (int i=1;i<=n;i++) ans=max(ans,f[i]);  cout<<ans;  return 0;}

【解题思路2】

使用O(n^2)算法提交后,我们发现最后两个点超时,这里介绍O(nlogn)算法。设dp[i]表示以i为结尾的最长递增子序列的长度,则状态转移方程为:dp[i] = max{dp[j]+1}, 1≤j<i,a[j]<a[i].考虑两个数a[x]和a[y],x<y且a[x]<a[y],且dp[x]=dp[y],当a[t]要选择时,到底取哪一个构成最优的呢?显然选取a[x]更有潜力,因为可能存在a[x]<a[z]<a[y],这样a[t]可以获得更优的值。在这里给我们一个启示,当dp[t]一样时,尽量选择更小的a[x].按dp[t]=k来分类,只需保留dp[t]=k的所有a[t]中的最小值,设g[k]记录这个值,g[k]=min{a[t],dp[t]=k}。 这时注意到g的两个特点(重点):1. g[k]在计算过程中单调不升;           2. g数组是有序的,g[1]<g[2]<..g[n]。利用这两个性质,可以很方便的求解:(1).设当前已求出的最长上升子序列的长度为len(初始时为1),每次读入一个新元素x:(2).若x>g[len],则直接加入到d的末尾,且len++;(利用性质2)   否则,在g中二分查找,找到第一个比x小的数g[k],并g[k+1]=x,在这里x≤g[k+1]一定成立(性质1,2)。
//解题思路2参考代码:#include<bits/stdc++.h>#define INF 2147483647using namespace std;int read(){    int x=0,f=1;char ch=getchar();    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}    return x*f;}int n,tot=1,a[2000000],f[2000000];int find(int m,int num){    int l=1,r=m,maxn;    while(r>l){        int mid=(l+r)>>1;        if(num>f[mid]) l=mid+1;        else r=mid;    }    return l;}int main(){    n=read();    for(int i=1;i<=n;i++) a[i]=read();    f[1]=a[1];    for(int i=2;i<=n;i++){        if(a[i]>f[tot]) f[++tot]=a[i];        else f[find(tot,a[i])]=a[i];    }    printf("%d",tot);    return 0;}
//使用lower_bound实现二分#include <bits/stdc++.h>using namespace std;#define INF 0x3f3f3fint dp[1000000+10];//dp[i]表示长度为i+1的子序列末尾元素最小值; int a[1000000+10];int read(){    int x=0,f=1;char ch=getchar();    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}    return x*f;}int main(){    int n;    n=read();    for(int i=0;i<n;i++)     {            a[i]=read();            dp[i]=INF;//不可以用memset对数组赋值INF,只能赋值0或-1;     }    for(int i=0;i<n;i++)        {            *lower_bound(dp,dp+n,a[i])=a[i];//找到>=a[i]的第一个元素,并用a[i]替换;         }        printf("%d\n",lower_bound(dp,dp+n,INF)-dp);//找到第一个INF的地址减去首地址就是最大子序列的长度;     return 0;}

【SSoi554】渡轮问题

【在线测试提交传送门】

【问题描述】

  Palmia河在某国从东向西流过,并把该国分为南北两个部分。河的两岸各有n个城市,且北岸的每一个城市都与南岸的某个城市是友好城市,而且对应的关系是一一对应的。    现在要求在二个友好城市之间建立一条航线,但由于天气的关系,所有航线都不能相交,因此,就不可能给所有的友好城市建立航线。     问题:当城市个数和友好关系建立以后,选择一种修建航线的方案,能建最多的航线而不相交。

这里写图片描述

【输入格式】

第一行:N,表示有N个城市;(N≤10000)第二行:N个没有重复自然数(每个自然的值≤10000);表示南岸的N个城市的编号.(北岸的城市按自然数顺序从1开始编号),相邻两个数间用空格隔开

【输出格式】

一个整数m,表示最多能建的航线数量.

【输入样例1】

 75 7 2 1 3 6 4

【输出样例1】

3

【解题思路】

LIS

【Hdu1257】最少拦截系统

【在线测试提交传送门】

【问题描述】

  N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形。合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1,2…,K,他们的身高分别为T1,T2,…,TK, 则他们的身高满足T1<...<Ti>Ti+1>…>TK(1≤i≤K)。  你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。

【输入格式】

输入文件chorus.in的第一行是一个整数N(2≤N≤100),表示同学的总数。第一行有n个整数,用空格分隔,第i个整数Ti(130≤Ti≤230)是第i位同学的身高(厘米)。

【输出格式】

输出文件chorus.out包括一行,这一行只包含一个整数,就是最少需要几位同学出列。

【输入样例1】

8186 186 150 200 160 130 197 220

【输出样例1】

4

【数据范围】

对于50%的数据,保证有n≤20;对于全部的数据,保证有n≤100。

【解题思路】

枚举中间点,左侧求最长上升序列,右侧求最长下降序列。
#include<bist/stdc++.h>using namespace std;const int MAX = 100005;const int INF = 0x3f3f3f3fint a[MAX];int dp[MAX];int n;int main(){    cin >> n;    for(int i=1; i<=n; i++){        cin >> a[i];    }    a[0] = 0;            //初始化,空段     a[n+1] = 0;    int ans = INF;    //枚举中间点    for(int i=1; i<=n; i++){        memset(dp, 0, sizeof(dp));        int cnt = 0;        //左边为上升序列        for(int j=1; j<=i; j++){            for(int k=0; k<j; k++){                if(a[k] < a[j]){                    dp[j] = max(dp[j], dp[k]+1);                }            }        }        cnt += i - dp[i];        //左边出队的人数         //右边为下降序列        dp[i] = 0;                //注意清零         for(int j=n; j>=i; j--){            for(int k=n+1; k>j; k--){                if(a[k] < a[j]){                    dp[j] = max(dp[j], dp[k]+1);                }            }        }        cnt += n - i + 1 - dp[i];        //右边出队的人数         ans = min(ans, cnt);        //取最小值     }     cout << ans;    return 0;}

【Hdu1257】最少拦截系统

【在线测试提交传送门】

【问题描述】

  某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统.但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能超过前一发的高度.某天,雷达捕捉到敌国的导弹来袭.由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。怎么办呢?多搞几套系统呗!你说说倒蛮容易,成本呢?成本是个大问题啊.所以俺就到这里来求救了,请帮助计算一下最少需要多少套拦截系统。

【输入格式】

输入若干组数据(≤10),每组数据包括:导弹总个数(正整数,≤1000),导弹依此飞来的高度(雷达给出的高度数据是不大于30000的正整数,用空格分隔)

【输出格式】

对应每组数据输出拦截所有导弹最少要配备多少套这种导弹拦截系统。

【输入样例】

8 389 207 155 300 299 170 158 65

【输出样例】

2

【解题思路1】

贪心:对于当前的导弹来说,如果我当前所有的拦截系统都不能挡住他,那只能新开一个拦截系统。否则将该导弹加到一套可行的系统之后。
#include <bits/stdc++.h> using namespace std;int read(){    int x=0,f=1;char ch=getchar();    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}    return x*f;}int main(){    int N;    int t;    int m;    while(N=read())    {        t=1;//记录非严格递减数组个数        int pa[10001];        pa[1]=30000+1;         for (int i=1;i<=N;i++)        {            m=read();            bool flag=0;            for(int j=1;j<=t;j++)            {                if(m<=pa[j])//如果 m 小于数组尾数更新尾数                {                    pa[j]=m;                    flag=1;                    break;                }            }            if(!flag)   pa[++t]=m; //数组第一个尾元素赋值为m        }        printf("%d\n",t);    }    return 0;}

【解题思路2】

LIS:最长上升子序列的长度就是不下降子序列的个数。为什么?请自行推导。
#include <bits/stdc++.h>using namespace std;#define INF 0x3f3f3fint dp[10000+10];//dp[i]表示长度为i+1的子序列末尾元素最小值; int a[10000+10];int read(){    int x=0,f=1;char ch=getchar();    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}    return x*f;}int main(){    int n;    while (cin>>n){    for(int i=0;i<n;i++)     {            a[i]=read();            dp[i]=INF;//不可以用memset对数组赋值INF,只能赋值0或-1;     }    for(int i=0;i<n;i++)        {            *lower_bound(dp,dp+n,a[i])=a[i];//找到>=a[i]的第一个元素,并用a[i]替换;         }        printf("%d\n",lower_bound(dp,dp+n,INF)-dp);//找到第一个INF的地址减去首地址就是最大子序列的长度;     }    return 0;}

【Hdu1160】FatMouse’s Speed

【在线测试提交传送门】

【问题描述】

  胖老鼠坚信,长得越胖跑得越快。为了证明他,给出一系列老鼠的体重和速度的数据,求一个最长的序列,老鼠的体重递增,同时速度递减。

【输入格式】

 输入不错过1000只老鼠的体重和速度; 每只老鼠的数据各占一行,包含两个整数,体重和速度,在[1,10000]范围内; 两只老鼠的体重可能相同,速度可能相同,甚至体重和速度均相同。

【输出格式】

第一行,一个整数n,表示最长长度;接下来n行,每行一个整数m[i],表示老鼠的序号,满足W[m[1]] < W[m[2]]  < ...  < W[m[n]]而且S[m[1]]  > S[m[2]] > ... > S[m[n]]输出可能有多种方案,输出其中的任意一种即可。

【输入样例】

6008 13006000 2100500 20001000 40001100 30006000 20008000 14006000 12002000 1900

【输出样例】

44597

【解题思路1】

先按体重递增进行排序,然后按照体重找到最长递减子序列即可。状态f[i]表示前i个老鼠中的最长递减子序列长度,状态转移方程为f[i] = max{f[j], mice[j].speed > mice[i].speed} + 1, 最后找出最大的f[i]即可。注意此题还需要输出找到的序列中的老鼠的最原始的标号,因此不仅要在刚开始的时候把每个老鼠的最初的序号记下来,还要在进行状态转移的时候把当前的老鼠的位置标记下来。
#include <bits/stdc++.h>using namespace std;struct node{    int w, s, pos;    bool operator<(node x)    {        return w < x.w;    }}mice[1005];int dp[1005], pre[1005];//dp[i]表示前i只老鼠的最长下降子序列void print(int pos){    if(pos == -1)return ;    print(pre[pos]);    printf("%d\n", mice[pos].pos);}int main(){    int n = 0, w, s;    memset(pre, -1, sizeof(pre));    while(scanf("%d%d", &w, &s)!=EOF)    {        mice[n].w = w;        mice[n].s = s;        mice[n].pos = n+1;        n++;    }    sort(mice, mice+n);    int pos = -1, mx = 0;    for(int i = 0; i < n; i++){        dp[i] = 1;        for(int j = 0; j < i; j++){            if(mice[j].w < mice[i].w && mice[j].s > mice[i].s){                if(dp[j]+1 > dp[i]){                    dp[i] = dp[j]+1;                    pre[i] = j;                }            }        }        if(dp[i] > mx){            mx = dp[i];            pos = i;        }    }    printf("%d\n", mx);    print(pos);    return 0;}

【Hdu5773】The All-purpose Zero

【在线测试提交传送门】

【问题描述】

  给定一个长度为n个整数序列S,你可以将序列中的0转换为任意整数,每个0转换成的整数不一定要相同。求该序列的最长上升子序列。

【输入格式】

第一行,一个整数T(T≤10),表示有T组测试数据,对于每组测试数据:第一行,一个整n;第二行,n个整数。

【输出格式】

【输入样例】

272 0 2 1 2 0 561 2 3 3 0 0

【输出样例】

55

【解题思路】

  0可以转化成任意整数,包括负数,显然求LIS时尽量把0都放进去必定是正确的,因为0放不进去的原因无非就是非严格递增。  为了保证严格递增,我们可以将每个权值S[i]减去i前面0的个数,再做LIS,就能保证结果是严格递增的。因此我们可以把0拿出来,对剩下的做O(nlogn)的LIS,统计结果的时候再算上0的数量。
#include<bits/stdc++.h> using namespace std;typedef long long LL;const int N=1000005;int a[N], cnt, num,n;int dp[N],ans;void work(){      for(int i=0;i<=cnt;i++) dp[i]=10000000;       int pos=0;  ans=0;       for(int i=1;i<=cnt;i++)       {               pos=lower_bound(dp,dp+i,a[i])-dp;               if(dp[pos]>a[i]) dp[pos]=a[i];               ans=max(ans,pos+1);       }}int main(){    int T=0, cas=0,x;    scanf("%d",&T);    while(T--)    {           scanf("%d",&n);           cnt=num=0;           for(int i=1;i<=n;i++)           {                  scanf("%d",&x);                  if(x==0) num++; else a[++cnt]=x-num;           }           work();           printf("%d\n",ans+num);    }}
原创粉丝点击