区间DP及其变形写法

区间DP及其变形写法

一、模板

前言

市面上的区间DP,大多都是从石子合并(链式)、石子合并(环式)开始讲起,但是笔者认为他们夹杂着前缀和,对初学者很不友好。所以我打算用另一题来引出。

P1435 [IOI2000] 回文字串

题意

给定一个字符串,求出将给定字符串变成回文词所需要插入的最少字符数。

思路

  • 朴素点的 :判断一个区间是不是回文字串,是就不用插入新的字符,反之要插入新的字符。

其实经过线性DP的折磨后,大家也应该明白DP,应该由状态定义边界状态转移 几个方面组成,下面就理清这三个东西。估计对于区间dp也应该会有一个理性的认识。

/*
状态:
	回文词所需要插入的最少字符数,是关于区间长度的函数
	dp[i][j] —— 在区间从i到j,回文词所需要插入的最少字符数
边界:
	区间长度为1(len == 1) —— 必是回文字串,dp[i][i] = 0;
状态转移:
	if (s[l] == s[r])	dp[l][r] = dp[l + 1][r - 1];//s[l]和s[r]相等,不需要插入 
	else	dp[l][r] = min(dp[l + 1][r], dp[l][r - 1]) + 1;//最少字符数,只跟上一个小区间有关,要么是左边的,要么是右边
递推的写法:
	从小区间到大区间。
*/

题解

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull	unsigned long long
#define P pair<int, int>
#define endl '\n'
#define MaxN 0x3f3f3f3f
#define MinN -MaxN
const int mod = 1e9 + 7;

string s;

void slove(){
	cin >> s;
	int n = s.size();
	s = " " + s;//下标从1开始 
	vector<vector<int>> dp(n + 7, vector<int> (n + 7, 0));
	
	for (int len = 1; len <= n; len++){//区间长度 
		for (int l = 1; l + len - 1 <= n; l++){//枚举,区间范围 
			int r = l + len - 1;
			if (s[l] == s[r])	dp[l][r] = dp[l + 1][r - 1];//不需要插入 
			else	dp[l][r] = min(dp[l + 1][r], dp[l][r - 1]) + 1;//从左边的区间或者右边的区间转移不过来,需要插入 
		}
	} 
	cout << dp[1][n];//1 ~ n 范围内的最小插入 
}

int main(){
	ios_base::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);

	int t = 1;
//	cin >> t;
	while (t--){
		slove();
	}
    return 0;
}

二、区间合并型(割点k——大区间分成两部分)

前言 :

本来是现在把石子合并放到这讲的,但是发现了另一个好题。能诠释==(割点k——大区间分成两部分)== ,也不用再讲前缀和增加理解负担。

P4170 [CQOI2007]涂色

题意 :

把一段连续的木板涂成一个给定的颜色,求解最少的涂色次数

思路 :

  • 朴素点的 :既然已经学过区间dp了,直接想能不能定义其状态。
/*
状态:
	最少的涂色次数。关于区间的函数
	dp[l][r]
边界:
	区间长度 len == 1、l == r 时。只需涂一次色, dp[i][i] = 1
	求最少的涂色次数	其他初始化为MaxN
状态转移:
	状态转移:
	s[i] == s[j]	
		dp[l][r] = min(dp[l + 1][r], dp[l][r - 1]);//过大上次涂的边界即可,不需要再涂 
	else 
		dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r]);// 我们需要考虑将子串断成两部分来涂色,需要枚举子串的断点
*/

题解 :

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull	unsigned long long
#define P pair<int, int>
#define endl '\n'
#define MaxN 0x3f3f3f3f
#define MinN -MaxN
const int mod = 1e9 + 7;

void slove(){
	string s;
	cin >> s;
	int n = s.size();
	s = " " + s;
	vector<vector<int >> dp(n + 7, vector<int>(n + 7, MaxN));//初始化
    
	for (int i = 1; i <= n; i++)	dp[i][i] = 1;//边界

	for (int len = 2; len <= n; len++){//len从2开始,1已经遍历过了
		for (int l = 1; l + len - 1 <= n; l++){
			int r = l + len - 1;
			if (s[l] == s[r])	dp[l][r] = min(dp[l + 1][r], dp[l][r - 1]);
			else	for (int k = l; k < r; k++)	dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r]); 
		}
	}
	cout << dp[1][n];
}

