区间DP

区间DP

区间DP也是线性DP,它把区间当成DP的阶段,用区间的两个端点描述状态和处理状态的转移。区间DP的主要思想就是先在小区间进行DP得到最优解,然后再合并小区间的最优解求得大区间的最优解。

例题

石子合并

有 n 堆石子排成一排,第 i 堆石子有 \(a_i\) 颗,每次我们可以选择相邻的两堆石子合并,代价是两堆石子数目的和,现在我们要一直合并这些石子,使得最后只剩下一堆石子,问总代价最少是多少?

将所有区间 [l, r] 的最小代价均算出来,而为了计算合并区间 [l, r] 的最小代价,我们需要先计算合并所有满足 $i \le k < j $ 的区间 $[i, k] + [k + 1, j] $ 的最小代价(最优子结构)
而且我们仅需要关注合并区间的最小代价,而不需要关注如何合并区间(无后效性)
故可以利用动态规划求解,时间复杂度 $O(n^3) $

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x, y, sizeof(x))
#define debug(x) cout << #x << " = " << x << '\n'
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n'
//#define int long long

using namespace std;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ull, ull> pull;
typedef pair<double, double> pdd;
/*

*/
const int maxm = 3e2 + 5, inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f, mod = 998244353;
ll n, a[maxm], dp[maxm][maxm];

void solve(){
	cin >> n;
	for(int i = 1; i <= n; ++ i){
		cin >> a[i]; a[i] += a[i - 1];
	}
	for(int len = 2; len <= n; ++ len){// 枚举合并区间长度
		for(int i = 1; i + len - 1 <= n; ++ i){// 枚举区间开始位置
			int j = i + len - 1;
			dp[i][j] = INF;
			for(int k = i; k < j; ++ k){// 枚举中间划分点
				dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + a[j] - a[i - 1]);
			}
		}
	}
	cout << dp[1][n] << '\n';
	return ;
}

signed main(){
	ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
	int _ = 1;
	// cin >> _;
	while(_ --){
		solve();
	}
	return 0;
}

圆形操场:第一堆和最后一堆可以合并!!!
处理环形问题,在原本的石子堆之后再加 n 堆一样的石子,再对这 2n 堆石子跑DP,最后遍历每 n 堆石子构成的区间,取结果的最大和最小即为最终答案
Qiansui_code


括号序列

题意
给定一个长度为 n 的字符串 s,字符串由 (, ), [, ] 组成,问其中最长的合法子序列有多长?也就是说,我们要找到最大的 m,使得存在 \(i_1,i_2,…,i_m\) 满足 \(1≤i_1<i_2<⋯<i_m≤n\) 并且 \(s_{i_1}s_{i_2}…s{i_m}\) 是一个合法的括号序列。

定义合法序列:
空串是一个合法的括号序列
若 A 是一个合法的括号序列,则 (A), [A] 也是合法的括号序列
若 A, B 都是合法的括号序列,则 AB 也是合法的括号序列

思路
区间 DP
状态:
dp[i][j] 代表区间 [i, j] 合法的最大序列长度
转移:
若 A 串[i, j] 符合题解,且 ss[i] == '(' && ss[j] == ')' || ss[i] == '[' && ss[j] == ']' ,dp[i - 1][j + 1] = dp[i][j] + 2

区间 [i, j] 的最大合法序列程度即为 $\max(dp[i][k] + dp[k + 1][j]), i \le k < j $

代码

int n, dp[maxm][maxm];
string ss;

void solve(){
	cin >> n >> ss;
	ss = "#" + ss;
	for(int len = 2; len <= n; ++ len){
		for(int i = 1; i + len - 1 <= n; ++ i){
			int j = i + len - 1;
			if(ss[i] == '(' && ss[j] == ')' || ss[i] == '[' && ss[j] == ']')
				dp[i][j] = dp[i + 1][j - 1] + 2;
			for(int k = i; k < j; ++ k){
				dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j]);
			}
		}
	}
	cout << dp[1][n] << '\n';
	return ;
}

String painter

https://vjudge.net/problem/HDU-2476

本例题详可见文末链接 1
\(DP[i][j]\)表示区间\([i,j]\)内从空白串转换到B的最少操作次数

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x,y,sizeof(x))
#define debug(x) cout << #x << " = " << x << endl
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << endl
//#define int long long

using namespace std;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<ull,ull> pull;
typedef pair<double,double> pdd;
/*
区间DP例题
*/
const int maxm=1e2+5,inf=0x3f3f3f3f,mod=998244353;
string ss,tt;
int dp[maxm][maxm];

void solve(){
	while(cin>>ss>>tt){
		int n=ss.size();
		//计算将空白串转为tt串
		for(int i=1;i<=n;++i) dp[i][i]=1;
		for(int len=2;len<=n;++len){
			for(int i=1;i+len-1<=n;++i){
				int j=i+len-1;
				dp[i][j]=inf;
				if(tt[i-1]==tt[j-1])
					dp[i][j]=dp[i+1][j];
				else
					for(int k=i;k<j;++k){
						dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]);
					}
			}
		}
		//计算将ss串转为tt串
		for(int i=1;i<=n;++i){
			if(ss[i-1]==tt[i-1]){
				dp[1][i]=dp[1][i-1];
			}else{
				for(int j=1;j<i;++j){
					dp[1][i]=min(dp[1][i],dp[1][j]+dp[j+1][i]);
				}
			}
		}
		cout<<dp[1][n]<<'\n';
	}
	return ;
}

signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _=1;
//	cin>>_;
	while(_--){
		solve();
	}
	return 0;
}

综合应用

状态:$DP[i][j] $ 表示区间[i, j] 可以合并出的最大值
转移:朴素的 $O(n^3) $ 遍历时,第三层循环转移的条件是 $dp[i][k] = dp[k + 1][j] $ 且 \(dp[i][k] \ne 0\)
因为只有当区间左右能合并成一个数且左右相等时才能再次合并

for(int len = 2; len <= n; ++ len){
	for(int i = 1; i + len - 1 <= n; ++ i){
		int j = i + len - 1;
		for(int k = i; k < j; ++ k){
			if(dp[i][k] == dp[k + 1][j] && dp[i][k]){// 转移条件~
				dp[i][j] = max(dp[i][j], dp[i][k] + 1);
			}
		}
		ans = max(ans, dp[i][j]);
	}
}

代码:Qiansui_code

依旧是基础的 $O(n^3) $ 思想的区间 DP,但是本题的下标应当扩大一位,为了便于状态的转移,详见代码

for(int len = 3; len <= n + 1; ++ len){// 头,间,尾至少三个位置
	for(int i = 1; i + len - 1 <= n * 2; ++ i){// 2n 处理环成链
		int j = i + len - 1;
		for(int k = i + 1; k < j; ++ k){// k 需要在 (i, j) 中,不能重合边界
			dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + a[i] * a[j] * a[k]);
		}
	}
}

代码:Qiansui_code

相关资料

1.罗勇军老师整理
https://blog.csdn.net/weixin_43914593/article/details/10616385
2.dx123 428【模板】区间DP 石子合并

posted on 2023-07-11 21:51  Qiansui  阅读(10)  评论(0编辑  收藏  举报