区间DP详解,思路分析,OJ详解

news2024/11/26 22:48:20

文章目录

    • 前言
    • 问题引入
      • 暴力枚举
      • 自下而上
      • 状态设计
      • 状态转移方程
    • 区间DP的分析
      • 状态设计
      • 状态转移
      • 时间复杂度
      • 翻译成递推
    • OJ详解
      • P1880 [NOI1995] 石子合并
        • 记忆化搜索版本
        • 递推版本
      • HDU Dire Wolf
      • Multiplication Puzzle
      • Polygon
    • 总结

前言

区间dp属于动态规划中一类比较好理解的问题,同样是将大问题分划成小问题来求解。主要要理解其对区间问题拆解的思想,掌握状态转移的处理细节,通过对区间dp的学习,也能更好的理解自底向上分析问题的思想。


问题引入

在一个圆形操场的四周摆放N堆石子,第i堆石子的重量为w[i],现要将石子有次序地合并成一堆,规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子重量,记为该次合并的代价。试设计出一个算法,计算出将N堆石子合并成1堆的最小代价。

在对区间dp没有了解的情况下,本蒟蒻解题过程很可能是这样的:

看题 -> 贪心?-> 代码一气呵成 -> WA -> 选择暴力 -> TLE -> 看题解 -> 区间DP?什么东西?-> orz

那么我们从暴力解法开始入手,寻找优化之处。

暴力枚举

即然要将n堆石子合并成一堆,我们自然要合并n - 1次,对于我们第一次合并,两两相邻的情况有n - 1种,那么第一次合并就有n - 1种情况

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

那么n - 1种情况继续向下分支,每种情况又能分出n - 2种情况

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从而我们得到了一棵非常庞大的搜索树,每条从根节点到叶子节点的路径为一个方案,一共有(n - 1)!种方案,暴力算法显然会超时,那么我们如何去优化呢?

自下而上

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们观察那棵庞大的搜索树,我们发现最后一层叶子节点相同都是一堆石子,倒数第二层是两堆石子,

我们发现对于倒数第二层而言,它们合并的花费是相同的,都是两堆石子重量之和——所有的石子重量之和,我们设倒数第二层的情况为合并(1……k)和(k+1……n),那么我们如果要最小花费,那么只需要合并(1……k)和(k+1……n)都达到最小花费即可,这样我们的问题就分划为了合并(1……k)和(k+1……n)的最小花费

同样的,(1……k)和(k+1……n)又可以分割成更小的区间,最终区间会不断缩小,会变成合并1个石子的最小花费,即0,因为一个石子不需要合并。

我们发现,此时问题就可以抽象为动态规划的模型了。

状态设计

设计状态f[l][r]为合并第l堆石子一直到第r堆石子的最小代价

状态转移方程

