区间 DPDP 顾名思义为在区间上进行的 DPDP ,常先计算出小区间,尔后从小区间逐渐并至大区间,得终。
对于区间 DPDP 而言,其结构往往大同小异,伪代码如下:
for(int i = 1; i <= n; ++i) { //枚举区间长度 for(int l = i; l <= n - i + 1; ++l) { //枚举左端点 int r = l + i - 1; for(int k = l; k <= r; ++k) { //枚举分割点 f[l][r] = f[l][k] + f[k + 1][r] // 合并区间 + bulabula //这里视题目不同往往也有变化,往往是合并区间的“代价”,“收益”等等。 } } }
还有一种写法:
for(int l = n; l >= 1; --l) { //枚举左端点 for(int r = l + 1; r <= n; ++r) { //右端点 for(int k = l + 1; k < r; ++k) { //枚举分割点 f[l][r] = f[l][k] + f[k + 1][r] + bulabula// 合并区间 } }
为什么这样子是可以的呢?
考虑我们对于左端点是从右往左枚举的,当我们拿到一个新的左端点,位于它右边的所有状态都是已经被计算过的,那么显然可以正常进行状态转移。
对于区间 DPDP 的题目,不论是状态设计亦或是状态转移,均需把握好从小区间到大区间的过渡,以及转移时的代价,此之为区间 DPDP 之难点所在。
Example~first :Example first:
给定数列 aa ,每次去掉一个数(要求既不是最左边的数也不是最右边的数),打完后这个数就消失了,而得到的价值是左边第一个还没去掉的数 *∗ 右边第一个还没去掉的数 *∗ 自己的值。
首先,设计状态,按照区间 DPDP 的传统,设 f[i][j]f[i][j] 为合并了 ii 到 jj 的区间得到的最大值。
如何转移呢?
对于每次合并的价值,考虑在我们枚举每个分割点的时候,对于 ll 到 rr 的区间,我们以 kk 为分割点,我们先合并了 ii到 kk 再合并 kk 到 ii ,那么当我们合并的时候,显然是这样一种状态: kk 左边的第一个数是 l - 1l−1 ,右边第一个数是 r + 1r+1 , 贡献为 a[l - 1] * a[k] * a[r + 1]a[l−1]∗a[k]∗a[r+1]
那么转移方程也就得到了: f[i][j]=max(f[i][j],f[i][k-1]+f[k+1][j]+a[k]*a[i-1]*a[j+1]);f[i][j]=max(f[i][j],f[i][k−1]+f[k+1][j]+a[k]∗a[i−1]∗a[j+1]);
完整代码:
/* Time:2018年12月8日20:57:02 Author:SpXace Place:lzyz Type: From: Input: Output: */ #include<iostream> #include<algorithm> #include<cstdio> #include<cstring> #define rep(i, a, b) for(int i = (a); i <= (b); ++i) #define per(i, a, b) for(int i = (a); i >= (b); --i) #define clr(a, b) memset(a, b, sizeof(a)) #define int long long using namespace std; inline int read() { int s = 0, w = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') w = -1; ch = getchar(); } while(ch >= '0' && ch <= '9') s = (s << 1) + (s << 3) + (ch ^ 48), ch = getchar(); return s * w; } const int N = 5050; int n, a[N], f[N][N]; signed main() { n = read(); rep(i, 1, n) a[i] = read(); rep(i, 2, n - 1) f[i][i] = a[i - 1] * a[i] * a[i + 1]; rep(i, 2, n) { rep(l, 1, n - i + 1) { int r = l + i - 1; rep(k, l, r) f[l][r] = max(f[l][r], f[l][k - 1] + f[k + 1][r] + a[k] * a[l - 1] * a[r + 1]); } } printf("%lld\n", f[2][n - 1]); return 0; }
Example~second:Example second:
给定一个正整数序列 a(1),a(2),...,a(n), (1<=n<=20)a(1),a(2),...,a(n),(1<=n<=20)
不改变序列中每个元素在序列中的位置,把它们相加,并用括号记每次加法所得的和,称为中间和。
例如:
给出序列是 4, 1, 2, 34,1,2,3 。
第一种添括号方法:
((4+1)+(2+3))=((5)+(5))=(10)((4+1)+(2+3))=((5)+(5))=(10)
有三个中间和是 5,5,105,5,10 它们之和为: 5+5+10=205+5+10=20
第二种添括号方法
(4+((1+2)+3))=(4+((3)+3))=(4+(6))=(10)(4+((1+2)+3))=(4+((3)+3))=(4+(6))=(10)
中间和是 3, 6, 103,6,10 ,它们之和为 1919 。
考虑状态:设 f[i][j]f[i][j] 为 ii 到 jj 的区间得到的中间值的最小值。
那么每次合并得到的价值呢?显然合并左右两个区间,对答案的贡献是区间里的所有数。用前缀和维护。
那么状态转移方程就显而易见了: f[l][r] = min(f[l][k] + f[k + 1][r] + sum[r] - sum[l - 1]);f[l][r]=min(f[l][k]+f[k+1][r]+sum[r]−sum[l−1]);
这道题的难点还在于输出的方式,由于不在本篇的讨论范围,这里不做赘述。
/* Time:2018年12月8日21:04:33 Author:SpXace Place:lzyz Type: From: Input: Output: */ #include<iostream> #include<algorithm> #include<cstdio> #include<cstring> #define rep(i, a, b) for(int i = (a); i <= (b); ++i) #define per(i, a, b) for(int i = (a); i >= (b); --i) #define clr(a, b) memset(a, b, sizeof(a)) using namespace std; inline int read() { int s = 0, w = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') w = -1; ch = getchar(); } while(ch >= '0' && ch <= '9') s = (s << 1) + (s << 3) + (ch ^ 48), ch = getchar(); return s * w; } const int N = 5050; int n, a[N], f[N][N], lef[N], rig[N], ans[N][N], sum[N]; void prinf(int l, int r) { if(l == r) return; ++lef[l]; ++rig[r]; prinf(l, ans[l][r]); prinf(ans[l][r] + 1, r); return; } void print(int l, int r) { if(l == r) return; print(l, ans[l][r]); print(ans[l][r] + 1, r); printf("%d ", sum[r] - sum[l - 1]); return; } int main() { n = read(); clr(f, 0x3f); rep(i, 1, n) { a[i] = read(); f[i][i] = 0; sum[i] = sum[i - 1] + a[i]; } rep(i, 2, n) rep(l, 1, n - i + 1) { int r = l + i - 1; rep(k, l, r) { if(f[l][r] >= f[l][k] + f[k + 1][r] + sum[r] - sum[l - 1]) { f[l][r] = f[l][k] + f[k + 1][r] + sum[r] - sum[l - 1]; ans[l][r] = k; } } } prinf(1, n); rep(i, 1, n) { rep(j, 1, lef[i]) printf("("); printf("%d", a[i]); rep(j, 1, rig[i]) printf(")"); if(i != n) printf("+"); } printf("\n%d\n", f[1][n]); print(1, n); return 0; }
总之,对于区间 DPDP 而言,状态往往比较类似,每道题的变换往往集中在合并区间时 DPDP 值的计算。而区间 DPDP的也往往具有独特的标志:通常可以抽象成一个数列,每次只能对相邻的两个对象进行操作。
Written By SpXace