int main(){
	ios_base::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);

	int t = 1;
//	cin >> t;
	while (t--){
		slove();
	}
    return 0;
}

三、区间合并型(加入前缀和的应用)

前言:

终于到激动人心的时候了!石子合并!这里提一嘴,石子合并有链式和环式,现在先将链式。

P1775 石子合并(弱化版)

题意 :

合并的代价为这两堆石子的质量之和,求总的代价最小

思路 :

  • 朴素点的 :如果是两个石头合并很容易,就两个相加就好了。但是他是一个区间,那怎么求?对嘛!铺垫这么久的前缀和就登场了!
  • 前缀和求重量,如果求[i, j]的重量,preS[j] - preS[i - 1];
/*
状态:
    总的代价最小,关于区间的函数
    dp[i][j]
边界:
	dp[i][i] = 0//不合并,没有代价
	求总的代价最小	其他初始化为MaxN
状态转移:
	两堆原先的重量,再加合并的重量
	dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + s[r] - s[l - 1]);
*/

题解 :

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull	unsigned long long
#define P pair<int, int>
#define endl '\n'
const int mod = 1e9 + 7;
const int maxl = 3 * 1e2 + 7;

int n;

void slove(){
	cin >> n;
	vector<int> a(n + 1, 0), s(n + 1, 0);//a ——存数, s ——前缀和 
	vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0x3f3f3f3f));
	
	for (int i = 1; i <= n; i++){//从下标1开始 
		cin >> a[i];
		s[i] = s[i - 1] + a[i];
		dp[i][i] = 0;
	}
	
	for (int len = 2; len <= n; len++){//枚举区间长度 
		for (int l = 1; l + len - 1 <= n; l++){//移动区间、范围[l,l + len) 
			int r = l + len - 1;
			for (int k = l; k < r; k++){//这个区间分成两个部分,[l, k] ~ [k + 1, r] ,这里是k + 1所以 k < r 
				dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + s[r] - s[l - 1]);
			}
		}
	}
	cout << dp[1][n];
}

int main(){
	ios_base::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);

	int t = 1;
//	cin >> t;
	while (t--){
		slove();
	}
    return 0;
}

四、环型DP的处理方式

前言 :

环形dp,其实不难,就多复制了一维,其他都一样的

P1880 [NOI1995] 石子合并

题意 :

合并的代价为这两堆石子的质量之和,求总的代价最小

首尾可以相连成环

思路:

  • 朴素点的 :如果是两个石头合并很容易,就两个相加就好了。但是他是一个区间,那怎么求?对嘛!铺垫这么久的前缀和就登场了!
  • 前缀和求重量,如果求[i, j]的重量,preS[j] - preS[i - 1];
  • 多复制一维,从n -> 2n,解决成环问题

题解 :

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull	unsigned long long
#define P pair<int, int>
#define endl '\n'
#define MaxN 0x3f3f3f3f
#define MinN -MaxN
const int mod = 1e9 + 7;

int n;

void slove(){
	cin >> n;
	vector<vector<int>> dpMax(n * 2 + 7, vector<int> (2 * n + 7, MinN)), dpMin(n * 2 + 1, vector<int> (2 * n + 1, MaxN));
	vector<int> s(2 * n + 7, 0), a(2 * n + 7, 0);
	
	for (int i = 1; i <= n; i++){
		cin >> a[i];
		a[i + n] = a[i];
	}
	
	for (int i = 1; i <= 2 * n; i++){
		s[i] = s[i - 1] + a[i];
		dpMax[i][i] = 0;
		dpMin[i][i] = 0;
	}
	
	for (int len = 2; len <= n; len++){
		for (int l = 1; l + len - 1 <= 2 * n; l++){
			int r = l + len - 1;
			for (int k = l; k < r; k++){
				dpMax[l][r] = max(dpMax[l][r], dpMax[l][k] + dpMax[k + 1][r] + s[r] - s[l - 1]);
				dpMin[l][r] = min(dpMin[l][r], dpMin[l][k] + dpMin[k + 1][r] + s[r] - s[l - 1]);
			}
		}
	}
	
	int maxAns = MinN, minAns = MaxN;
	for (int i = 1; i <= n; i++){
		minAns = min(minAns, dpMin[i][i + n - 1]);
		maxAns = max(maxAns, dpMax[i][i + n - 1]);
//		cout << maxAns << endl;
	}
	cout << minAns << endl << maxAns << endl;
}

