线性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;
}