动态规划

线性DP&区间DP

一、前言:

动态规划(英语:Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。

二、一般思路:

首先要把原问题分解为若干个子问题,子问题经常与原问题形式相似,只不过规模缩小。
将和子问题相关的各个变量的一组取值称为一个“状态”,一个状态对应于一或多个子问题,所谓该状态下的“”,就是这个状态对应子问题的解。
找出不同状态之间如何迁移,就是如何从一或多个已知值的状态求出另一个状态的值。可以用递推公式表示,即称为“状态转移方程”。
在这里插入图片描述
注意要满足:
(1)问题具有最优子结构性质。(问题的最优解包含的子问题的解也是最优的)
(2)无后效性。(当前若干个状态值一旦确定,则此后过程的演变仅与该值有关,而与之前如何演变到当前若干个状态无关。)

三、线性DP:

  • 数字三角形http://poj.org/problem?id=1163)
    状态转移方程

    从maxSum[N-1]这一行元素开始向上逐层递推。
    (因为maxSum[i][j]在计算出maxSum[i-1][j]后就无用了,所以可以用一维数组代替二维数组节省空间)
    代码实现:

#include <iostream>
#include <algorithm>
using namespace std;
const int MAX=101;
int D[MAX][MAX];
int n;
int *maxSum;
int main(int argc, const char * argv[]) {
    cin >>n;
    for (int i=1; i<=n; i++) {
        for (int j=1; j<=i; j++) {
            cin >>D[i][j];
        }
    }
    maxSum=D[n];
    for (int i=n-1; i>=1; i--) {
        for (int j=1; j<=i; j++) {
            maxSum[j]=max(maxSum[j],maxSum[j+1])+D[i][j];
        }
    }
    cout <<maxSum[1]<<endl;
    return 0;
}
  • 最长上升子序列http://poj.org/problem?id=2533)
    “求序列的前n个元素的最长上升子序列的长度”该子问题不具有无后效性。因此考虑“求以a_k为终点的最长上升子序列的长度”,该子问题仅与数字的位置相关,那么k就是“状态”,k对应的“值”就是所求a_k为终点的最长上升子序列的长度。maxLen(k)表示以a_k为终点的最长上升子序列的长度,有:maxLen(1)=1
    状态转移方程:
    在这里插入图片描述
    代码实现:
#include <iostream>
#include <cstdio>
using namespace std;
const int MAX_N=10000;
int b[MAX_N+10];
int maxLen[MAX_N+10];
int main(int argc, const char * argv[]) {
    int N;
    scanf("%d",&N);
    for (int i=1; i<=N; i++) {
        scanf("%d",&b[i]);
    }
    maxLen[1]=1;
    for (int i=2; i<=N; i++) {
        //每次求以第i个数为终点的最长上升子序列的长度
        int tmp=0;//记录满足条件的,第i个数左边的上升子序列的最大长度
        for (int j=1; j<i; j++) {
            //查看以第j个数为终点的最长上升子序列
            if (b[i]>b[j]) {
                if (tmp<maxLen[j]) {
                    tmp=maxLen[j];
                }
            }
        }
        maxLen[i]=tmp+1;
    }
    int maxL=-1;
    for (int i=1; i<=N; i++) {
        if (maxL<maxLen[i]) {
            maxL=maxLen[i];
        }
    }
    printf("%d\n",maxL);
    
    return 0;
}
  • 最长公共子序列http://poj.org/problem?id=1458)
    如果用字符数组s1、s2存放两个字符串,用s1[i]表示s1中的第i个字符,s2[j]表示s2中的第j个字符(字符编号从1开始,不存在“第0个字符”),用{s1}_i表示s1的前i个字符所构成的子串,{s2}_j表示s2的前j个字符构成的子串, maxLen(i,j)表示s1和s2的最长公共子序列的长度,那么递推关系如下:
    if (i==0||j==0)
        MaxLen(i,j)=0;
    else if(s1[i]==s2[j])
        Maxlen(i,j)=MaxLen(i-1,j-1)+1;
    else
        MaxLen(i,j)=Max(MaxLen(i,j-1),MaxLen(i-1,j));

s1中的位置i和s2中的位置j就是“状态”,maxLen(i,j)就是“值”。状态的数目是s1长度和s2长度的乘积。
代码实现:

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

const int MAX_N=1000;
char str1[MAX_N+10];
char str2[MAX_N+10];
int maxLen[MAX_N+10][MAX_N+10];

int main(int argc, const char * argv[]) {
    while (scanf("%s%s",str1+1,str2+1)>0) {
        int length1=strlen(str1+1);
        int length2=strlen(str2+1);
        int tmp;
        
        for (int i=0; i <=length1; i++) {
            maxLen[i][0]=0;
        }
        for (int i=0; i <=length2; i++) {
            maxLen[0][i]=0;
        }
        for (int i=1; i <=length1; i++) {
            for (int j=1; j <=length2; j++) {
                if (str1[i]==str2[j]) {
                    maxLen[i][j]=maxLen[i-1][j-1]+1;
                } else {
                    int len1=maxLen[i][j-1];
                    int len2=maxLen[i-1][j];
                    if (len1 >len2 )
                        maxLen[i][j]=len1;
                    else
                        maxLen[i][j]=len2;
                }
            }
        }
        printf("%d\n",maxLen[length1][length2]);
    }
    return 0;
}

四、区间DP:

开始一共有n个“大块”,编号从左到右依次为1~n。用 color[i]表示第i个大块的颜色,len[i]表示其包含的方块数目即长度,用 ClickBox(i)表示从大块1到大块i这一段消除后所能得到的最高分,整个问题就是求 Click Box(n)。按照惯常的想法,求ClickBox(i)时,先处理第i个大块。对于大块i,有直接将其消除和留着等待以后消除两种处理方式。对于第一种方式剩下的问题就是求ClickBox(i-1);但是对于第二种方式,原问题的形式已经无法描述拆分得到的子问题,所以无法形成递推关系。

在无法形成递推关系时,考虑把问题的描述形式细化,即描述问题时增加个条件,这样,用来描述问题的函数(即“状态”)参数就会增加一个。例如,考虑用ClickBox(i,j)表示从大块i到大块j这一段消除后所能得到的最高分,则整个问题就是求 ClickBox(1,n)。这里增加的条件就是起点大块。同样的,要求 Clickbox(i,j)时,考虑最右边的大块j,对它有两种处理方式,要取其优者
(1)直接消除它,此时能得到的最高分是 ClickBox(i,j-1)+len[j]×len[j];
(2)保留它,希望以后它能和左边的某个同色大块合并。
左边的同色大块可能有很多个到底和哪个合并最好并不容易知道,因此只能枚举,然后取最优的结果。假设大块j和左边的大块k(i≤k<j-1)合并,此时能得到的最高分为
在这里插入图片描述

上述式子表达的是,要将k+1到j-1这连续的几个大块合并并消去,这样大块j才能和大块k相邻。消去k+1到j-1能得的最高分是ClickBox(k+1,j-1)。然后,将j和k一起消去得分为(len[k]+len[j])^2 。最后,将大块i~k-1都消去,最高得分是 ClickBox(i,k-1)。三者相加就是 ClickBox(i,j),即将大块j和大块k合并的情况下所能得到的最高分。显然这是不对的。因为上面的式子规定了大块k和大块j合并后就要一起消去,但实际上,k和j合并后还可以留着,等待以后和左边的同色大块进一步合并。于是, ClickBox(i,j)这种问题描述方式还是不能形成递推关系。实际上,如果允许状态转移的过程更复杂,状态转移的时间成本更高,则ClickBox(i,)这种描述问题的方式也能形成递推关系。但是,动态规划的核心思想就是要用空间换时间,所以希望能用增加空间的方式降低状态转移的时间成本。为此,可以将问题描述进一步细化,即为 ClickBox()函数再增加一个参数,相应地,记录 ClickBox()计算结果的数组也要增加一维(用更多的空间换时间),则可将问题描述成用ClickBox(i,j,extraLen)表示在大块j的右边已经有一个长度为extraLen的大块(该大块可能是在合并过程中形成的,不妨称其为大块extraLen),且其颜色和大块j相同,在此情况下,将大块i~j以及大块extraLen都消除所能得到的最高分。于是整个问题就是求ClickBox(1,n,0)。
用这种方式描述问题,就可以形成递推关系。假设j和 extraLen合并后的大块称作Q(其长度为len[j]+ extraLen),求ClickBox(i,j,extraLen)时,有以下两种处理方法,取最优者:
(1)将Q直接消除,这种做法能得到的最高分是
在这里插入图片描述
(2)希望Q以后能和左边的某个同色大块合并。需要枚举可能和Q合并的同色大块。假设让大块k和Q合并,则此时能得到的最高分是
ClickBox(k+1,j-1,0)+ClickBox(i,k,len[j]+extraLen
将大块k+1到j-1消去所能得到的最高分是ClickBox(k+1,j-1,0)。消去完成后,大块Q和大块k相邻且两者同色,因为大块Q的长度是len[j]+ extraLen,所以剩下的问题就是求ClickBox(i, k,len[j] +extraLen)。

用程序具体实现时,用“递归+记录计算结果”的方式。即将CliekBox()写成一个递归函数,另外用三维数组元素 score [i][j][k]记录函数 ClickBox(i,j,k)的计算结果。递归的终止条件,就是当i时, ClickBox(i,j, extraLen)的值为(len[i]+ extraLen)2

代码实现:

#include <iostream>
#include <cstring>
#include <algorithm>
const int MAXN=210;
using namespace std;

struct Block{      //表示一个块
    int color;
    int len;
};
struct Block blocks[MAXN];    //存放块的信息
int score [MAXN][MAXN][MAXN];

int ClickBox(int i,int j,int extraLen){
    if (score[i][j][extraLen]!=-1)
        return score[i][j][extraLen];
    int newLen =blocks[j].len+extraLen;
    if (i==j) {
        score[i][j][extraLen] =newLen*newLen;
        return score[i][j][extraLen];
    }
    int sc=ClickBox(i, j-1, 0) +newLen*newLen;     //大块Q单独消去
    for (int k=j-1; k>=i; --k) {                   //枚举可能和大块j合并的大块k
        if (blocks[k].color==blocks[j].color) {
            int tmp=ClickBox(k+1, j-1, 0)+ClickBox(i, k, newLen);
            sc =max(sc, tmp);
        }
    }
    score[i][j][extraLen] =sc;
    return sc;
    
}

int main(int argc, const char * argv[]) {
    int t;
    cin >>t;
    for (int c=1; c<=t; ++c) {
        int n;
        cin >>n;
        int blocksNum=1;  //大块总数
        blocks[1].len=1;
        cin >>blocks[1].color;
        for (int j=2; j<=n; ++j) {
            int color;
            cin >>color;
            if(color ==blocks[blocksNum].color)
                ++blocks[blocksNum].len;
            else{
                ++blocksNum;
                blocks[blocksNum].len=1;
                blocks[blocksNum].color=color;
            }
        }
        memset(score, 0xff, sizeof(score));
        cout <<"Case"<<c<<":"<<ClickBox(1, blocksNum, 0) <<endl;
    }
    
    
    return 0;
}

五、参考资料:

https://en.wikipedia.org/wiki/Dynamic_programming

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值