BNUOJ51279 组队活动(cdq分治&&NTT)

6 篇文章 0 订阅
3 篇文章 0 订阅

题意:N个人进行组队,每个队不超过M人,求方案数模998244353。

这个递推方程我都没想到。。枚举当前这个人所在队伍剩余人数j,则f[i]=Σf[i-1-j]*C(i-1, j),把组合数展开后发现是个卷积的形式,但是不能直接NTT,因为之前的f值没有求出来。套用cash一题的策略使用cdq分治解决,先递归左边,再用左边的更新右边,再递归右边。注意NTT需要补成2的次幂,常数有点大,但只要保证每次多项式的长度不超过2*(R-L+1)就不会影响时间复杂度。

这样看来NTT和cdq分治挺容易结合的。。只有出题人稍微构造一下递推式就可以造一道毒瘤题。。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#define rep(i,a,b) for(int i=a;i<=b;++i)
#define erp(i,a,b) for(int i=a;i>=b;--i)
#define LL long long
using namespace std;
const int MAXN = 100010, MAXL = 270000;
const int mo = 998244353, g = 3, T = 262144;
inline void add(int&a, int b) { a+=b; if(a>=mo) a-=mo; }

LL ksm(LL a, LL b)
{
	LL r = 1;
	for (; b>0; b>>=1, a=a*a%mo)
		if (b&1) r = r*a%mo;
	return r;
}

int jc[MAXL], rjc[MAXL], inv[MAXL];
void makejc()
{
	jc[0] = rjc[0] = inv[0] = 1;
	jc[1] = rjc[1] = inv[1] = 1;
	rep(i, 2, T)
	{
		jc[i] = 1ll * i * jc[i-1] % mo;
		inv[i] = 1ll * (mo-mo/i) * inv[mo%i] % mo;
		rjc[i] = 1ll * rjc[i-1] * inv[i] % mo;
	}
}

namespace NTT
{
	int r[MAXL], L;
	int a[MAXL], b[MAXL], wn[MAXL], invwn[MAXL], invn;
	void init()
	{
		LL r = ksm(g, (mo-1)/T), invr = ksm(r, mo-2);
		wn[0] = invwn[0] = 1;
		rep(i, 1, T-1)
		{
			wn[i] = 1ll*wn[i-1]*r%mo;
			invwn[i] = 1ll*invwn[i-1]*invr%mo;
		}
	}
	void ntt(int*a, int flag, int N)
	{
		int *w = (flag==1?wn:invwn);
		rep(i, 0, N-1) if (i<r[i]) swap(a[i], a[r[i]]);
		for (int i = 1; i<N; i<<=1)
			for (int j = 0; j<N; j+=i<<1)
				for (int k = 0; k<i; ++k)
				{
					int x = a[j+k], y = 1ll*w[T/(i<<1)*k]*a[j+k+i]%mo;
					a[j+k] = (x+y) % mo;
					a[j+k+i] = (x+mo-y) % mo;
				}
		if (flag<0)
		{
			invn = ksm(N, mo-2);
			rep(i, 0, N-1) a[i] = 1ll*a[i]*invn%mo;
		}
	}
	void mul(int*pol1, int*pol2, int N, int M)
	{
		rep(i, 0, N) a[i] = pol1[i];
		rep(i, 0, M) b[i] = pol2[i];
		int tn = N+1, tm = M+1; r[0] = 0;
		for (M+=N, L=0, N=1; N<=M; N<<=1) ++L;
		rep(i, tn, N) a[i] = 0;
		rep(i, tm, N) b[i] = 0;
		rep(i, 0, N) r[i] = (r[i>>1]>>1)|((i&1)<<(L-1));
		ntt(a, 1, N); ntt(b, 1, N);
		rep(i, 0, N) a[i] = 1ll*a[i]*b[i]%mo;
		ntt(a, -1, N);
		rep(i, 0, M) pol1[i] = a[i];
	}
}

int p1[MAXL], p2[MAXL], f[MAXN];
int cas, n, m;
void cdq(int L, int R)
{
	if (L==R) return;
	int mid = (L+R)>>1, len = R - L + 1;
	cdq(L, mid); //f[L...mid] is clear
	rep(i, 0, mid-L) p1[i] = 1ll * f[i+L] * rjc[i+L] % mo;
	rep(i, mid-L+1, len*2) p1[i] = 0;
	rep(i, 0, len) p2[i] = i<m ? rjc[i] : 0;
	rep(i, len+1, len*2) p2[i] = 0;
	NTT::mul(p1, p2, len, len);
	rep(i, mid+1, R) add(f[i], 1ll*p1[i-L-1]*jc[i-1]%mo);
	cdq(mid+1, R);
}

int main()
{
	NTT::init();
	makejc();
	scanf("%d", &cas);
	while (cas--)
	{
		scanf("%d%d", &n, &m);
		memset(f, 0, sizeof f);
		f[0] = 1;
		cdq(0, n);
		cout << f[n] << '\n';
	}
	return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值