CodeForces 1400G - Mercenaries

洛谷爬 & CF 题目页面传送门

给定 \(n\) 个区间 \([l_i,r_i]\),和 \(m\) 条形如 \((a_i,b_i)\) 的限制。求能选出多少个这 \(n\) 个区间组成的(可重)集合的非空子集 \(S\),使得 \(\forall [l,r]\in S,|S|\in[l,r]\),且任意一条限制中的两个区间不得同时出现在 \(S\) 中。答案对 \(p=998244353\) 取模。

\(n\in\left[1,3\times10^5\right],m\in[0,20]\)\(\mathrm{TL}=7\mathrm s\)

先说一个通过阅读 tzc 代码得到的方法,这应该是正解。(tzc yyds)

注意到,对于任意一条限制,若要满足它,有 \(3\) 种情况;而要不满足,只有 \(1\) 种情况(都选)。于是反面考虑,算出有多少种情况是不满足至少一条限制的,然后用总情况数减去它。

熟悉容斥的同学应该很容易想到用容斥解决(然鹅我不熟悉,所以现场没想出来,这大概是我第二次写容斥吧)。

先放出容斥原理的公式:设 \(S\) 是一个由集合组成的集合,则

\[\left|\bigcup_{A\in S}A\right|=\sum_{S'\subseteq S}(-1)^{|S'|+1}\left|\bigcap_{A\in S'}A\right| \]

在本题中,我们要算的是「不满足至少一条限制的情况集合的大小」,可以表示为「分别不满足第 \(1,2,\cdots m\) 条限制的情况集合的并的大小」,而一些情况集合的交就是不满足所有这些情况集合所对应的限制的情况集合,然后直接套公式枚举 \(2^m\) 个子集即可。

考虑如何算出不满足若干条限制的情况数。显然,与这些限制有关的所有集合都必须要被选,设有 \(cnt\) 个。考虑枚举最终选的区间数量 \(i\)。由于这 \(cnt\) 个区间是无论如何都要选的,所以 \(i\) 要属于这 \(cnt\) 个区间的交,设为 \([L,R]\)\(cnt=0\)\(L=1,R=n\))。然后对于每个 \(i\),考虑除了这 \(cnt\) 个的区间,只有那些包括 \(i\) 的区间可以被选,设为 \(x_i-cnt\)(这个 \(x\) 数组可以预处理的时候差分算出来),要选 \(i-cnt\) 个,那么一共有 \(\dbinom{x_i-cnt}{i-cnt}\)。于是总情况数就是 \(\sum\limits_{i=L}^{R}\dbinom{x_i-cnt}{i-cnt}\)。这个暴力算肯定会炸,注意到 \(cnt\in[0,2m]\),范围很小,于是只需要对于每个 \(cnt\) 都预处理个前缀和即可。

至于怎么算总情况数?哈哈哈这也太简单了吧。显然是 \(\sum\limits_{i=1}^n\dbinom{x_i}i\)

时间复杂度 \(\mathrm O(nm+2^mm)\)(只计核心操作,像那些类似预处理阶乘和阶乘逆元的操作就不管了,反正也不会 T)。

代码特短:

#include<bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f,mod=998244353;
int qpow(int x,int y){
	int res=1;
	while(y){
		if(y&1)res=1ll*res*x%mod;
		x=1ll*x*x%mod;
		y>>=1;
	}
	return res;
}
int inv(int x){return qpow(x,mod-2);}
const int N=300000,M=20;
int n,m;
int l[N+1],r[N+1];
int a[M+1],b[M+1];
int d[N+2];
bool hav[N+1];
int Sum[2*M+1][N+1];
int fact[N+1],factinv[N+1];
int comb(int x,int y){
	if(x<0||y<0||x<y)return 0;
	return 1ll*fact[x]*factinv[y]%mod*factinv[x-y]%mod;
}
int sum(int x,int l,int r){
	if(l>r)return 0;
	return (Sum[x][r]-Sum[x][l-1])%mod;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)scanf("%d%d",l+i,r+i),d[l[i]]++,d[r[i]+1]--;
	for(int i=1;i<=n;i++)d[i]+=d[i-1];
	for(int i=1;i<=m;i++)cin>>a[i]>>b[i];
	fact[0]=factinv[0]=1;
	for(int i=1;i<=n;i++)factinv[i]=inv(fact[i]=1ll*fact[i-1]*i%mod);
	for(int i=0;i<=2*m;i++)for(int j=1;j<=n;j++)Sum[i][j]=(Sum[i][j-1]+comb(d[j]-i,j-i))%mod;
	int ans=0;
	for(int i=1;i<1<<m;i++){
		int L=1,R=n,cnt=0;
		for(int j=1;j<=m;j++)if(i&1<<j-1){
			if(!hav[a[j]])hav[a[j]]=true,cnt++,L=max(L,l[a[j]]),R=min(R,r[a[j]]);
			if(!hav[b[j]])hav[b[j]]=true,cnt++,L=max(L,l[b[j]]),R=min(R,r[b[j]]);
		}
		(ans+=(__builtin_popcount(i)&1?1:-1)*sum(cnt,L,R))%=mod;
		for(int j=1;j<=m;j++)hav[a[j]]=hav[b[j]]=false;
	}
	ans*=-1;
//	cout<<ans<<"\n";
	for(int i=1;i<=n;i++)(ans+=comb(d[i],i))%=mod;
	cout<<(ans+mod)%mod;
	return 0;
}

接下来是我现场想的野鸡方法。看了一下 neal 的非官方题解,好像就是我这个方法?

