关于区间DP的一点点心得(虽然还是很菜)
自己今天对于区间 DP 的一个总结
区间 DP 的数组一般是二维,其状态一般表示区间 \((l,r)\)。
区间 DP 在思考的时候是有一定套路的,思考时可以按照如下方式进行思考:
- 这段区间要维护的信息是什么(即 \(dp\) 或 \(f\) 数组内的值应该存什么)?
- 状态的边界如何设计(这个我认为对于所有的 DP 来说,都应当是其思考过程之一)?
- 这段区间如何从它的更小的区间推广过来(即如何从 \(dp(l,r)\) 或 \(f(l,r)\) 的子区间转移到本身)?
- 怎样合并或更新信息(即如何统计信息或如何设计状态转移方程,是取最大值还是最小值,是应该使用加法原理合并方案数还是用乘法原理)?
对于大部分人(尤其是我)来说,以上四点基本上是区间 DP 的全部思维难点。
对于第一条来说,如果让我们维护最大值,我们应当怎么办,维护方案个数我们又应当怎么设计维护的信息。
对于第二条来讲,我们应当考虑一下数组的初始值应当是什么值,边界值又应当是什么值,那些情况下就到达了边界情况等等都应当被考虑,并且编写代码的时候千万不要忘了这一步,否则很有可能会莫名其妙地 WA 掉。
而对于第三条如何推广,是通过两个子区间推广过来(即为 \(dp(l,r)\) 是从 \(dp(l,k)\) 和 \(dp(k+1,r)\) 转移而来),还是单纯的左端点减一或右端点减一推广而来(例如题目 P3205 [HNOI2010]合唱队)。
至于第四条,就是要考虑合并子区间信息并更新区间信息时,我们如何正确的合并(例如单纯的加减),并且如何正确的更新区间信息(例如单纯的赋值),还有就是需不需要进行分类讨论,有没有什么可能会出现的坑点之类。
区间 DP 的板子:
众所周知,DP 类题目一般是没有板子的,但是根据自己那微薄的做题量分析,发现其中还是有一定的规律的,基本形式如下:
memset(dp,状态初始值,sizeof(dp));
for(int i=1;i<=n;i++)
dp[i][i] = 状态边界值;
for(int len = 2;len<=n;++len){
for(int l=1;l+len-1<=n;++l){
int r = l+len-1;
//这里写如何从子区间当中推广出来
}
}
cout<<dp[1][n]<<endl;//统计答案并输出(注意,这里所写的方法不唯一,在某些情况下不适用!!)
例题:
P1775 石子合并(弱化版):
区间 DP 的板子题,不像他的标配版那样还需要断环成链。
这道题的思考过程如下:- 这段区间我们需要维护的是这段区间内可行的最小代价。
- 状态边界应该是 \(\forall i \in [1,n] dp(i,i) = 0\),其他地方设为 \(\infty\)。原因非常简单,因为我们在没有合并的时候(也就是最初始的每一堆),我们没有任何代价的产生,故 \(dp(i,i)\) 应该设为 \(0\)。
- 大区间应当是把其自己分成两个小区间,从而推广而来,因为我们每次合并都是合并两堆。
- 大区间的值(状态转移方程)应当是 \(dp(l,r) = \min_{\,l\le k\le r}\{dp(l,k)+dp(k+1,r)\}\),原因是根据第三条,我们需要把大区间分成待合并的两个小区间,那么此时两个合成小区间的代价加上本次合成成本区间的代价才是最终的代价,因为合成本区间的代价是固定的,所以本区间的代价的最小值应当为两个小区间的代价的和的最小值,故大区间的值(状态转移方程)为:\(dp(l,r) = \min_{\,l\le k\le r}\{dp(l,k)+dp(k+1,r)\}\)。
代码如下:
#include <bits/stdc++.h>
using namespace std;
#define MAX_SIZE (int)8e2
int dpmin[MAX_SIZE][MAX_SIZE];
int a[MAX_SIZE];
int sum[MAX_SIZE];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
memset(dpmin,0x3f,sizeof(dpmin));
for(int i=1;i<=2*n;i++){
dpmin[i][i] = 0;
sum[i] = sum[i-1] + a[i];
}
for(int len=2;len<=n;len++){
for(int l=1;l<n;l++){
int r = l+len-1;
for(int k=l;k<r;k++){
dpmin[l][r] = min(dpmin[l][r],dpmin[l][k]+dpmin[k+1][r]);
}
dpmin[l][r] += sum[r]-sum[l-1];
}
}
cout<<dpmin[1][n]<<endl;
return 0;
}
P1880 [NOI1995] 石子合并:
这道题和弱化版的思路是一样的,只是多了一个断环成链的小 trick,下面我们来浅浅的说一下怎么断环成链和为什么这样做的正确的。- 首先我们有一个环(这里以长度为 8 举例,如下图):

