2025.10~11 集训做题杂记

[CTS2024] 众生之门

小清新构造题。

观察大样例可以发现答案不大于 \(3\),感性猜测可以在路径长度不超过 \(3\) 的情况下遍历整棵树,事实也确实如此。

进一步考虑答案一般为 \(0\) 和 \(1\),只有 \(n\) 比较小或者图为菊花时答案是固定的。

所以我们对较小的 \(n\) 跑暴力,菊花图特判,对其余情况随机一个排列,每次交换两项,直到答案小于等于 \(1\) 输出即可。

由于答案值域是 \(O(n)\) 的,所以期望也是 \(O(n)\) 次找到答案,复杂度没问题。

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=5e4+5;
int T,n,s,t,dep[N],p[N],ANS,ans[N];
int siz[N],wc[N],top[N],fa[N];
vector<int>e[N];
void dfs1(int now,int f){
	siz[now]=1,wc[now]=0,fa[now]=f;
	for(auto it:e[now]){
		if(it!=f){
			dep[it]=dep[now]+1;
			dfs1(it,now);
			siz[now]+=siz[it];
			if(siz[it]>siz[wc[now]])	wc[now]=it;
		}
	}
}
void dfs2(int now,int Top){
	top[now]=Top;
	if(wc[now])	dfs2(wc[now],Top);
	for(auto it:e[now]){
		if(it!=fa[now]&&it!=wc[now])	dfs2(it,it);
	}
}
int LCA(int x,int y){
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]])	swap(x,y);
		x=fa[top[x]];
	}
	return dep[x]<dep[y]?x:y;
}
bool flower(){
	for(int i=1;i<=n;i++)	if(e[i].size()==n-1)	return 1;
	return 0;
}
int dis(int x,int y){return dep[x]+dep[y]-2*dep[LCA(x,y)];}
mt19937 rd(std::random_device{}());
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>T;
	while(T--){
		cin>>n>>s>>t;
		for(int i=1;i<=n;i++)	e[i].clear();
		for(int i=1,u,v;i<n;i++){
			cin>>u>>v;
			e[u].push_back(v);
			e[v].push_back(u);
		}
		dep[1]=0;dfs1(1,1),dfs2(1,1);
		if(n<=8){
			ANS=10;
			for(int i=1;i<=n;i++)	p[i]=i;
			do{
				if(p[1]!=s||p[n]!=t)	continue;
				int tmp=0;
				for(int i=2;i<=n;i++)	tmp^=dis(p[i],p[i-1]);
				if(tmp<ANS){
					ANS=tmp;
					for(int i=1;i<=n;i++)	ans[i]=p[i];
				}	
			}while(next_permutation(p+1,p+n+1));
			for(int i=1;i<=n;i++)	cout<<ans[i]<<" ";
			cout<<"\n";
		}else{
			p[1]=s,p[n]=t;
//			for(int i=2;i<=n-1;i++)	ans[i]=p[i]=i-1+(i-1>=s)+(i-1+(i-1>=s)>=t);
			for(int i=1;i<=n;i++)if(i!=s&&i!=t)	p[i-(i>s)-(i>t)+1]=i;
			if(flower()){
				for(int i=1;i<=n;i++)	cout<<p[i]<<" ";
				cout<<"\n";
				continue;
			}
			int tmp=0;
			for(int i=2;i<=n;i++)	tmp^=dis(p[i],p[i-1]);
			while(tmp>1){
				int l=rd()%(n-2)+2,r=rd()%(n-2)+2;
				while(l==r)	r=rd()%(n-2)+2;
				tmp^=dis(p[l],p[l-1])^dis(p[l],p[l+1])^dis(p[r],p[r-1])^dis(p[r],p[r+1]);
				swap(p[l],p[r]);
				tmp^=dis(p[l],p[l-1])^dis(p[l],p[l+1])^dis(p[r],p[r-1])^dis(p[r],p[r+1]);
			}
			for(int i=1;i<=n;i++)	cout<<p[i]<<" ";
			cout<<"\n";
		}
	}
}

