区间DP

本文介绍了多个关于区间动态规划的编程问题,包括合并石子、环形石子合并、矩阵链乘、邮局选址以及一系列其他区间DP问题,如扰乱字符串、奇怪的打印机、删除回文子数组等。每个问题都提供了问题描述、解决方案和状态转移方程,展示了解决这类问题的思路和技巧。
摘要由CSDN通过智能技术生成

目录

1. 合并石子

2. 合并石子(环形)

3. 矩阵链乘

4. 邮局选址

5. 其它区间DP问题

5.1 扰乱字符串

5.2 奇怪的打印机

5.3 删除回文子数组

5.4 最大平均值和的分组

5.5 分割回文串 III

5.6 分隔数组以得到最大和

5.7 多边形三角剖分的最低得分

5.8 叶值的最小代价生成树

5.9 猜数字大小 II

 


1. 合并石子

https://www.nowcoder.com/questionTerminal/6d3ccbc5b6ad4f12b8fe4c97eaf969e0

 有 N 堆金币排成一排,第 i 堆中有 C[i] 块金币。每次合并都会将相邻的两堆金币合并为一堆,成本为这两堆金币块数之和。经过N-1次合并,最终将所有金币合并为一堆。请找出将金币合并为一堆的最低成本。

其中,1 <= N <= 30,1 <= C[i] <= 100

 

输入描述:

第一行输入一个数字 N 表示有 N 堆金币

第二行输入 N 个数字表示每堆金币的数量 C[i]

 

输出描述:

输出一个数字 S 表示最小的合并成一堆的成本

输入

4
3 2 4 1

输出

20

经典区间DP问题。如果用 dp[i][j] 表示将 [i...j] 的石子合并成一堆的最小成本,最后要求的就是 dp[0][N-1]。显然固有成本是 [i ... j] 的数量和,因为不管怎么移动每一堆的数量至少都要计算一次,能优化的就是选择哪一堆作为分界点,然后将左右两边分别合并的决策。所以有状态转移方程

dp[i][j] = min\{dp[i][k] + dp[k+1][j]\} + sum(i...j), i <= k < j

观察状态转移方程, 发现dp[i][j] 同时依赖于左边和下边的值,因此不能通过常规的遍历方式来填表。注意到 dp[i][k] 和 dp[k+1][j]的区间长度都小于 dp[i][j],所以应当使用区间长度作为DP的阶段,对每一个 i ,根据区间长度len 得到对应的右端点 j = i + len - 1。 len 从 2 遍历到 N。

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int n;
    scanf("%d", &n);
    vector<int> c(n);
    vector<int> sum(n + 1, 0);
    // [i...j]的和:sum[j+1] - sum[i]
    for(int i = 0; i < n; i++)
    {
        scanf("%d", &c[i]);
        sum[i+1] = c[i] + sum[i];
    }
    vector<vector<int>> dp(n, vector<int>(n, 1e9));
    // dp[i][j]: 将[i...j]进行合并,最低成本
    // dp[i][j] = min{dp[i][k] + dp[k+1][j]} + sum[i..j], i <= k < j;
    for (int i = 0; i < n; ++i)
    {
        dp[i][i] = 0;
    }
    for (int len = 2; len <= n; ++len)
    {
        for (int i = 0; i < n; ++i)
        {
            int j = i + len - 1;
            if(j > n - 1) break;
            for (int k = i; k < j; ++k)
            {
                dp[i][j] = min(dp[i][k]+dp[k+1][j] + sum[j+1] - sum[i], dp[i][j]);
            }
        }
    }
    printf("%d\n", dp[0][n-1]);
    return 0;
}

把合并的决策过程(也就是填表的过程)打印出来,可能更方便理解:

-----len = 2-----
dp[0][1] = dp[0][0] + dp[1][1] + 5 = 5
dp[0][1] = 5

dp[1][2] = dp[1][1] + dp[2][2] + 6 = 6
dp[1][2] = 6

dp[2][3] = dp[2][2] + dp[3][3] + 5 = 5
dp[2][3] = 5


-----len = 3-----
dp[0][2] = dp[0][0] + dp[1][2] + 9 = 15
dp[0][2] = dp[0][1] + dp[2][2] + 9 = 14
dp[0][2] = 14

dp[1][3] = dp[1][1] + dp[2][3] + 7 = 12
dp[1][3] = dp[1][2] + dp[3][3] + 7 = 13
dp[1][3] = 12


-----len = 4-----
dp[0][3] = dp[0][0] + dp[1][3] + 10 = 22
dp[0][3] = dp[0][1] + dp[2][3] + 10 = 20
dp[0][3] = dp[0][2] + dp[3][3] + 10 = 24
dp[0][3] = 20

相同的题目:1547. 切棍子的最小成本 

2. 合并石子(环形)

https://loj.ac/problem/10147

将 n堆石子绕圆形操场排放,现要将石子有序地合并成一堆。规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。

请编写一个程序,读入堆数n(1 <= n <= 200) 及每堆的石子数,并进行如下计算:

  1. 选择一种合并石子的方案,使得做 n-1次合并得分总和最大。
  2. 选择一种合并石子的方案,使得做 n-1次合并得分总和最小

输出共两行:

第一行为合并得分总和最小值,

第二行为合并得分总和最大值。

样例输入

4
4 5 9 4

样例输出

43
54

 