f [ l ] [ r ] = { 0 l = r m i n i = l r − 1 ( f [ l ] [ i ] + f [ i + 1 ] [ r ] + s u m ( l , r ) ) l ≠ r s u m ( l , r ) = ∑ j = l r w [ j ] f[l][r] = \left\{\begin{align} &0&l=r\\ &min_{i=l}^{r-1}(f[l][i] + f[i + 1][r] + sum(l,r))&l \ne r \end{align}\right. \\ sum(l,r) = \sum_{j = l}^{r}w[j] f[l][r]={0mini=lr1(f[l][i]+f[i+1][r]+sum(l,r))l=rl=rsum(l,r)=j=lrw[j]

通过对递归树的分析观察,这个状态转移方程其实不难理解。

  • l = r,那么已经是一堆了,合并代价为0
  • l ≠ r,那么对于倒数第二次合并一定是由两堆合并,由r - l种情况,一堆为(l , i),另一堆为(i + 1, r),两堆合并为一堆的代价sum(l,r)是确定的,只需要f[l][i] + f[i + 1][r]最小即可。

我们通过深搜计算答案,但是会有大量重复走的递归树路径,所以采用记忆化搜索进行剪枝,这样我们的时间复杂度就大大提升。

以上就是最经典的区间dp问题


区间DP的分析

状态设计

区间DP的状态与区间有关,一般为二维数组f[i][j]来表示问题在区间[i , j]的解

对于一些变形问题,需要额外空间来辅助求解时,也会将空间扩展为三维,即f[i][j][k],这也是动态规划问题中常见的辅助手段

状态转移

长区间问题的解小区间问题的解转移过来

时间复杂度

以引例为例,区间长度为n,状态数为n2,每个状态只计算了一次,每次O(n)转移,那么时间复杂度为O(n3)

具体的时间复杂度具体问题具体分析

翻译成递推

对于记忆化搜索的做法其实是把顶层问题拆分为了下层问题,下层问题先计算,传递到上层

那么我们也可以直接从下层开始计算,每次由前面计算过的值转移即可,这样就把递归问题翻译成了递推问题,省去了递归开销。

具体实现可以自行实现,也可以看后面第一道OJ详解中的实现。


OJ详解

P1880 [NOI1995] 石子合并

原题链接

[P1880 NOI1995] 石子合并 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

这其实就是我们问题引入的问题,只不过要求我们同时计算出最大值和最小值,解法显然相同,只是初始化不同罢了

对于求最大值的,我们就把f[][]初始化为0,非0的状态就是访问过的,直接返回,l = r直接返回0,否则就进行状态转移

对于求最小值,我们就把f[][]初始化为无穷大的数(记为inf),对于不是inf的,说明访问过,直接返回,l = r直接返回0,否则就进行状态转移

小细节:由于石子是环形放置,这里采用无脑将数组倍增一倍,破环成链。那么最终的答案还需要枚举长度为n的区间中的最值

记忆化搜索版本
#include <iostream>
#include <cstring>
#include <vector>
#include <functional>
#include <algorithm>
#include <cmath>
#include <functional>
#include <climits>
#include <unordered_set>
#include <bitset>
#include <stack>
#include <cstring>
using namespace std;
#define N 220
int a[N]{0}, f1[N][N], f2[N][N]{0}, n;
const int inf = 0x3f3f3f3f;
int dfs1(int l, int r)
{
    if (f1[l][r] != inf)
        return f1[l][r];
    if (l == r)
        return f1[l][r] = 0;

    int &dp = f1[l][r];
    for (int k = l; k < r; k++)
        dp = min(dp, dfs1(l, k) + dfs1(k + 1, r) + a[r] - a[l - 1]);
    return dp;
}
int dfs2(int l, int r)
{
    if (f2[l][r])
        return f2[l][r];
    if (l == r)
        return f2[l][r] = 0;

    int &dp = f2[l][r];
    for (int k = l; k < r; k++)
        dp = max(dp, dfs2(l, k) + dfs2(k + 1, r) + a[r] - a[l - 1]);
    return dp;
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);
    //freopen("in.txt", "r", stdin);
    //freopen("out.txt", "w", stdout);
    memset(f1, 0x3f, sizeof(f1));
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i], a[i + n] = a[i];
    for (int i = 1; i <= n * 2; i++)
        a[i] += a[i - 1];
    dfs1(1, n * 2);
    dfs2(1, n * 2);
    int ans1 = 0x3f3f3f3f, ans2 = 0;
    for (int i = 1; i < n; i++)
        ans1 = min(ans1, f1[i][i + n - 1]), ans2 = max(ans2, f2[i][i + n - 1]);
    cout << ans1 << '\n'
         << ans2;
    return 0;
}

递推版本
#include <iostream>
#include <cstring>
#include <vector>
#include <functional>
#include <algorithm>
#include <cmath>
#include <functional>
#include <climits>
#include <unordered_set>
#include <bitset>
#include <stack>
#include <cstring>
using namespace std;
#define N 220
int a[N]{0}, f1[N][N], f2[N][N]{0}, n;
const int inf = 0x3f3f3f3f;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);
    //freopen("in.txt", "r", stdin);
    //freopen("out.txt", "w", stdout);
    memset(f1, 0x3f, sizeof(f1));
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i], a[i + n] = a[i], f1[i][i] = f1[i + n][i + n] = f2[i][i] = f2[i + n][i + n] = 0;
    for (int i = 1; i <= n * 2; i++)
        a[i] += a[i - 1];

    for (int i = 2; i <= n; i++) // 枚举长度
    {
        for (int j = 1; j <= n * 2 - i + 1; j++) // 枚举区间起点
        {
            for (int k = j; k < j + i - 1; k++) // 枚举左子区间右边界
            {
                f1[j][j + i - 1] = min(f1[j][j + i - 1], f1[j][k] + f1[k + 1][j + i - 1] + a[j + i - 1] - a[j - 1]);
                f2[j][j + i - 1] = max(f2[j][j + i - 1], f2[j][k] + f2[k + 1][j + i - 1] + a[j + i - 1] - a[j - 1]);
            }
        }
    }

    int ans1 = 0x3f3f3f3f, ans2 = 0;
    for (int i = 1; i < n; i++)
        ans1 = min(ans1, f1[i][i + n - 1]), ans2 = max(ans2, f2[i][i + n - 1]);
    cout << ans1 << '\n'
         << ans2;
    return 0;
}