int main(){
	ios_base::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);

	int t = 1;
//	cin >> t;
	while (t--){
		slove();
	}
    return 0;
}

五、环型DP的处理方式 —— 去除前缀和,普通的环

前言 :

前缀和,区间长度为n,但是如果不是前缀和,区间长度就要变成n + 1

P1063 [NOIP2006 提高组] 能量项链

题意 :

和石子合并差不多,只不过那个是相加,这个是相乘

思路 :

和石子合并一个思路不多赘述,唯一一个点就是不用前缀和,首尾相连的话,区间范围为n + 1,包含首尾相连

题解 :

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull	unsigned long long
#define P pair<int, int>
#define endl '\n'
#define MaxN 0x3f3f3f3f
#define MinN -MaxN
const int mod = 1e9 + 7;

int n, ans = 0;

void slove(){
	cin >> n;
	vector<int> a(2 * n + 7, 0);
	vector<vector<int>> dp(2 * n + 7, vector<int>(2 * n + 7, 0));
	for (int i = 1; i <= n; i++)	cin >> a[i], a[i + n] = a[i];
	
	for (int len = 3; len <= n + 1; len++){
		for (int l = 1; l + len - 1 <= 2 * n; l++){
			int r = l + len - 1;
			for (int k = l  +1; k < r; k++)	dp[l][r] = max(dp[l][r], dp[l][k] + dp[k][r] + a[l] * a[k] * a[r]);
		}
	}
	
	for (int i = 1; i <= n; i++)	ans = max(ans, dp[i][i + n]);
	
	cout << ans;
}

int main(){
	ios_base::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);

	int t = 1;
//	cin >> t;
	while (t--){
		slove();
	}
    return 0;
}

六、结语

完结散花,如有不明白欢迎私信交流。都看到这了,给个三连呗!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
区间DP是一种动态规划的方法,用于解决区间范围内的问题。在Codeforces竞赛中,区间DP经常被用于解决一些复杂的字符串或序列相关的问题。 在区间DP中,dp[i][j]表示第一个序列前i个元素和第二个序列前j个元素的最优解。具体的转移方程会根据具体的问题而变化,但是通常会涉及到比较两个序列的元素是否相等,然后根据不同的情况进行状态转移。 对于区间长度为1的情况,可以先进行初始化,然后再通过枚举区间长度和区间左端点,计算出dp[i][j]的值。 以下是一个示例代码,展示了如何使用区间DP来解决一个字符串匹配的问题: #include <cstdio> #include <cstring> #include <string> #include <iostream> #include <algorithm> using namespace std; const int maxn=510; const int inf=0x3f3f3f3f; int n,dp[maxn][maxn]; char s[maxn]; int main() { scanf("%d", &n); scanf("%s", s + 1); for(int i = 1; i <= n; i++) dp[i][i] = 1; for(int i = 1; i <= n; i++) { if(s[i] == s[i - 1]) dp[i][i - 1] = 1; else dp[i][i - 1] = 2; } for(int len = 3; len <= n; len++) { int r; for(int l = 1; l + len - 1 <= n; l++) { r = l + len - 1; dp[l][r] = inf; if(s[l] == s[r]) dp[l][r] = min(dp[l + 1][r], dp[l][r - 1]); else { for(int k = l; k <= r; k++) { dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r]); } } } } printf("%d\n", dp[n]); return 0; } 希望这个例子能帮助你理解区间DP的基本思想和应用方法。如果你还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值