P10813 【MX-S2-T4】 换

对于这种排序网络的题,可以考虑一个经典 trick,将序列 \(\{A_n\}\) 重赋值为 \(\{B_n(V)\}\) 满足:\(B_{i}(V)=[A_i\le V]\)。

一个序列 \(\{A_n\}\) 合法,当且仅当 \(\forall V,\{B_n(V)\}\) 合法。

考虑直接预处理出所有 01 序列是否合法,这是 \(O(m2^n)\) 的。

发现序列 \(A\) 离散化后只有 \(O(n)\) 个 \(V\) 是有意义的,考虑状压 DP,记 \(dp_{i,S}\) 表示离散化后 \(\{B_n(i)\}=S\) 且 \(\exists x,A_x=i\) 的方案数。

转移就是填入 \(i+1\),有 \(dp_{i+1,S}=\sum_{T\subset S}dp_{i,T}\)。

直接上子集枚举是 \(O(n3^n)\) 的,不太可行,考虑每一层 DP 前做一遍高维前缀和,于是复杂度变为 \(O(n^22^n)\)。

总时间复杂度 \(O(n^2+m)2^n\)。

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=18,M=505,mod=1e9+7;
ll n,V,m,dp[1<<N],SS[1<<N],ans;
int x[M],y[M];
bool f[1<<N],p[N+5];
bool check(int S){
	for(int i=1;i<=n;i++)	p[i]=(S>>(i-1))&1;
	for(int i=1;i<=m;i++){
		if(p[x[i]]>p[y[i]])	swap(p[x[i]],p[y[i]]);
	}
	bool tmp=1;
	for(int i=1;i<n;i++)	tmp&=(p[i]<=p[i+1]);
	return tmp;
}
ll q_pow(ll x,ll b){
	ll c=1;
	while(b){
		if(b&1)	c=c*x%mod;
		x=x*x%mod,b>>=1;
	}
	return c;
}
ll inv(ll x){return q_pow(x,mod-2);}
ll C(ll x,ll y){
	if(x<y||y<0)return 0;
	ll ret=1;
	for(int i=1;i<=y;i++)	ret=ret*(x-i+1)%mod*inv(i)%mod;
	return ret;
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>n>>V>>m;
	for(int i=1;i<=m;i++)	cin>>x[i]>>y[i];
	for(int S=0;S<(1<<n);S++)	f[S]=check(S);
	dp[0]=1;
	for(int i=1;i<=n;i++){
		for(int S=0;S<(1<<n);S++)	SS[S]=dp[S];
		for(int j=0;j<n;j++){
			for(int S=0;S<(1<<n);S++){
				if(S&(1<<j))	SS[S]=(SS[S]+SS[S^(1<<j)])%mod;
			}
		}
		for(int S=0;S<(1<<n);S++)	dp[S]=(SS[S]-dp[S]+mod)*f[S]%mod;
		ans=(ans+C(V,i)*dp[(1<<n)-1]%mod)%mod;
	}
	cout<<ans<<"\n";
}

The only survival

发现每次可以钦定一堆点满足 \(dis_{1,n}\in [1,k] \cup \{> k\}\),这里 \(>k\) 当作同一类点,因为对 \(dis_{1,n}=k\) 没有意义。同时 \(dis_{1,n}\) 必须钦定为 \(k\)。

新加入一个点 \(v\) 时,对于一条边 \(<u,v,w>\) 有 \(w\in[dis_{1,v}-dis_{1,u},lim]\) 乘法原理即可算出总方案数,同时注意必须存在一条边满足 \(w=dis_{1,v}-dis_{1,u}\) ,减掉 \(\forall <u,v,w>,w>dis_{1,v}-dis_{1,u}\) 的方案数即可,快速求方案数可以记录 \(cnt_i=\sum_x [dis_{1,x}=i]\)。

