【动态规划】区间 DP :三层循环逻辑与模板实现细节(洛谷 P1880)

题目链接:P1880 NOI1995 石子合并 - 洛谷

区间 DP 相比于其他类型的动态规划 DP 比较简单一点,因为理解起来难度不算大,而且相对来说我认为这个用法不算特别灵活。只要事先理解了区间 DP 的模板,实际做起题还是很好做的。

我先以最经典的区间 DP 模板题目说一下实现细节,再详细说如何识别题目要用区间 DP 的意图。在这题描述很简单也很好理解,读完题目能让人困惑的也许也就只有两点:1. 这个石子是摆成环形 的,那我找最优合并方法还怎么分辨方向? 2. 怎么才能找到最优的合并顺序使得得分最大和最小呢?

首先是处理环形问题,只要见到环形的基本都是直接把它原本的数复制一次放在后面,利用类似链状的方式来做题,也就是化环为链。比如一个数组 arr 元素 2,6,5,9 组成一个环状。

对此,我们可以存储数组的时候直接复制一份接在后面,看看是什么样的效果。

原本我们存储的,也就是下标 1-4 的 2659 。这不足以表达出环状。而我们复制一份到后面接上后,可以发现,下标 2-5 是 6592 ,3-6 是 5926 ,4-7 是 9265 。因为我们在环上,是没有一个固定的起点的,任何一个点都可以作为一个起点。所以要变成链状,就只能向上面一样复制一份一样的放在后面接上,那么就可以完美模拟好我们的环状了。

这样做之后,在接下来的区间 DP ,我们就不用纠结应该从那个作为起点了,要注意的是我们虽然数组长度变成了 2n ,可是我们接下来的枚举的区间长度最长的肯定还是 n 。

接下来,我们需要考虑怎么找到这个最优的合并方法了。我们先理清楚它的合并规则:规定每次必须只能合并相邻的两堆,且合并后的得分是两堆的石子数的总质量 。当我们看到有这种题目有**"区间""相邻合并"**的类似意思的字眼,且还是那种求方案数,最优值等等还不要求记录合并过程只要结果,几乎就可以确定就是区间 DP 了。

我们首先要定义一个 dp 数组 dpij ,代表合并区间 i,j 中所有石头为一堆所获得的最优分数 。因为这题需要我们求最大和最小都要求出来,所以我以dpmaxij 代表合并区间 i,j 中所有石头为一堆所获得的最大分数,dpminij 代表合并区间 i,j 中所有石头为一堆所获得的最小分数。然后推导状态转移方程。

我们要聚焦的重点是**"相邻"** ,由于它限制了只能合并相邻的两堆石子,所以可以知道,想要把 i,j 区间所有石子堆合并成一个,那必然是由 i,kk+1,j 这两堆( ,k是区间 i,j 内任意一个石子堆下标)已经合并好的石子合并而成的。如图示

至于要取得哪一个 k 位置来合并成 i,j ,我们就遍历一次 i,j 来比较即可。而根据我们的合成规则,我们需要快速得知区间 i,kk+1,j 分别的和,所以我们在进行 dp 之前,需要对原数组进行一次前缀和预处理。我先定义我的前缀和数组为 sum 。

可以发现,我们将 i,kk+1,j 合并,而我们也能根据我们的 dp 定义,直接拿到 i,kk+1,j 的合并成一堆的得分即 dpik 和 dpk+1j ,那这样加上它们合并产生的新得分,就是 dpij 了。

所以最后,我们推导出来的状态转移方程就是

dpmaxij=max(dpmaxij,dpmaxik+dpmaxk+1j+sumj-sumi-1) (i<=k<j)

dpminij=min(dpminij,dpminik+dpmink+1j+sumj-sumi-1) (i<=k<j)

推导出状态转移方程,我们还需要考虑一点就是我们该如何写循环进行 dp了。先给出正确的代码给大家参考,再逐一解释。

cpp 复制代码
//最外层枚举区间长度len
for(int len=2;len<=n;++len){
    //第二层枚举左端点i
    for(int i=1;i+len-1<=2*n;++i){
        int j=i+len-1;//区间长度为len,左端点为i,可直接算出右端点
        //得知左右端点后,枚举k的位置,注意k不能等于j,否则k+1会大于j
        for(int k=i;k<j;++k){
            dpmin[i][j]=min(dpmin[i][j],dpmin[i][k]+dpmin[k+1][j]+sum[j]-sum[i-1]);
            dpmax[i][j]=max(dpmax[i][j],dpmax[i][k]+dpmax[k+1][j]+sum[j]-sum[i-1]);
        }
    }
}

