区间DP篇

区间DP篇(无他,菜我)

本苟若感觉简单(难), 但是区间DP是一种有迹可循的动规,通常来说,适用于区间DP的,问题的解决是可以看出是在一定的区间内,不断扩展区间的长度,从局部最优解达到全局最优解。就经典例题:合并相邻石头(去掉相邻的话,可以考虑贪心的思路,请读者自行思考)中,全局的答案一定是最后合并了某块区间。乍一听,像分治起来了。其实不然,具有最优子结构,其次这是有重叠子问题的过程,所以不是分治。

1. 首先引入经典例题——石子合并(简单,重在理解过程)。

Problem Description: 有N堆石子,现要将石子有序的合并成一堆,规定如下:每次只能移动相邻的2堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费最小(或最大)。

也许第一眼你会想到贪心的经典例题——合并石子,问题仅去掉相邻两个字,此时贪心策略确实是一份好策略,但是加上相邻了呢?我们需要统计每一个相邻两石子需要的花费,小的优先,更新答案后,再继续统计相邻…O(N!),大的地球都不可承载之重。

排除了贪心之后,继续考虑,如果让[i,j]段合并,当i==j时,即只包含一个石子,答案很简单,若包含两个石子,可以直接合并。

考虑三个石子:

  1. 我们可以合并前两个,然后合并最后一个。[i,j]=[i,i+1]+[j]

  2. 我们可以合并后两个,然后合并第一个。 [i,j]=[i,i]+[i+1,j]

对于两种合并方式,都是需要下降到长度为1和2的子区间,即我们需要用长度为1和2的子区间来转移出长度为3的区间。读者可以自行考虑四个石子的情况。

解决问题思路逐渐明显——区间DP;

定义状态: dp[i][j]:合并[i,j]段的最小花费。

状态转移: dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]);k[i,j]段的分割点。而DP下降过程中,[i,j]是一定会取到一个石子,两个石子的情况去更新后面更为复杂的情况的。

我们只需要枚举长度,枚举起点i,选取[i,j]之间的分隔点k,那么,最终得到的dp[1][n]即是所求答案。注意我们需要用前缀和的方式来快速计算。

Solve-Code:
#include <iostream>
#include <cstdio>
#include <algorithm>
#define INF 0x3f
using namespace std;
int a[300], sum[300];
int dp_min[300][300], dp_max[300][300];
int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        sum[i] = sum[i - 1] + a[i];
    }

    memset(dp_max, 0, sizeof dp_max);
    memset(dp_min, INF, sizeof dp_min);
    for (int i = 1; i <= n; i++)
        dp_min[i][i] = dp_max[i][i] = 0; //初始化

    for (int len = 1; len < n; len++) {
        for (int i = 1; i + len <= n; i++) {
            int j = i + len;//右端
            for (int k = i; k < j; k++) {
                dp_min[i][j] = min(dp_min[i][j], dp_min[i][k] + dp_min[k + 1][j] + sum[j] - sum[i - 1]);
                dp_max[i][j] = max(dp_max[i][j], dp_max[i][k] + dp_max[k + 1][j] + sum[j] - sum[i - 1]);
            }
        }
    }
    cout << dp_max[1][n] << endl << dp_min[1][n] << endl;
    return 0;   //健康习惯
}

此处简要提一下优化问题了,,我们的k是在枚举分割点,但是其实对于[i,j]段,如果我们可以记录下最优分割点,就可以直接枚举k=s[i][j-1];k<=s[i+1][j];k++,证明省略。

2. 回文串

回文串是比较常见的区间DP的问题,leetcode有红题印刷机修改字符串pojPrinter

首先介绍回文串:一个正读和反读都一样的字符串(地球语言)。

Problem Description: 给定字符串s,长度为m,由n个小写字母组成,在s的任意位置增删字母,把它变成回文串,求最小代价。

增删代价会在输入给出。

问题求解属性:最小代价。

对于一个回文串,局部一定也会是回文串,当局部都是回文串,整体也一定是回文串。

定义状态: dp[i][j],将[i,j]段改为回文串的最小代价。

状态转移:

dp[i][j]=min(dp[i][j],dp[i+1][j-1]); if(s[i]==s[j]) 本身就是回文串,目的让它成为回文串,与其操作它,不如不操作它。

dp[i][j]=min(dp[i][j],dp[i+1][j]+del_cost[s[i]],dp[i+1][j]+add_cost[s[i]], dp[i][j-1]+del_cost[s[j]],dp[i][j-1]+del_cost[s[j]]) if(s[i]!=s[j]),分析同前面一章——简单DP

Solve-Code:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int N = 2010;
int dp[N][N];
int del[30],add[30];  
int n,m; char x; string s;
int main(){
   cin>>n>>m>>s; 
   int v,k;
   for(int i=0;i<n;i++){ //n个字符花费
       cin>>x>>v>>k;
       del[x-'a']=k;
       add[x-'a']=v;
   }
   for(int i=m-2;i>=0;i--)
       for(int j=i+1;j<=m-1;j++){
           dp[i][j]=min(dp[i+1][j]+del[s[i]-'a'],min(dp[i][j-1]+del[s[j]-'a'], min(dp[i][j-1]+add[s[j]-'a'],dp[i+1][j]+add[s[i]-'a']))); //有点杂乱,建议拆开。
           if(s[i]==s[j]) dp[i][j]=min(dp[i][j],dp[i+1][j-1]);
       }
    cout<<dp[0][m-1]<<endl;
    return 0;
}

