jzoj5999 【WC2019模拟2019.1.14】选数 (FWT,容斥,平衡规划)

本文深入探讨了数论中的反演公式与快速沃尔什-哈达玛变换(FWT)在组合计数问题上的巧妙结合。通过分析特定倍数的方案数,利用FWT加速计算,解决了在大规模数据集上高效统计特定条件下元素组合数量的问题。文章详细解释了如何处理重复元素的选择,并通过容斥原理修正计算结果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述
在这里插入图片描述

看到gcd,直接求也不好求,就可以先考虑一下反演。然后发现答案就是∑f(u)⋅ϕ\sum f(u)\cdot \phif(u)ϕ,f(u)是gcd是u的倍数的方案数。
有个结论是∑d∣xμ(d)⋅(x/d)=ϕ(x)\sum_{d|x}\mu(d)\cdot(x/d)=\phi(x)dxμ(d)(x/d)=ϕ(x),就是一个容斥,最终只有与x无公共质因子的数会被算到。

首先将所有数映射到值域的一个桶上。
可以发现当计算f(u)时,只有u的倍数位置是有用的。假如这样的位置比较多的时候,可以直接FWT,否则就使用n^2的方法统计。 复杂度有待平衡。(能过)

只分析k=4的容斥,其他类推:
考虑到FWT不好计算无重复选择且无序的方案数,我们换而使用有重复选择且有序的方案数来容斥。 最后再除掉k!即可。
首先对于原序列中四个值为u的倍数的不同位置a b c d,FWT取一个4次方再搞回去会算4!=24次。

但是,很显然对于有两个重复位置的形式多算了,要减去。
如何方便的统计呢? 不妨枚举他重复位置c,然后再枚举一个有序对(a,b)。
对于使用两个c,一个有序对(a,b)的,共有C(4,2)=6种排列满足要求。都减去就可以了。

这里的a,b是可以与c重复的,这样会发现又减多了,最后加回来。
考虑有三个相同的形式(可以发现不会有四个相同的,因为s!=0),枚举相同的,对应四种排列。这四种在第一次里被算了4次,在第二次里被算了2*C(4,2)=12次,因此加回8次即可。

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 1e6 + 10, mo = 998244353;
ll n,k,s;
int cnt[N],mx,a[N],p[N],is[N],phi[N];

int gcd(int a,int b) {
	return b == 0 ? a : gcd(b, a % b);
}
void add(ll &x,ll y){
	x=(x+y)%mo;
}

ll ksm(ll x,ll y) {
	ll ret = 1; for (; y; y>>=1) {
		if (y & 1) ret = ret * x % mo;
		x = x * x % mo;
	}
	return ret;
}

void init() {
	n=50000;
	phi[1] = 1;
	for (int i = 2; i <= n; i++) {
		if (!is[i]) p[++p[0]]=i,phi[i]=i-1;
		for (int j=1;j<=p[0]&&p[j]*i<=n;j++){
			is[p[j]*i]=1;
			if (i%p[j]==0) {
				phi[i*p[j]]=phi[i]*p[j];
				break;
			} else phi[i*p[j]]=phi[i]*(p[j]-1);
		}
	}
}

const int E = 7e4;
ll d[E], r[E], M = 65536,fur[E];
ll ans,buc[E],ee[E];

void fwt(ll *a, int sig) {
	for (int m = 1; m < M; m <<= 1) {
		for (int i = 0; i < M; i+=(m<<1)) {
			for (int j = 0; j < m; j++) {
				ll T = a[i + j + m];
				a[i + j + m] = (a[i + j] - T);
				a[i + j] = (a[i + j] + T);
			}
		}
	}
	if (sig == -1) {
		ll ny = ksm(M, mo - 2);
		for (int i = 0; i < M; i++) a[i] = a[i] % mo * ny % mo;
	} else 
	for (int i = 0; i < M; i++) a[i] = a[i] % mo;
}
const int L = 800;
int main() {
	freopen("choose.in","r",stdin);
	// freopen("choose.out","w",stdout);
	init();
	cin>>n>>k>>s;
	for (int i = 1; i <= n; i++) {
		int x; scanf("%d",&x);a[i]=x;
		cnt[x]++; mx = max(mx, x);
	}
	if (k == 1) {
		printf("%lld\n",cnt[s]*s%mo);
		return 0;
	}
	for (int u = 1; u <= mx; u++) {
		ll c1=0,c2=0,c3=0; //abcd / abcc / abbb
		ll sa=0;
		if (mx / u > L) {
			memset(d,0,sizeof d);
			for (int i = u; i <= mx; i+=u) d[i]=cnt[i];
			fwt(d,1);
			for (int i = 0; i < M; i++) {
				buc[i] = d[i] * d[i] % mo;
				fur[i] = buc[i] * buc[i] % mo;
			}
			fwt(buc, -1);
			fwt(fur, -1);

			// memset(ee,0,sizeof ee);
			// for (int i = u; i <= mx; i+=u) {
			// 	for (int j = u; j <= mx; j+=u) {
			// 		add(ee[i^j], cnt[i] * cnt[j]);
			// 	}
			// }
			// for (int i = 0; i < M; i++) if (ee[i] != buc[i]) {
			// 	printf("WOCAO");
			// }
		} else {
			for (int i = u; i <= mx; i+=u) {
				for (int j = u; j <= mx; j+=u) {
					add(buc[i^j], cnt[i] * cnt[j]);
				}
			}
		}
		if (k == 2) sa = buc[s]; else 
		if (k == 3) {
			int zs=0;
			for (int i=u; i <= mx; i+=u) {
				add(sa, buc[s^i] * cnt[i]);
				zs += cnt[i];
			}
			if (s%u==0) {
				add(sa, -3 * zs * cnt[s] % mo);
				add(sa, 2 * cnt[s] % mo);
			}
		} else {
			if (mx / u > L) {
				c1 = fur[s];
				// ll zz=0;
				// for (int i = u; i <= mx; i+=u) {
				// 	for (int j = u; j <= mx; j+=u) {
				// 		add(zz, buc[s^i^j] * cnt[i] % mo * cnt[j]);
				// 	}
				// }
				// if (zz!=c1) {
				// 	printf("WOCAO2");
				// }
			} else {
				for (int i = u; i <= mx; i+=u) {
					for (int j = u; j <= mx; j+=u) {
						add(c1, buc[s^i^j] * cnt[i] % mo * cnt[j]);
					}
				}
			}

			for (int i = u; i <= mx; i+=u) {
				add(c2, buc[s] * cnt[i]);
			}

			for (int i = u; i <= mx; i+=u) {
				if ((s^i)%u==0) add(c3, cnt[s^i] * cnt[i]);
			}
		}

		if (mx / u <= L) {
			for (int i = u; i <= mx; i+=u) {
				for (int j = u; j <= mx; j+=u) {
					add(buc[i^j], -cnt[i] * cnt[j]);
				}
			}
		} else memset(buc,0,sizeof buc);

		if (k == 4) {
			sa = c1 - c2 * 6 + c3 * 8;
			sa %= mo;
		}
		// if (sa) printf("%d %lld\n",u,sa);
		add(ans,sa*phi[u]);
	}
	if (k>=2)ans=ksm(2,mo-2)*ans%mo;
	if (k>=3)ans=ksm(3,mo-2)*ans%mo;
	if (k>=4)ans=ksm(4,mo-2)*ans%mo;
	cout<<(ans+mo)%mo<<endl;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值