时间复杂度是划分数,所以 \(n\le 13\) 能跑。

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=15;
ll n,lim,k,mod,cnt[N],ans;
ll q_pow(ll x,ll b){
	ll c=1;
	while(b){
		if(b&1)	c=c*x%mod;
		x=x*x%mod,b>>=1;
	}
	return c;
}
ll inv(ll x){return q_pow(x,mod-2);}
ll fac[N],C[N][N];
void init(int x){fac[0]=1;for(int i=1;i<=x;i++)	fac[i]=fac[i-1]*i%mod;}
void dfs(int dep,int num,ll tmp){
	if(dep>k){
		ll tmpv=1;
		for(int i=0;i<=k;i++)	tmpv=tmpv*q_pow(lim-(k-i),cnt[i])%mod;
		return ans=(ans+tmp*q_pow(tmpv,n-num)%mod*q_pow(lim,(n-num)*(n-num-1)/2)%mod)%mod,void();
	}else{
		ll tmpv=1,subv=1;
		for(int i=0;i<dep;i++)	tmpv=tmpv*q_pow(lim-(dep-i)+1,cnt[i])%mod,subv=subv*q_pow(lim-(dep-i),cnt[i])%mod;
		tmpv=(tmpv+mod-subv)%mod;
		for(int i=(dep==k);num+i<=n;i++)	cnt[dep]=i,dfs(dep+1,num+i,tmp*q_pow(tmpv,i)%mod*q_pow(lim,i*(i-1)/2)%mod*C[n-num-1][i-(dep==k)]%mod);
	}
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	freopen("graph.in","r",stdin);
	freopen("graph.out","w",stdout);
	cin>>n>>k>>lim>>mod;
	if(lim<k)	return cout<<"0\n",0;
	C[0][0]=1;
	for(int i=1;i<=n;i++){
		C[i][0]=1;
		for(int j=1;j<=i;j++)	C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
	}
	init(n);cnt[0]=1,n--;
	dfs(1,0,1);
	cout<<ans<<"\n";
}

神秘式子一则

原题钦定顺序然后就是求将 \(n\) 个不同小球放进任意个相同盒子使得每个盒子小球数量 \(\ge2\) 的方案数。

每个盒子至少放一个小球方案数是简单的斯特林数,考虑对只有一个小球的盒子个数容斥。

于是有:

\[ans=\sum_{m=0}^{n}(-1)^m \binom{n}{m}\sum_{k\ge 0}S_{n-m,k} \]

考虑拆开斯特林数:

\[\begin{align} ans=\sum_{m=0}^{n}(-1)^m \binom{n}{m}\sum_{k\ge 0}S_{n-m,k} &=\sum_{m=0}^{n}(-1)^m \binom{n}{m}\sum_{k=0}^n\sum_{i=0}^k(-1)^{k-i}\frac{i^{n-m}}{i!(k-i)!} \\ &=\sum_{m=0}^{n}(-1)^m \binom{n}{m}\sum_{i=0}^n\sum_{k=i}^n(-1)^{k-i}\frac{i^{n-m}}{i!(k-i)!}\\ &=\sum_{i=0}^n\sum_{m=0}^n\binom{n}{m}(-1)^mi^{n-m}\frac{1}{i!}\sum_{k=0}^{n-i}\frac{(-1)^k}{k!}\\ &=\sum_{i=0}^n\frac{(i-1)^n}{i!}\sum_{k=0}^{n-i}\frac{(-1)^k}{k!} \end{align} \]

后半是个前缀和的形式,可以预处理出来,于是复杂度 \(O(n)\)。

TEST_90

从矩阵乘法的形式来理解区间历史和问题,继而可以推广到任意双半群模型,算是个经典 trick 了。

排序后扫描线,转换为历史和问题是平凡的。

