第五章 动态规划(6):区间DP模型

本文详细介绍了区间动态规划在解决石子合并、环形石子合并、能量项链、凸多边形划分、加分二叉树和棋盘分割等问题中的应用。通过分析问题特点,将环形区间转换为一维处理,利用迭代和记忆化搜索等方法,求解各种复杂问题的最优解。此外,还讨论了在处理区间DP时如何进行状态划分和状态计算,并给出了具体的代码实现。
摘要由CSDN通过智能技术生成

区间DP问题的代码一般有两种:

  • 迭代式:适用于一维区间DP
  • 记忆化搜索:适用于二维区间DP

1、石子合并

ACWing 282

一个思路、代码完全一样但是题目难读的题目:ACwing 3240 压缩编码

集合
f [ i ] [ j ] f[i][j] f[i][j]:所有将从 i i i j j j 这段石子合并成一堆石子的合并方式的价值最大值

集合划分
以最后一次合并的分界线来分类,因为最后一次一定是两个集合合并。假设总共有 k = j − i + 1 k = j - i + 1 k=ji+1 个元素,则划分方案有:

  • 左边 1 1 1 个,右边 k − 1 k-1 k1 个;
  • 左边 2 2 2 个,右边 k − 2 k-2 k2 个;
  • … \dots
  • 左边 k − 1 k-1 k1 个,右边 1 1 1 个;

状态计算
假设在最后一步划分情况为 [ i , k ] , [ k + 1 , j ] [i,k], [k+1, j] [i,k],[k+1,j],那么在计算最终最小代价之前的一步的最小代价为 f [ i ] [ k ] + f [ k + 1 ] [ j ] f[i][k]+f[k+1][j] f[i][k]+f[k+1][j],最终的最小代价就是加上区间 [ i , j ] [i,j] [i,j] 的和(最后一步的代价是固定的)。总的代价为

f [ i ] [ j ] = m i n ( f [ i ] [ j ] ,   f [ i ] [ k ] + f [ k + 1 ] [ j ] + s [ j ] − s [ i − 1 ] ) f[i][j] = min(f[i][j], \ f[i][k]+f[k+1][j]+s[j]-s[i-1]) f[i][j]=min(f[i][j], f[i][k]+f[k+1][j]+s[j]s[i1])

其中, s [ i ] s[i] s[i] 表示区间 1 ∼ i 1\sim i 1i 的前缀和, k ∈ [ i ] [ j − 1 ] k\in [i][j-1] k[i][j1](右边至少要有一堆,不能枚举到 k = j k=j k=j)。

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

const int N = 310;

int n, s[N], f[N][N];

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) {
        scanf("%d", s + i);
        s[i] += s[i - 1];
    }
    for (int len = 2; len <= n; len ++ )  // 指定区间长度
        for (int i = 1; i + len - 1 <= n; i ++ ) {  // 区间左右边界
            int l = i, r = i + len - 1;
            f[l][r] = 1e9;
            for (int k = l; k < r; k ++ )  // 划分区间
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
        }
    printf("%d\n", f[1][n]);
    return 0;
}

2、环形石子合并(环形区间转换成一维)

ACwing 1068

将环拆成链的做法是将原来长度为n的环在任一点断开,将其长度扩展成2n后,就能在一条长度为2n的一维链上找到所有环里面长度为n的链。

在这里插入图片描述

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 410, INF = 0x3f3f3f3f;

int n;
int s[N], w[N];
int f[N][N], g[N][N]; // 一个最小值DP,一个最大值DP

int main() {
    cin >> n;
    for (int i = 1; i <= n; i ++ )
    {
        cin >> w[i];
        w[i + n] = w[i];
    }

    for (int i = 1; i <= n * 2; i ++ ) s[i] = s[i - 1] + w[i];

    memset(f, 0x3f, sizeof f);
    memset(g, -0x3f, sizeof g);

    for (int len = 1; len <= n; len ++ )
        for (int l = 1; l + len - 1 <= n * 2; l ++ )
        {
            int r = l + len - 1;
            if (len == 1)
                f[l][r] = g[l][r] = 0; // 仅有一个值的时候
            else
            {
                for (int k = l; k < r; k ++ )
                {
                    f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
                    g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + s[r] - s[l - 1]);
                }
            }
        }

    int minv = INF, maxv = -INF;
    for (int i = 1; i <= n; i ++ )
    {
        minv = min(minv, f[i][i + n - 1]);
        maxv = max(maxv, g[i][i + n - 1]);
    }

    cout << minv << endl << maxv << endl;
    return 0;
}

3、能量项链(环形区间转换成一维)

ACwing 320

先考虑链式,最后环形根据上题经验计算。

