zoukankan      html  css  js  c++  java
  • [省选前集训2021] 模拟赛2

    总结

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

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

    礼物

    题目描述

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

    [Tleq 5,1leq nleq 10^6,0leq kleq mleq n ]

    解法

    这个直接上 ( t 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-1choose i}(-x^{k+1})^i ]

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

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

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

    我能说我主要是不会 ( t 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
    ",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
    ",ans*qkpow(n,MOD-2)%MOD);
    }
    
    

    染色问题

    题目描述

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

    (n,mleq 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}] ]

    现在的问题就是算那个求和式,可以分治套 ( t NTT),时间复杂度 (O(nlog^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_isum_{j=0}^i x^jcdot t^{i-j}cdot C(i,j) ]

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

    [sum_{j=0}^tx^jsum_{i=j}^t f_icdot t^{i-j}cdotfrac{i!}{j!(i-j)!} ]

    [sum_{j=0}^tx^jcdotfrac{1}{j!}sum_{i=j}^t (f_icdot i!)cdot(t^{i-j}cdotfrac{1}{(i-j)!}) ]

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

    然后直接 (F_t(x))(F_t(x+t)) 暴力卷积就可以了,时间复杂度 (O(nlog 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
    ",ans);
    }
    
  • 相关阅读:
    判断一个数组是不是一维数组
    XML5个转义符:<,>,&,”,©;的转义字符分别如下: &lt; &gt;&amp; &quot; &apos;
    linux crontab & 每隔10秒执行一次
    微信企业号-根据code获取成员信息(过期code)
    Namespace declaration statement has to be the very first statement in the script-去除bom头
    去掉一个字符前面的全部0
    检查一个数字是否为个位数
    nginx配置location总结及rewrite规则写法
    QT 4.7支持中文(QT4.7)(中文)(makeqpf)
    【转】vlc android 代码编译
  • 原文地址:https://www.cnblogs.com/C202044zxy/p/14546290.html
Copyright © 2011-2022 走看看