我们观察我们的 dp 状态转移方程,能发现我们的区间 i,j 是由小区间 i,kk+1,j 推导而得的。所以如果我要计算一个大区间的得分,那我肯定要知道比它小的所有区间的得分 ,否则我们很容易就会用没被计算好的 dp 值进行计算导致结果出错。所以,我们最外层的 for 循环应该从小到大枚举区间长度 len ,且要注意 len 最大一定是 n 。即保证算一个区间长度的所有情况时,比它小的区间长度情况肯定已经算好了,保证不会出现计算错误,用没有算过的地方。

第二层当然是枚举左端点 i ,枚举 i 还可以直接利用区间长度算出右端点 j 了。要注意的是枚举的区间 i,j 不能大于 2n ,否则就超过范围了。这时得知了左端点 i 和右端点 j ,我们就可以直接枚举 k 了,按状态转移方程取得最优值。

最后要注意的是初始化 问题。区间长度为 1 本身就不需要合并,自成一堆,所以只用把所有的 dpii)初始化为 0 就好了。而且我们进行完 dp 循环后,我们还要找到环形的最优答案,也就是分别以 1-n 每个作为链的起点来合并的方案哪个最优。最终代码如下。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
int dpmin[210][210];
int dpmax[210][210];
int n;
int sum[210];
int arr[210];
int main(){
    cin>>n;
    for(int i=1;i<=n;++i){
        cin>>arr[i];
        sum[i]=sum[i-1]+arr[i];
    }
    //将环变成链状
    for(int i=n+1;i<=2*n;++i){
        sum[i]=sum[i-1]+arr[i-n];
    }
    memset(dpmin,0x3f,sizeof(dpmin));
    memset(dpmax,0xc0,sizeof(dpmax));
    for(int i=1;i<=2*n;++i) dpmin[i][i]=dpmax[i][i]=0;//初始化
    //区间dp主循环
    for(int len=2;len<=n;++len){
        for(int i=1;i+len-1<=2*n;++i){
            int j=i+len-1;
            for(int k=i;k<j;++k){
                dpmin[i][j]=min(dpmin[i][j],dpmin[i][k]+dpmin[k+1][j]+sum[j]-sum[i-1]);
                dpmax[i][j]=max(dpmax[i][j],dpmax[i][k]+dpmax[k+1][j]+sum[j]-sum[i-1]);
            }
        }
    }
    //找最大值和最小值
    int tmin=1e9,tmax=-1e9;
    for(int i=1;i<=n;++i){
        tmin=min(tmin,dpmin[i][i+n-1]);
        tmax=max(tmax,dpmax[i][i+n-1]);
    }
    cout<<tmin<<"\n"<<tmax;
}

其实基本上大部分的区间 dp 都是类似这样的模板,都是先枚举区间长度,后枚举左端点然后算出右端点,最后枚举从哪个地方作为合并 。所以,区间 dp 的题目基本都是时间复杂度会来到 O(n\^3) ,可以看到这个题目的数据量也是只有 100 而已。所以也可以确定出,一道题如果有出现"相邻合并"之类的意思,而且数据量还能够满足 O(n\^3) 也就是 100 左右,基本上都是完全可以考虑用区间 dp 了。

相关推荐
从此以后自律1 小时前
迪杰斯特拉
算法
QiLinkOS1 小时前
第三视觉理解徐玉生与他的商业活动(32)
大数据·c++·人工智能·算法·开源协议
KaMeidebaby1 小时前
卡梅德生物技术快报|实操手册:CXCL4 蛋白原核表达全套工艺,两步层析去除蛋白多聚体附完整电泳数据
人工智能·算法·机器学习·架构·spark
AIGCmagic社区2 小时前
Unlimited OCR 论文精读:R-SWA 如何实现一次性长文档解析
人工智能·算法·aigc
汉克老师2 小时前
GESP2026年6月认证C++一级( 第一部分选择题(8-15))精讲
c++·continue·运算符·while·break·gesp1级·模运算
鹏易灵2 小时前
C++——7.类与对象,掌握封装、继承、多态.详解
开发语言·c++·算法
Mortalbreeze2 小时前
深入 Linux 线程机制(三):线程互斥——竞争条件与互斥锁的本质
linux·运维·服务器·c++·算法
Mr_Controll2 小时前
【运动控制——电子凸轮曲线设计(多项式)】
算法·自动化
Sam09272 小时前
【AI 算法精讲 15】余弦相似度:向量检索与归一化内积
人工智能·python·算法·ai