本题输入的矩阵是(2,3)(3,5)(5,10)(10,2),现将其处理成2, 3, 5, 10, 2,也就是(2, 3)、(3, 5)...的简化表示。

集合

f[L,R]:所有将[L,R]合并为一个矩阵(珠子)的方式的价值的最大值

集合划分

按划分的分界线来划分:
在这里插入图片描述

状态计算

若划分的分界线为K,所以第K个能量f[L, K] + f[K,R] + W[L]*W[K]*W[R],故总的f[L,R] = max(f[L, K] + f[K,R] + W[L]*W[K]*W[R])

环形只需要枚举2n上的所有n + 1个长度的区间中的最大值即可(环形的处理跟上一题思路一样)。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 210;

int n;
int w[N];
int f[N][N];

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> w[i];
        w[i + n] = w[i];
    }

    for (int len = 3; len <= n + 1; len++) // 注意这里的长度,要合并最小长度是3;从1开始转一圈会回到1,所以是n+1
        for (int l = 1; l + len - 1 <= n * 2; l++) {
            int r = l + len - 1;
            for (int k = l + 1; k < r; k++)
                f[l][r] = max(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]);
        }

    int res = 0;
    for (int i = 1; i <= n; i++) // 枚举所有长度是n+1的区间
        res = max(res, f[i][i + n]);

    cout << res << endl;

    return 0;
}

4、凸多边形的划分(区间DP+高精度)

ACwing 1069

考虑一种随机情况,当多边形被划分成如下情况时候,又因为题目中有“N−2三角形个互不相交”的前提条件,所以这三个部分相互独立,故这个时候的权值之和就可以表示成:左边的权值+右边的权值+这个三角形的权值,整个的最小值就是三个部分的最小值求和。
在这里插入图片描述

这里的状态表示成:f[1, 6] = f[1, 4] + f[4, 6] + w[1] * w[4] * w[6]

集合

f[L, R]:所有将(L,L+1),(L+1,L+2),...,(R-1, R),(R,L)这个多边形划分成若干个三角形的方案数的价值的最小值

集合划分

根据(L,R)这条边属于那个三角形来划分,将多边形划分成三个部分

在这里插入图片描述

状态计算

f[L, R] = f[L, K] + f[K, R] + w[L] * w[K] * w[R] (与上一个题的状态转移方程一模一样)

未使用高精度的代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 55, INF = 1e9;

int n;
int w[N];
int f[N][N];

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> w[i];

    for (int len = 3; len <= n; len++)
        for (int l = 1; l + len - 1 <= n; l++) {
            int r = l + len - 1;
            f[l][r] = INF;
            for (int k = l + 1; k < r; k++) // k只能循环到r-1,才能构成三角形
                f[l][r] = min(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]);
        }

    cout << f[1][n] << endl;

    return 0;
}

使用高精度的代码:

#include <iostream>
#include <vector>

using namespace std;

typedef long long LL;

const int N = 55;

int n;
int w[N];
vector<int> f[N][N];

// 比较大小
bool cmp(vector<int> &a, vector<int> &b)
{
    if (a.size() != b.size())
        return a.size() < b.size();
        
    for (int i = a.size() - 1; i >= 0; i -- )
        if (a[i] != b[i])
            return a[i] < b[i];
            
    return true;
}

vector<int> add(vector<int> a, vector<int> b)
{
    vector<int> c;
    int t = 0;
    for (int i = 0; i < a.size() || i < b.size(); i ++ )
    {
        if (i < a.size()) t += a[i];
        if (i < b.size()) t += b[i];
        c.push_back(t % 10);
        t /= 10;
    }
    
    while (t) c.push_back(t % 10), t /= 10;
    return c;
}

vector<int> mul(vector<int> a, LL b)
{
    vector<int> c;
    LL t = 0;
    for (int i = 0; i < a.size(); i ++ )
    {
        t += b * a[i];
        c.push_back(t % 10);
        t /= 10;
    }
    while (t) c.push_back(t % 10), t /= 10;
    return c;
}

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
    
    for (int len = 3; len <= n; len ++ )
        for (int l = 1, r; (r = l + len - 1) <= n; l ++ )
            for (int k = l + 1; k < r; k ++ )
            {
                auto new_val = mul(mul({w[l]}, w[k]), w[r]);
                new_val = add(add(new_val, f[l][k]), f[k][r]);
                if (f[l][r].empty() || cmp(new_val, f[l][r]))
                    f[l][r] = new_val;
            }
            
    auto res = f[1][n];
    
    for (int i = res.size() - 1; i >= 0; i -- )
        printf("%d", res[i]);
    printf("\n");
    
    return 0;
}

5、加分二叉树(区间DP记录方案数)

ACwing 479

由题:每棵子树的中序遍历序列都是连续的。

集合