几乎与第一题合并金币一模一样,不过这里是环形,显然要变成链形才方便处理。在每个点都可以断开,所以有n种断开的方法,如果枚举断开的端点,再依次采用第一题的做法的话,复杂度是O(n^4),不是很合适。处理环的一个简单方法是,将相同的数组复制一份接在后面,在这个长度为2n的链形数组里使用第一题的解法,找到合并[i...j]的最大得分dp[i][j] (0 <= i < j <= 2*n-1),复杂度是O(n^3)。之后再找到每一个长度是n的(i,j)对,dp[i][j]的最大值就是所求的环形数组能得到的最大值。最小值同理。

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int n;
    scanf("%d", &n);
    vector<int> c(2*n), sum(2*n+1, 0);
    for (int i = 0; i < n; ++i)
    {
        scanf("%d", &c[i]);
        c[i + n] = c[i];
    }
    n*= 2;
    for(int i = 0; i < n; i++) sum[i+1] = sum[i] + c[i];
    // sum:前缀和, [i...j]的总和 sum[j+1] - sum[i];
    vector<vector<int>> mindp(n, vector<int>(n, 1e9));
    vector<vector<int>> maxdp(n, vector<int>(n, -1e9));
    // dp[i][j]: 合并[i...j]的最大(小)得分
    // dp[i][j] = max{dp[i][k] + dp[k+1][j] + sum(i...j)}, i <= k < j;
    for (int i = 0; i < n; ++i)
    {
        mindp[i][i] = maxdp[i][i] = 0;
    }
    for (int len = 2; len < n; ++len)
    {
        for (int i = 0; i < n; ++i)
        {
            int j = i + len - 1;
            if(j > n - 1) break;
            for (int k = i; k < j; ++k)
            {
                mindp[i][j] = min(mindp[i][j], mindp[i][k] + mindp[k+1][j]);
                maxdp[i][j] = max(maxdp[i][j], maxdp[i][k] + maxdp[k+1][j]);
            }
            mindp[i][j] += sum[j+1] - sum[i];
            maxdp[i][j] += sum[j+1] - sum[i];
        }
    }
    //环形数组,对所有长度是n的(i, j)对进行遍历,找到最大和最小值
    int minval = INT32_MAX, maxval = INT32_MIN;
    n /= 2;
    for (int i = 0; i < n; ++i)
    {
        int j = i + n - 1;
        minval = min(minval, mindp[i][j]);
        maxval = max(maxval, maxdp[i][j]);
    }
    printf("%d\n%d\n", minval, maxval);
    return 0;
}

3. 矩阵链乘

给定n个矩阵:A1,A2,...,An,其中Ai与Ai+1是可乘的,i=1,2...,n-1。确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。

矩阵链乘是算法导论里的经典DP问题,也是区间DP问题。

首先有这样一个事实:Ai 的列数应该等于Ai+1 的行数。那么,如果用p_i表示Ai的列数,Ai+1的行数也是p_i

计算A[ i:j ]的数乘次数,可以先找一个分割点k,左边的数乘次数是A[ i:k ]的最小数乘次数,右边的数乘次数是A[ k+1:j ]的最小数乘次数。最后剩下两个矩阵:左边剩下的矩阵的行数是Ai的行数(也就是 Ai-1的列数),列数是Ak的列数;右边剩下的矩阵行数是Ak+1的行数(也就是Ak的列数),列数是Aj的列数。那么这两个矩阵相乘的数乘次数是 Ai-1的列数 * Ak的列数 * Aj的列数 =  p_i_-_1 * p_k * p_j

那么,状态转移方程就很明显了:

dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + p_i_-_1*p_k*p_j), i <= k < j

和上面的问题一样,也是从len = 2...N进行枚举,对每个 0 <= i < N 枚举右端点j = i + len - 1。 最后得到dp[0][N-1]就是最少的数乘次数。

下面是一个套壳的环形矩阵链乘问题:

https://vijos.org/p/1312

题目描述很长,大意如下:

有一串珠子,每个珠子上有两个数,记为head和tail。前一颗珠子的tail等于后一颗珠子的head。如果将相邻的两颗珠子合并,将会得到 a.head * a.tail * (a+1).tail 的得分,求出将所有珠子合并能得到的最大得分。

首先考虑链形的问题,其实可以把每个珠子看成 head * tail 的矩阵,合并两个珠子的得分也就是 两个矩阵相乘的数乘次数。那么对于链形问题来说,这道题就是给出了每个矩阵的规模,求出最大的数乘次数。

转移方程:

\dpi{120} dp[i][j] = max(dp[i][j], dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j]) i<= k < j

其中 p[i] 存放的应该是矩阵 i 的列大小(第i颗珠子的tail),也就是第i+1颗珠子的 head,因为题目是按顺序给出 head值,因此实际上应该采用head[i+1]进行计算。

解决了链形问题,环形问题的处理方法就和第二题一样,复制相同的head数组接在后面,做规模是2n * 2n的DP矩阵,之后,找到长度是n的(i, j)对中,dp[i][j]的最大值。

#include <bits/stdc++.h>
using namespace std;

int main()
{
    int n;
    scanf("%d", &n);
    vector<int> head(2*n);
    for (int i = 0; i < n; ++i)
    {
        scanf("%d", &head[i]);
        head[i+n] = head[i];
    }
    vector<vector<int>> dp(2*n, vector<int>(2*n, 0));
    // dp[i][j]: [i...j]释放的最大能量,看成矩阵乘法,head[i]是第i个矩阵的行,tail[i]是第i个矩阵的列
    // dp[i][j] = max(dp[i][k] + dp[k+1][j] + head[i] * tail[k] * tail[j]) i <= k < j
    // tail[k] = head[k+1], tail[j] = head[j+1];
    for (int len = 2; len <= n; ++len)
    {
        for (int i = 0; i + len - 1< 2*n - 1; ++i)
   
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
区间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的基本思想和应用方法。如果你还有其他问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值