[省选前集训2021] 模拟赛2

总结

一直在想第一题,因为看到第三题是 \(\tt polya\) 根本不会,\(T1\) 想了好多个 \(dp\) 做法但都是错的,最后发现是个套路 \(dp\)\(...\)

怎么说呢,还是没有 \(\tt bfs\) 策略所以只拿了 \(15\) 分,下次不管心态多炸都认真打暴力吧。

礼物

题目描述

有一个长度为 \(n\) 的手环,需要选 \(m\) 个位置变成金色的,但是变成金色的最长连续段不能超过 \(k\),如果两个手环能通过旋转变成一模一样的那么就算相同,问有多少个不同的手环。输出模 \(998244353\) 的结果。

\[T\leq 5,1\leq n\leq 10^6,0\leq k\leq m\leq n \]

解法

这个直接上 \(\tt polya\) 啊,那个结论是答案等于所有置换的不动点个数的算术平均数,设 \(f(n,m)\) 表示 \(n\) 个位置里面放 \(m\) 个金色位置的方案数,那么枚举置换 \(k\),考虑前 \(d=\gcd(n,k)\) 放的情况:

\[\frac{1}{n}\sum_{k=1}^nf(d,\frac{m}{\frac{n}{d}})\cdot[\frac{n}{d}|m] \]

上面的柿子有亿点点不好看,把 \(d\) 换成 \(\frac{n}{d}\)

\[\frac{1}{n}\sum_{k=1}^nf(\frac{n}{d},\frac{m}{d})\cdot[d|m] \]

然后按套路把后面的 \([d|m]\) 反演掉:

\[\frac{1}{n}\sum_{d}f(\frac{n}{d},\frac{m}{d})\sum_{k=1}^n[(n,k)=\frac{n}{d}] \]

\[\frac{1}{n}\sum_{d|\gcd(n,m)}f(\frac{n}{d},\frac{m}{d})\cdot \varphi(d) \]

现在问题变成了求 \(f(n,m)\),首先可以写出关于有多少个金色的生成函数,一开始有 \(n-m\) 个不是金色的,可以把 \(m\) 个金色的插入到这 \(n-m-1\) 个空隙中,或者是放在首尾的空隙中(如果有 \(i\) 个金色就有 \(i+1\) 中放法):

\[(\sum_{i=0}^kx^i)^{n-m-1}(\sum_{i=0}^k(i+1)x^i) \]

优化的方式就是写成闭形式然后相乘再展开,先把后面的东西写成闭形式:

\[G(x)=\sum_{i=0}^k(i+1)x^i \]

\[xG(x)+\sum_{i=0}^{k} x^i=G(x)+(k+1)x^{k+1} \]

\[G(x)=\frac{1+(k+1)x^{k+2}-(k+2)x^{k+1}}{(1-x)^2} \]

前面的东西很容易写成闭形式:

\[\frac{(1-x^{k+1})^{n-m-1}}{(1-x)^{n-m-1}} \]

把两个式子乘出来:

\[\frac{(1-x^{k+1})^{n-m-1}}{(1-x)^{n-m+1}}(1+(k+1)x^{k+2}-(k+2)x^{k+1}) \]

分子可以直接二项式展开:

\[\sum_{i=0}^{n-m-1}{n-m-1\choose i}(-x^{k+1})^i \]

分母也可以展开,据说是用什么广义二项式定理,但是不用那么复杂度,直接把它当成 \(\sum_{i=0}^\infty x^i\) 的闭形式展开就可以了,用组合意义是很好算的,也就是选 \(n-m+1\) 个非负数使得和为 \(i\),直接插板法:

\[\sum_{i=0}^\infty{n-m+i\choose i}x^i \]

因为我们只需要算 \([x^m]F(x)\),所以计算的时候枚举分子的指数,再讨论一下搭配三项式中的哪一个,然后再分母里面对应拿一个即可。单次时间复杂度就是 \(O(\frac{m}{k+1})\),总时间复杂度 \(O(\frac{\sigma_1(\gcd(n,m))}{k+1})\),由于约数和大约是 \(O(n\log\log n)\) 的,所以时间接近线性。

我能说我主要是不会 \(\tt polya\) 么?