- 然后我们我们把它 copy 一倍,挂在后面(如下图):

- 我们可以简单看一下,如果我们想从 2 号节点来遍历环的话,对于断链之前,它是这样的:

断链之后,他是这样的(蓝色部分为得到的结果):

可以看出,通过这种方式断环成链,实际访问到的结果和真实的结果是一样的,但这样有一个好处,就是它把一个具有后效性的环变成了没有后效性的链,致使其可以 DP。
代码如下:
#include <bits/stdc++.h>
using namespace std;
#define MAX_SIZE (int)3e2
int dpmin[MAX_SIZE][MAX_SIZE];
int dpmax[MAX_SIZE][MAX_SIZE];
int a[MAX_SIZE];
int sum[MAX_SIZE];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=n+1;i<=2*n;i++)
a[i] = a[i-n];
memset(dpmin,0x3f,sizeof(dpmin));
memset(dpmax,0xff,sizeof(dpmax));
for(int i=1;i<=2*n;i++){
dpmin[i][i] = 0;
dpmax[i][i] = 0;
sum[i] = sum[i-1] + a[i];
}
for(int len=2;len<=n;len++){
for(int l=1,r=l+len-1;(l<2*n)&&(r<2*n);l++,r=l+len-1){
for(int k=l;k<r;k++){
dpmin[l][r] = min(dpmin[l][r],dpmin[l][k]+dpmin[k+1][r]);
dpmax[l][r] = max(dpmax[l][r],dpmax[l][k]+dpmax[k+1][r]);
}
dpmin[l][r] += sum[r] - sum[l-1];
dpmax[l][r] += sum[r] - sum[l-1];
}
}
int minans = INT_MAX;
int maxans = INT_MIN;
for(int i=1;i<=n;i++){
minans = min(minans,dpmin[i][i+n-1]);
maxans = max(maxans,dpmax[i][i+n-1]);
}
cout<<minans<<endl;
cout<<maxans<<endl;
return 0;
}
- 首先我们有一个环(这里以长度为 8 举例,如下图):
P1063 [NOIP2006 提高组] 能量项链:
有点水,就是石子合并(弱化版)的一个变种,区别在于我们合并信息时从两个子区间的和加区间和变成了加左端点、右端点和子区间断点的乘积,即把方程变成:\(dp(l,r) = \max_{k\in [l,r)} \{dp(l,r),dp(l,k)+dp(k+1,r)+a_i\times a_{k+1} \times a_{r+1}\}\)
代码如下:
#include <bits/stdc++.h>
using namespace std;
#define MAX_SIZE (int)400
int a[MAX_SIZE];
int dp[MAX_SIZE][MAX_SIZE];
int n;
int main(){
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=n+1;i<=2*n+1;i++)
a[i] = a[i-n];
memset(dp,0xcf,sizeof(dp));
for(int i=1;i<2*n;i++)
dp[i][i] = 0;
for(int len=2;len<=n;len++){
for(int l=1,r=l+len-1;l<=2*n&&r<=2*n;++l,r=l+len-1){
for(int k=l;k<r;k++)
dp[l][r] = max(dp[l][r],dp[l][k]+dp[k+1][r]+a[l]*a[k+1]*a[r+1]);
}
}
int maxans = INT_MIN;
for(int i=1;i<=n;i++)
maxans = max(maxans,dp[i][i+n-1]);
cout<<maxans<<endl;
return 0;
}
- P3146 [USACO16OPEN]248 G:
是一道很不错的石子合并类问题的变种,刚开始没有明白思路,因为不知道怎么达成游戏中 “合并” 的操作,于是大概想了有 1 个小时,然后意识到合并是针对于两个块块来说的。
这下思路就很简单了,我们可以用 \(dp(l,r)\) 表示若从 \(l\) 到 \(r\) 能合并,则代表合并后的值,否则为 0 或 \(-\infty\)(这里为什么为 0 或 \(-\infty\) 呢,因为虽然不能合并,但是却不影响我们的子区间的答案,或者也可以理解这个区间因为不能合并,所以他不存在,那么不存在的话如何让其不影响我们的答案呢,因为要统计最大值且保证答案属于 \(\mathbb{N_+}\),所以我们直接使用 0 或 \(-\infty\) 来填充这一块,表示我这一个块块不存在)。
故可以列出其状态转移方程,如下:- 若子区间存在,且能合并,则:\(dp(l,r) = \max_{k \in (l,r)}\{\,dp(l,r),\;dp(l,k+1)\,\}\)
- 否则:\(dp(l,r) = 0\)
代码如下:
#include <bits/stdc++.h>
using namespace std;
#define MAX_SIZE (int)300
int n;
int a[MAX_SIZE];
int dp[MAX_SIZE][MAX_SIZE];
int main(){
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
dp[i][i] = a[i];
for(int len=2;len<=n;len++){
for(int l=1;l<=n-len+1;l++){
int r = l+len-1;
for(int k=l;k<r;k++){
if(dp[l][k]==dp[k+1][r]&&dp[l][k])
dp[l][r] = max(dp[l][r],dp[l][k]+1);
}
}
}
int maxnum = -1;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
maxnum = max(maxnum,dp[i][j]);
cout<<maxnum;
return 0;
}
- P4342 [IOI1998]Polygon:
感觉难度有点虚标,这道题的思考时间比上一题短得多,大概也就 25 min 左右。
实际上核心思考还是石子合并(需要断环成链),就是因为乘法和负整数的存在,我们需要进行一个小小的分类讨论:
首先,我们要明确一个问题,两个负整数相乘不一定比两个正整数大,因为有负负得正这一规则存在,但是,不论是两个负整数相加还是一个正整数加上一个负整数,都一定比两个正整数相加小,根据这一点性质,我们需要多维护一个最小值,并且对上述性质进行讨论。- 如果操作是相加,那么正常合并,即为:
- \(dpmax(l,r)=\max_{k\in[l,r)}\{dpmax(l,r),dpmax(l,k)+dpmax(k+1,r)\}\)
- \(dpmin(l,r) = \min_{k\in[l,r)}\{dpmin(l,r),dpmin(l,k)+dpmin(k+1,r)\}\)
- 如果是相乘,我们就需要分类讨论,即:
- \(dpmax(l,r) = \max_{k\in[l,r)}\{dpmax(l,r),\max\{dpmax(l,k)*dpmax(k+1,r),dpmin(l,k)*dpmin(k+1,r)\}\}\)
- \(dpmin(l,r) = \min_{k\in[l,r)}\{dpmin(l,r),\min\{\min\{dpmin(l,k)*dpmin(k+1,r),dpmin(l,k)*dpmax(k+1,r)\},dpmax(l,k)*dpmin(k+1,r)\}\}\)
代码如下:
- 如果操作是相加,那么正常合并,即为:
#include <bits/stdc++.h>
using namespace std;
#define MAX_SIZE (int)200
long long dpmax[MAX_SIZE][MAX_SIZE];
long long dpmin[MAX_SIZE][MAX_SIZE];
char edge[MAX_SIZE];
int ver[MAX_SIZE];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;++i)
cin>>edge[i]>>ver[i];
for(int i=n+1;i<=2*n;++i){
edge[i] = edge[i-n];
ver[i] = ver[i-n];
}
for(int i=0;i<MAX_SIZE;++i)
for(int j=0;j<MAX_SIZE;++j){
dpmax[i][j] = LONG_LONG_MIN;
dpmin[i][j] = LONG_LONG_MAX;
}
for(int i=1;i<=2*n;++i){
dpmax[i][i] = ver[i];
dpmin[i][i] = ver[i];
}
for(int len=2;len<=n;++len){
for(int l=1,r=l+len-1;(l<2*n)&&(r<2*n);++l,r=l+len-1){
for(int k=l;k<r;++k){
switch(edge[k+1]){
case 't':{
dpmax[l][r] = max(dpmax[l][r],dpmax[l][k]+dpmax[k+1][r]);
dpmin[l][r] = min(dpmin[l][r],dpmin[l][k]+dpmin[k+1][r]);
break;
}
case 'x':{
dpmax[l][r] = max(dpmax[l][r],max(dpmax[l][k]*dpmax[k+1][r],dpmin[l][k]*dpmin[k+1][r]));
dpmin[l][r] = min(dpmin[l][r],min(dpmin[l][k]*dpmin[k+1][r],min(dpmin[l][k]*dpmax[k+1][r],dpmax[l][k]*dpmin[k+1][r])));
break;
}
}
}
}
}
long long maxans = LONG_LONG_MIN;
for(int i=1;i<=n;i++){
maxans = max(maxans,dpmax[i][i+n-1]);
}
cout<<maxans<<endl;
for(int i=1;i<=n;i++){
if(dpmax[i][i+n-1]==maxans)
cout<<i<<' ';
}
return 0;
}
- P3205 [HNOI2010]合唱队:
这道题是一道经典的统计方案数题目。但是如何统计方案是一个问题,如何转移也是一个问题。
我们可以给 \(dp\) 数组增加一个维度,来表示这一次插入的时候是怎么插的,0 表示从左边插入,1 表示从右边插入,然后进行一个小小的分类讨论就可以了。
代码如下:
#include <bits/stdc++.h>
using namespace std;
#define MAX_SIZE (int)1005
#define MOD 19650827
int n;
int fin[MAX_SIZE];
int dp[MAX_SIZE][MAX_SIZE][2];
int main(){
ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;i++)
cin>>fin[i];
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
dp[i][i][0] = 1;
for(int len=2;len<=n;len++){
for(int l=1;l<=n-len+1;l++){
int r = l+len-1;
if(fin[l]<fin[r])
dp[l][r][1] += dp[l][r-1][0];
if(fin[l+1]>fin[l])
dp[l][r][0] += dp[l+1][r][0];
if(fin[l]<fin[r])
dp[l][r][0] += dp[l+1][r][1];
if(fin[r]>fin[r-1])
dp[l][r][1] += dp[l][r-1][1];
dp[l][r][0] %= MOD;
dp[l][r][1] %= MOD;
}
}
cout<<(dp[1][n][0]+dp[1][n][1])%MOD;
return 0;
}
关于区间DP的一点点心得(虽然还是很菜)的更多相关文章
- UVA Live Archive 4394 String painter(区间dp)
区间dp,两个str一起考虑很难转移. 看了别人题解以后才知道是做两次dp. dp1.str1最坏情况下和str2完全不相同,相当于从空白串开始刷. 对于一个区间,有两种刷法,一起刷,或者分开来刷. ...
- 【HIHOCODER 1320】压缩字符串(区间DP)
描述 小Hi希望压缩一个只包含大写字母'A'-'Z'的字符串.他使用的方法是:如果某个子串 S 连续出现了 X 次,就用'X(S)'来表示.例如AAAAAAAAAABABABCCD可以用10(A)2( ...
- 区间DP的思路(摘自NewErA)及自己的心得
以下为摘要 区间dp能解决的问题就是通过小区间更新大区间,最后得出指定区间的最优解 个人认为,想要用区间dp解决问题,首先要确定一个大问题能够剖分成几个相同较小问题,且小问题很容易组合成大问题,从而从 ...
- Brackets (区间DP)
个人心得:今天就做了这些区间DP,这一题开始想用最长子序列那些套路的,后面发现不满足无后效性的问题,即(,)的配对 对结果有一定的影响,后面想着就用上一题的思想就慢慢的从小一步一步递增,后面想着越来越 ...
- Cheapest Palindrome(区间DP)
个人心得:动态规划真的是够烦人的,这题好不容易写出了转移方程,结果超时,然后看题解,为什么这些题目都是这样一步一步的 递推,在我看来就是懵逼的状态,还有那个背包也是,硬是从最大的V一直到0,而这个就是 ...
- 区间dp C - Two Rabbits
C - Two Rabbits 这个题目的意思是,n块石头围一圈.一只兔子顺时针,一只兔子逆时针(限制在一圈的范围内). 这个题目我觉得还比较难,不太好想,不过后来lj大佬给了我一点点提示,因为是需要 ...
- 区间dp 例题
D - 石子合并问题--直线版 HRBUST - 1818 这个题目是一个区间dp的入门,写完这个题目对于区间dp有那么一点点的感觉,不过还是不太会. 注意这个区间dp的定义 dp[i][j] 表示的 ...
- 【BZOJ-4380】Myjnie 区间DP
4380: [POI2015]Myjnie Time Limit: 40 Sec Memory Limit: 256 MBSec Special JudgeSubmit: 162 Solved: ...
- 【POJ-1390】Blocks 区间DP
Blocks Time Limit: 5000MS Memory Limit: 65536K Total Submissions: 5252 Accepted: 2165 Descriptio ...
- 区间DP LightOJ 1422 Halloween Costumes
http://lightoj.com/volume_showproblem.php?problem=1422 做的第一道区间DP的题目,试水. 参考解题报告: http://www.cnblogs.c ...
随机推荐
- IIC总线学习笔记
IIC(Inter-Integrated Circuit)其实是IICBus简称,所以中文应该叫集成电路总线,它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让主板.嵌入式系统或手 ...
- Go中 net/http 使用
转载请注明出处: net/http是Go语言标准库中的一个包,提供了实现HTTP客户端和服务器的功能.它使得编写基于HTTP协议的Web应用程序变得简单和方便. net/http包的主要用途包括: 实 ...
- 零基础入门——从零开始学习PHP反序列化笔记(一)
靶场环境搭建 方法一:PHPstudy搭建 GitHub地址 https://github.com/mcc0624/php_ser_Class 方法二:Docker部署 pull镜像文件 docker ...
- 《高级程序员 面试攻略 》RabbitMQ 如何实现可靠性
RabbitMQ 提供了多种机制来实现消息传递的可靠性.下面是一些常见的方法: 1. 持久化消息:RabbitMQ 允许将消息标记为持久化,以确保即使在发生故障或重启后,消息也不会丢失.通过将消息的` ...
- Combobox后台绑定
本文主要介绍WPF中Combobox的后台绑定,我在这里主要讲解数据驱动 1.对于前台绑定,我们首先写出想要绑定的对象 新建一个Models文件夹,将Student类写入 public class S ...
- 如何在达梦数据库中追踪慢SQL
在达梦数据库中,我们可以通过开启日志记录和设置最小执行时间来追踪慢SQL.下面是具体的步骤: 1. 修改dm.ini文件 使用以下命令编辑dm.ini文件: cd /home/dmdba/dmdbms ...
- mybatis-plus+nacos配置中心和服务发现保姆级教程
默认你已经看了我的Mybatis-Plus+Mysql的教程,现在有了一个简单的项目如下(之前的教程: https://www.cnblogs.com/leafstar/p/17638741.htm ...
- uniapp APP微信登录、支付、分享以及支付宝支付 实战踩坑记录
1.微信支付和支付宝支付 先上代码.封装好了的组件 html部分 <template> <view class="rows"> < ...
- 【路由器】OpenWrt 手动编译 ipk
目录 .ipk 文件 编译准备 编译 .ipk 文件 更新 feeds 配置平台 获取交叉编译链 添加需要编译的第三方软件包 参考资料 .ipk 文件 .ipk 文件是可以通过 OpenWrt 的包管 ...
- 带你上手基于Pytorch和Transformers的中文NLP训练框架
本文分享自华为云社区<全套解决方案:基于pytorch.transformers的中文NLP训练框架,支持大模型训练和文本生成,快速上手,海量训练数据>,作者: 汀丶 . 1.简介 目标: ...