进击高手【第八期】区间dp

本文介绍了区间动态规划(Interval DP)在石子合并问题、最长回文子串和能量项链等经典问题中的应用,通过实例演示了如何构造状态转移方程和逐步计算过程。理解区间DP的关键在于理解区间划分和状态组合。
摘要由CSDN通过智能技术生成

概念

区间DP 也属于 线性DP 中的一种,它 区间长度 作为 DP 的 阶段 ,使用两个坐标(区间的左、右端点)描述每个维度。在 区间DP 中,一个状态由若干个比它更小且包含于它的区间所代表的状态转移而来,因此 区间DP 的决策往往就是划分区间的方法。区间DP 的初态一般就由长度为1的 元区间 构成。

实现办法

区间DP 的操作步骤有其特殊性,需要按照长度递增的顺序作为阶段,先计算出长度 ≤ l e n \leq len len 的所有状态,以此为基础,再计算长度为 l e n + 1 len + 1 len+1 的所有状态。直到最后计算出长度为 n n n 的状态。

一般形式:

	for(int len = 2; len <= n; len++){
		for(int i = 1; i <= n - len + 1; 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] + prev[j] - prev[i - 1]);
				// 动态转移方程视情况而定	
		}
	}

例题

  1. 石子合并

假设只有 2堆 石子,显然只有 1种 合并方案
如果有 3堆 石子,则有 2种 合并方案,((1, 2), 3) 和 (1, (2, 3)) 如果有k堆石子呢?
在这里插入图片描述

不管怎么合并,总之最后总会归结为 2堆 ,如果我们把最后两堆分开,左边和右边无论怎么合并,都必须满足最优合并方案,整个问题才能得到最优解。
令m[i,j]表示归并第i个数到第j数的最小代价,w[i,j]表示第i个数到第j个数的和,这个可以事先计算出来。有如下的状态转移方程:

预处理 w [ i ] [ j ] w[i][j] w[i][j]
先算出前缀和 s [ i ] s[i] s[i],然后 w [ i ] [ j ] = s [ j ] – s [ i − 1 ] w[i][j] = s[j] – s[i - 1] w[i][j]=s[j]s[i1]
因此, w [ i ] [ j ] w[i][j] w[i][j] 可以不事先存放,在DP时直接 O ( 1 ) O(1) O(1) 计算

	s[0] = 0;
  	for(int i = 1; i<=n; i++) 
  		s[i] = s[i-1]+a[i];

核心代码:

拓展成环:
对于环状序列,通用的处理方法是把这条链延长 2倍,扩展成 2 n − 1 2n - 1 2n1 堆,其中 第1堆 与 n + 1 n+1 n+1 堆完全相同,第 i i i 堆与 n + i n+i n+i 堆完全相同,这样我们只要对这 2 n 2n 2n 堆动态规划后,枚举 f ( 1 , n ) , f ( 2 , n + 1 ) , ⋯   , f ( n , 2 n − 1 ) f(1, n), f(2, n + 1), \cdots ,f(n, 2n - 1) f(1,n),f(2,n+1),,f(n,2n1) 取最优值即可即可。
在这里插入图片描述
2. 最长回文子串
d p [ i ] [ j ] dp[i][j] dp[i][j] 表示在字符串区间 s [ i ] . . s [ j ] s[i]..s[j] s[i]..s[j]之间的最长回文串的长度。
分几种情况讨论:
(1)如果s[i] = s[j],这时如果 s [ i + 1 ] ⋯ s [ j − 1 ] s[i+1] \cdots s[j-1] s[i+1]s[j1] 是回文串,则加上两端的回文,可构成一个更长的回文串。怎以判断 s [ i + 1 ] ⋯ s [ j − 1 ] s[i+1] \cdots s[j-1] s[i+1]s[j1] 是不是回文串呢?
一个非常简单的办法是: d p [ i + 1 ] [ j − 1 ] dp[i+1][j-1] dp[i+1][j1]是否等于 j − i − 1 j-i-1 ji1
在这里插入图片描述
如果 s [ i + 1 ] ⋯ s [ j − 1 ] s[i+1] \cdots s[j-1] s[i+1]s[j1] 不构成回文串,则 d p [ i ] [ j ] dp[i][j] dp[i][j] d p [ i + 1 ] [ j ] dp[i+1][j] dp[i+1][j] d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j1] 中取较大值。
(2)如果 s [ i ] ≠ s [ j ] s[i] \neq s[j] s[i]=s[j] ,则 d p [ i ] [ j ] dp[i][j] dp[i][j] d p [ i + 1 ] [ j ] dp[i+1][j] dp[i+1][j] d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j1] 中取较大值者,这仍然是一个 区间DP 的模型。

#include<bits/stdc++.h>
using namespace std;
const int Max = 3005;
char a[Max];
int dp[Max][Max], n;
int main(){
	scanf("%s", a + 1);
	n = strlen(a + 1);
	for(int i = 1; i <= n; i++)
		dp[i][i] = 1;
	for(int len = 2; len <= n; len++){
		for(int i = 1; i <= n; i++){
			int j = i + len - 1;
			if(dp[i + 1][j - 1] == len - 2 && a[i] == a[j])
				dp[i][j] = len;
			else 
				dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
		} 
	}
	printf("%d", dp[1][n]);
	return 0;
} 
  1. 删除字符串

