2019 Multi-University Training Contest 1:Sequence(NTT)

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6589
在这里插入图片描述
题目大意:初始有一个长为 n 的序列 a,有三类操作,每一类操作的定义(k为操作类型, 1 &lt; = k &lt; = 3 1 &lt;= k &lt;= 3 1<=k<=3 ):对于所有的 i,在这里插入图片描述,每次操作之后会把 a 数组替换成 b,输入会给出一个长为 m 的操作序列。
最后求 ans = (1 * a[1]) ^ (2 * a[2) ^ (3 * a[3])… (^为异或运算符)

题解:贴一篇讲得比较详细的大佬的题解
https://www.cnblogs.com/xusirui/p/11229450.html

这题一开始连题目都没看懂,完全是跟着题解补的。

观察那个操作,其实就是求间隔 k 的前缀和,令b[i] = a 数组从 1 到 i 间隔 k - 1 的前缀和,然后用b数组替换掉a数组。看不懂这题就没了。

然后有一个操作序列,本来是让你按着这个序列的顺序从左往右依次做这种操作,对最后得到的序列求答案。然后这题有一个性质,就是操作序列的操作顺序并不影响最后的结果,如果没有发现这个性质,这题也没了,官方题解的证明也没看懂,不过可以通过暴力的方式发现这个性质,暴力的话每一次操作都要求前缀和,然后变换序列,复杂度为o(n * m),然后良心样例给的全是一样的操作,所以你写出暴力之后得自己出数据,这个好解决。

有了这个性质之后,既然可以交换操作顺序,那么一样的操作放一起做,所以开一个桶,记录每种操作的次数,一次性把某种操作做完,然后做下一种。

对于某一类操作,举例来说,设 k = 1,操作变成一个间隔为1的最朴素的前缀和,假设这种操作有 c 次,就是对 a 序列做 c 次前缀和。

初始 a 数组: a [ 1 ] , a [ 2 ] , a [ 3 ] , a [ 4 ] . . . a[1],a[2],a[3],a[4]... a[1],a[2],a[3],a[4]...
做一次前缀和: a [ 1 ] , a [ 1 ] + a [ 2 ] , a [ 1 ] + a [ 2 ] + a [ 3 ] , a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] a[1],a[1] + a[2],a[1] + a[2] + a[3],a[1] + a[2] + a[3] + a[4] a[1],a[1]+a[2],a[1]+a[2]+a[3],a[1]+a[2]+a[3]+a[4]
做两次前缀和: a [ 1 ] , 2 ∗ a [ 1 ] + a [ 2 ] , 3 ∗ a [ 1 ] + 2 ∗ a [ 2 ] + a [ 3 ] , 4 ∗ a [ 1 ] + 3 ∗ a [ 2 ] + 2 ∗ a [ 3 ] + a [ 1 ] a[1],2 * a[1] + a[2],3 * a[1] + 2 * a[2] + a[3],4 * a[1] + 3 * a[2] + 2 * a[3] + a[1] a[1],2a[1]+a[2],3a[1]+2a[2]+a[3],4a[1]+3a[2]+2a[3]+a[1]
做三次前缀和: a [ 1 ] , 3 ∗ a [ 1 ] + a [ 2 ] , 6 ∗ a [ 1 ] + 3 ∗ a [ 2 ] + a [ 3 ] , 10 ∗ a [ 1 ] + 6 ∗ a [ 2 ] + 3 ∗ a [ 3 ] + a [ 4 ] a[1],3 * a[1] + a[2],6 * a[1] + 3 * a[2] + a[3],10 * a[1] + 6 * a[2] + 3 * a[3] + a[4] a[1],3a[1]+a[2],6a[1]+3a[2]+a[3],10a[1]+6a[2]+3a[3]+a[4]

观察一下系数,尤其是到第三次前缀和之后可以发现跟组合数有点关系,但一眼不好确定,没关系,画出杨辉三角。

在这里插入图片描述
会发现,系数就是那条斜线,而且是卷积形式,而且第几次前缀和就是第几条斜线。
用一个数组第 c 条斜线弄出来,快速求一下卷积,也就能快速搞出 c 次操作之后的 a 数组,搞三次即可。

对于 k = 2 和 k = 3的情况,需要将数组拆开,然后分开求卷积。标程用了更好的写法:将斜线数组间隔开,例如 C 表示那条斜线,让 C 数组下标每间隔 k - 1 取一个值,然后直接和原数组求卷积。
例如 K = 2:C[0],0,C[1],0,C[2]… ,(C[0],C[1],C[2] 表示斜线的值)
k = 3:C[0],0,0,C[1],0,0,C[2],0,0,C[3]

由于要取模,FFT是行不通了,不得不采用NTT(快速数论变换)。听起来好像需要学新东西,实际上NTT只是用来求取模的卷积,卷积形式没有变。

