模板 - 动态规划 - 区间dp
因为昨天在Codeforces上设计的区间dp错了(错过了上紫的机会),觉得很难受。看看学长好像也有学,就不用看别的神犇的了。
区间dp处理环的时候可以把序列延长一倍。
下面是 $O(n^3)$ 的朴素区间dp:
for(int len = 1; len<=n; len++) { //枚举长度 for(int i = 1; i+len<=n+1; i++) { //枚举起点 int j = i+len - 1; for(int k = i; k<j; k++) { //枚举分割点,更新小区间最优解 dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+cost[i][j]); } } }
下面是四边形优化的 $O(n^2)$ 区间dp:
首先,使用四边形优化要满足下面的性质:
1.区间包含的单调性:
当小区间包含在大区间中,则小区间的成本不高于大区间的成本
2.四边形不等式:交叉小于包含
对于 $a<b≤c<d$ ,若 $f[a][c]+f[b][d]≤f[a][d]+f[c][d]$ ,则称 $f$ 满足四边形不等式。
定理:若能证明 $cost$ 满足1和2,则 $dp$ 也满足2。
定理:记 $s[i][j]$ 为 $dp[i][j]$ 取得最值时的分割点的下标 $k$ ,若 $dp$ 满足2,则 $s[i][j]$ 单调,也就是 $s[i][j]≤s[i][j+1]≤s[i+1][j+1]$ 。
应用上述结论:
$dp[i][j]=min\{dp[i][k]+dp[k+1][j]\}+cost[i][j],s[i][j-1]≤k≤s[i+1][j]$
我们减少了k的枚举量,而k的枚举量为 $O(n^2)$。
而上述定理的证明……先省略吧……那我们只需要证明cost满足1和2,就可以使用四边形优化了。
另外要注意
s[i][i]=i;
for(int len = 2; len<=n; len++) { //枚举长度 for(int i = 1; i+len<=n+1; i++) { //枚举起点 int j = i+len - 1; dp[i][j]=INF; for(int k = s[i][j-1]; k<=s[i+1][j]; k++) { //枚举分割点,更新小区间最优解 if(dp[i][k]+dp[k+1][j]<dp[i][j]){ dp[i][j]=dp[i][k]+dp[k+1][j]; s[i][j]=k; } } dp[i][j]+=cost[i][j]; } }
事先计算出 $cost[i][j]$ 就可以了。
来,开始看看学长搞了什么。
BZOJ 1260 涂色paint
https://www.lydsy.com/JudgeOnline/problem.php?id=1260
看了学长说的,设 $dp[i][j]$ 为把 $[i,j]$ 涂成指定颜色需要的最少cost,那么转移的时候怎么搞呢?
洛谷 P1880 石子合并
https://www.luogu.org/problemnew/show/P1880
#include<bits/stdc++.h> using namespace std; #define ll long long int n; int a[205]; int prefix[205]; int dp[205][205]; inline int sum(int l,int r) { return prefix[r]-prefix[l-1]; } int main() { while(~scanf("%d",&n)) { memset(dp,0x3f,sizeof(dp)); for(int i=1; i<=n; i++) { scanf("%d",&a[i]); prefix[i]=prefix[i-1]+a[i]; dp[i][i]=0; } for(int i=n+1; i<=2*n; i++) { a[i]=a[i-n]; prefix[i]=prefix[i-1]+a[i]; dp[i][i]=0; } for(int len = 1; len<=n; len++) { //枚举长度 for(int i = 1; i+len-1<=2*n; i++) { //枚举起点 int j = i+len - 1; for(int k = i; k<j; k++) { //枚举分割点,更新小区间最优解 dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+sum(i,j)); //printf("k=%d\n",k); } //printf("sum(i,j)=%d dp[%d][%d]=%d\n",sum(i,j),i,j,dp[i][j]); } } int ans=0x3f3f3f3f; for(int i=1;i<=n;i++){ ans=min(ans,dp[i][i+n-1]); } printf("%d\n",ans); memset(dp,0,sizeof(dp)); for(int len = 1; len<=n; len++) { //枚举长度 for(int i = 1; i+len-1<=2*n; i++) { //枚举起点 int j = i+len - 1; for(int k = i; k<j; k++) { //枚举分割点,更新小区间最优解 dp[i][j] = max(dp[i][j],dp[i][k]+dp[k+1][j]+sum(i,j)); } } } ans=0; for(int i=1;i<=n;i++){ ans=max(ans,dp[i][i+n-1]); } printf("%d\n",ans); } }
水题,记得要扩大一倍。
使用四边形不等式时,运算必须是最小值,最大值不满足单调性,如下。
#include<bits/stdc++.h> using namespace std; #define ll long long int n; int a[205]; int prefix[205]; int dp[205][205]; int dp2[205][205]; int s[205][205]; inline int sum(int l,int r) { return prefix[r]-prefix[l-1]; } int main() { while(~scanf("%d",&n)) { memset(dp,0x3f,sizeof(dp)); memset(dp2,0x3f,sizeof(dp2)); for(int i=1; i<=n; i++) { scanf("%d",&a[i]); prefix[i]=prefix[i-1]+a[i]; dp[i][i]=0; dp2[i][i]=0; s[i][i]=i; } for(int i=n+1; i<=2*n; i++) { a[i]=a[i-n]; prefix[i]=prefix[i-1]+a[i]; dp[i][i]=0; dp2[i][i]=0; s[i][i]=i; } for(int len = 2; len<=n; len++) { //枚举长度 for(int i = 1; i+len<=2*n+1; i++) { //枚举起点 int j = i+len - 1; for(int k = s[i][j-1]; k<=s[i+1][j]; k++) { //枚举分割点,更新小区间最优解 if(dp[i][k]+dp[k+1][j]<dp[i][j]){ dp[i][j]=dp[i][k]+dp[k+1][j]; s[i][j]=k; } //printf("k=%d\n",k); } dp[i][j]+=sum(i,j); //printf("dp[%d][%d]=%d\n",i,j,dp[i][j]); } } for(int len = 1; len<=n; len++) { //枚举长度 for(int i = 1; i+len-1<=2*n; i++) { //枚举起点 int j = i+len - 1; for(int k = i; k<j; k++) { //枚举分割点,更新小区间最优解 dp2[i][j] = min(dp2[i][j],dp2[i][k]+dp2[k+1][j]+sum(i,j)); //printf("k=%d\n",k); } if(dp[i][j]!=dp2[i][j]) printf("sum(i,j)=%d dp[%d][%d]=%d dp2[%d][%d]=%d\n",sum(i,j),i,j,dp[i][j],i,j,dp2[i][j]); } } int ans=0x3f3f3f3f; for(int i=1;i<=n;i++){ ans=min(ans,dp[i][i+n-1]); } printf("%d\n",ans); for(int i=1; i<=2*n; i++) { s[i][i]=i; } memset(dp,0,sizeof(dp)); memset(dp2,0,sizeof(dp2)); for(int len = 2; len<=n; len++) { //枚举长度 for(int i = 1; i+len<=2*n+1; i++) { //枚举起点 int j = i+len - 1; for(int k = s[i][j-1]; k<=s[i+1][j]; k++) { //枚举分割点,更新小区间最优解 if(dp[i][k]+dp[k+1][j]>dp[i][j]){ dp[i][j]=dp[i][k]+dp[k+1][j]; s[i][j]=k; } } dp[i][j]+=sum(i,j); } } for(int len = 1; len<=n; len++) { //枚举长度 for(int i = 1; i+len-1<=2*n; i++) { //枚举起点 int j = i+len - 1; for(int k = i; k<j; k++) { //枚举分割点,更新小区间最优解 dp2[i][j] = max(dp2[i][j],dp2[i][k]+dp2[k+1][j]+sum(i,j)); //printf("k=%d\n",k); } if(i<10&&j<10&&dp[i][j]!=dp2[i][j]) printf("sum(i,j)=%d dp[%d][%d]=%d dp2[%d][%d]=%d\n",sum(i,j),i,j,dp[i][j],i,j,dp2[i][j]); } } ans=0; for(int i=1;i<=n;i++){ ans=max(ans,dp[i][i+n-1]); } printf("%d\n",ans); } }