f[L,R]:所有中序遍历是[L,R]这一段的二叉树的集合的分值最大值

集合划分

按根结点的位置来划分
在这里插入图片描述

状态计算

假设根结点的位置在第k个点,左子树[L,k-1],右子树[k+1,R],且左右子树是独立的。所以总的最大值即为左边最大值 + 右边最大值 + w[k],即为f[L, k-1] + f[k+1, R] + w[k]

#include <iostream>
#include <cstring>

using namespace std;

const int N = 30;

int n;
int w[N];
int f[N][N], g[N][N]; //g(l, r)用于记录(l, r)区间内的根结点

// 输出方案
void dfs(int l, int r) {
    if (l > r) return;

    int root = g[l][r];
    cout << root << ' ';
    dfs(l, root - 1);
    dfs(root + 1, r);
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> w[i];

    for (int len = 1; len <= n; len++)
        for (int l = 1; l + len - 1 <= n; l++) {
            int r = l + len - 1;
            if (len == 1) // 如果是叶结点
            {
                f[l][r] = w[l];
                g[l][r] = l; // 根结点是自己
            } else {
                for (int k = l; k <= r; k++) {
                    int left = k == l ? 1 : f[l][k - 1]; // k == l 表示左子树为空
                    int right = k == r ? 1 : f[k + 1][r]; // k == r 表示右子树为空
                    int score = left * right + w[k];
                    if (f[l][r] < score) // 是为了找到字典序最小的方案,找尽可能左边的,也就是说如果在k取不同的值但是score相同,我们优先取得最左边(第一个出现的)k
                    {
                        f[l][r] = score;
                        g[l][r] = k;
                    }
                }
            }
        }

    cout << f[1][n] << endl;
    dfs(1, n);

    return 0;
}

6、棋盘分割(二维区间DP、记忆化搜索实现(循环太长不方便))

ACwing 321

由题,要使均方差 ∑ i = 1 n ( x i − x ˉ ) 2 n \sqrt{\frac{\sum_{i=1}^{n}(x_i-\bar{x})^2}{n}} ni=1n(xixˉ)2 最小,只需要使 ∑ i = 1 n ( x i − x ˉ ) 2 n \frac{\sum_{i=1}^{n}(x_i-\bar{x})^2}{n} ni=1n(xixˉ)2 最小即可,又因为有: ∑ i = 1 n ( x i − x ˉ ) 2 n = ∑ i = 1 n x i 2 n − x ˉ 2 \frac{\sum_{i=1}^{n}(x_i-\bar{x})^2}{n}=\frac{\sum_{i=1}^nx_i^2}{n}-\bar{x}^2 ni=1n(xixˉ)2=ni=1nxi2xˉ2,且 x ˉ 2 \bar{x}^2 xˉ2 固定,所以我们只需求 ∑ i = 1 n x i 2 \sum_{i=1}^nx_i^2 i=1nxi2 ,即每一部分的平方和的最小值即可。

化简过程: ∑ i = 1 n ( x i − x ˉ ) 2 n = 1 n ( ∑ i = 1 n ( x i 2 − 2 x i x ˉ + x ˉ 2 ) ) = 1 n ( ∑ i = 1 n x i 2 − x ˉ ∑ i = 1 n 2 x i + n x ˉ 2 ) = 1 n ( ∑ i = 1 n x i 2 − x ˉ 2 n x ˉ + n x ˉ 2 ) = 1 n ( ∑ i = 1 n x i 2 − n x ˉ 2 ) = ∑ i = 1 n x i 2 n − x ˉ 2 \frac{\sum_{i=1}^{n}(x_i-\bar{x})^2}{n} = \frac{1}{n}(\sum_{i=1}^{n}(x_i^2-2x_i\bar{x}+\bar{x}^2))=\frac{1}{n}(\sum_{i=1}^nx_i^2-\bar{x}\sum_{i=1}^n2x_i+n\bar{x}^2)=\frac{1}{n}(\sum_{i=1}^nx_i^2-\bar{x}2n\bar{x}+n\bar{x}^2)=\frac{1}{n}(\sum_{i=1}^nx_i^2-n\bar{x}^2)=\frac{\sum_{i=1}^nx_i^2}{n}-\bar{x}^2 ni=1n(xixˉ)2=n1(i=1n(xi22xixˉ+xˉ2))=n1(i=1nxi2xˉi=1n2xi+nxˉ2)=n1(i=1nxi2xˉ2nxˉ+nxˉ2)=n1(i=1nxi2nxˉ2)=ni=1nxi2xˉ2

上面化简过程偶尔会用到,注意推导。下面是不化简解决问题的过程。

集合

