栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

区间DP.

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

区间DP.

区间DP

环形石子合并能量项链加分二叉树凸多边形的划分棋盘分割

环形石子合并

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

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

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

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

输入格式
第一行包含整数 n,表示共有 n 堆石子。

第二行包含 n 个整数,分别表示每堆石子的数量。

输出格式
输出共两行:

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

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

数据范围
1 ≤ n ≤ 200

输入样例:

4
4 5 9 4

输出样例:

43
54


分析
拓展:

如果每轮合并的石子 可以是任意 的 两堆 石子,那么用到的就是经典的 Huffman Tree 的二叉堆模型
如果每轮合并的石子 可以是任意 的 nn 堆 石子,那么用到的就是经典的 Huffman Tree 的 nn 叉堆模型
以上两种题型可以参考:

    二叉堆:AcWing 148. 合并果子nn 叉堆:AcWing 149. 荷马史诗

回归本题,本题要求每轮合并的石子 必须是相邻的 两堆石子,因此不能采用 Huffman Tree 的模型

这类限制只能合并相邻两堆石子的模型,用到的是经典的 区间DP 模型

考虑如何设定 动态规划 的阶段,既可以表示出初始每个石子的费用,也可以表示出合并后整个堆的费用

不妨把当前合并的石子堆的大小作为DP的阶段

这样 len=1 表示初值,即每个堆只有一个石子; len=n 表示终值,即一个堆中有所有的石子

这种阶段设置方法保证了我们每次合并两个区间时,他们的所有子区间的合并费用都已经被计算出来了

阶段设定好后,考虑如何记录当前的状态,无外乎就两个参数:

    石子堆的左端点 l石子堆的右端点 r

在区间DP中,我们也常常省去 lenlen 这一维的空间
因为 r−l+1=lenr−l+1=len,也就保证了在已知 ll 和 rr 的情况下,不会出现状态定义重复的情况
根据线性代数中方程的解的基本概念,我们就可以删掉 lenlen 这一维不存在的约束
但为了方便读者理解,以及介绍区间DP的阶段是如何划分的,我还是写了出来

以上就是所有有关石子合并的区间DP分析

在考虑一下本题的 环形相邻 情况如何解决,方案有如下两种:

    我们可以枚举环中分开的位置,将环还原成链,这样就需要枚举 n 次,时间复杂度为 O(n4)我们可以把链延长两倍,变成 2n 个堆,其中 i 和 i+n 是相同的两个堆,然后直接套 区间DP 模板,但对于 阶段 len 只枚举到 n,根据 状态的定义,最终可以得到所求的方案,时间复杂度为 O(n3)

一般常用的都是第二种方法,我也只会演示第二种方法的写法,对第一种有兴趣的读者可以自行尝试

时间复杂度:O(n3)

#include 
#include 
using namespace std;
const int N = 410;
int n;
int a[N], s[N];
int mx[N][N]; // 从i到j合并的最大值
int mi[N][N]; // 从i到j合并的最小值
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++ ) {
        cin >> a[i];
        a[i + n] = a[i];
    }
    // 前缀和
    for (int i = 1; i <= 2 * n; i ++ ) s[i] = s[i - 1] + a[i];
    // 初始化数组
    memset(mx, -0x3f, sizeof mx);
    memset(mi, 0x3f, sizeof mi);
    // DP 迭代式
    for (int len = 1; len <= n; len ++ ) // 区间长度
        for (int l = 1; l + len - 1 <= 2 * n; l ++ ) { // 左右端点
            int r = l + len - 1;
            if (l == r) mx[l][r] = mi[l][r] = 0; // 一堆不需要合并
            else {
                // 枚举中间点
                for (int k = l; k < r; k ++ ) {
                    mx[l][r] = max(mx[l][r], mx[l][k] + mx[k + 1][r] + s[r] - s[l - 1]);
                    mi[l][r] = min(mi[l][r], mi[l][k] + mi[k + 1][r] + s[r] - s[l - 1]);
                }
            }
        }
    // 求结果
    int minv = 0x3f3f3f3f, maxv = -0x3f3f3f3f;
    for (int i = 1; i <= n; i ++ ) {
        minv = min(minv, mi[i][i + n - 1]);
        maxv = max(maxv, mx[i][i + n - 1]);
    }
    cout << minv << endl << maxv;
    return 0; 
}

为什么枚举中间点(分开点)时,可以取到左端点,但是不可以取到右端点?
经大佬点拨,明白,k 的取值范围,取决于定义的递推公式。以上代码定义的 k 为左半区间的右端点作为中间点,故 k 的取值范围为 l <= k < r,这样左半、右半区间都可以有仅取一个点的情况,不重不漏实现对左右区间的分割。
也可以将 k 定义为右半区间的左端点,这样k的范围就不一样了。

蒟蒻再次感谢彩色铅笔大佬

笔记学习:
作者:彩色铅笔
链接:https://www.acwing.com/solution/content/59932/
来源:AcWing

能量项链

