动态规划系列 | 一文搞定区间DP

发布时间:2023年12月20日

特点

区间 DP 可以用于解决一些涉及到区间合并或分割的问题。区间 DP 通常有以下三个特点:

  1. 合并(分割):将两个或多个部分进行整合,或者反过来将一个区间分解成多个部分。
  2. 特征:能将问题分解为能两两合并的形式。
  3. 求解:对整个问题设最优解,枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值。

石子合并

题目描述

原题链接

设有 N 堆石子排成一排,其编号为 1 , 2 , 3 , … , N 1,2,3,…,N 1,2,3,,N

每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 4,得到 4 5 2, 又合并 1、2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4 + 9 + 11 = 24 4+9+11=24 4+9+11=24

如果第二步是先合并 2、3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4 + 7 + 11 = 22 4+7+11=22 4+7+11=22

问题是:找出一种合理的方法,使总的代价最小,输出最小代价。

问题分析

状态表示dp[i][j]表示将第 i 堆石子到第 j 堆石子合并成一堆石子的最小代价。

状态计算

  • 按照区间长度,从小到大枚举。
  • 遍历最后一次分界线 k 的位置,将区间[i, j]分成[i, k][k, j],求将第 i 堆石子到第 j 堆石子合并成一堆石子的最小代价,即dp[i][j] = max(dp[i][k] + dp[k+1][j]) + cost(i, j)

程序代码

#include <iostream>
#include <vector>
#include <algorithm>
#include <limits.h>
using namespace std;

int main()
{
    int n;
    cin >> n;
    vector<int> a(n + 1, 0), sum(n + 1, 0);
    vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0));
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
        sum[i] = sum[i-1] + a[i];
    }
    // 枚举区间长度
    for(int len = 2; len <= n; len++) {
        // 枚举区间的左端点
        for(int i = 1; i <= n - len + 1; i++) {
            int j = i + len - 1;
            // 初始化
            dp[i][j] = INT_MAX;
            // 枚举分界点
            for(int k = i; k < j; k++) {
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1]);
            }
        }
    }
    cout << dp[1][n] << endl;
    return 0;
}

复杂度分析

时间复杂度为 O ( N 3 ) O(N^3) O(N3)

环形石子合并

题目描述

将 n 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。

规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。

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

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

问题分析

前面我们已经考虑了线性区间的石子合并问题,仔细观察环形区间的石子合并问题,可以发现将两个石子合并,可以看作在两个石子之间连一条边。将 n 个石子合并需要 n - 1 次合并操作,即连 n - 1 条边。沿着缺口将环形区间拉直,便可转换为线性区间问题。

在这里插入图片描述

为了遍历所有可能的缺口情况,我们可以将原问题复制一份,接在原数组后面,构成一个具有 2n 个石子的问题。求解该线性区间问题,最终找dp[i][i+n]的最大值(其中 1 ≤ i ≤ n 1 \leq i \leq n 1in),得到环形区间问题的最大值。

在这里插入图片描述

状态定义和状态转移方程同上一题(石子合并问题)相同。

程序代码

#include <iostream>
#include <limits.h>
#include <vector>
using namespace std;

int main()
{
    int n;
    cin >> n;
    vector<int> a(2 * n + 1, 0), sum(2 * n + 1, 0);
    vector<vector<int>> dp(2 * n + 1, vector<int>(2 * n + 1, 0));
    vector<vector<int>> f(2 * n + 1, vector<int>(2 * n + 1, 0));
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
        a[i + n] = a[i];
    }
    for(int i = 1; i <= 2 * n; i++) {
        sum[i] = sum[i-1] + a[i];
    }
    for(int len = 2; len <= n; len++) {
        for(int i = 1; i <= 2 * n - len + 1; i++) {
            int j = i + len - 1;
            dp[i][j] = INT_MAX;
            f[i][j] = INT_MIN;
            for(int k = i; k < j; k++) {
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1]);
                f[i][j] = max(f[i][j], f[i][k] + f[k+1][j] + sum[j] - sum[i-1]);
            }
        }
    }
    
    int maxVal = INT_MIN, minVal = INT_MAX;
    for(int i = 1; i <= n; i++) {
        minVal = min(minVal, dp[i][i + n - 1]);
        maxVal = max(maxVal, f[i][i + n - 1]);
    }
    cout << minVal << endl;
    cout << maxVal << endl;
    return 0;
}