f[x1, y1, x2, ye, k]:子矩阵(x1,y1)(x2,y2)切分成k部分的所有方案中 ∑ i = 1 n ( x i − x ˉ ) 2 n \frac{\sum_{i=1}^{n}(x_i-\bar{x})^2}{n} ni=1n(xixˉ)2 的最小值,其中(x1,y1)是矩阵左上角,(x2,y2)是矩阵右下角坐标。

集合划分

对于一个mxn的矩阵

  • 按最后一次切的方向来划分,分成横切纵切
  • 在每种切法里面,可以分别切横向m刀和纵向n
  • 在每一刀里面,还可以进一步划分
    • 对于横切,分为取上半部分和下半部分
    • 对于纵切,分为取左半部分和右半部分

在这里插入图片描述

状态计算

在每一种切法中取一个最小值,然后再对所有切法取一个最小值即可。

#include <iostream>
#include <cstring>
#include <cmath>
#include <algorithm>

using namespace std;

const int N = 15, M = 9;
const double INF = 1e9;

int n, m = 8;
int s[M][M];
double f[M][M][M][M][N];
double X; // 表示x的平均值

// 那个公式的计算
double get(int x1, int y1, int x2, int y2) {
    double sum = s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1] - X;
    return sum * sum / n;
}

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;
    if (k == 1) return v = get(x1, y1, x2, y2);

    v = INF;
    for (int i = x1; i < x2; i++) // 横切
    {
        v = min(v, dp(x1, y1, i, y2, k - 1) + get(i + 1, y1, x2, y2)); // 取上面
        v = min(v, dp(i + 1, y1, x2, y2, k - 1) + get(x1, y1, i, y2)); // 取下面
    }

    for (int i = y1; i < y2; i++) // 竖切
    {
        v = min(v, dp(x1, y1, x2, i, k - 1) + get(x1, i + 1, x2, y2)); // 取左边
        v = min(v, dp(x1, i + 1, x2, y2, k - 1) + get(x1, y1, x2, i)); // 取右边
    }

    return v;
}

int main() {
    cin >> n;
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= m; j++) {
            cin >> s[i][j];
            s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
        }

    memset(f, -1, sizeof f); // double数组是使用科学计数法存储,当值为-1表示的是-NAN
    X = (double) s[m][m] / n;

    printf("%.3lf\n", sqrt(dp(1, 1, 8, 8, n)));

    return 0;
}

8、统计不同回文子序列

LeetCode 730

DP三维:

class Solution {
public:
    const int MOD = 1e9 + 7;

    int countPalindromicSubsequences(string s) {
        int n = s.size();
        vector < vector < vector < int>>> dp(4, vector < vector < int >> (n, vector<int>(n, 0)));
        for (int i = 0; i < n; i++) dp[s[i] - 'a'][i][i] = 1;
        for (int len = 2; len <= n; len++)
            for (int i = 0, j = len - 1; j < n; i++, j++)
                for (char c = 'a', k = 0; c <= 'd'; c++, k++)
                    if (s[i] == c && s[j] == c)
                        dp[k][i][j] = (2LL + dp[0][i + 1][j - 1] + dp[1][i + 1][j - 1]
                                       + dp[2][i + 1][j - 1] + dp[3][i + 1][j - 1]) % MOD;
                    else if (s[i] == c) dp[k][i][j] = dp[k][i][j - 1];
                    else if (s[j] == c) dp[k][i][j] = dp[k][i + 1][j];
                    else dp[k][i][j] = dp[k][i + 1][j - 1];
        int res = 0;
        for (int i = 0; i < 4; i++) res = (res + dp[i][0][n - 1]) % MOD;
        return res;
    }
};

二维DP

class Solution {
public:
    const int MOD = 1e9 + 7;

    int countPalindromicSubsequences(string s) {
        int n = s.size();
        vector <vector<int>> dp(n, vector<int>(n));
        for (int i = 0; i < n; i++) dp[i][i] = 1;
        for (int len = 2; len <= n; len++)
            for (int i = 0; i + len <= n; i++) {
                int j = i + len - 1;
                if (s[i] == s[j]) {
                    int low = i + 1, high = j - 1;
                    while (low <= high && s[low] != s[i]) low++;
                    while (high >= low && s[high] != s[j]) high--;
                    if (low > high) dp[i][j] = (2 + dp[i + 1][j - 1] * 2) % MOD;
                    else if (low == high) dp[i][j] = (1 + dp[i + 1][j - 1] * 2) % MOD;
                    else dp[i][j] = (0LL + dp[i + 1][j - 1] * 2 - dp[low + 1][high - 1] + MOD) % MOD;
                } else dp[i][j] = (0LL + dp[i + 1][j] + dp[i][j - 1] - dp[i + 1][j - 1] + MOD) % MOD;
            }
        return dp[0][n - 1];
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值