区间DP
区间动态规划特点
区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来由很大的关系。令状态 \(f(i,j)\) 表示将下标位置 \(i\) 到 \(j\) 的所有元素合并能获得的价值的最大值,那么 \(f(i,j)=max\{ f(i,k)+f(k+1,j)+cost \}\) , \(cost\) 为将这两组元素合并起来的代价。 ——转自 OI Wiki
简单来说,区间动归的一大特点就是,将多个部分整合(或者反过来)。求解的时候,可以将一段区间分成两部分来处理。由于先处理的是区间短的情况,所以就可以递推出完整区间。
状态转移方程
考虑一条链的情况,用 \(dp[i][j]\) 来表示区间 \([i,j]\) 下的最优解(比如,最大值):
- 为了先得到短区间的解,再从中递推出长区间,所以最外层循环应是区间长度的循环
for (int len = 1; len <= n - 1; len++)
- 随后遍历起点
for (int l = 1; l + len <= n; l++)
- 于是终点 \(r=l+len\) ,遍历 \(k(l+1\leqslant k\leqslant r)\) ,\(dp[l][r]\) 可以从 \(dp[l][k-1]+dp[k][r]+cost\) 中更新
for (int k = l + 1; k <= r; k++) dp[l][r] = max(dp[l][r], dp[l][k - 1] + dp[k][r] + cost(l, r);
- 由于是一条链,最长区间肯定是 \([1,n]\) ,输出 \(dp[1][n]\) 就好了
而对于环形的区间DP题,区间不一定是 \([i,j](i\leqslant j)\) ,最后结果的最优解也不一定是 \([1,n]\) ,而可能是 \([3,2]\) ,这对我们的循环造成了麻烦。然鹅这样的区间都是由 \([j,n]\) 和 \([1,i]\) 组成的,如果我们把环拆成链,再在 \(n\) 后接上 \(1\sim n\) 同样的一条链,就能解决问题。
原数据 | 1 | 2 | …… | n | 1 | 2 | …… | n |
---|---|---|---|---|---|---|---|---|
存入数组 | a[1] | a[2] | …… | a[n] | a[n+1] | a[n+2] | …… | a[2n] |
区间 \([j,i](i\leqslant j)\) 等价于 \([j,n+i]\) ,区间正常了,随后的递推和链状DP是一样的。不过最后输出结果的时候还要注意一点,最优解不一定是 \([1,n]\) 了,也有可能是 \([2,n+1]\) 等等,需要用循环来跑一遍。
核心代码
for (int len = 1; len <= n - 1; len++)
for (int l = 1; l + len <= 2 * n; l++)
{
int r = l + len;
for (int k = l + 1; k <= r; k++)
dp[l][r] = max(dp[l][r], dp[l][k - 1] + dp[k][r] + cost(l, r));
}
int ans = 0;
for (int i = 1; i <= n; i++)
ans = max(ans, dp[i][i + n - 1]);
典型例题
链式区间DP
看来对一条链的区间DP过于简单,连道题目都没找到(逃) 打脸了,找一道看吧:P3146 【USACO16OPEN】248 G
#include <bits/stdc++.h>
using namespace std;
int n, ans = 0, dp[250][250];
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%d", &dp[i][i]);
ans = max(ans, dp[i][i]);
}
for (int len = 1; len <= n - 1; len++)
for (int l = 1; l + len <= n; l++)
{
int r = l + len;
for (int k = l + 1; k <= r; k++)
if (dp[l][k - 1] == dp[k][r] && dp[l][k - 1])
dp[l][r] = max(dp[l][r], dp[l][k - 1] + 1);
ans = max(ans, dp[l][r]);
}
printf("%d\n", ans);
return 0;
}
环式区间DP
#include <bits/stdc++.h>
using namespace std;
int n, a[205], sum[205] = {0}, dp_min[205][205] = {0}, dp_max[205][205] = {0};
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%d", a + i);
a[n + i] = a[i];
}
for (int i = 1; i <= 2 * n; i++)
sum[i] = sum[i - 1] + a[i]; //前缀和为 cost 函数做好铺垫
for (int len = 1; len <= n - 1; len++)
for (int l = 1; l + len <= 2 * n; l++)
{
int r = l + len;
dp_min[l][r] = __INT_MAX__;
for (int k = l + 1; k <= r; k++)
{
dp_min[l][r] = min(dp_min[l][r], dp_min[l][k - 1] + dp_min[k][r] + sum[r] - sum[l - 1]);
dp_max[l][r] = max(dp_max[l][r], dp_max[l][k - 1] + dp_max[k][r] + sum[r] - sum[l - 1]);
}
}
int ans_min = __INT_MAX__, ans_max = 0;
for (int i = 1; i <= n; i++)
{
ans_min = min(ans_min, dp_min[i][i + n - 1]);
ans_max = max(ans_max, dp_max[i][i + n - 1]);
}
printf("%d\n%d\n", ans_min, ans_max);
return 0;
}
不适合递推的类型
上面两道题目,从 \(len=1\) 开始递推是比较方便的,但有些题目 \(len=1\) 的时候,并不见得很好处理,反而从 \(len\) 大的情况递归下去会更方便一点。我们常常可以用记忆化搜索来处理这类题目。
来看一道例题:
这道题当 \(len=1\) 的时候,对于不匹配的括号,根本无法处理。所以采用记忆化搜索, \(dfs(l,r)\) 表示搜索区间 \([l,r]\) 中的所有可能结果。
- 若 \(l\) , \(r\) 紧挨着(由于从长的字符串递归而来,这时候紧挨着的两个括号必然是匹配的)
dp[l][r][0][1] = dp[l][r][0][2] = dp[l][r][1][0] = dp[l][r][2][0] = 1;
- 若 \(l\) , \(r\) 中间有其他括号,但 \(l\) , \(r\) 两个括号匹配,则先处理 \([l+1,r-1]\)
dfs(l + 1, r - 1); //做完这一步,l 和 r 之间的括号已解决 for (int i = 0; i <= 2; i++) for (int j = 0; j <= 2; j++) //只要相邻括号不同色就可以 { if (j != 1) dp[l][r][0][1] = (dp[l][r][0][1] + dp[l + 1][r - 1][i][j]) % MOD; if (j != 2) dp[l][r][0][2] = (dp[l][r][0][2] + dp[l + 1][r - 1][i][j]) % MOD; if (i != 1) dp[l][r][1][0] = (dp[l][r][1][0] + dp[l + 1][r - 1][i][j]) % MOD; if (i != 2) dp[l][r][2][0] = (dp[l][r][2][0] + dp[l + 1][r - 1][i][j]) % MOD; }
- 若 \(l\) , \(r\) 不挨着,且不匹配,则先处理 \([l+1,pairing[l]]\) 和 \([pairing[l]+1,r]\) (这里 \(pairing\) 数组记录了括号的匹配情况,用一个栈预处理可得),循环中设四个边界括号的颜色是 \((_i \ \ldots \ )_j (_k \ \ldots \ )_h\)
dfs(l, pairing[l]), dfs(pairing[l] + 1, r); //注意不是 dfs(pairing[r], r) ,反例如 ()()() for (int i = 0; i <= 2; i++) for (int j = 0; j <= 2; j++) for (int k = 0; k <= 2; k++) for (int h = 0; h <= 2; h++) { if (j != 0 && j == k) //紧挨着的两个括号都上过色且颜色一样 continue; dp[l][r][i][h] = (dp[l][r][i][h] + dp[l][pairing[l]][i][j] * dp[pairing[l] + 1][r][k][h] % MOD) % MOD; }
完整代码
#include <bits/stdc++.h>
using namespace std;
//0 — black, 1 — red, 2 — blue
#define MAXLEN 705
#define MOD 1000000007
int len, pairing[MAXLEN];
long long dp[MAXLEN][MAXLEN][3][3] = {0};
char str[MAXLEN];
void dfs(int l, int r)
{
if (l + 1 == r)
{
dp[l][r][0][1] = dp[l][r][0][2] = dp[l][r][1][0] = dp[l][r][2][0] = 1;
return;
}
if (pairing[l] == r)
{
dfs(l + 1, r - 1);
for (int i = 0; i <= 2; i++)
for (int j = 0; j <= 2; j++)
{
if (j != 1)
dp[l][r][0][1] = (dp[l][r][0][1] + dp[l + 1][r - 1][i][j]) % MOD;
if (j != 2)
dp[l][r][0][2] = (dp[l][r][0][2] + dp[l + 1][r - 1][i][j]) % MOD;
if (i != 1)
dp[l][r][1][0] = (dp[l][r][1][0] + dp[l + 1][r - 1][i][j]) % MOD;
if (i != 2)
dp[l][r][2][0] = (dp[l][r][2][0] + dp[l + 1][r - 1][i][j]) % MOD;
}
return;
}
dfs(l, pairing[l]), dfs(pairing[l] + 1, r); //注意不是 dfs(pairing[r], r) ,反例如 ()()()
for (int i = 0; i <= 2; i++)
for (int j = 0; j <= 2; j++)
for (int k = 0; k <= 2; k++)
for (int h = 0; h <= 2; h++)
{
if (j != 0 && j == k) //紧挨着的两个括号都上过色且颜色一样
continue;
dp[l][r][i][h] = (dp[l][r][i][h] + dp[l][pairing[l]][i][j] * dp[pairing[l] + 1][r][k][h] % MOD) % MOD;
}
}
int main()
{
scanf("%705s", str);
len = strlen(str);
stack<int> sta;
for (int i = 0; i < len; i++)
if (str[i] == '(')
sta.push(i);
else
{
pairing[sta.top()] = i;
pairing[i] = sta.top();
sta.pop();
}
dfs(0, len - 1);
int ans = 0;
for (int i = 0; i <= 2; i++)
for (int j = 0; j <= 2; j++)
ans = (ans + dp[0][len - 1][i][j]) % MOD;
printf("%d\n", ans);
return 0;
}