复杂度分析

时间复杂度为 O ( N 3 ) O(N^3) O(N3) 量级

能量项链

题目描述

在 Mars 星球上,每个 Mars 人都随身佩带着一串能量项链。在项链上有 N N N 颗能量珠。能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样,通过吸盘(吸盘是 Mars 人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。如果前一颗能量珠的头标记为 m m m,尾标记为 r r r,后一颗能量珠的头标记为 r r r,尾标记为 n n n,则聚合后释放的能量为 m × r × n m \times r \times n m×r×n(Mars 单位),新产生的珠子的头标记为 m m m,尾标记为 n n n

需要时,Mars 人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。

例如:设 N = 4 N=4 N=4 4 4 4 颗珠子的头标记与尾标记依次为 ( 2 , 3 ) ( 3 , 5 ) ( 5 , 10 ) ( 10 , 2 ) (2,3)(3,5)(5,10)(10,2) (2,3)(3,5)(5,10)(10,2)。我们用记号 ⊕ \oplus 表示两颗珠子的聚合操作, ( j ⊕ k ) (j \oplus k) (jk) 表示第 j , k j,k j,k 两颗珠子聚合后所释放的能量。则第 4 4 4 1 1 1 两颗珠子聚合后释放的能量为:

( 4 ⊕ 1 ) = 10 × 2 × 3 = 60 (4 \oplus 1)=10 \times 2 \times 3=60 (41)=10×2×3=60

这一串项链可以得到最优值的一个聚合顺序所释放的总能量为:

( ( ( 4 ⊕ 1 ) ⊕ 2 ) ⊕ 3 ) = 10 × 2 × 3 + 10 × 3 × 5 + 10 × 5 × 10 = 710 (((4 \oplus 1) \oplus 2) \oplus 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

问题分析

不难发现,此题就是环形矩阵连乘问题。与上题类似,我们可以先考虑线性区间问题的求解。

状态表示:dp[i][j]表示将第 i 个珠子到第 j 个珠子合并在一起,所能释放的最大能量。

状态计算:

  • 考虑所有的分界点,将区间[i, j]分成[i, k][k+1, j],表示最后一次的合并点。
  • 对于所有可能的分界点,取可能值最大的分界点,即dp[i][j] = max(dp[i][k] + dp[k+1][j] + profit(i, k, j))

环形的情况:【这里偷个懒,环形石子合并问题的图进行解释】

假设珠子构成的环形有 n 个点,每合并两个珠子,在他们之间连一条线。将所有珠子都合并,需要连 n - 1 条线。可以发现,珠子并没有连成环,而是存在一个缺口。因此,我们可以考虑缺口的所有可能位置,将这个带缺口的环形拉直,变成一条直线,转换成线性区间问题。

在这里插入图片描述

为了遍历所有可能情况,我们可以将原问题复制一份,接在原数组后面,构成一个具有 2n 个珠子的问题。求解该线性区间问题,最终找dp[i][i+n]的最大值(其中 1 ≤ i ≤ n 1 \leq i \leq n 1in),得到环形区间问题的最大值。

在这里插入图片描述

Note:这里注意,2 个珠子合并需要 3 个参数,因此区间长度应该从 3 开始,最大区间长度应该是n + 1

程序代码

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main()
{
    int n;
    cin >> n;
    vector<int> w(2 * n + 1);
    vector<vector<int>> dp(2 * n + 1, vector<int>(2 * n + 1, 0));
    for(int i = 1; i <= n; i++) {
        cin >> w[i];
        w[i + n] = w[i];
    }
    for(int len = 2; len <= n + 1; len++) {
        for(int i = 1; i <= n * 2 - len + 1; i++) {
            int j = i + len - 1;
            for(int k = i + 1; k < j; k++) {
                dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + w[i]*w[k]*w[j]);
            }
        }
    }
    int res = 0;
    for(int i = 1; i <= n; i++) {
        res = max(res, dp[i][i + n]);
    }
    
    cout << res << endl;
    return 0;
}

复杂度分析

时间复杂度为 O ( N 3 ) O(N^3) O(N3)

加分二叉树

题目描述

原题链接

设一个 n n n 个节点的二叉树 tree \text{tree} tree 的中序遍历为 ( 1 , 2 , 3 , … , n ) (1,2,3,\ldots,n) (1,2,3,,n),其中数字 1 , 2 , 3 , … , n 1,2,3,\ldots,n 1,2,3,,n 为节点编号。每个节点都有一个分数(均为正整数),记第 i i i 个节点的分数为 d i d_i di? tree \text{tree} tree 及它的每个子树都有一个加分,任一棵子树 subtree \text{subtree} subtree(也包含 tree \text{tree} tree 本身)的加分计算方法如下:

subtree \text{subtree} subtree 的左子树的加分 × \times × subtree \text{subtree} subtree 的右子树的加分 + + + subtree \text{subtree} subtree 的根的分数。

若某个子树为空,规定其加分为 1 1 1,叶子的加分就是叶节点本身的分数。不考虑它的空子树。

试求一棵符合中序遍历为 ( 1 , 2 , 3 , … , n ) (1,2,3,\ldots,n) (1,2,3,,n) 且加分最高的二叉树 tree \text{tree} tree。要求输出

  1. tree \text{tree} tree 的最高加分。
  2. tree \text{tree} tree 的前序遍历。

问题分析

这道题的主要考察区间 DP 问题的方案记录

先回顾下,二叉树的中序遍历:先递归遍历左子树,输出根节点,再递归遍历右子树。

在这里插入图片描述

状态表示:dp[L][R]表示所有中序遍历是[L, R]序列的二叉树的集合。

状态计算:dp[L][R] = max(dp[L][k-1] * dp[k+1][R] + w[k]),其中 k 为根节点所有可能的位置,即分界点。

方案记录与输出:核心在于记录每次区间划分的根节点

  • g[L][R] = root:表示对于[L, R]这个二叉树的中序遍历,其根节点为root,将二叉树划分成左子树[L, root - 1]和右子树[root + 1, R]
  • 输出则按照二叉树前序遍历的模式递归输出。

边界情况处理:

  • 若区间长度为 1:表示处理叶节点的情况,叶节点的加分就是叶节点本身的分数,即dp[i][i] = w[i]。根节点就是本身,即g[i][i] = i
  • 若区间长度大于 1:
    • k == l时,说明左子树为空,则dp[i][j] = dp[k+1][r] + w[k]
    • k == r时,说明右子树为空,则dp[i][j] = dp[l][k-1] + w[k]

程序代码

#include <iostream>
#include <algorithm>
#include <vector>
#include <limits.h>
using namespace std;

// 前序遍历
void dfs(const vector<vector<int>>& g, int left, int right)
{
    if(left > right)  return ;
    int root = g[left][right];
    cout << root << " ";
    dfs(g, left, root - 1);
    dfs(g, root + 1, right);
}

int main()
{
    int n;
    cin >> n;
    vector<int> w(n + 1, 0);
    vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0));
    vector<vector<int>> g(n + 1, vector<int>(n + 1, 0));
    for(int i = 1; i <= n; i++) {
        cin >> w[i];
    }
    for(int len = 1; len <= n; len++) {
        for(int l = 1; l <= n - len + 1; l++) {
            int r = l + len - 1;
            if(len == 1) {
                dp[l][l] = w[l];
                g[l][l] = l;
                continue;
            }
            for(int k = l; k <= r; k++) {
                int left = l == k ? 1 : dp[l][k-1];
                int right = r == k ? 1 : dp[k+1][r];
                int t = left * right + w[k];
                if( dp[l][r] < t ) {
                    dp[l][r] = t;
                    g[l][r] = k;
                }
            }
        }
    }
    
    cout << dp[1][n] << endl;
    dfs(g, 1, n);
    return 0;
}