小优化,如果我们考虑[i,j]段,当[i,j-1]为回文串时,我们可以采用删除第j个字符,或者添加第j个字符到i-1的位置,那么两者本质都是为了变成回文串的情况下,我们可以采用最小的操作方式即可。

3. 统计回文子序列

统计一个字符串s中出现的回文串个数。

回文串的分析同上,所有子区间的回文串之和就是总回文串的数目。

状态表示:dp[i][j]表示[i,j]段回文串的数目。

状态转移:

dp[i][j]=dp[i+1][j]+dp[i][j-1]-dp[i+1][j-1] if(s[i]!=s[j]);请读者自行思考为什么要减(当然是重复了)

处理完了吗?我们考虑了加第i个,加第j个位置,那一起难道没影响吗?当然有影响。ij可以与内部所有回文序列构成新的回文串。

所以

if(s[i]==s[j]) dp[i][j]+=dp[i+1][j-1] +1;

Solve-Code:
#include<iostream>
#include<string>
#include<cstring>
using namespace std;
const int N = 1010;
int dp[N][N];
const int MOD = 10007; //一般结果较大,会有取模运算
int main() {
	string s;
	int t;
	cin >> t;
	while (t--) {
		cin >> s;
		for (int i = 0; i < s.size(); i++)
			dp[i][i] = 1;
		for (int len = 1; len < s.size(); len++) {  //长度
			for (int l = 0; l + len < s.size(); l++) { //起点
				int r = l + len;                       //终点
			    dp[l][r] = (dp[l + 1][r] + dp[l][r - 1]-dp[l+1][r-1]+MOD)%MOD;
				if (s[l] == s[r]) dp[l][r] =(dp[l][r]+dp[l + 1][r - 1] + 1+MOD)%MOD;
			}
		}
		cout << dp[0][s.size() - 1]%MOD << endl;;
	}
	return 0;
}
4. 其次还有小变化题目:

hdu3506 Monkey Party环形的石子合并可还行。

我采用的是一种取模判断策略。

Solve-Code:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<climits>
using namespace std;
const int N = 1010;
int a[N];
int dp[N][N];
int sum[N];
#define INF 0x3f;
int main() {
	int n;
	cin >> n;
	for (int i = 0; i < n; i++) {
		cin >> a[i];
		sum[i+1] = sum[i] + a[i];
		dp[i][i] = 0;
	}
  for(int len=1;len<n;len++) //长度
	  for (int i = 0; i < n; i++) { //起点,没有限制啦,环形的,
		  int j = (i + len) % n;  //终点需要取模,因为可能是n的另一边。
		  dp[i][j] = INF; int cc;  //cc记录需要的花费。
		  for (int k = i % n; k != j; k=(k+1)%n) { //疯狂找割点。
			  if (i > j) cc = sum[n] - sum[i] + sum[j + 1]; //越过了环
			  else cc = sum[j + 1] - sum[i];      //没越过n
				  dp[i][j] = min(dp[i][j], dp[i][k] + dp[(k + 1)%n][j]+cc);
		  }
	  }
int ans = INF;
	for (int i = 0; i < n; i++) {
		ans = min(ans, dp[i][(i + n - 1) % n]);
	}
	cout << ans << endl;
	return 0;
}

看了大佬的题解,都是这般,竟然头和尾相邻了,何不直接拉长捏?

第三层for循环遍历区间 [l,r] 的子区间时,缩小查找分割点的范围,用一个数组(rcdg[l][r])来记录每次循环出的区间的最优分割点。之后当要搜索[i , j]区间的最优分割点时,可以在区间[ rcd[l][r-1], rcd[l+1][l] ]之间枚举。

Solve-Code:
#include<iostream>
#include<algorithm>
using namespace std;
const int INF = 1 << 30;
const int MAX_N = 2005;
int n, m[MAX_N], sum[MAX_N], dp[MAX_N][MAX_N], rcd[MAX_N][MAX_N];
void solve() {
	int N = 2 * n - 1;
	for (int len = 1; len < n; len++)
		for (int i = 1; i <= N - len; i++) {
			int j = i + len;
			dp[i][j] = INF;
			for (int k = rcd[i][j - 1]; k <= rcd[i + 1][j]; k++)
				if (dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1] < dp[i][j]) {
					dp[i][j] = dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1];
					rcd[i][j] = k;
				}
		}
	int ans = dp[1][n];
	for (int i = 2; i <= n; i++) ans = min(ans, dp[i][i + n - 1]);
	cout << ans << '\n';
}
void clear() {
	for (int i = 0; i <= 2 * n; i++) {
		dp[i][i] = 0; rcd[i][i] = i;
		for (int j = i + 1; j <= 2 * n; j++)
			dp[i][j] = dp[j][i] = rcd[i][j] = rcd[j][i] = 0;
	}
}
int main() {
	while (cin >> n) {
		for (int i = 1; i <= n; i++) cin >> m[i], sum[i] = sum[i - 1] + m[i];
		for (int i = 1; i < n; i++) sum[n + i] = sum[n + i - 1] + m[i];
		clear(); solve();
	}
	return 0;
}

练习题目:

You Are the One;请认真读题,不然大寄特寄。

关路灯

Brackets

打完收工!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值