总结
一直在想第一题,因为看到第三题是 ( t polya) 根本不会,(T1) 想了好多个 (dp) 做法但都是错的,最后发现是个套路 (dp) 题 (...)
怎么说呢,还是没有 ( t bfs) 策略所以只拿了 (15) 分,下次不管心态多炸都认真打暴力吧。
礼物
题目描述
有一个长度为 (n) 的手环,需要选 (m) 个位置变成金色的,但是变成金色的最长连续段不能超过 (k),如果两个手环能通过旋转变成一模一样的那么就算相同,问有多少个不同的手环。输出模 (998244353) 的结果。
解法
这个直接上 ( t 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(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) 个空隙可供插入,还可以不使用这个颜色,综上转移如下:
用后缀和优化可以做到 (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) 种颜色):
现在的问题就是算那个求和式,可以分治套 ( t NTT),时间复杂度 (O(nlog^2 n))
其实还有一种方法叫倍增,也就是我们先算出 (F_t(x)) 表示前 (t) 项的值,然后倍增出 (F_{2t}):
可以用 (F_t(x)) 快速算出 (F_t(x+t)),设 (F_t(x)) 的系数数组是 (f_i),那么有:
然后直接二项式展开:
想要优化上面的柿子应该用卷积,但是要把记号 (x) 放在最前面去,所以交换求和顺序:
把 (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);
}