设线段树节点维护 \(\{S,PS,L\}\),代表当前区间和,历史和,区间长度,考虑两个 tag 所对应的矩阵。

Xor:

\[\begin{bmatrix} -1 & 0 & 0 \\ 0 & 1 & 0 \\ 1 & 0 & 1 \end{bmatrix} \]

Tim:

\[\begin{bmatrix} 1 & 1 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \]

直接做就行,但你发现直接矩乘是过不去的。

这个里面说的很好:

矩阵乘法更多是用来理解标记的,而不是用在代码实现上的。

发现矩阵中只有四个值会变化,其余值都是固定的,所以只维护这四个值就可以。

struct M{
	ll d[4];
	ll& operator[](size_t x){return d[x];}
	friend M operator*(M x,M y){
		return	{x[0]*y[1]+y[0],x[1]*y[1],x[2]+x[1]*y[2],x[3]+y[3]+x[0]*y[2]};
	}
};
const M I={0,1,0,0};
M xort(){return {1,-1,0,0};}
M tim(){return {0,1,1,0};}

其中 \(d_0: L\rightarrow S,d_1: S\rightarrow S,d_2: S\rightarrow PS,d_3: L\rightarrow PS\)。

然后矩阵乘法常数就很小了。

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=1e6+5;
int n,d,m,a[N],l[N],lst[N];
ll ans[N];
vector<PII>q[N];
struct M{
	ll d[4];
	ll& operator[](size_t x){return d[x];}
	friend M operator*(M x,M y){
		return	{x[0]*y[1]+y[0],x[1]*y[1],x[2]+x[1]*y[2],x[3]+y[3]+x[0]*y[2]};
	}
};
const M I={0,1,0,0};
M xort(){return {1,-1,0,0};}
M tim(){return {0,1,1,0};}
struct Segment_Tree{
	struct Segment_Tree_node{
		ll S,PS,L;M tag;
		friend Segment_Tree_node operator+(Segment_Tree_node x,Segment_Tree_node y){return {x.S+y.S,x.PS+y.PS,x.L+y.L,I};}
		friend Segment_Tree_node operator*(Segment_Tree_node x,M y){return {x.S*y[1]+x.L*y[0],x.PS+x.S*y[2]+x.L*y[3],x.L,x.tag*y};}
	}t[N<<2];
	inline int ls(int x){return x<<1;}
	inline int rs(int x){return x<<1|1;}
#define mid ((l+r)>>1)
	inline void f(int p,M x){t[p]=t[p]*x;}
	inline void push_up(int p){t[p]=t[ls(p)]+t[rs(p)];}
	inline void push_down(int p){f(ls(p),t[p].tag),f(rs(p),t[p].tag),t[p].tag=I;}
	void build(int p,int l,int r){
		if(l==r)	return t[p]={0,0,1,I},void();
		else{
			build(ls(p),l,mid),build(rs(p),mid+1,r);
			push_up(p);
		}
	}
	void change(int p,int l,int r,int re_l,int re_r,int op){
		if(re_l<=l&&r<=re_r)	return f(p,op?tim():xort());
		else{
			push_down(p);
			if(re_l<=mid)	change(ls(p),l,mid,re_l,re_r,op);
			if(mid<re_r)	change(rs(p),mid+1,r,re_l,re_r,op);
			push_up(p);
		}
	}
	ll query(int p,int l,int r,int re_l,int re_r){
		if(re_l<=l&&r<=re_r)	return t[p].PS;
		else if(!(r<re_l||l>re_r))	return push_down(p),query(ls(p),l,mid,re_l,re_r)+query(rs(p),mid+1,r,re_l,re_r);
		else	return 0;
	}
}T;
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>n;
	for(int i=1;i<=n;i++)	cin>>a[i];
	cin>>m;
	for(int i=1,l,r;i<=m;i++)	cin>>l>>r,q[r].push_back({l,i});
	T.build(1,1,n);
	for(int i=1;i<=n;i++){
		T.change(1,1,n,lst[a[i]]+1,i,0);
		T.change(1,1,n,1,i,1);
		for(auto it:q[i])	ans[it.se]=T.query(1,1,n,it.fi,i);
		lst[a[i]]=i;
	}
	for(int i=1;i<=m;i++)	cout<<ans[i]<<"\n";
}

