[省选前集训2021] 模拟赛2
总结
一直在想第一题,因为看到第三题是 \(\tt polya\) 根本不会,\(T1\) 想了好多个 \(dp\) 做法但都是错的,最后发现是个套路 \(dp\) 题 \(...\)
怎么说呢,还是没有 \(\tt bfs\) 策略所以只拿了 \(15\) 分,下次不管心态多炸都认真打暴力吧。
礼物
题目描述
有一个长度为 \(n\) 的手环,需要选 \(m\) 个位置变成金色的,但是变成金色的最长连续段不能超过 \(k\),如果两个手环能通过旋转变成一模一样的那么就算相同,问有多少个不同的手环。输出模 \(998244353\) 的结果。
解法
这个直接上 \(\tt polya\) 啊,那个结论是答案等于所有置换的不动点个数的算术平均数,设 \(f(n,m)\) 表示 \(n\) 个位置里面放 \(m\) 个金色位置的方案数,那么枚举置换 \(k\),考虑前 \(d=\gcd(n,k)\) 放的情况:
上面的柿子有亿点点不好看,把 \(d\) 换成 \(\frac{n}{d}\):
然后按套路把后面的 \([d|m]\) 反演掉:
现在问题变成了求 \(f(n,m)\),首先可以写出关于有多少个金色的生成函数,一开始有 \(n-m\) 个不是金色的,可以把 \(m\) 个金色的插入到这 \(n-m-1\) 个空隙中,或者是放在首尾的空隙中(如果有 \(i\) 个金色就有 \(i+1\) 中放法):
优化的方式就是写成闭形式然后相乘再展开,先把后面的东西写成闭形式:
前面的东西很容易写成闭形式:
把两个式子乘出来:
分子可以直接二项式展开:
分母也可以展开,据说是用什么广义二项式定理,但是不用那么复杂度,直接把它当成 \(\sum_{i=0}^\infty x^i\) 的闭形式展开就可以了,用组合意义是很好算的,也就是选 \(n-m+1\) 个非负数使得和为 \(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\) 个空隙可供插入,还可以不使用这个颜色,综上转移如下:
用后缀和优化可以做到 \(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\) 作为不选颜色个数的记号:
那么最后指数为 \(n-k\) 项的系数就是我们要求的答案,最终的答案是这样的(强制选了第 \(m\) 种颜色):
现在的问题就是算那个求和式,可以分治套 \(\tt NTT\),时间复杂度 \(O(n\log^2 n)\)
其实还有一种方法叫倍增,也就是我们先算出 \(F_t(x)\) 表示前 \(t\) 项的值,然后倍增出 \(F_{2t}\):
可以用 \(F_t(x)\) 快速算出 \(F_t(x+t)\),设 \(F_t(x)\) 的系数数组是 \(f_i\),那么有:
然后直接二项式展开:
想要优化上面的柿子应该用卷积,但是要把记号 \(x\) 放在最前面去,所以交换求和顺序:
把 \(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);
}