HDU Dire Wolf

原题链接

Problem - 5115 (hdu.edu.cn)

同样是板子题,我们思考发现每次杀一匹狼,最后一次的情形一定是杀最后一匹狼,也就是说这匹狼左边的狼和右边的狼都寄了,所以我们就可以抽象为区间dp问题了

我们定义状态f[l][r]为杀死第l匹到第r匹狼的最小伤害,那么对于每访问过的状态如何转移呢?

考虑最后一次一定是杀一匹狼i,那么我们枚举i,问题就转化为了杀死i在区间[l , r]内左边和右边狼的最小伤害加上狼i的伤害(注意这个狼i可以被区间[l,r]外相邻的狼加buff)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define int long long
#define N 210
int f[N][N], t, n;
int a[N], b[N];
const int inf = 0x3f3f3f3f3f3f3f3f;
int dfs(int l, int r)
{
    if (l > r)
        return 0;
    if (f[l][r] != inf)
        return f[l][r];
    int &res = f[l][r];
    for (int i = l; i <= r; i++)
        res = min(res, dfs(l, i - 1) + dfs(i + 1, r) + a[i] + b[l - 1] + b[r + 1]);

    return res;
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);
    cin >> t;
    while (t--)
    {
        memset(f, 0x3f, sizeof(f));
        memset(a, 0, sizeof(a));
        memset(b, 0, sizeof(b));

        cin >> n;
        for (int i = 1; i <= n; i++)
            cin >> a[i];
        for (int i = 1; i <= n; i++)
            cin >> b[i];
        cout << "Case #" << idx++ << ": " << dfs(1, n) << '\n';
    }

    return 0;
}

Multiplication Puzzle

原题链接

1651 – Multiplication Puzzle (poj.org)

区间DP板子题,我们采用记忆化搜索来计算状态,即dfs(l,r)为抽走第l张到第r张卡牌获取的最小总点数,用二维数组f[l][r]来剪枝保存

数组a[]来保存每张牌的点数,初始化为-1

如果l > r,那么返回0

如果f[l][r]已经访问,那么直接返回

如果未访问,那么有f[l][r] = min(f[l][r] , dfs(l, i - 1) + dfs(i + 1, r) + abs(a[i] * a[l - 1] * a[r + 1]))

