To_Heart—总结——连通块 dp(抑或 连续块 dp)

简介

有一类问题,他们需要计算满足在序列上插入给定顺序的元素,其贡献/限制只与两旁的元素有关,但元素插入的位置是不一定的,所以会有代价的最值和方案数的统计。而对于这类问题,我们其实可以不关心每一次插入的具体位置在哪里,而只关注他的相对位置(比如在某个数左边、在某个数左边且与其相邻之类的),从而将状态大大简化。

模型

通常以二维表示 dp[i][j] 表示插入到第 i 个元素,前 i-1 个元素已经插入到序列上,一共形成了 j 个连通块的 方案数/代价。转移有三种大类,分别是合并两个连通块、新加一个连通块、延续一个连通块。

通常转移有三种形式:

  1. 合并两个连通块 \(dp[i+1][j-1]<-dp[i][j]*(j-1)\) ,因为每次合并可以选择任意的两个合并;
  2. 延续某个连通块 \(dp[i+1][j] <- dp[i][j]*2*j\)
  3. 增加一个连通块 \(dp[i][j+1] <- dp[i][j]*(j+1)\)

值得注意的是这里的转移后面的系数多半用于处理方案数。如果要用 连通块dp 求最值则需要考虑插入的当前元素与左右插入元素的关系从而改变的转移状态。

考虑为什么要用连通块表示 相对位置的信息。因为连通块可以表示左右是否有数、以及左右的数是否与其相邻。这里面有个隐藏的参数是当前场上的元素数量,但我们发现用 i 就表示完了。

纸上得来终觉浅,下面进入实战。

题目讲解

1.摩天大楼

首先注意到限制有绝对值,考虑将 a[i] 按照从大到小的顺序插入到序列中,那么绝对值的限制就去掉了。我们发现此时插入的元素顺序确定,并且发现当前插入一个数的代价与其它已经在序列上的数有关,符合模型,即可使用 连通块dp 解决。细节交给读者思考。

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

const ll Mod=1e9+7;

int n,L;
int a[105];
ll dp[105][105][1005][2][2];
//int now[105][105][1005][2][2];

bool cmp(int x,int y){ return x>y; }

int main(){
	cin>>n>>L;
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	sort(a+1,a+n+1,cmp);
	dp[1][1][0][0][0]=dp[1][1][0][1][1]=dp[1][1][0][0][1]=dp[1][1][0][1][0]=1;
	for(int i=2;i<=n;i++){
		for(int j=1;j<i;j++) for(int k=0;k<=L;k++) for(int p=0;p<=1;p++) for(int q=0;q<=1;q++) if(dp[i-1][j][k][p][q]){
			int now=(a[i-1]-a[i])*(j*2-p-q);
			if(k+now>L) continue;
			if(j>1) dp[i][j-1][k+now][p][q]+=dp[i-1][j][k][p][q]*(j-1),dp[i][j-1][k+now][p][q]%=Mod;
			if(j>1) dp[i][j+1][k+now][p][q]+=dp[i-1][j][k][p][q]*(j-1),dp[i][j+1][k+now][p][q]%=Mod;
			if(j>1) dp[i][j][k+now][p][q]+=dp[i-1][j][k][p][q]*(2*j-2),dp[i][j][k+now][p][q]%=Mod; 
			if(!p){
				dp[i][j][k+now][1][q]+=dp[i-1][j][k][p][q],dp[i][j][k+now][1][q]%=Mod; 
				dp[i][j][k+now][0][q]+=dp[i-1][j][k][p][q],dp[i][j][k+now][0][q]%=Mod;
				dp[i][j+1][k+now][1][q]+=dp[i-1][j][k][p][q],dp[i][j+1][k+now][1][q]%=Mod; 
				dp[i][j+1][k+now][0][q]+=dp[i-1][j][k][p][q],dp[i][j+1][k+now][0][q]%=Mod;
			}
			if(!q){
				dp[i][j][k+now][p][1]+=dp[i-1][j][k][p][q],dp[i][j][k+now][p][1]%=Mod; 
				dp[i][j][k+now][p][0]+=dp[i-1][j][k][p][q],dp[i][j][k+now][p][0]%=Mod;
				dp[i][j+1][k+now][p][1]+=dp[i-1][j][k][p][q],dp[i][j+1][k+now][p][1]%=Mod;
				dp[i][j+1][k+now][p][0]+=dp[i-1][j][k][p][q],dp[i][j+1][k+now][p][0]%=Mod;
			}
		}
	}
	ll ans=0;
	for(int i=0;i<=L;i++) ans+=dp[n][1][i][1][1],ans%=Mod;
	cout<<ans<<endl;
	return 0;
}

类似题目:CF704B

2.P5999

一开始看到这类题目的时候其实并不好考虑其dp状态和转移。看似袋鼠跳来跳去对于插入序列来说有个天然的时间顺序,但这道题关于位置 s 和 t 会有转移上的限制,用时间顺序插入的话不好考虑,于是我们考虑按位置顺序将每个元素插入到操作序列中。于是状态就出来了,dp[i][j] 表示插入了前 i 个树丛,在操作序列上形成了 j 个连通块的方案数。答案依旧为 dp[n][1]。

值得注意的是,这里的连通块有什么实际的意义吗?其实是没有的。我们只是用它来限制我们能在 i=s 以及 i=t 的时候能做特殊的转移。其实很多时候 连通块dp 能够提供给我们的只是一种枚举策略。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll Mod=1e9+7;

ll dp[2005][2005];
int n,s,t;

int main(){
	cin>>n>>s>>t;
	dp[1][1]=1;
	for(int i=2;i<=n;i++) for(int j=1;j<=i;j++){
		if(i==s||i==t) dp[i][j]=dp[i-1][j-1]+dp[i-1][j],dp[i][j]%=Mod;
		else dp[i][j]=dp[i-1][j-1]*(j-(i>s)-(i>t))%Mod+dp[i-1][j+1]*j%Mod;	
	}
	cout<<dp[n][1]<<endl;
	return 0;
}

类似题目:CF1515E

posted @ 2023-11-10 22:38  To_Heart  阅读(65)  评论(5编辑  收藏  举报