一.概述
区间DP也属于线性DP中的一种,它以“区间长度”作为DP的“阶段”,使用两个坐标(区间的左、右端点)描述每个维度。在区间DP中,一个状态由若干个比它更小且包含于它的区间所代表的状态转移而来,因此区间DP的决策往往就是划分区间的方法。区间DP的初态一般就由长度为1的“元区间”构成。
把大区间划分成小区间,应该是问题的精髓
二.题目
http://222.180.160.110:1024/contest/629
http://222.180.160.110:1024/contest/597
删除字符串
题目描述
给出一个长度为n的字符串,每次可以删除一个字母相同的子串,问最少需要删多少次。 数据规模:n <= 500
输入格式
第1行:1个整数,表示字符串的长度
第2行:n个字符的字符串
输出格式
第1行:1个整数,表示答案
样例
样例输入
5
abaca
样例输出
3
解析:我们考虑割点
设 d p [ i ] [ j ] dp[i][j] dp[i][j]表示删去i到j的最小步数
- 若 a [ i ] = = b [ j ] , 则 d p [ i ] [ j ] = d p [ i + 1 ] [ j − 1 ] a[i]==b[j],则dp[i][j]=dp[i+1][j-1] a[i]==b[j],则dp[i][j]=dp[i+1][j−1]
- 若 a [ i ] ! = b [ j ] , 则 d p [ i ] [ j ] = m i n ( d p [ i + 1 ] [ j ] , d p [ i ] [ j − 1 ] ) + 1 a[i]!=b[j],则dp[i][j]=min(dp[i+1][j],dp[i][j-1])+1 a[i]!=b[j],则dp[i][j]=min(dp[i+1][j],dp[i][j−1])+1
然而对于数据aabb,显然aa、bb分别为一组,而上述代码的答案是3,错误 - 考虑割点: d p [ i ] [ j ] = m i n ( d p [ i ] [ j ] , d p [ i ] [ k ] + d p [ k ] [ j ] − 1 ) , i + 1 < = k < = j − 1 dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]-1),i+1<=k<=j-1 dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]−1),i+1<=k<=j−1
无论怎样我们都要考虑第3点,这样才是完整的。
法二:
dp[i][j]=dp[i+1][j]+1
for(int k=i+1;k<=j;k++) {
if(a[i]==a[k]) dp[i][j]=min(dp[i][j],dp[i+1][k-1]+dp[k][j]);
}
这里考虑左端点i,有两种情况:一是直接消i,二是i与其他点拼成一个点,但本质仍是割点。
小优化:初始序列中连续的相同的数可以看成一个点
代码一:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 505;
char c[maxn];
int n, cnt, a[maxn], dp[maxn][maxn];
int main() {
scanf("%d", &n);
scanf("%s", c);
for (int i = 0; i < n; i++) {
if (i == 0 || c[i] != c[i - 1])
a[++cnt] = c[i];
}
for (int i = 1; i <= cnt; i++) dp[i][i] = 1;
for (int len = 2; len <= cnt; len++) {
for (int i = 1; i <= cnt - len + 1; i++) {
int j = i + len - 1;
if (a[i] == a[j])
dp[i][j] = dp[i + 1][j - 1] + 1;
else
dp[i][j] = min(dp[i + 1][j], dp[i][j - 1]) + 1;
for (int k = i + 1; k <= j; k++) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] - 1);
}
}
}
printf("%d", dp[1][cnt]);
}
代码二:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 505;
char c[maxn];
int n, cnt, a[maxn], dp[maxn][maxn];
int main() {
scanf("%d", &n);
scanf("%s", c);
for (int i = 0; i < n; i++) {
if (i == 0 || c[i] != c[i - 1])
a[++cnt] = c[i];
}
for (int i = 1; i <= cnt; i++) dp[i][i] = 1;
for (int len = 2; len <= cnt; len++) {
for (int i = 1; i <= cnt - len + 1; i++) {
int j = i + len - 1;
dp[i][j] = dp[i + 1][j] + 1;
for (int k = i + 1; k <= j; k++) {
if (a[i] == a[k])
dp[i][j] = min(dp[i][j], dp[i + 1][k - 1] + dp[k][j]);
}
}
}
printf("%d", dp[1][cnt]);
}
[CQOI2007]涂色
解析:同上题,代码一样,只不过要转化一下:本题相当于从最终序列去还原成0的序列(全部删完),是逆向思维,请读者仔细思考
CF607B Zuma
思路:仍然是分类讨论,看i的消除方式
- 若a[i]==a[j],则dp[i][j]=dp[i+1][j-1],[i,j]一起消除
- 若a[i]==a[j],dp[i][j]=dp[i+1][j]+1,i单独消除
- 枚举k,若 a[i]==a[k],则[i,k]可能一起消除,dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]);
#include <cstdio>
#include <cstring>
#include <algorithm>
#define ll long long
using namespace std;
const int maxn = 505;
int n, a[maxn], dp[maxn][maxn];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) dp[i][i] = 1;
for (int len = 2; len <= n; len++) {
for (int i = 1; i <= n - len + 1; i++) {
int j = i + len - 1;
if (a[i] == a[j]) {
if (i + 1 == j)
dp[i][j] = 1;
else
dp[i][j] = dp[i + 1][j - 1];
} else
dp[i][j] = dp[i + 1][j] + 1;
for (int k = i; k <= j; k++) {
if (a[i] == a[k])
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j]);
}
}
}
printf("%d", dp[1][n]);
}
两只兔子
解析:枚举断点k,先求出 [i,j] 的最大回文长度,ans=max(ans,dp[1][i]+dp[i+1][n])
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
const int maxn=2005;
int n,a[maxn],dp[maxn][maxn];
void read(int &x) {
char c=getchar();
while(c<'0'||c>'9') c=getchar();
while(c>='0'&&c<='9') {
x=x*10+c-'0';c=getchar();}
}
int main() {
while(scanf("%d",&n)&&n) {
int ans=0;
for(int i=1;i<=n;i++) {
a[i]=0;
read(a[i]);
}
for(int i=1;i<=n;i++) dp[i][i]=1;
for(int len=2;len<=n;len++) {
for(int i=1;i<=n-len+1;i++) {
int j=i+len-1;
if(a[i]==a[j]) dp[i][j]=dp[i+1][j-1]+2;
else dp[i][j]=max(dp[i+1][j],dp[i][j-1]);
}
}
for(int i=1;i<=n;i++) ans=max(ans,dp[1][i]+dp[i+1][n]);
printf("%d\n",ans);
}
}
括号涂色
解析:分类讨论想到了,区间端点颜色也想到了,就是没有预处理左括号 l l l的对应位置 m a t c h [ l ] match_[l] match[l](可以证明是唯一的),然后综合能力不够,导致wa一片。。。
解决此类问题,首先要定义好状态,才能不重不漏,然后分类讨论,状态转移方程一定不能重复,若只有一种转移方式,可以预处理出来。然后就是记得取模。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int mul = 1e9 + 7;
char s[800];
int lens;
int match_[800];
int stack_[800];
long long f[800][800][3][3];
long long ans;
void dp(int x, int y) {
if (x + 1 == y) {
f[x][y][0][1] = f[x][y][0][2] = 1LL;
f[x][y][1][0] = f[x][y][2][0] = 1LL;
return;
}
if (match_[x] == y) {
dp(x + 1, y - 1);
for (int i = 0; i <= 2; ++i)
for (int j = 0; j <= 2; ++j) {
if (i != 1)
f[x][y][1][0] = (f[x][y][1][0] + f[x + 1][y - 1][i][j]) % mul;
if (j != 1)
f[x][y][0][1] = (f[x][y][0][1] + f[x + 1][y - 1][i][j]) % mul;
if (i != 2)
f[x][y][2][0] = (f[x][y][2][0] + f[x + 1][y - 1][i][j]) % mul;
if (j != 2)
f[x][y][0][2] = (f[x][y][0][2] + f[x + 1][y - 1][i][j]) % mul;
}
} else {
int pair_ = match_[x];
dp(x, pair_);
dp(pair_ + 1, y);
for (int i = 0; i <= 2; ++i)
for (int j = 0; j <= 2; ++j)
for (int k = 0; k <= 2; ++k)
for (int l = 0; l <= 2; ++l)
if (!((k == 1 && l == 1) || (k == 2 && l == 2)))
f[x][y][i][j] =
(f[x][y][i][j] + (f[x][pair_][i][k] * f[pair_ + 1][y][l][j]) % mul) % mul;
}
}
void get_match() {
lens = strlen(s + 1);
int top_ = 0;
for (int i = 1; i <= lens; ++i)
if (s[i] == '(')
stack_[++top_] = i;
else {
match_[i] = top_;
match_[stack_[top_]] = i;
--top_;
}
}
int main() {
scanf("%s", s + 1);
get_match();
memset(f, 0, sizeof(f));
dp(1, lens);
ans = 0LL;
for (int i = 0; i <= 2; ++i)
for (int j = 0; j <= 2; ++j) ans = (ans + f[1][lens][i][j]) % mul;
printf("%lld", ans);
return 0;
}