CF995F Cowmpany Cowmpensation

比较好计数题。

容易想到一个树形 DP,记 \(dp_{i,j}\) 表示考虑完点 \(i\) 的子树,点 \(i\) 填 \(j\) 的方案数,于是有:

\[dp_{i,j}=\prod_{v\in son_i}\sum_{k=1}^jdp_{v,k} \]

这个是容易前缀和优化的,但考虑到点 \(1\) 填 \(j\),整棵树树内却不一定能覆盖完 \([1,j]\),直接统计答案会算重,考虑设 \(g_i\) 表示填数恰好覆盖 \([1,i]\) 的方案数,有:

\[g_i=dp_{1,i}-\sum_{j=1}^i\binom{i-1}{j-1}g_j \]

系数 \(\binom{i-1}{j-1}\) 是因为 \(g_j\) 中点 \(1\) 填的值已经确定,所以剩下的点只需要从 \(i-1\) 种颜色选 \(j-1\) 个。

答案即为:

\[\sum_{i=1}^n\binom{D}{i}g_i \]

整体可以做到 \(O(n^2)\)。

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=3005,mod=1e9+7;
template<int mod> struct Modint{......};//Modint 类
#define MI Modint<mod>
int n,D;
vector<int>e[N];
MI C[N][N],dp[N][N],S[N][N],g[N],ans;
MI binom(ll x,ll y){
	MI ret=1;
	for(ll i=1;i<=y;i++)	ret=ret*(x-i+1)/i;
	return ret;
}
void DP(int now,int fa){
	for(int i=1;i<=n;i++)	dp[now][i]=1;
	for(auto it:e[now]){
		DP(it,now);
		for(int i=1;i<=n;i++)	dp[now][i]*=S[it][i];
	}
	for(int i=1;i<=n;i++)	S[now][i]=S[now][i-1]+dp[now][i];
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>n>>D;
	for(int i=2,fa;i<=n;i++)	cin>>fa,e[fa].push_back(i);
	for(int i=0;i<=n;i++){
		C[i][0]=1;
		for(int j=1;j<=i;j++)	C[i][j]=C[i-1][j]+C[i-1][j-1];
	}
	DP(1,0);
	g[1]=1;ans+=D;
	for(int i=2;i<=min(n,D);i++){
		g[i]=dp[1][i];
		for(int j=1;j<i;j++)	g[i]-=C[i-1][j-1]*g[j];	
		ans+=binom(D,i)*g[i];
	}
	cout<<ans<<"\n";
}

Tree Tweaking

很有意思的一道题。

给定一个长度为 \(n\) 的排列 \({A_n}\),按此顺序构建一棵二叉搜索树。

你可以重新排列排列中下标在 \([l, r]\) 范围内的键的插入顺序,其余键按原顺序插入。

目标是通过重排 \([l, r]\) 部分的插入顺序,使得最终 BST 的所有节点的深度之和最小,输出这个最小的深度和。

根节点深度为 1。

\(1\le n\le 10^5\),\(1\le l\le r\le n\),\(r-l\le200\)。

首先有 \(\sum dep=\sum siz\)。于是问题转换为子树大小之和最小。

考虑由于 \({A_n}\) 是一个排列,所以一个节点的子树大小必然等于插入这个节点时的 \(nxt-pre-1\),其中 \(nxt\) 为这个值的后继,\(pre\) 为这个值的前驱。

