[提高组集训2021] 乘法
一、题目
求 \(n!\) 转成 \(16\) 进制后除去末尾 \(0\) 的最后 \(16\) 位。
\(n<2^{64},T\leq 10\)
二、解法
首先考虑暴力怎么打,我们把所有 \(2\) 的因子提出来之后,剩下的数直接暴力乘法之后自然溢出即可,最后 \(2\) 的因子数模 \(4\) 之后乘上去,转成 \(16\) 进制输出即可。
瓶颈在于把所有数乘起来,设 \(f(n)=\sum_{i=1}^n(2i+1)\),那么答案是 \(\prod_{i=0}^{63}f((\lfloor\frac{n}{2^i}\rfloor-1)/2)\),问题转化为怎么求 \(f(n)\),因为本题是在模 \(2^{64}\) 意义下的,所以选 \(2i\) 这一项的次数是 \(\leq 63\) 的,要不然就被模成 \(0\) 了。
设 \(g(x)\) 表示选了 \(x\) 次 \(2i\) 的 \(i\) 之积,转移考虑加入一个数,但是这个数可能和前面重复所以需要容斥,这里我们容斥最后一个数只出现一次这个条件,然后就足以进入子问题了,我们枚举出现次数 \(j\):
\[x\cdot g(x)=\sum_{j=1}^x (-1)^{j-1}\cdot g(x-j)\cdot \sum_{i\in[1,n]}i^j
\]
最后是一个幂次求和的结构,可以套路地第二类斯特林数反演(直接):
\[\sum_{i=1}^n i^k=\sum_{i=0}^k{k\brace i}\times \frac{1}{i+1}\times\frac{(n+1)!}{(n-i)!}
\]
时间复杂度 \(O(\log^3n)\),至于模意义下的除法可以奇数使用逆元,偶数就一直除 \(2\) 即可。
还有多项式平移倍增的做法,上次省选学过我就不想复读了。
三、总结
本题 \(dp\) 加容斥的做法让人耳目一新,两两不同的限制可以被我们拆分成每个数和其他数不同的限制,在我们考虑最后一个数是只用解决它的限制就可以归纳到子问题。
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 1000;
#define int unsigned long long
int T,n,inv[M],S[M][M],tmp[M],g[M];
void init(int n)
{
//get inv about mod=2^64
for(int i=1;i<=n;i++)
{
inv[i]=1;int nw=i,b=(1ull<<63)-1;
while(b) {if(b&1)inv[i]*=nw;nw*=nw;b>>=1;}
}
//get S(n,m)
S[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
S[i][j]=S[i-1][j-1]+j*S[i-1][j];
}
int div(int x,int y)
{
while(y%2==0) x/=2,y/=2;
return x*inv[y];
}
int work(int n)
{
if(n==0) return 1;
n=(n-1)/2;
for(int i=0;i<=63;i++) tmp[i]=g[i]=0;
for(int i=1;i<=63 && i<=n;i++)
{
int A=n+1;
for(int j=1;j<=i;j++)
{
A=A*(n+1-j);
tmp[i]+=div(A,j+1)*S[i][j];
}
}
g[0]=1;
for(int i=1;i<=63 && i<=n;i++)
{
for(int j=1;j<=i;j++)
{
if(j&1) g[i]+=g[i-j]*tmp[j];
else g[i]-=g[i-j]*tmp[j];
}
g[i]=div(g[i],i);
}
int res=0;
for(int i=0;i<=63;i++) res+=g[i]<<i;
return res;
}
signed main()
{
freopen("multiplication.in","r",stdin);
freopen("multiplication.out","w",stdout);
cin>>T;init(64);
while(T--)
{
cin>>n;int c=0,ans=1;
for(int i=1;i<=63;i++) c+=n>>i;
for(int i=0;i<=63;i++) ans*=work(n>>i);
for(int i=1;i<=c%4;i++) ans*=2;
printf("%llX\n",ans);
}
}