复杂度分析

时间复杂度为 O ( N 3 ) O(N^3) O(N3) 量级

凸多边形的划分

题目描述

给定一个具有 N 个顶点的凸多边形,将顶点从 1 至 N 标号,每个顶点的权值都是一个正整数。

将这个凸多边形划分成 N?2 个互不相交的三角形,对于每个三角形,其三个顶点的权值相乘都可得到一个权值乘积,试求所有三角形的顶点权值乘积之和至少为多少。

问题分析

在这里插入图片描述

观察上述凸多边形,我们以(3, 4)为边,6为分界点,可以将凸多边形分成leftright两个多边形,以及一个三角形(3, 4, 6)。对于这种情况,凸多边形的最小值就是左边凸多边形left的最小值,加上右边凸多边形right的最小值,再加上三角形(3, 4, 6)权值乘积。

通过遍历所有可能的分界点,我们便可以得到以(3, 4)为底边的三角形,所有可能情况凸多边形的最小值。

在这里插入图片描述

状态表示:dp[L][R]表示所有将(L, L+1), (L+1, L+2), ..., (R-1, R), (R, L)这个多边形分成三角形的最小值。

状态计算:

  • 固定三角形的底边,遍历所有可能的分界点,即三角形的顶点 k
  • 状态转移方程:dp[L][R] = min(dp[L][k] + dp[k][R] + w[R] * w[L] * w[k])