在 Mars 星球上,每个 Mars 人都随身佩带着一串能量项链,在项链上有 N 颗能量珠。

能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。

并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。

因为只有这样,通过吸盘(吸盘是 Mars 人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。

如果前一颗能量珠的头标记为 m,尾标记为 r,后一颗能量珠的头标记为 r,尾标记为 n,则聚合后释放的能量为 m × r × n(Mars 单位),新产生的珠子的头标记为 m,尾标记为 n。

需要时,Mars 人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。

显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。

例如:设 N=4,4 颗珠子的头标记与尾标记依次为 (2,3)(3,5)(5,10)(10,2)。

我们用记号 ⊕ 表示两颗珠子的聚合操作,(j⊕k) 表示第 j,k 两颗珠子聚合后所释放的能量。则

第 4、1 两颗珠子聚合后释放的能量为:(4⊕1)=10×2×3=60。

这一串项链可以得到最优值的一个聚合顺序所释放的总能量为 ((4⊕1)⊕2)⊕3)=10×2×3+10×3×5+10×5×10=710。

输入格式
输入的第一行是一个正整数 N,表示项链上珠子的个数。

第二行是 N 个用空格隔开的正整数,所有的数均不超过 1000,第 i 个数为第 i 颗珠子的头标记,当 i

至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。

输出格式
输出只有一行,是一个正整数 E,为一个最优聚合顺序所释放的总能量。

数据范围
4 ≤ N ≤ 100,
1 ≤ E ≤ 2.1×109

输入样例:

4
2 3 5 10

输出样例:

710

#include 
using namespace std;
const int N = 210;
int n, a[N];
int f[N][N];
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++ ) {
        cin >> a[i];
        a[i + n] = a[i];
    }
    for (int len = 3; len <= n + 1; len ++ ) // 珠子的开头(2,3)结尾(10,2) 比输入的n多一个
        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 + 1][r] + a[l] * a[k] * a[r]);
        }
    int maxv = -0x3f3f3f3f;
    for (int i = 1; i <= n; i ++ )
        maxv = max(maxv, f[i][i + n]); // 右端点不减1是因为多一个
    cout << maxv;
    return 0;
}
加分二叉树

设一个 n 个节点的二叉树 tree 的中序遍历为(1,2,3,…,n),其中数字 1,2,3,…,n 为节点编号。

每个节点都有一个分数(均为正整数),记第 i 个节点的分数为 di,tree 及它的每个子树都有一个加分,任一棵子树 subtree(也包含 tree 本身)的加分计算方法如下:

subtree的左子树的加分 × subtree的右子树的加分 + subtree的根的分数

若某个子树为空,规定其加分为 1。

叶子的加分就是叶节点本身的分数,不考虑它的空子树。

试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树 tree。

要求输出:

(1)tree的最高加分

(2)tree的前序遍历

输入格式
第 1 行:一个整数 n,为节点个数。

第 2 行:n 个用空格隔开的整数,为每个节点的分数(0<分数<100)。

输出格式
第 1 行:一个整数,为最高加分(结果不会超过int范围)。

第 2 行:n 个用空格隔开的整数,为该树的前序遍历。如果存在多种方案,则输出字典序最小的方案。

数据范围
n < 30

输入样例:

5
5 7 1 2 10

输出样例:

145
3 1 2 4 5

算法

(区间DP,二叉树的遍历) O(n3)

状态表示: f[i][j] 表示中序遍历是 w[i ~ j] 的所有二叉树的得分的最大值。

状态计算: f[i][j] = max(f[i][k - 1] * f[k + 1][j] + w[k]),即将f[i][j]表示的二叉树集合按根节点分类,则根节点在 k 时的最大得分即为 f[i][k - 1] * f[k + 1][j] + w[k],则f[i][j]即为遍历 k 所取到的最大值。

在计算每个状态的过程中,记录每个区间的最大值所对应的根节点编号。

那么最后就可以通过DFS求出最大加分二叉树的前序遍历了。

时间复杂度
状态总数是 O(n2),计算每个状态需要 O(n) 的计算量,因此总时间复杂度是 O(n3)。

#include 
using namespace std;
const int N = 30;
int n;
int w[N];
int f[N][N]; // i到j形成的加分二叉树的最大值
int g[N][N]; // i到j形成的加分二叉树的根节点
void dfs(int l, int r) 
{
    if (l > r) return;
    int k = g[l][r];
    cout << k << ' ';
    dfs(l, k - 1);
    dfs(k + 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];
                    int right = k == r ? 1 : f[k + 1][r];
                    int score = left * right + w[k];
                    if (f[l][r] < score) {
                        f[l][r] = score;
                        g[l][r] = k;
                    }
                }
            }
        }
    cout << f[1][n] << endl;
    dfs(1, n);
    return 0;
}

笔记、代码学习:
作者:yxc
链接:https://www.acwing.com/solution/content/3804/
来源:AcWing


凸多边形的划分
棋盘分割
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/722387.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号