6724. 【2020.06.15省选模拟】T1 s1mple

题目


正解

首先这题的 B B B矩阵可以看成邻接矩阵,于是 a a a的意义相当于选择若干条链,这些链之间首尾不相接。
按照套路,首先将链首尾不相接的限制去掉,只需要满足这些链内部是互相连接的。简单容斥一下,把“恰好”变成“至少”,计算完之后再反演回去。
可以观察到去掉这个限制之后,若干条链的排列顺序和方案数是无关的。于是状态数缩减为 n n n的划分数, 17 17 17的划分数为 297 297 297

接下来枚举每种划分,并且计算它们的贡献。
设划分中第 i i i段长度为 p i p_i pi,默认 p i p_i pi从大到小排列。
f ( S ) f(S) f(S)为选了集合 S S S的点连出一条链的方案数。
于是它的贡献为 ∑ ∣ S i ∣ = p i , S 1.. n 之 间 无 交 集 ∏ f ( S i ) \sum_{|S_i|=p_i,S{1..n}之间无交集} \prod f(S_i) Si=pi,S1..nf(Si)
感觉这个东西不是很好求,要求 S 1.. n S_{1..n} S1..n之间无交集的条件好像不好搞。
但是可以发现,由于满足了 ∣ S i ∣ = p i |S_i|=p_i Si=pi,所以如果有交集,那么它们的并集就不是全集。
我们只需要求或卷积之后全集的方案数。

整理一下,设 F k ( S ) = ∑ ∣ S ∣ = k f ( S ) F_k(S)=\sum_{|S|=k}f(S) Fk(S)=S=kf(S)
我们需要算 ( F p 1 ∗ F p 2 ∗ . . . ∗ F p k ) ( U ) (F_{p_1}*F_{p_2}*...*F_{p_k})(U) (Fp1Fp2...Fpk)(U),这里的 ∗ * 表示或卷积, U U U表示全集。
先预处理出 f ( S ) f(S) f(S),然后得到 F k ( S ) F_k(S) Fk(S),对所有的 F k F_k Fk F W T FWT FWT
接下来枚举划分,对于划分中的一段长度 p i p_i pi,用当前的序列按位乘 F p i F_{p_i} Fpi
搞完之后计算 U U U的方案数,由于我们只需要计算 U U U,所以没有必要 I F W T IFWT IFWT,直接子集反演将 U U U单个位置的方案数算出来即可。
这一部分的时间复杂度 O ( 297 ∗ 2 n ∗ ? ) O(297*2^n*?) O(2972n?) ? ? ?是某个玄学有关 n n n的函数(还是常数?),反正非常小……(产生自枚举划分的过程中,边枚举边按位乘)

接下来将划分还原成 a a a
一种朴素的想法是直接枚举划分的全排列,但是发现这样是阶乘复杂度,显然不能过(而且这样做的时候,前面算出来的方案数还要除以一个东西。具体来说就是相同长度的个数的阶乘的乘积)。
比较正确的想法是将划分中长度相同的压在一起考虑,不用考虑它们之间的相对顺序。这是因为在前面计算的过程中已经会把相对顺序的贡献计算进去(就是上文说的除以的那个东西)。
这样时间复杂度是对的,因为每个不同的 a a a只会被枚举到一次。总时间复杂度约 O ( 2 n − 1 ) O(2^{n-1}) O(2n1)
枚举到 a a a之后,直接将它的贡献加入一个数组的对应位置。
全部搞完之后,最后在做一遍与卷积的 I F W T IFWT IFWT(注意这个和前面的或卷积 F W T FWT FWT没有关系),即可以将“至少”反演回“恰好”,得到真正的答案。
询问时 O ( 1 ) O(1) O(1)的,因为前面已经求出了所有 a a a的答案。


代码

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 17
#define ll long long
int n;
char str[N+3];
int b[N][N];
ll dp[1<<N][N];
int cnt[1<<N];
ll F[N+1][1<<N];
void fwt_or(ll f[]){
	for (int i=1;i<1<<n;i<<=1)
		for (int j=0;j<1<<n;j+=i<<1)
			for (int k=j;k<j+i;++k)
				f[k+i]+=f[k];
}
void ifwt_and(ll f[]){
	for (int i=1;i<1<<n-1;i<<=1)
		for (int j=0;j<1<<n-1;j+=i<<1)
			for (int k=j;k<j+i;++k)
				f[k]-=f[k+i];
}
ll G[N+1][1<<N];
ll H[1<<N];
int p[N],q[N];
bool used[N];
void dfs2(int k,int n,int m,int s,int t,ll v){
	if (k==n){
		H[s]+=v;
		return;
	}
	for (int i=0;i<m;++i)
		if (q[i]){
			q[i]--;
			dfs2(k+1,n,m,s|(1<<p[i]-1)-1<<t,t+p[i],v);
			q[i]++;
		}
}
void dfs(int k,int m,int s,int x){
	if (s==0){
		ll sum=0;
		for (int i=0;i<1<<n;++i)
			sum+=(n-cnt[i]&1?-1:1)*G[k][i];
		dfs2(0,k,m,0,0,sum);
		return;
	}
	for (;x>=1;--x){
		for (int i=0;i<1<<n;++i)
			G[k+1][i]=G[k][i]*F[x][i];
		if (m && p[m-1]==x){
			q[m-1]++;
			dfs(k+1,m,s-x,min(x,s-x));
			q[m-1]--;
		}
		else{
			p[m]=x,q[m]=1;
			dfs(k+1,m+1,s-x,min(x,s-x));
		}
	}
}
int main(){
	freopen("s1mple.in","r",stdin);
	freopen("s1mple.out","w",stdout);
	scanf("%d",&n);
	for (int i=0;i<n;++i){
		scanf("%s",str);
		for (int j=0;j<n;++j)
			b[i][j]=str[j]-'0';
	}
	for (int i=0;i<n;++i)
		dp[1<<i][i]=1;
	for (int i=1;i<1<<n;++i)
		for (int j=0;j<n;++j)
			if (i>>j&1)
				for (int k=0;k<n;++k)
					if (!(i>>k&1) && b[j][k])
						dp[i|1<<k][k]+=dp[i][j];
	cnt[0]=0;
	F[0][0]=1;
	for (int i=1;i<1<<n;++i){
		cnt[i]=cnt[i>>1]+(i&1);
		ll sum=0;
		for (int j=0;j<n;++j)
			if (i>>j&1)
				sum+=dp[i][j];
		F[cnt[i]][i]+=sum;
	}
	for (int i=0;i<=n;++i)
		fwt_or(F[i]);
	for (int i=0;i<1<<n;++i)
		G[0][i]=1;
	dfs(0,0,n,n);
	ifwt_and(H);
	int Q;
	scanf("%d",&Q);
	while (Q--){
		scanf("%s",str);
		ll s=0;
		for (int i=0;i<n-1;++i)	
			s|=str[i]-'0'<<i;
		printf("%lld\n",H[s]);
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值