代码甚至没怎么变

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define N 210
int f[N][N], t, n;
int a[N];
const int inf = 0x3f3f3f3f3f3f3f3f;
int dfs(int l, int r)
{
    if (l > r)
        return 0;
    if (f[l][r] != inf)
        return f[l][r];

    int &res = f[l][r];
    for (int i = l; i <= r; i++)
        res = min(res, dfs(l, i - 1) + dfs(i + 1, r) + abs(a[i] * a[l - 1] * a[r + 1]));
    return res;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);
    memset(f, 0x3f, sizeof(f));
    memset(a, -1, sizeof(a));

    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    cout << dfs(2, n - 1);
    return 0;
}

Polygon

原题链接

1179 – Polygon (poj.org)

这个题就涉及到我们前面区间DP分析中说的,二维状态已经不满足需求,需要增加辅助维度

这道题之所以需要加辅助维度,其实看完题就能明白,这种经典最大值可能是两个负数相乘

所以我们直接跑板子得到的不一定是最大值

我们还是自下而上的分析,最后一步自然是合并两个点为一个点,然后游戏结束了,那么问题转化为左区间合并最值和右区间合并最值

我们开两个二维数组(也可以开成第三个维度长度为2的三维数组),f存最大值,g存最小值,f[l][r]就是删除编号l到r的点的最大得分

读数据同样倍增一倍,破环为链,然后跑递推(记忆化搜索也行,主函数一层循环记忆化搜索)

枚举长度,对于长度为1的,即一个点,那么最大值最小值就是这个点的值

否则,我们枚举左子区间的右边界,区间最大值最小值由两个子区间最大值最小值共同转移

代码相比前面长了一点,但其实还是一个板子,没什么复杂的。

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

const int N = 110, INF = 1 << 15;
int w[N], f[N][N], g[N][N] , n;
char op[N];

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);//poj没nullptr
    //freopen("in.txt", "r", stdin);
    //freopen("out.txt", "w", stdout);
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> op[i] >> w[i];
        op[i + n] = op[i], w[i + n] = w[i];
    }
    for (int len = 1; len <= n; len++)
    {
        for (int l = 1; l <= n * 2 - len + 1; l++)
        {
            int r = l + len - 1;

            if (len > 1)//分支预测常数优化
            {
                f[l][r] = -INF, g[l][r] = INF;
                for (int k = l; k < r; k++)
                {
                    char c = op[k + 1];
                    int minl = g[l][k], minr = g[k + 1][r];
                    int maxl = f[l][k], maxr = f[k + 1][r];
                    if (c == 't')
                    {
                        f[l][r] = max(f[l][r], maxl + maxr);
                        g[l][r] = min(g[l][r], minl + minr);
                    }
                    else
                    {
                        int x1 = maxl * maxr, x2 = maxl * minr, x3 = minl * maxr, x4 = minl * minr;
                        f[l][r] = max(f[l][r], max(max(x1, x2), max(x3, x4)));
                        g[l][r] = min(g[l][r], min(min(x1, x2), min(x3, x4)));
                    }
                }
            }
            else
                f[l][r] = g[l][r] = w[l];
        }
    }
    int res = -INF;
    for (int i = 1; i <= n; i++)
        res = max(res, f[i][i + n - 1]);
    cout << res << endl;

    for (int i = 1; i <= n; i++)
        if (res == f[i][i + n - 1])
            cout << i << " ";
    return 0;
}

总结

上面四道OJ详解,分析思路有个共同点就是自下而上分析,我们自然是要将问题抽象为区间DP模型,而这一步往往是由最后一步也就是递归树倒数第二层得来的,因为最后一层叶子节点都是最终状态。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1342658.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

概率论相关题型

文章目录 概率论的基本概念放杯子问题条件概率与重要公式的结合独立的运用 随机变量以及分布离散随机变量的分布函数特点连续随机变量的分布函数在某一点的值为0正态分布标准化随机变量函数的分布 多维随机变量以及分布条件概率max 与 min 函数的相关计算二维随机变量二维随机变…

超级详细的YOLOV8教程