将插入的值拍到数轴上,因为当 \(x<l\) 或者 \(x>r\) 时,数轴上有值的位置时固定的,所以 \(siz_{[1,l-1]}\) 和 \(siz_{[r+1,n]}\) 是固定的。

于是可以修改的部分被前 \(l-1\) 个插入的数划分成了若干个连续段,对每一段分开考虑。

记 \(dp_{l,r}\) 表示这个连续段里插入所有段内第 \(l\) 到 \(r\) 个数的最小贡献,\(v_i\) 表示段内第 \(i\) 个数的值。

有 \(dp_{l,r}=v_{r+1}-v_{l-1}-1+\min \limits_{p=l}^r\{dp_{l,p-1}+dp_{p+1,r}\}\)。

一个段的答案即为 \(dp_{2,tp-1}\)。

具体见代码。

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=1e5+5,M=205;
ll n,a[N],L,R,v[N],dp[M][M],ans;
vector<int>num[N];
set<int>se;
void add(int x){
	auto nxt=se.upper_bound(x);
	ans+=(*nxt)-(*(--nxt))-1;
	se.insert(x);
}
ll DP(int l,int r){
	int tp=0;
	v[++tp]=l;sort(num[r].begin(),num[r].end());
	for(auto it:num[r])	v[++tp]=it;
	v[++tp]=r;
	for(int i=1;i<=tp;i++){
		for(int j=i;j<=tp;j++)	dp[i][j]=0x3f3f3f3f;
	}
	for(int len=1;len<tp;len++){
		for(int i=2;i+len-1<tp;i++){
			int j=i+len-1;
			for(int k=i;k<=j;k++)	dp[i][j]=min(dp[i][j],v[j+1]-v[i-1]-1+dp[i][k-1]+dp[k+1][j]);
		}
	}
	return dp[2][tp-1];
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>n;
	for(int i=1;i<=n;i++)	cin>>a[i];
	se.insert(0),se.insert(n+1);
	cin>>L>>R;
	for(int i=1;i<L;i++)	add(a[i]);
	for(int i=L;i<=R;i++)	num[*se.upper_bound(a[i])].push_back(a[i]);
	for(auto it=se.begin();it!=se.end();it++){
		if(num[*it].size())	ans+=DP(*prev(it),*it);
	}
	for(int i=L;i<=R;i++)	se.insert(a[i]);
	for(int i=R+1;i<=n;i++)	add(a[i]);
	cout<<ans<<"\n";
}

Chords

钦定代表元的 trick?

断环为链,考虑一个联通分量必然包含于唯一一个极小区间 \([l,r]\),那么钦定 \([l,r]\) 为这个联通分量的代表元,考虑对代表元计数。

观察一个代表元需要满足什么样的条件:

  • \(l\) 和 \(r\) 联通。
  • \([l,r]\) 与外部没有连边。

第二个条件是好判断的,枚举代表元的时候 check 一下出边就行。

强制 \(l\) 和 \(r\) 联通是一个区间完全覆盖问题,不太好办,考虑用总方案数减去不联通的方案数。

总方案数是简单的,即为 \([2\mid c(l,r)]*c(l,r)!!\),其中 \(c(l,r)\) 为 \([l,r]\) 没有被钦定连边的点数。

考虑减去不联通的方案数,枚举与 \(l\) 联通的最右边的点 \(k\) 即可。

记 \(f(x)=[2\mid x]*x!!\),于是:

\[dp_{l,r}=f(c(l,r))-\sum_{k=l}^{r-1}dp_{l,k}\times f(c(k+1,r)) \]

统计答案是简单的,代表元内部组成方案乘以外部连边方案即可:

