区间DP例题总结——看似不简单的简单题

引子:

今天,我们开始学那奇怪的区间DP。简单来说,区间dp分为三个部分:

  1. 阶段
  2. 枚举左端点,再枚举右端点
  3. 策略——顾名思义就是因为有些题,需要求出中点K,但有些题却又不需要。所以我们应该判断,用与不用。
    --------------------------------现在我们来看几道经典例题------------------------------

例题:

石子合并

题目描述:

N堆石子摆成一条线。现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的代价。计算将N堆石子合并成一堆的最小代价。
例如: 1 2 3 4,有不少合并方法
1 2 3 4 => 3 3 4(3) => 6 4(9) => 10(19)
1 2 3 4 => 1 5 4(5) => 1 9(14) => 10(24)
1 2 3 4 => 1 2 7(7) => 3 7(10) => 10(20)
括号里面为总代价可以看出,第一种方法的代价最低,现在给出堆石子的数量,计算最小合并代价。

输入格式:

第1行:N
第2行:有N个空格分开的整数,表示N堆石子的数量

输出格式:

输出最小合并代价

样例:

输入

4
1 2 3 4

输出

19

思路:

那么这道题,是最基础的。
考虑不在环上,而在一条链上的情况。
令 f[i][j[表示将区间[i,j]内的所有石子合并到一起的最大得分。

所以,状态转移方程就是,额~

dp[j][r]=min(dp[j][r],dp[j][k]+dp[k+1][r]+s[r]-s[j-1]);

然后就没什么难点了,根据某些尿性,我们知道还有些坑点。

那么注意(呵…):

就是要赋初值为0,以及求前缀和。为什么呢?能够更方便的计算区间和就是这么点。那么最核心的代码呢就是:

	memset(dp,0x3f3f3f3f,sizeof(dp));
	for(int i=1;i<=n;i++){
    
    
		scanf("%d",&x);
		s[i]=s[i-1]+x;
	}
	for(int i=1;i<=n;i++){
    
    
		dp[i][i]=0;
	}
	for(int len=2;len<=n;len++){
    
    
		for(int j=1;j<=n-len+1;j++){
    
    
			int r=j+len-1;
			for(int k=1;k<r;k++)
			dp[j][r]=min(dp[j][r],dp[j][k]+dp[k+1][r]+s[r]-s[j-1]+s[r]-s[j-1]);

为了不坑住小朋友…我还是把memset的用法贴上了。

那么提出一个疑问:

怎样处理环?

题目中石子围成一个环,而不是一条链,怎么办呢?
方法一 :由于石子围成一个环,我们可以枚举分开的位置,将这个环转化成一个链,由于要枚举 n次,最终的时间复杂度为 O(n^4) 。
方法二 :我们将这条链延长两倍,变成2*n堆,其中第i堆与第n+i堆相同,用动态规划求解后,取f(1,n)…f(i,n+i-1)中的最优值,即为最后的答案。时间复杂度 O(n^3) 。

核心代码:

for (len = 1; len <= n; len++)
  for (i = 1; i <= 2 * n - 1; i++) {
    
    
    int j = len + i - 1;
    for (k = i; k < j && k <= 2 * n - 1; k++)
      f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]);
  }

删除字符串

题目描述:

给出一个长度为n的字符串,每次可以删除一个字母相同的子串,问最少需要删多少次。 数据规模:n <= 500

输入格式:

第1行:1个整数,表示字符串的长度
第2行:n个字符的字符串

输出格式:

第1行:1个整数,表示答案

样例:

输入:

5
abaca

输出:

3

思路:

首先,我们应该考虑割点。
设dp[i][j]表示删去i到j的最小步数。
如果 a[i]==b[j],则dp[i][j]=dp[i+1][j−1]a[i]==b[ j],则dp[i][j]=dp[i+1][j-1]a[i]==b[j],则dp[i][j]=dp[i+1][j−1]
如果a[i]!=b[j],则dp[i][j]=min(dp[i+1][j],dp[i][j−1])+1a[i]!=b[j],则dp[i][j]=min(dp[i+1][j],dp[i][j-1])+1a[i]!=b[j],则dp[i][j]=min(dp[i+1][j],dp[i][j−1])+1
然而对于数据aabb,显然aa、bb分别为一组,但答案是3,毫无疑问,是错的。
由此,我们就可以的到状态转移方程了。
dp[i][j]表示的是从i个元素到j个元素之间,删元素用的步数最少。k呢是一个中点。最后的目标数

dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]1)  i+1<=k<=j−1

由此可得核心代码:

dp[i][j]=dp[i+1][j]+1
for(int k=i+1;k<=j;k++) {
    
    
	if(a[i]==a[k]) dp[i][j]=min(dp[i][j],dp[i+1][k-1]+dp[k][j]);
}

现在就是你们,最喜欢的代码了

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 505;
char c[maxn];
int n, m, a[maxn], dp[maxn][maxn];
int main() {
    
    
    scanf("%d", &n);
    scanf("%s", c);
    for (int i = 0; i < n; i++) {
    
    
        if (i == 0 || c[i] != c[i - 1])
            a[++m] = c[i];
    }
    for (int i = 1; i <= m; i++) dp[i][i] = 1;
    for (int len = 2; len <= m; len++) {
    
    
        for (int i = 1; i <= m - len + 1; i++) {
    
    
            int j = i + len - 1;
            dp[i][j] = dp[i + 1][j] + 1;
            for (int k = i + 1; k <= j; k++) {
    
    
                if (a[i] == a[k])
                    dp[i][j] = min(dp[i][j], dp[i + 1][k - 1] + dp[k][j]);
            }
        }
    }
    printf("%d", dp[1][m]);
}

注意:这里的题都需要用memset ,但有些不用

问了一下某个高中的大佬,我终于知道了memset和初始化的区别了我太弱了

memset是清空数组里的所有东西,而初始化是把一个数组里的一个数,或几个数赋为你想赋值的数。
但在某些因素下,这两种用法是一样的。
那么引用一句话:

我个人以为,要把用循环来初始化变成一种常态。


猜你喜欢

转载自blog.csdn.net/C202207LYX/article/details/107435293