题目描述

给出一个长度为 n n n 的字符串,每次可以删除一个字母相同的子串,问最少需要删多少次。

定义 d p [ i ] [ j ] dp[i][j] dp[i][j] 为删除区间 [ i , j ] [i,j] [i,j] 的最少次数
(1)如果 s [ i ] = s [ j ] s[i]=s[j] s[i]=s[j]
d p [ i ] [ j ] = d p [ i + 1 ] [ j − 1 ] + 1 dp[i][j] = dp[i+1][j-1] + 1 dp[i][j]=dp[i+1][j1]+1,即先删除区间 [ i + 1 , j − 1 ] [i+1,j-1] [i+1,j1] 再把相同的 s [ i ] s[i] s[i] s [ j ] s[j] s[j]一次删除;
(2)如果 s [ i ] ≠ s [ j ] s[i] \neq s[j] s[i]=s[j]
d p [ i ] [ j ] = m i n ( d p [ i + 1 ] [ j ] , d p [ i ] [ j − 1 ] ) + 1 dp[i][j] = min(dp[i+1][j],dp[i][j-1]) + 1 dp[i][j]=min(dp[i+1][j],dp[i][j1])+1,只能先删除区间 [ i + 1 , j ] [i+1,j] [i+1,j] 或者 [ i , j + 1 ] [i,j+1] [i,j+1] ,最后删除区间端点的单个字符;
但是如果有 a a b b aabb aabb 这种串的话,上面第二种做法就要删3次,显然不对!
(3)然后枚举区间 [ i , j ] [i,j] [ij] 的分割点 k k k d p [ i ] [ j ] = m i n ( d p [ i ] [ j ] , d p [ i ] [ k ] + d p [ k ] [ j ] − 1 ) dp[i][j] = min(dp[i][j],dp[i][k]+dp[k][j]-1) dp[i][j]=min(dp[i][j]dp[i][k]+dp[k][j]1),这样的话 k k k 这个点删了两次,所以要 − 1 -1 1

#include<bits/stdc++.h>
using namespace std;
const int Max = 1005;
char s[Max];
int dp[Max][Max];
int main(){
	int n;
	scanf("%d",&n);
	scanf("%s",&s[1]);
	for(int i = 1; i <= n; i++)
		dp[i][i] = 1;
	for(int len = 1; len <= n; len++){
		for(int i = 1; i <= n - len; i++){
			int j = len + i;
			if(s[i] == s[j])
				dp[i][j] = dp[i + 1][j - 1] + 1;
			else
				dp[i][j] = min(dp[i + 1][j], dp[i][j - 1]) + 1;
			for(int k = i; k <= j; k++)
				dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] - 1);
		}
	}
	printf("%d",dp[1][n]);
}
  1. 能量项链

分析样例:
N = 4 N=4 N=4 , 4颗珠子的头标记与尾标记依次为(2,3) (3,5) (5,10) (10,2)。
我们用记号 ⨂ \bigotimes 表示两颗珠子的聚合操作,释放总能量: ( ( 4 ⨂ 1 ) ⨂ 2 ) ⨂ 3 ) = 10 × 2 × 3 + 10 × 3 × 5 + 10 × 5 × 10 = 710 ((4 \bigotimes 1) \bigotimes 2) \bigotimes 3)= 10 \times 2 \times 3 + 10 \times 3 \times 5+10 \times 5 \times 10 = 710 ((41)2)3=10×2×3+10×3×5+10×5×10=710

在这里插入图片描述
该题与石子合并完全类似。
设链中的第 i i i 颗珠子头尾标记为 ( S i − 1 与 S i ) (S_{i} - 1与S_{i}) (Si1Si)
d p [ i ] [ j ] dp[i][j] dp[i][j] 表示从第i颗珠子一直合并到第j颗珠子所能产生的最大能量,则有:
d p [ i ] [ j ] = m a x ( d p [ i ] [ k ] + d p [ k + 1 ] [ j ] + S i − 1 × S k × S j ( i ≤ k < j ) dp[i][j] = max(dp[i][k] + dp[k+1][j] + S_{i}-1 \times S_{k} \times S_{j} (i \leq k<j) dp[i][j]=max(dp[i][k]+dp[k+1][j]+Si1×Sk×Sj(ik<j)

边界条件: d p [ i ] [ i ] = 0 ( 1 ≤ i < k < j ≤ n ) dp[i][i] = 0 (1 \leq i < k<j \leq n) dp[i][i]=0(1i<k<jn)
项链是一个首尾闭合的环,处理方法与石子合并完全相同,即拆环为链。

时间复杂度 O ( n 3 ) O(n^{3}) O(n3)

#include<bits/stdc++.h>
using namespace std;
const int Max = 205;
int n, a[Max];
int dp[Max][Max];
int main(){
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)	{
		scanf("%d", &a[i]);
		a[n + i] = a[i];
	}	
	for(int len = 2; len <= n; len++){
		for(int i = 1; i <= 2 * n - len + 1; 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] + a[i] * a[k + 1] * a[j + 1]);
		}
	}
	int ans = 0;
	for(int i = 1; i <= n; i++)
		ans = max(ans, dp[i][i + n - 1]); 
	printf("%d", ans);
	return 0;
}

end

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值