\[Ans=\sum_{l\le r}^{ } dp_{l,r}\times f(n-2K-c(l,r)) \]

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=605,mod=1e9+7;
template<int mod> struct Modint{......};
#define MI Modint<mod>
int n,m,to[N],v[N][N];
MI dp[N][N],g[N],ans;
bool f[N];
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>n>>m;g[0]=1;n<<=1;
	for (int i=2;i<=n;i+=2)	g[i]=g[i-2]*(i-1);
	for(int i=1,x,y;i<=m;i++)	cin>>x>>y,f[x]=f[y]=1,to[x]=y,to[y]=x;
	for(int l=1;l<=n;l++){
		for(int r=l;r<=n;r++)	v[l][r]=v[l][r-1]+(!f[r]);
	}
	for(int l=1;l<=n;l++){
		for(int r=l+1;r<=n;r+=2){
			bool ok=1;
			for(int k=l;k<=r;k++)	ok&=(!f[k]||(f[k]&&(to[k]>=l&&to[k]<=r)));
			if(!ok)	continue;
			dp[l][r]=g[v[l][r]];
			for(int k=l;k<r;k++)	dp[l][r]-=dp[l][k]*g[v[k+1][r]];
			ans+=dp[l][r]*g[n-2*m-v[l][r]];
		}
	}
	cout<<ans<<"\n";
}

[SCOI2016] 萌萌哒

\(O(nm\alpha(n))\) 做法是简单的,考虑优化。

这种连边问题首先可能会往线段树优化建图的方面去想,但是两个区间在线段树上的划分方式可能不同,比如一个划分成长度为 \([1,4,1]\) 的三个区间,另外一个划分成 \([4,2]\),这个做法就破产了。

这时候有一个 trick 是倍增优化并查集,因为并查集满足结合律,所以考虑类似 ST 表的结构,我们可以把一个区间划分成 \(O(log n)\) 个长度为 \(2^k\) 的段,这时候划分方式就只与长度相关了,可以每段分别合并。

考虑统计答案,我们可以直接把第 \(k\) 层的并查集与 \(k-1\) 层的并查集合并,将 \([i,i+2^k)\) 拆成 \([i,i+2^{k-1}),[i+2^{k-1},i+2^k)\),由于是同层整个块一起合并,我们容易根据点 \(i\) 的合并对象推出点 \(i+2^{k-1}\) 的合并对象,然后在第 \(0\) 层查联通块数量。

答案就是 \(9\times 10^{num-1}\),\(num\) 即为联通块数量。

具体看代码,复杂度 \(O(n\log n\alpha(n))\)。

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=1e5+5,mod=1e9+7,LG=18;
template<int siz>struct DSU{
	int fa[siz],sz[siz];
	void clear(int x){for(int i=1;i<=x;i++)	fa[i]=i,sz[i]=1;}
	int find(int x){return (fa[x]==x?x:fa[x]=find(fa[x]));}
	void merge(int x,int y){return same(x,y)?void():(sz[find(y)]+=sz[find(x)],fa[find(x)]=find(y),void());}
	bool same(int x,int y){return find(x)==find(y);}
};
DSU<N>dsu[LG];
ll n,m,ans;
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>n>>m;
	for(int i=0;i<LG;i++)	dsu[i].clear(n);
	for(int i=1,la,ra,lb,rb;i<=m;i++){
		cin>>la>>ra>>lb>>rb;
		for(int k=LG-1;k>=0;k--){
			if(la+(1<<k)-1<=ra){
				dsu[k].merge(la,lb);
				la+=(1<<k),lb+=(1<<k);
			}
		}
	}
	for(int i=LG-1;i>0;i--){
		for(int j=1;j+(1<<i)-1<=n;j++){
			int p=dsu[i].find(j);
			dsu[i-1].merge(j,p),dsu[i-1].merge(j+(1<<i-1),p+(1<<i-1));
		}
	}
	for(int i=1;i<=n;i++){
		if(dsu[0].find(i)==i)	ans=(!ans?9:(ans*10%mod));
	}
	cout<<ans<<"\n";
}
posted @ 2025-10-28 20:37  -MornStar-  阅读(55)  评论(0)    收藏  举报