超级详细的YOLOV8教程 YOLOV8介绍1. 数据标记1.1 第一种为在网站上下载&#xff0c;1.2 第二种为在CVAT上自定义数据 2. 制作数据集3. 部署YOLOV8的代码3.1 远程部署3.1.1 项目下载3.1.2 修改代码3.1.2.1 训练模型3.1.2.1 验证模型 3.2 本地部署3.2.1 YOLOV8项目部署3.2.2 cuda…

2023总结与展望--Empirefree

今年一篇博客都没写过了&#xff0c;好像完全在忙在工作和生活上面了&#xff0c;珍惜自我&#xff0c;保持热情&#xff0c;2024对我好点 文章目录 &#x1f525;1. 年终总结1.1.学习工作计划1.2. 生活计划1.3 个人总结 &#x1f525;2. 未来展望 &#x1f525;1. 年终总结 1…

基于Java学生成绩管理系统设计与实现(源码+部署文档+报告)

博主介绍&#xff1a; ✌至今服务客户已经1000、专注于Java技术领域、项目定制、技术答疑、开发工具、毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精彩专栏 推荐订阅 &#x1f447;&#x1f3fb; 不然下次找不到 Java项目精品实…

两阶段提交协议

数据的强一致性 要么都修改 要么就都不修改。 不同的实体和过程 领导者和参与者、表决阶段和提交阶段 过程 一个不同意 提交就终止 存在的问题和解决的方案 如果一个领导者或者参与者的状态机中有阻塞状态&#xff0c;那么系统必须等他完成才能执行&#xff0c;这样就会…

【并发编程篇】线程安全问题_—_ConcurrentHashMap

文章目录 &#x1f354;情景引入&#x1f339;报错了&#xff0c;解决方案 &#x1f354;情景引入 我们运行下面的代码 package org.example.unsafe;import java.util.HashMap; import java.util.Map; import java.util.UUID;public class MapTest {public static void main(…

SpringBoot单点登录认证系统MaxKey(附开源项目地址)

1 项目介绍 MaxKey 单点登录认证系统&#xff0c;谐音马克思的钥匙寓意是最大钥匙&#xff0c;支持 OAuth 2.x/OpenID Connect、SAML 2.0、JWT、CAS、SCIM 等标准协议&#xff0c;提供简单、标准、安全和开放的用户身份管理(IDM)、身份认证(AM)、单点登录(SSO)、RBAC 权限管理…

如何把握品牌新五感,打造小红书品牌

随着社会经济的发展&#xff0c;市场的进步&#xff0c;以及人们思维方式的改变。年轻人面对市场&#xff0c;面对营销&#xff0c;关注点也在发生着改变。那什么是小红书品牌新五感&#xff0c;如何把握品牌新五感&#xff0c;打造小红书品牌&#xff01; 一、品牌五感是什么 …

查找dll的开放函数以及dll的依赖dll

1.进入一个vs的cmd窗口 2.dumpbin /exports XXX.dll&#xff0c;分析 XXX.dll 中有哪些函数。 例如查询: C:\Users\levi0\Desktop\testPro\NewCSDll\NewCSDll\bin\x64\Debug\GBRAnalyze.dll 3. dumpbin /dependents 文件名&#xff08;带路径&#xff09;命令&#xff0c;回车&…

servlet+jdbc实现用户注册功能

一、需求 在Servlet中可以使用JDBC技术访问数据库&#xff0c;常见功能如下&#xff1a; 查询DB数据&#xff0c;然后生成显示页面&#xff0c;例如&#xff1a;列表显示功能。接收请求参数&#xff0c;然后对DB操作&#xff0c;例如&#xff1a;注册、登录、修改密码等功能。…

Linux中磁盘管理与文件系统