这里再讲一下NTT,NTT和FFT过程完全一样。FFT用的是 n次单位根 + 迭代蝴蝶操作将系数表达式转成点值表达式。NTT用的是原根,所谓原根: g i m o d &ThinSpace;&ThinSpace; p g^i \mod p gimodp g ∈ [ 2 , p − 1 ] g \in [2,p - 1] g[2,p1] 对于 i ∈ [ 0 , p − 1 ] i \in [0,p - 1] i[0,p1],取模结果两两不相同,那么称 g 是 p的原根。关于原根的性质,可以百度查资料,还有一种定义是 对于 g i m o d &ThinSpace;&ThinSpace; p = 1 g^i \mod p = 1 gimodp=1 成立的 最小的 i (称为 g mod p 的阶),若 i = ϕ ( p ) \phi(p) ϕ(p),那么 g 是 p的原根,稍加思考就能发现这两种定义是一样。
NTT就用 g p − 1 n m o d &ThinSpace;&ThinSpace; p g^{\frac{p - 1}{n}}\mod p gnp1modp,代替FFT中的 e 2 ∏ i n e^{\frac{2\prod i}{n}} en2i,原根具有和单位复数根相似的性质,它们的蝴蝶操作是一样的,逆变换也是一样的,因为 n 必须处理成 2 的幂,n 必须是 p -1 的因子,所以 p 必须是费马素数,费马素数是形如: c ∗ 2 k + 1 c * 2 ^ k + 1 c2k+1的素数,998244353就是一个费马素数,且它的原根为3。

除此之外求卷积方式完全一样,直接套模板即可。

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 10;
const int mod = 998244353;		//原根是3 
typedef long long ll;
int t,n,m,x;
ll a[maxn],c[maxn],d[maxn],b[maxn],tmp[maxn],num[5];
ll fact[maxn],ifact[maxn];
ll fpow(ll a,ll b) {
	ll r = 1;
	while(b) {
		if(b & 1) r = r * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return r % mod;
}
void change(ll t[],int len) {
	for(int i = 1, j = len / 2; i < len - 1; i++) {
		if(i < j) swap(t[i],t[j]);
		int k = len / 2;
		while(j >= k) {
			j -= k;
			k /= 2;
		}
		if(j < k) j += k;
	}
}
void NTT(ll t[],int len,int type) {
	change(t,len);
	for(int s = 2; s <= len; s <<= 1) {
		ll wn = fpow(3,(mod - 1) / s);
		if(type == -1) wn = fpow(wn,mod - 2);
		for(int j = 0; j < len; j += s) {
			ll w = 1;
			for(int k = 0; k < s / 2; k++) {
				ll u = t[j + k],v = t[j + k + s / 2] * w % mod;
				t[j + k] = (u + v) % mod;
				t[j + k + s / 2] = (u - v + mod) % mod;
				w = w * wn % mod;
			}
		}
	} 
	if(type == -1) {
		ll inv = fpow(len,mod - 2);
		for(int i = 0; i < len; i++) 
			t[i] = t[i] * inv % mod;
	}
}
void solve(ll x[],ll y[],int n) {
	int len = 1;
	while(len <= 2 * n) len <<= 1;
	for(int i = n; i < len; i++)
		x[i] = y[i] = 0;
	NTT(x,len,1);
	NTT(y,len,1);
	for(int i = 0; i < len; i++)
		tmp[i] = x[i] * y[i] % mod;
	NTT(tmp,len,-1);
	for(int i = 0; i < n; i++) a[i] = tmp[i];
}
ll C(int n,int m) {
	return m<=n? fact[n]*ifact[m]%mod*ifact[n-m]%mod:0;
}
int main() {
	fact[0]=1;
	for(int i=1;i<=maxn-1;++i)
		fact[i]=(long long)fact[i-1]*i%mod;
	ifact[maxn-1]=fpow(fact[maxn-1],mod-2);
	for(int i=maxn-2;i>=0;--i)
		ifact[i]=(long long)ifact[i+1]*(i+1)%mod;	
	scanf("%d",&t);
	while(t--) {
		memset(num,0,sizeof num);
		scanf("%d%d",&n,&m);
		for(int i = 0; i < n; i++)
			scanf("%lld",&a[i]);
		for(int i = 1; i <= m; i++) {
			scanf("%d",&x);
			num[x]++;
		}
		for(int j = 1; j <= 3; j++) {
			if(!num[j]) continue;
			for(int k = 0; k < n; k++) d[k] = 0;
			for(int k = 0; k * j < n; k++)
				d[k * j] = C(num[j] + k - 1,k);
			for(int k = 0; k < n; k++) b[k] = a[k];
			solve(d,b,n);
		}
		ll res = 0;
		for(int i = 0; i < n; i++)
			res = res ^ ((i + 1) * a[i]);
		printf("%lld\n",res);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值