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\) 是一个 “不确定” 的元素。
它的决策会影响我们的状态和转移。(比如下面两道题里的 邮局 和 花朵 )。
那么、我们就要去枚举这个东西的决策点。
一般来说,它的转移是这样子的:
然后中间的就是我们的决策点 \(k\) 了。
然后有些人是直接用后一段的 \(f\) ,因为他们给对应的值初始化了。有的时候这种方法没问题,不过上面的那种更通用。
当然有些题目的 clac()
的值可以通过DP或者递推预先处理出来。(邮局一题便是)
Problems:
(这里第二题是要输出方案的,我们一般的方式是拿一个和dp同步更新的数组,记录当前最优操作从哪一步推来,然后递归输出)
以区间阶段的类型。
这种就比较简单了(一般题目里会是“合并,删除”一类的词)
你可以直接用分割点之前的状态加上之后的状态再加上转移代价。
这种的注意点和上面的基本一样。
然后有的时候合并或删除区间是要特判区间首尾或者条件的(甚至是分条件讨论合并)(下面的三道例题都是)
至于为什么要枚举长度呢?
因为正常的线性DP都是这样子转移的(或者类似):
(也就是填一个表,然后等号赋值的时候被赋值的是未知的,用来赋值的则是已知的。)
区间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都实际上是这两个的变式。
不要拘泥于板子,要看情况灵活的设计状态。