我没有反面考虑,直接枚举选的区间数量,对于每个数量的答案就是枚举跟限制有关的区间中选的数量,剩下的组合数一通算,然后加起来,最后再加起来即可。难点在于如何算「对于每个跟限制有关的区间中选的数量,有多少种选法」。

一个比较 naive 的想法是,暴力枚举 \(2^{2m}\) 种选法,然后判断。显然会爆。注意到,若在不能同时选的区间对之间连边,那么不同连通分量的情况是独立的,可以每个连通分量算完之后,一通类似背包的 DP 随便合并。对于连通图,有一个性质:\(|E|\geq|V|-1\)(这个性质在 NOI Day2T1 的最终结论证明里也用到了),很容易证明这样枚举量只有 \(\mathrm O(2^m)\)。对于每个枚举到的选法,还要花 \(\mathrm O(m)\) 的时间判断是否合法,于是一次算「对于每个跟限制有关的区间中选的数量,有多少种选法」是 \(\mathrm O(2^mm)\) 的。

但是这样一共要算 \(n\) 次,总时间复杂度还是会爆炸。又注意到一个性质:在所有选的区间数量中,跟限制有关的可选区间的本质不同集合数量是 \(\mathrm O(m)\) 的。证明很简单,每个跟限制有关的区间只增加 \(2\) 个断点。于是总时间复杂度是辣鸡的 \(\mathrm O\!\left(2^mm^2\right)\),但由于 TL 超大,可以通过。

下面是在现场没 rush 完的超丑代码上魔改的超丑代码:

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
const int mod=998244353;
int qpow(int x,int y){
	int res=1;
	while(y){
		if(y&1)res=1ll*res*x%mod;
		x=1ll*x*x%mod;
		y>>=1;
	}
	return res;
}
int inv(int x){return qpow(x,mod-2);}
const int N=300000,M=20;
int n,m;
int l[N+1],r[N+1];
int a[M+1],b[M+1];
int a1[M+1],a2[M+1],b1[M+1],b2[M+1];
vector<int> nei[N+1];
vector<int> key;
bool iskey[N+1];
int d[N+1];
vector<int> add[N+1],del[N+2];
bool in[N+1];
vector<vector<int> > ccs;
vector<int> cc;
bool vis[N+1];
void dfs(int x){
	vis[x]=true;
	cc.pb(x);
	for(int i=0;i<nei[x].size();i++){
		int y=nei[x][i];
		if(!vis[y])dfs(y);
	}
}
int hav[100][100];
int dp[100];
int fact[N+1],factinv[N+1];
int comb(int x,int y){return x<y?0:1ll*fact[x]*factinv[y]%mod*factinv[x-y]%mod;}
int main(){
	cin>>n>>m;
	fact[0]=factinv[0]=1;
	for(int i=1;i<=n;i++)factinv[i]=inv(fact[i]=1ll*fact[i-1]*i%mod);
	for(int i=1;i<=n;i++)cin>>l[i]>>r[i],add[l[i]].pb(i),del[r[i]+1].pb(i);
	for(int i=1;i<=m;i++)cin>>a[i]>>b[i],key.pb(a[i]),key.pb(b[i]),iskey[a[i]]=iskey[b[i]]=true,nei[a[i]].pb(b[i]),nei[b[i]].pb(a[i]);
	sort(key.begin(),key.end());
	key.resize(unique(key.begin(),key.end())-key.begin());
	for(int i=0;i<key.size();i++)if(!vis[key[i]])cc.clear(),dfs(key[i]),ccs.pb(cc);
	for(int i=1;i<=m;i++)for(int j=0;j<ccs.size();j++)for(int k=0;k<ccs[j].size();k++){
		if(ccs[j][k]==a[i])a1[i]=j,a2[i]=k;
		if(ccs[j][k]==b[i])b1[i]=j,b2[i]=k;
	}
	int cnt=0,ans=0;
	dp[0]=1;
	for(int i=1;i<=n;i++){
//		cout<<"now "<<i<<"\n";
		bool ok=false;
		for(int j=0;j<add[i].size();j++)in[add[i][j]]=true,cnt+=!iskey[add[i][j]],ok|=iskey[add[i][j]];
		for(int j=0;j<del[i].size();j++)in[del[i][j]]=false,cnt-=!iskey[del[i][j]],ok|=iskey[del[i][j]];
		if(ok){
			memset(hav,0,sizeof(hav));
			for(int j=0;j<ccs.size();j++){
				vector<int> &v=ccs[j];	
				for(int k=0;k<1<<v.size();k++){
					bool flg=true;
					for(int o=0;o<v.size();o++)if(!in[v[o]]&&k&1<<o)flg=false;
					for(int o=1;o<=m;o++)if(a1[o]==j&&b1[o]==j&&k&1<<a2[o]&&k&1<<b2[o])flg=false;
					hav[j][__builtin_popcount(k)]+=flg;
				}
			}
			memset(dp,0,sizeof(dp));
			dp[0]=1;
			for(int j=0;j<ccs.size();j++)for(int k=50;k>=0;k--)for(int o=1;o<=50;o++)
				if(k>=o)(dp[k]+=1ll*dp[k-o]*hav[j][o]%mod)%=mod;
		}
		for(int j=0;j<=50;j++)if(i>=j)(ans+=1ll*dp[j]*comb(cnt,i-j)%mod)%=mod;
	}
	cout<<ans;
	return 0;
}
posted @ 2020-08-26 15:10  ycx060617  阅读(183)  评论(2编辑  收藏  举报