Note:这里不做环形处理是因为,凸多边形的划分与划分顺序无关,即影响权值乘积的因素只有划分结果。因此,任意一种环形情况,都能在一个线性区间情况中找到一个对应的划分

程序代码

无高精度,只能过部分样例

#include <iostream>
#include <vector>
#include <algorithm>
#include <limits.h>
using namespace std;

int main()
{
    int n;
    cin >> n;
    vector<int> w(n + 1, 0);
    vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0));
    for(int i = 1; i <= n; i++) {
        cin >> w[i];
    }
    // 区间长度最小值为3
    for(int len = 3; len <= n; len++) {
        for(int l = 1; l <= n - len + 1; l++) {
            int r = l + len - 1;
            dp[l][r] = INT_MAX;
            // 遍历三角形的顶点(分界点)
            for(int k = l + 1; k < r; k++) {
                dp[l][r] = min(dp[l][r], dp[l][k] + dp[k][r] + w[r] * w[l] * w[k]);
            }
        }
    }
    cout << dp[1][n] << endl;
    return 0;
}

这里用 Python 偷懒过高精度(手动狗头

n = int(input())
a = list(map(int, input().split()))
w = [0] + a
dp = [[0 for i in range(n + 1)] for i in range(n + 1)]
for len in range(3, n + 1):
    for l in range(1, n - len + 2):
        r = l + len - 1
        dp[l][r] = float('inf')
        for k in range(l + 1, r):
            dp[l][r] = min(dp[l][r], dp[l][k] + dp[k][r] + w[r] * w[l] * w[k])
print(dp[1][n])

复杂度分析

时间复杂度为 O ( N 3 ) O(N^3) O(N3) 量级

棋盘分割

题目描述

原题链接

将一个 8 × \times × 8 的棋盘进行如下分割:将原棋盘割下一块矩形棋盘并使剩下部分也是矩形,再将剩下的部分继续如此分割,这样割了 ( n ? 1 ) (n-1) (n?1) 次后,连同最后剩下的矩形棋盘共有 n n n 块矩形棋盘。 (每次切割都只能沿着棋盘格子的边进行)

原棋盘上每一格有一个分值,一块矩形棋盘的总分为其所含各格分值之和。现在需要把棋盘按上述规则分割成 n n n 块矩形棋盘,并使各矩形棋盘总分的均方差最小。

均方差 σ = ∑ i = 1 n ( x i ? x ˉ ) 2 n \sigma = \sqrt{ \frac{ \sum_{i=1}^n (x_i - \bar x)^2 } { n }} σ=ni=1n?(xi??xˉ)2? ? ,其中平均值 x ˉ = ∑ i = 1 n x i n \bar x = \frac{\sum_{i=1}^n x_i}{n} xˉ=ni=1n?xi?? , x i x_i xi? 为第 i i i 块矩形棋盘的分。

请编程对给出的棋盘及 n n n ,求出 σ \sigma σ 的最小值。

问题分析

这道题属于二维的区间 DP 问题,为了简化代码,可以使用记忆化搜索来求解

状态表示:dp[x1][y1][x2][y2][k]表示将子矩阵(x1, y1)(x2, y2)切分成 k 部分,即切 k - 1 刀,均方差的最小值。

在这里插入图片描述

状态计算:

  • 横着切一刀dp(x1, y1, x2, y2, k) = min(get(x1, y1, i, y2) + dp(i + 1, y1, x2, y2, k - 1), get(i + 1, y1, x2, y2) + dp(x1, y1, i, y2, k - 1))
  • 竖着切一刀dp(x1, y2, x2, y2, k) = min(get(x1, y1, x2, j) + dp(x1, j + 1, x2, y2, k - 1), get(x1, j + 1, x2, y2) + dp(x1, y1, x2, j, k - 1))

程序代码

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;

const int N = 10, M = 17;
const double INF = 1e9 + 7;

int n = 8, m;  // 划分成m个矩形
int s[N][N];  // 二维前缀和
double f[N][N][N][N][M];
double xmean;  // 均值

// 求方块元素之和
int getSum(int x1, int y1, int x2, int y2)
{
    return s[x2][y2] + s[x1 - 1][y1 - 1] - s[x1 - 1][y2] - s[x2][y1 - 1];
}

// 求方差的其中一项
double get(int x1, int y1, int x2, int y2)
{
    double t = getSum(x1, y1, x2, y2) - xmean;
    return (double)t * t / m;
}

// 记忆化搜索求二维区间DP问题
double dp(int x1, int y1, int x2, int y2, int k)
{
    double &v = f[x1][y1][x2][y2][k];
    // 已经计算过,直接返回
    if(v >= 0)  return v;
    // 划分成1个矩形,无需再切
    if(k == 1)  return v = get(x1, y1, x2, y2);
    
    v = INF;
    // 横切
    for(int i = x1; i < x2; i++) {
        // 留下上面部分,继续切下面部分
        v = min(v, get(x1, y1, i, y2) + dp(i + 1, y1, x2, y2, k - 1));
        // 留下下边部分,继续切上面部分
        v = min(v, get(i + 1, y1, x2, y2) + dp(x1, y1, i, y2, k - 1));
    }
    // 竖切
    for(int i = y1; i < y2; i++) {
        // 留下左边,继续切右边
        v = min(v, get(x1, y1, x2, i) + dp(x1, i + 1, x2, y2, k - 1));
        // 留下右边,继续切左边
        v = min(v, get(x1, i + 1, x2, y2) + dp(x1, y1, x2, i, k - 1));
    }
    
    return v;
}

int main()
{
    cin >> m;
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            cin >> s[i][j];
            s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
        }
    }
    // m个矩形的均值
    xmean = (double)s[n][n] / m;
    // 初始化
    memset(f, -1, sizeof(f));
    printf("%.3lf\n", sqrt(dp(1, 1, n, n, m)));
    
    return 0;
}
文章来源:https://blog.csdn.net/qq_45931691/article/details/135114338
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。