#include <cstdio>
const int M = 1000005;
const int MOD = 998244353;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int T,n,m,k,cnt,p[M],phi[M],inv[M],fac[M];
void init(int n)
{
	phi[1]=1;
	for(int i=2;i<=n;i++)
	{
		if(!phi[i])
		{
			phi[i]=i-1;
			p[++cnt]=i;
		}
		for(int j=1;j<=cnt && i*p[j]<=n;j++)
		{
			if(i%p[j]==0)
			{
				phi[i*p[j]]=phi[i]*p[j];
				break;
			}
			phi[i*p[j]]=phi[i]*(p[j]-1);
		}
	}
	fac[0]=inv[0]=inv[1]=1;
	for(int i=2;i<=n;i++) inv[i]=(MOD-MOD/i)*inv[MOD%i]%MOD;
	for(int i=2;i<=n;i++) inv[i]=inv[i]*inv[i-1]%MOD;
	for(int i=1;i<=n;i++) fac[i]=i*fac[i-1]%MOD;
}
int C(int n,int m)
{
	if(n<m || n<0 || m<0) return 0;
	return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
int F(int n,int m)
{
	int res=0;
	for(int i=0;i<n-m;i++)
	{
		int fl=((i&1)?-1:1)*C(n-m-1,i);//系数
		//当前的指数是i(k+1)
		if(i*(k+1)<=m)
		{
			int t=m-i*(k+1);
			res=(res+fl*C(n-m+t,t))%MOD;
		}
		if(i*(k+1)+k+2<=m)
		{
			int t=m-i*(k+1)-k-2;
			res=(res+fl*(k+1)%MOD*C(n-m+t,t))%MOD; 
		}
		if(i*(k+1)+k+1<=m)
		{
			int t=m-i*(k+1)-k-1;
			res=(res-fl*(k+2)%MOD*C(n-m+t,t))%MOD; 
		}
	}
	return (res+MOD)%MOD;
}
int gcd(int a,int b)
{
	return !b?a:gcd(b,a%b);
}
signed main()
{
	freopen("gift.in","r",stdin);
	freopen("gift.out","w",stdout);
	T=read();
	init(1e6);
	while(T--)
	{
		n=read();m=read();k=read();
		int d=gcd(n,m),ans=0;
		for(int i=1;i<=d;i++)
			if(d%i==0)
				ans=(ans+F(n/i,m/i)*phi[i])%MOD;
		ans=ans*inv[n]%MOD*fac[n-1]%MOD;
		printf("%lld\n",ans);
	}
}

还有一个弱化版,就是少了 \(m\) 的限制,这个时候直接 \(dp\) 就行了。

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 100005;
const int MOD = 1e9+7;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,k,ans,cnt,dp[M],p[M],phi[M];
void init(int n)
{
	//下面是预处理phi
	phi[1]=1;
	for(int i=2;i<=n;i++)
	{
		if(!phi[i])
		{
			phi[i]=i-1;
			p[++cnt]=i;
		}
		for(int j=1;j<=cnt && i*p[j]<=n;j++)
		{
			if(i%p[j]==0)
			{
				phi[i*p[j]]=phi[i]*p[j];
				break;
			}
			phi[i*p[j]]=phi[i]*phi[p[j]];
		} 
	}
	//下面是预处理递推 
	dp[0]=1;
	for(int i=1,sum=1;i<=n;i++)
	{
		if(i-k-2>=0) sum=(sum-dp[i-k-2]+MOD)%MOD;
		dp[i]=sum;
		sum=(sum+dp[i])%MOD;
	}
}
int f(int d)
{
	int res=0;
	for(int y=0;y<=min(k,d-1);y++)
		res=(res+(y+1)*dp[d-y-1])%MOD;
	return res;
}
int qkpow(int a,int b)
{
	int r=1;
	while(b>0)
	{
		if(b&1) r=r*a%MOD;
		a=a*a%MOD;
		b>>=1; 
	}
	return r;
}
signed main()
{
	freopen("girls.in","r",stdin);
	freopen("girls.out","w",stdout);
	n=read();k=read();
	init(1e5);
	for(int i=1;i*i<=n;i++)
		if(n%i==0)
		{
			ans=(ans+f(i)*phi[n/i])%MOD;
			if(i*i!=n) ans=(ans+f(n/i)*phi[i])%MOD;
		}
	printf("%lld\n",ans*qkpow(n,MOD-2)%MOD);
}

染色问题

题目描述

有一个长度为 \(n\) 的序列,有 \(m\) 种颜色编号为 \([1,m]\),按编号从小到大染色,后染的颜色会把先染的颜色给覆盖掉,每种颜色必须染一段连续非空区间,问最后形成的颜色序列方案数,模 \(998244353\)

\(n,m\leq 10^6\)

解法

这种题目有一种惯用的思考方法:不考虑中间过程是怎么染色的,先考虑最后会形成怎样的局面

如果一个局面是不合法的,设编号 \(i\) 的最早出现位置是 \(st_i\),最晚出现位置是 \(ed_i\),那么有 \(i>j\)\([st_j,ed_j]\)\([st_i,ed_j]\) 包含,也就是说不能出现编号大包小的情况,而且就是第 \(m\) 种颜色是必须出现的,其他颜色不是一定要出现因为可以往 \(m\) 要覆盖的地方撞。

然后就根据上面的限制条件来统计最后的颜色序列方案数即可,但是我不知道如何 \(dp\) 想了很久。其实这题的 \(dp\) 比较套路,我们从小到大加入颜色,每次颜色作为一整段插入进去,设 \(dp[i][j]\) 表示前 \(i\) 种颜色染了 \(j\) 个格子,转移枚举 \(i-1\) 种颜色染了 \(k\) 个格子,那么就有 \(k+1\) 个空隙可供插入,还可以不使用这个颜色,综上转移如下:

\[dp[i][j]=dp[i-1][j]+\sum_{k=0}^{j-1}(k+1)\cdot dp[i-1][k] \]

用后缀和优化可以做到 \(O(n^2)\)

正解需要更仔细地观察转移式,考虑整体转移的路径,从 \(dp[i-1][j]\) 转移其实就是没使用这个颜色,那么我们枚举最后真正使用了 \(k\) 个颜色,设颜色 \(i\) 染之前的序列长度是 \(a_i\)\(0=a_1<a_2...<a_k<n\)),那么使用这 \(k\) 个颜色的贡献是 \(\prod_{i=1}^k(a_i+1)\),相当于从 \([2,n]\) 这些数里面选 \(k-1\) 个乘起来,所以构造这样的生成函数,\(x\) 作为不选颜色个数的记号:

\[\prod_{i=2}^n(x+i)=\prod_{i=1}^{n-1}(x+i+1) \]

那么最后指数为 \(n-k\) 项的系数就是我们要求的答案,最终的答案是这样的(强制选了第 \(m\) 种颜色):

\[\sum_{k=1}^n C(m-1,i-1)\cdot [x^{n-k}] \]

现在的问题就是算那个求和式,可以分治套 \(\tt NTT\),时间复杂度 \(O(n\log^2 n)\)

其实还有一种方法叫倍增,也就是我们先算出 \(F_t(x)\) 表示前 \(t\) 项的值,然后倍增出 \(F_{2t}\)

\[F_{2t}(x)=F_t(x)F_t(x+t) \]

可以用 \(F_t(x)\) 快速算出 \(F_t(x+t)\),设 \(F_t(x)\) 的系数数组是 \(f_i\),那么有:

\[F_t(x+t)=\sum_{i=0}^t f_i(x+t)^i \]

然后直接二项式展开:

\[\sum_{i=0}^tf_i\sum_{j=0}^i x^j\cdot t^{i-j}\cdot C(i,j) \]

想要优化上面的柿子应该用卷积,但是要把记号 \(x\) 放在最前面去,所以交换求和顺序:

\[\sum_{j=0}^tx^j\sum_{i=j}^t f_i\cdot t^{i-j}\cdot\frac{i!}{j!(i-j)!} \]

\[\sum_{j=0}^tx^j\cdot\frac{1}{j!}\sum_{i=j}^t (f_i\cdot i!)\cdot(t^{i-j}\cdot\frac{1}{(i-j)!}) \]

