【算法笔记】区间DP_赋值



NMD初始化害死人錒。

记得区分清楚初始化的顺序。

还有赋的初值的意义。

初始化有时候没必要纠结范围,因为有些状态根本就用不到(状态定义问题

特殊的情形一定不要多也不要少的找全然后赋值。记得不要忽略赋值顺序

断环成链记得在最后取一下最值。



才发现自己区间DP有好多bug没补上。

借DC的讲义改了一些部分。



前 i 前 j 找分割点型:

所谓区间分割问题,是指在长度为 \(n\) 的线性序列上,分割为 \(m\) 部分,每个部分对应的权值不同,问如何分割才能使总权值最大。

(当然你遇见“分割,划分,分成”这些词的时候就可以想一想这个类型)

我们定义 \(f[i][j]\) 表示长度为 \(i\) 的序列,分割为 \(j\) 部分所能获得的最大权值,这是个前 \(i\) 前 \(j\) 问题,

那么我们先考虑最后一个分割,即第 \(j\) 个分割,肯定存在一个分割点 \(k\),使得 \(k+1\) 到 \(i\) 构成前 \(j\) 部分,获得权值和为 \(w[k+1][i]\),

那么问题转化为前面长度为 \(k\) 的序列分割成 \(j-1\) 个部分获得的最大权值。

状态转移方程:\(f[i][j]=max\{f[k][j-1]+w[k+1][i] \}\) $ (j-1\le k < i)$

有了大致的方程,针对每道题目可能会有小调整,随机应变即可。

例如:有 \(m\) 个位置,\(n\) 种花盆,编号从小到大,每种花盆有 \(a[i]\) 个,每种花盆摆在一起,问一共有多少种花方案?

\(f[i][j]\) 表示前\(i\)种花,摆在前 \(j\) 个位置的方案数。

\(f[i][j] += f[i-1][j-k]\),其中 \(k \in [0,a[i]]\),即枚举第i种花盆的个数k。

也可以\(f[i][j]\)表示前 \(i\) 个位置,摆前 \(j\) 种花盆的方案数。

\(f[i][j] += f[i-k][j-1]\),只要赋值符号右边是事先计算过的,值是确定的,就可以,所以DP数组的定义不是唯一的。

在这个例子里:一旦某个值 \(f[i-k][j-1]\) 计算出来,后边就不会再回来改变,我们称之为无后效性(也就是转移顺序是一张DAG的拓扑序),

这个值是 \(f[i][j]\) 的最优子结构,可以在后边阶段其他状态的计算过程中多次被引用。

#include<bits/stdc++.h>
using namespace std;

const int si=10000;
int f[si][si];
int n;

int clac(int l,int r){
/*return value of range [l,r]*/
}//注意这里会有很多判断,甚至会有模拟在里面,或许是DP

int main(){
scanf("%d%d",&n,&m);//length ->n break into m range.
init();
for(register int i=1;i<=n;++i){
for(register int j=1;j<=m;++j){
for(register int k=j-1;k<i;++k){
//*last* break point.
f[i][j]=max(f[i][j],f[k][j-1]+calc(k+1,i));
//前一段的 *状态* +后一段的 *贡献*
}
}
}
}


注意这个\(k\)一定是小于\(i\)的,不然\(k+1\)会出事。

然后你枚举 \(k\) 的时候要看情况,有的时候是这样的。有的时候是 \([0 , i)\) (邮局那题) 所以要视情况而定。(普通DP也一样)

然后有一个点,你会发现,我们枚举的这个 \(k\) 是一个 “不确定” 的元素。

它的决策会影响我们的状态和转移。(比如下面两道题里的 邮局 和 花朵 )。

那么、我们就要去枚举这个东西的决策点。

一般来说,它的转移是这样子的:

【算法笔记】区间DP_Algor-区间DP_02

然后中间的就是我们的决策点 \(k\) 了。

然后有些人是直接用后一段的 \(f\) ,因为他们给对应的值初始化了。有的时候这种方法没问题,不过上面的那种更通用。

当然有些题目的 ​​clac()​​的值可以通过DP或者递推预先处理出来。(邮局一题便是)

Problems:

(这里第二题是要输出方案的,我们一般的方式是拿一个和dp同步更新的数组,记录当前最优操作从哪一步推来,然后递归输出)



以区间阶段的类型。

这种就比较简单了(一般题目里会是“合并,删除”一类的词)

你可以直接用分割点之前的状态加上之后的状态再加上转移代价。

这种的注意点和上面的基本一样。

然后有的时候合并或删除区间是要特判区间首尾或者条件的(甚至是分条件讨论合并)(下面的三道例题都是)

至于为什么要枚举长度呢?

因为正常的线性DP都是这样子转移的(或者类似):

【算法笔记】区间DP_Type-算法笔记_03

(也就是填一个表,然后等号赋值的时候被赋值的是未知的,用来赋值的则是已知的。)

区间DP不太一样,它并不是说下标小的就会先填出来(意思是这张图中的“行”下标 \(i\) ,因为第二维第三维不一定)

它是用小区间凑大区间,所以转移状态就不一样了,它的阶段就不是“行号” \(i\) ,而是区间长 \(len\) ,(\(\text{Floyd}\) 外层为什么是 \(k\) 这里也能用相同的思想解释了)

tips:这种题一般那个 ​​if​​(限制)动手脚的地方都是考虑一整个区间的首尾。

所以从首尾开始考虑转移就行。

然后这种题大部分情况都是用区间长度作为阶段。

所以区间长放在外面,(前面你已经初始化了的区间就不用枚举了)

然后内层只需要循环一个 \(i\) 就行, \(j\) 可以用 \(i-len+1\) 确定。

最里面有的时候不一定要 \(k\) ,我们需要找分割点的时候才用 \(k\) 。

如果是决策只与区间的任意两个元素有关,那么我们就把 \(k\) 去掉,直接利用 \(i,j\) 转移。

code:

    init(0~a);
for(register int len=a+1;len<=n;++len){
for(register int i=1;i<=n;++i){
register int j=i+len-1;
//if ...
//f{i,j} = fuc{...};
}
}

or

init(0~a);
for(register int len=a+1;len<=n;++len){
for(register int i=1;i<=n;++i){
register int j=i+len-1;
for(register int k=i;k<j;++k){
//f{i,j} = fuc {... f[i][k]+f[k+1][j]+v ...};
}//break point
}
}


Problems:



其他的新东西请见例题选讲。

因为所有区间DP都实际上是这两个的变式。

不要拘泥于板子,要看情况灵活的设计状态。