【学习笔记】连续段 DP(线头 DP)
连续段 DP(线头 DP)
连续段 DP 是一种用来处理状态转移依赖邻项的排列计数或求值问题。
对于一些问题,状态转移可能与相邻的已插入元素的具体信息相关(可能需要知道具体的两个元素的值是多少)。这类问题通常的特点是,如果只考虑在序列的两端插入,问题将容易解决(或者说有更简便的状态记录方式)。
连续段 dp 的元素插入操作只会在连续段的两端进行,通常,我们会按某种特定的顺序插入所有元素,每次插入元素时,对三类转移方式进行分类讨论:
- 将插入的元素作为一个新连续段插入
- 将元素插入至一个已有连续段的两端
- 将元素用于合并两个连续段。
考虑三类情况分别会产生怎样的情况。最后考虑边界情况。
求方案数
以 [CEOI 2016] kangaroo 为例。
题目相当于求长度为 \(n\) 的排列 \(a\) 的方案数,满足 \(a_1=s,a_n=t\),且对于任意的 \(1<i<n\),满足 \(i\) 左右两边的数均比它大或小。
先不考虑 \(a_1\) 和 \(a_n\),考虑从小到大插入一个数。设 \(f_{i,j}\) 表示已经插入 \(i\) 个数,形成了 \(j\) 段的方案数。
- 将 \(i\) 作为一个新的段。左右两端插入的数一定比 \(i\) 大,可以满足条件,有 \(f_{i,j}\leftarrow f_{i,j}+f_{i-1,j-1}\times j\)。
- 将 \(i\) 插在已有段的两边。因为其中一边比 \(i\) 小,另一边比 \(i\) 大,不满足条件。
- 将 \(i\) 用于合并两个段。两边的数均比 \(i\) 小,满足条件,有 \(f_{i,j}\leftarrow f_{i,j}+f_{i-1,j+1}\times j\)。
因为要满足 \(a_1=s,a_n=t\),因此当 \(i=s\) 或 \(i=t\) 时,只能插在最左/右端或新开一个在最左/右边的段,有 \(f_{i,j}=f_{i-1,j}+f_{i-1,j-1}\)。
在考虑新开一个段的情况,若 \(s\) 或 \(t\) 有一个或两个已经被处理,那么对应的边界就不能放了。
那么作为新段的转移式为 \(f_{i,j}\leftarrow f_{i,j}+f_{i-1,j-1}\times (i-[i>s]-[i>t])\)。
最后答案为 \(f_{n,1}\),因为最后只能剩下一个段。
Code
f[1][1]=1;
for(int i=2;i<=n;i++){
for(int j=1;j<=n;j++){
if(i^s && i^t){
ll tp1=1ll*(j-(i>s)-(i>t))*f[i-1][j-1]%mod;
ll tp2=1ll*j*f[i-1][j+1]%mod;
f[i][j]=(tp1+tp2)%mod;
}
else f[i][j]=(f[i-1][j-1]+f[i-1][j])%mod;
}
}
cout<<f[n][1]<<"\n";
求值
以 CF704B 为例:
相当于要求一个排列 \(p\) 使 \(\sum_{i=1}^{n-1} f(p_i,p_{i+1})\) 的值最小,同时满足 \(p_1=s,p_n=t\)。
其中\( f(i,j)=\begin{cases}x_j-x_i+d_i+a_j \ \ \ \ i<j\\x_i-x_j+c_i+b_j\ \ \ \ \ i>j\end{cases}\)。
\(x\) 是一个单调递增的序列。
考虑将 \(i\) 从小到大插入,考虑三种情况:
- 将 \(i\) 作为一个新的段。后面插入的均比 \(i\) 大,有 \(f_{i,j}=f_{i-1,j-1}-2x_i+b_i+d_i\)。
- 将 \(i\) 接插在已有段两边。插在左边:\(f_{i,j}=f_{i-1,j}+b_i+c_i\),右边 \(f_{i,j}=f_{i-1,j}+a_i+d_i\)。
- 将 \(i\) 用于合并两个段。左右两个均比 \(i\) 小,有 \(f_{i,j}=f_{i-1,j+1}+2x_i+a_i+c_i\)。
将 \(4\) 种情况取 \(\min\) 即可。
再考虑 \(i=s\) 或 \(i=t\) 的情况:
- 若 \(i=s\),只能插在最左端,可以作为新的段插入最左边或是接在最左边段的左边,有 \(f_{s,j}=f_{s-1,j-1}-x_i+d_i\) 或 \(f_{s,j}=f_{s-1,j}+x_i+c_i\)。
- 若 \(i=t\),只能插在最右端,可以作为新的段插入最右边或是接在最右边段的右边,有 \(f_{s,j}=f_{s-1,j-1}-x_i+b_i\) 或 \(f_{s,j}=f_{s-1,j}+x_i+s_i\)。
枚举 \(i\) 时要注意与 \(s,t\) 的大小关系。
code
for(int i=0;i<=n;i++){
for(int j=0;j<=n;j++) f[i][j]=INF;
}
f[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
if(i==s){
f[i][j]=min(f[i-1][j-1]-x[i]+d[i],f[i-1][j]+x[i]+c[i]);
}
else if(i==t){
f[i][j]=min(f[i-1][j-1]-x[i]+b[i],f[i-1][j]+x[i]+a[i]);
}
else{
if(j>(i>s)+(i>t)) f[i][j]=min(f[i][j],f[i-1][j-1]-2*x[i]+b[i]+d[i]);//新开一段
if(j>1 || i<s) f[i][j]=min(f[i][j],f[i-1][j]+b[i]+c[i]);//加在左边
if(j>1 || i<t) f[i][j]=min(f[i][j],f[i-1][j]+a[i]+d[i]);//加在右边
f[i][j]=min(f[i][j],f[i-1][j+1]+2*x[i]+a[i]+c[i]);//合并两段。
}
}
}
cout<<f[n][1]<<"\n";
例题:
参考资料:

浙公网安备 33010602011771号