[USACO23FEB] Problem Setting P 题解

[USACO23FEB] Problem Setting P

题目说的很绕,意思就是所有验题人都认为题目难度顺序单增

发现 \(m\) 很小,很容易想到状压。把 H 看作 \(\tt1\)E 看作 \(\tt0\),则我们得到 \(m\) 个长度为 \(n\)\(\tt01\) 串,这就是每道题的 “状态”。发现状态相同的题没有本质区别,所以我们对于每个状态都统计有多少个题目,记为 \(c_i\)

而对于单增的限制条件,我们转化为:对于所有相邻的两道题 \(i\)\(i+1\),设 \(s_i\) 为第 \(i\) 题的状态,则一定满足:\(s_i\subseteq s_{i+1}\)

于是设 \(f_i\) 为当前选中最后一道题的状态为 \(i\) 的方案数。那么转移时,我们首先要找到上一道题的状态 \(j\),且这个 \(j\) 一定满足 \(j\subseteq i\)。注意若 \(j=i\),则一定有一种方案,这里需要预先加上 \(1\)。综上,统计:

\[v=1+\sum_{j\subsetneq i}f_j \]

这就是能选的状态集合。至于当前 \(i\) 有多少种排列方式,这也是好计算的。相当于从 \(c_i\) 个中挑选任意个(不为 \(0\))并任意排列,统计公式为:

\[s=\sum_{k=1}^{c_i}(c_i)^{\underline k}=\sum_{k=0}^{c_i-1}\frac{c_i!}{k!} \]

最后的 \(f_i=vs\)

这个方法的复杂度是 \(O(3^m)\) 的,瓶颈在于遍历子集。

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

using ll=long long;
constexpr int MAXN=1e5+5,MAXM=1<<20,MOD=1e9+7;
int n,m,num[MAXN],c[MAXM],f[MAXN],ans;
int fac[MAXN],inv[MAXN];
int g[MAXM][21];
string s;
void add(int&x,int y){
	x=x+y>=MOD?x+y-MOD:x+y;
}
int power(ll a,int b){
	ll res=1;
	for(;b;a=a*a%MOD,b>>=1)if(b&1)res=res*a%MOD;
	return res;
}
void init(){
	fac[0]=1;
	for(int i=1;i<=n;++i) fac[i]=(ll)fac[i-1]*i%MOD;
	inv[n]=power(fac[n],MOD-2);
	for(int i=n-1;~i;--i) inv[i]=(ll)inv[i+1]*(i+1)%MOD; 
}
int calc(int ci){
	int res=0;
	for(int i=0;i<ci;++i)
		add(res,(ll)fac[ci]*inv[i]%MOD);
	return res;
}

int main(){
	ios::sync_with_stdio(0);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>n>>m;
	init();
	for(int i=1;i<=m;++i){
		cin>>s;
		for(int j=0;j<n;++j)
			if(s[j]=='H')
				num[j+1]|=1<<(i-1);
	}
	for(int i=1;i<=n;++i) ++c[num[i]];
	f[0]=calc(c[0]);
	for(int i=1;i<=m;++i) g[0][i]=f[0];
	ans=f[0];
	for(int i=1,B=1<<m,v;i<B;++i){
		v=1;
		for(int j=1;j<=m;++j)
			if(i&(1<<(j-1)))
				add(v,g[i^(1<<(j-1))][j]);
		f[i]=(ll)v*calc(c[i])%MOD;
		add(ans,f[i]);
		g[i][1]=f[i];
		for(int j=1;j<m;++j){
			g[i][j+1]=g[i][j];
			if(i&(1<<(j-1))) add(g[i][j+1],g[i^(1<<(j-1))][j]);
		}
	}
	cout<<ans<<'\n';
	return 0;
}

考虑满分做法。引入辅助数组 \(g_{i,j}\) 表示:找到所有的 \(f_k\) 并求和,这些 \(k\) 满足 \(k\subseteq i\) 且对 \(k\)\(i\) 有不同意见的编号最大的验题人的编号为 \(j-1\)\(j=0\) 时即为没有区别),公式表示下来就是:

\[g_{i,j}=\sum_{\substack{k\subseteq i\\[0.5ex]\max x\in\complement_ik=j-1}}f_k \]

所以原本的 \(O(3^m)\) 枚举子集的转移就被优化为了 \(O(m2^m)\) 转移,因为对于 \(f_i\) 的转移我们只需要枚举有不同意见的编号最大的验题人的编号就行了。

当然对于 \(g_{i,j}\) 的转移也有,首先 \(g_{i,0}=f_i\),然后:

  • \(i\)\(j-1\) 位为 \(\tt0\),则 \(g_{i,j}=g_{i,j-1}\)
  • 否则 \(g_{i,j}=g_{i,j-1}+g_{i\setminus\{\max x\in i\},j-1}\),其中 \(i\setminus\{\max x\in i\}\) 意思是 \(i\) 去掉最大的元素后组成的集合。
#include<bits/stdc++.h>
using namespace std;

using ll=long long;
constexpr int MAXN=1e5+5,MAXM=1<<20,MOD=1e9+7;
int n,m,num[MAXN],c[MAXM],f[MAXN],ans;
int fac[MAXN],inv[MAXN];
int g[MAXM][21];
string s;
void add(int&x,int y){
	x=x+y>=MOD?x+y-MOD:x+y;
}
int power(ll a,int b){
	ll res=1;
	for(;b;a=a*a%MOD,b>>=1)if(b&1)res=res*a%MOD;
	return res;
}
void init(){
	fac[0]=1;
	for(int i=1;i<=n;++i) fac[i]=(ll)fac[i-1]*i%MOD;
	inv[n]=power(fac[n],MOD-2);
	for(int i=n-1;~i;--i) inv[i]=(ll)inv[i+1]*(i+1)%MOD; 
}
int calc(int ci){
	int res=0;
	for(int i=0;i<ci;++i)
		add(res,(ll)fac[ci]*inv[i]%MOD);
	return res;
}

int main(){
	ios::sync_with_stdio(0);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>n>>m;
	init();
	for(int i=1;i<=m;++i){
		cin>>s;
		for(int j=0;j<n;++j)
			if(s[j]=='H')
				num[j+1]|=1<<(i-1);
	}
	for(int i=1;i<=n;++i) ++c[num[i]];
	f[0]=calc(c[0]);
	for(int i=1;i<=m;++i) g[0][i]=f[0];
	ans=f[0];
	for(int i=1,B=1<<m,v;i<B;++i){
		v=1;
		for(int j=1;j<=m;++j)
			if(i&(1<<(j-1)))
				add(v,g[i^(1<<(j-1))][j]);
		f[i]=(ll)v*calc(c[i])%MOD;
		add(ans,f[i]);
		g[i][1]=f[i];
		for(int j=1;j<m;++j){
			g[i][j+1]=g[i][j];
			if(i&(1<<(j-1))) add(g[i][j+1],g[i^(1<<(j-1))][j]);
		}
	}
	cout<<ans<<'\n';
	return 0;
}
posted @ 2024-11-08 21:44  Laoshan_PLUS  阅读(37)  评论(0)    收藏  举报