目录 一.磁盘基础&#xff1a; 1.磁盘的结构&#xff1a; 2.硬盘的数据结构&#xff1a; 3.硬盘存储容量 &#xff1a; 4.硬盘接口类型&#xff1a; 二.MBR与磁盘分区&#xff1a; 1.MBR的概念&#xff1a; 2.硬盘的分区&#xff1a; 为什么分区&#xff1a; 2.表示&am…

【PHP】B/S手术室麻醉信息管理系统源码

手术麻醉临床信息系统全面覆盖从患者入院&#xff0c;经过术前、术中、术后&#xff0c;直至出院的全过程。通过与相关医疗仪器的设备集成&#xff0c;不但可以轻松集成手术室传统监护设备如监护仪、麻醉机、呼吸机&#xff0c;也能与血气分析仪等设备对接&#xff0c;快速获取…

java并发编程十三 线程池

文章目录 线程池自定义线程池ThreadPoolExecutor提交任务关闭线程池任务调度线程池正确处理执行任务异常 线程池 自定义线程池 步骤1&#xff1a;自定义拒绝策略接口 FunctionalInterface // 拒绝策略 public interface RejectPolicy<T> {void reject(BlockingQueue&l…

LeetCode---120双周赛

题目列表 2970. 统计移除递增子数组的数目 I 2971. 找到最大周长的多边形 2972. 统计移除递增子数组的数目 II 2973. 树中每个节点放置的金币数目 一、统计移除递增子数组的数目I 这题的数据范围不高&#xff0c;可以直接暴力&#xff0c;后面的第三题和它一样&#xff0c…

程序员面试笔试通关宝典系列丛书(由清华大学出版社出版)

程序员面试笔试通关宝典系列——编程职场成功的必备秘籍 由清华大学出版社出版的专为编程爱好者和职业开发者打造的“程序员面试笔试通关宝典”系列丛书。该系列包含五本专业指南&#xff0c;覆盖数据库、Java、前端、通用编程和Python五个领域。 这些书籍深度解析各领域的核…

面试题:MySQL 自增主键一定是连续的吗?

文章目录 测试环境&#xff1a;一、自增值的属性特征&#xff1a;1. 自增主键值是存储在哪的&#xff1f;2. 自增主键值的修改机制&#xff1f; 二、新增语句自增主键是如何变化的&#xff1a;三、自增主键值不连续情况&#xff1a;&#xff08;唯一主键冲突&#xff09;四、自…

AIGC开发:调用openai的API接口

简介 开始进行最简单的使用&#xff1a;通过API调用openai的模型能力 OpenAI的能力如下图&#xff1a; 文本生成模型 OpenAI 的文本生成模型&#xff08;通常称为生成式预训练 Transformer 或大型语言模型&#xff09;经过训练可以理解自然语言、代码和图像。这些模型提供文…

扫雷(c语言)

先开一个test.c文件用来游戏的逻辑测试&#xff0c;在分别开一个game.c文件和game.h头文件用来实现游戏的逻辑 主要步骤&#xff1a; 游戏规则&#xff1a; 输入1&#xff08;0&#xff09;开始&#xff08;结束&#xff09;游戏&#xff0c;输入一个坐标&#xff0c;如果该坐…

新药(化药)注册申报资料都包含哪些?

新药的注册申报是新药上市前的重要步骤,其流程可以简单概括为①前期准备→②申报材料准备→③递交注册申请→④审评和审批→⑤监管和跟踪。本文将着重介绍新药(化药)注册申报的一般流程和主要环节(附流程图)。(关于新药注册申报成功率和耗费时间问题写到最后) ①前期准备 在开…

年底医保新改革,如何短,平,快搞定his医保接口升级

1. 先说说我负责运维的医院背景&#xff1a; 省会中心城市三甲&#xff0c;电脑共计1500台左右。其中安装有his系统的电脑大约600台&#xff0c;也就是说需要给600台医护人员使用电脑进行医保接口升级。 近期有幸参与了院里HIS系统的升级工作&#xff0c;赶上特殊时期&#x…