\(f_i\cdot i!\) 这个东西按 \(t\) 翻转一下,那么下标之和就是 \(t-j\),所以再翻转回来就可以得到 \(F_t(x+t)\)

然后直接 \(F_t(x)\)\(F_t(x+t)\) 暴力卷积就可以了,时间复杂度 \(O(n\log n)\),注意实际写法中并不是倍增而是分治,因为长度可能是奇数,分治下去然后剩下的哪一项暴力乘上去即可。

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 4000005;
const int MOD = 998244353;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,ans,a[M],b[M],c[M],fac[M],inv[M],rev[M];
void init(int n)
{
	fac[0]=inv[0]=inv[1]=1;
	for(int i=2;i<=n;i++) inv[i]=(MOD-MOD/i)*inv[MOD%i]%MOD;
	for(int i=2;i<=n;i++) inv[i]=inv[i]*inv[i-1]%MOD;
	for(int i=1;i<=n;i++) fac[i]=i*fac[i-1]%MOD;
}
int qkpow(int a,int b)
{
	int r=1;
	while(b>0)
	{
		if(b&1) r=r*a%MOD;
		a=a*a%MOD;
		b>>=1;
	}
	return r;
}
void NTT(int *a,int len,int tmp)
{
	for(int i=0;i<len;i++)
	{
		rev[i]=(rev[i>>1]>>1)|((i&1)*(len/2));
		if(i<rev[i]) swap(a[i],a[rev[i]]);
	}
	for(int s=2;s<=len;s<<=1)
	{
		int t=s/2,w=(tmp==1)?qkpow(3,(MOD-1)/s):qkpow(3,(MOD-1)-(MOD-1)/s);
		for(int i=0;i<len;i+=s)
		{
			for(int j=0,x=1;j<t;j++,x=x*w%MOD)
			{
				int fe=a[i+j],fo=a[i+j+t];
				a[i+j]=(fe+x*fo)%MOD;
				a[i+j+t]=((fe-x*fo)%MOD+MOD)%MOD;
			}
		}
	}
	if(tmp==1) return ;
	int inv=qkpow(len,MOD-2);
	for(int i=0;i<len;i++)
		a[i]=a[i]*inv%MOD; 
}
void work(int n)
{
	if(n==1)
	{
		a[0]=2;a[1]=1;
		return ;
	}
	int len=1,md=n/2;
	work(md);
	while(len<=n) len<<=1;
	//求F_{2t}(x)
	for(int i=0;i<len;i++) b[i]=c[i]=0;
	for(int i=0;i<=md;i++) b[md-i]=fac[i]*a[i]%MOD;
	for(int i=0,t=1;i<=md;i++,t=t*md%MOD) c[i]=t*inv[i]%MOD;
	NTT(b,len,1);NTT(c,len,1);
	for(int i=0;i<len;i++) b[i]=b[i]*c[i]%MOD;
	NTT(b,len,-1);
	for(int i=md+1;i<len;i++) b[i]=0;
	for(int i=0;2*i<=md;i++) swap(b[i],b[md-i]);
	for(int i=0;i<=md;i++) b[i]=b[i]*inv[i]%MOD;
	//第二次NTT 
	NTT(a,len,1);NTT(b,len,1);
	for(int i=0;i<len;i++) a[i]=a[i]*b[i]%MOD;
	NTT(a,len,-1);
	for(int i=n+1;i<len;i++) a[i]=0; 
	if(n&1)
	{
		for(int i=n;i>0;i--)
			a[i]=(a[i]*(n+1)+a[i-1])%MOD;
		a[0]=a[0]*(n+1)%MOD; 
	}
}
int C(int n,int m)
{
	if(n<m) return 0;
	return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
signed main()
{
	freopen("color.in","r",stdin);
	freopen("color.out","w",stdout);
	init(1e6);
	n=read();m=read();
	work(n-1);
	for(int i=1;i<=n;i++)
		ans=(ans+C(m-1,i-1)*a[n-i])%MOD;
		//强制选第m种颜色 
	printf("%lld\n",ans);
}
posted @ 2021-03-16 22:10  C202044zxy  阅读(119)  评论(0编辑  收藏  举报