题目描述
求 $n!$ 的十六进制下去尾零后的后十六位。多组测试数据。
数据范围
$T \le 10,n < 2^{64}$
这题目太简洁了,awsl
思路
开始裂开
十六进制下的十六位就是 $(2^4)^{16}=2^{64}$, 就是 unsigned long long
的数据范围, 暴力直接自然溢出即可。十六进制下的尾零就是 $2^4$, 所以每次乘一个数先将它的所有 $2$ 的质因子除去, 并记录 $2$ 的个数, 最后 $\bmod 4$ 再乘回去就可以了。
这启发我们首先将所有的 $2$ 提取出来, 记录 $2$ 的个数。我们可以发现:
1
2 -> 1
3
4 -> 2 -> 1
5
6 -> 3
7
8 -> 4 -> 2 -> 1
9
10 -> 5
11
12 -> 6 -> 3
13
14 -> 7
15
16 -> 8 -> 4 -> 2 -> 1
...
不难发现最后得到的是若干个奇数序列的乘积, 并且奇数序列个数是 $\log$ 级别的。
所以我们考虑求 $[1,n]$ 中所有奇数的乘积, 即 $\displaystyle \prod_{i=0}^{\frac{n-1}{2}}(2i+1)$。
这个式子可以将括号拆开,得到的若干项相加就是结果。而我们可以发现, $2i$ 项如果选择超过 $64$ 就对答案没有贡献了,因为这一项就一定是 $2^{64}$ 的倍数, 自然溢出后就是 $0$。
所以我们不妨再进一步,求当选择 $m$ 个 $2i$ 项时的乘积和为多少。我们可以将 $2$ 提出来,也就是求 $2^m$ 乘上从 $[1,n]$ 中选择 $m$ 个数的所有方案的乘积之和。
打 表 可 得:
n\m | 1 | 2 | 3 | 4 |
---|---|---|---|---|
1 | 1 | |||
2 | 3 | 2 | ||
3 | 6 | 11 | 6 | |
4 | 10 | 35 | 50 | 24 |
发现这个数和第一类斯特林数很像,事实上这个数就是 $\displaystyle {n + 1 \brack n - m + 1}$
考虑第一类斯特林数的含义:将 $n + 1$ 个数划分为 $n - m + 1$ 个轮换。 $n$ 很大,但是 $m$ 相对较小, 最大值为 $64$, 也就是划分出的轮换大部分长度应该都是 $1$。
于是我们可以考虑求出 $n$ 个数组成 $m$ 个长度大于 $1$ 的轮换的方案数。
设 $dp_{n,m}$ 为以上定义,那么可以得出:
\[\begin{aligned} dp_{i, j}&=\sum_{k=2}^i\binom{i-1}{k-1}{k \brack 1}dp_{i-k,j-1}\\ &=\sum_{k=2}^i\binom{i-1}{k-1}(k-1)!dp_{i-k,j-1}\\ &=\sum_{k=2}^i\mathrm{A}_{i-1}^{k-1}dp_{i-k,j-1}\\ \end{aligned}\]设 $b=64$, 那么排列数可以 $O(b^2)$ 预处理出来, $dp$ 数组可以 $O(b^3)$ 预处理出来。
然后回到 $\displaystyle {n + 1 \brack n - m + 1}$, 我们就可以得出:
\[{n + 1\brack n-m+1}=\sum_{i}\binom{n + 1}{i}dp_{i,i-m}\]这个组合数的 $n$ 很大, 我们可以尝试直接用式子算:
\[\binom{n + 1}{i}=\frac{(n+1)!}{i!(n+1-i)!}\]$i$ 很小, 所以我们可以直接处理出 $i!$ 的逆元。 但是模数为 $2^{64}$, 与偶数不互质, 而正好我们需要把所有的 $2$ 都提取出来, 所以直接处理所有奇数的逆元就可以了。
这样, 再暴力将 $\frac{(n+1)!}{(n+1-i)!}$ 中的 $2$ 算去, 算结果即可。
处理奇数的逆元我们需要用到欧拉定理:
\[x^b\equiv x^{b\bmod \varphi(p)}\pmod{p}\]$\varphi(2) = 1$, 那么 $\varphi(2^{64}) = 2^{63}$, 那么 $a^{-1}\equiv a^{2^{63}-1} \pmod{2^{64}}$, 直接快速幂可以算出,也可以算出 $a^{2^{63}-1} = a^{2^0+2^1+2^2+\cdots+2^{63}} = a^{2^0} \times a^{2^1} \times \cdots \times a^{a^{62}}$, 将 $a$ 不断平方并乘起来即可。
接下来我们解决最后的问题:求出 $[1,n]$ 中所有奇数的乘积。
和上面所说的一样,枚举选择 $m$ 个 $2i$ 项, 然后利用上面得出的结论计算即可。
\[\sum_{m=1}^{64}2^i\sum_{m}\binom{\frac{n+1}{2}}{i}dp_{i,i-m}\]$n + 1$ 变为 $\frac{n+1}{2}$ 的原因有 $\frac{n+1}{2}$ 个奇数, 而 $1$ 拆分为 $1\times 0+1$ 不在 $[1,\frac{n+1}{2}]$ 的范围内, 所以要减去 $1$。
最后, 将 $n$ 不断除以 $2$, 算出每一个奇数序列的乘积相乘就是最后的答案了。
代码
#include <bits/stdc++.h>
#define ull unsigned long long
using namespace std;
ull cnt[150];
ull dp[150][150], a[150][150], inv[150];
char c[17] = "0123456789ABCDEF";
void printHex(unsigned long long a) {
for (int i = 16; i >= 1; i--) {
if ((a >> ((i - 1) * 4)) != 0) printf("%c", c[(a >> ((i - 1) * 4)) % (1 << 4)]);
}
printf("\n");
}
ull C(ull n, ull m) {
ull cc = 0;
ull ans = 1;
for (ull i = 1; i <= m; i++) {
cc += cnt[i];
ans *= inv[i];
}
for (ull i = n; i >= n - m + 1; i--) {
ull j = i;
while (cc && j % 2 == 0) j /= 2, cc--;
ans *= j;
}
return ans;
}
ull f(ull n) {
n = ((n - 1) >> 1) + 1;
ull ans = 0;
for (int i = 0; i < 64 && i < n; i++) {
ull a = 0;
for (int j = i; j <= 2 * i && j <= n; j++) {
a += C(n, j) * dp[j][j - i];
}
ans += (a << i);
}
return ans;
}
int main () {
a[0][0] = 1;
for (int i = 1; i <= 140; i++) {
a[i][0] = 1;
for (int j = 1; j <= i; j++) {
a[i][j] = a[i][j - 1] * (i - j + 1);
}
}
dp[0][0] = 1;
for (int i = 1; i <= 140; i++) {
for (int j = 1; j <= i; j++) {
for (int k = 2; k <= i; k++) {
dp[i][j] += a[i - 1][k - 1] * dp[i - k][j - 1];
}
}
}
for (int i = 1; i <= 130; i++) {
ull j = i;
while (j % 2 == 0) j /= 2, cnt[i]++;
ull z = 1;
for (int q = 0; q <= 62; q++) z *= j, j *= j;
inv[i] = z;
}
int T; scanf("%d", &T);
while (T--) {
ull n; scanf("%llu", &n);
ull cc = 0;
ull ans = 1;
while (n) ans *= f(n), n >>= 1, cc = (cc + n) % 4;
printHex(ans << cc);
}
return 0;
}
小记
简介的题面,不简洁的做法
可以说是很裂开了
关于前三道题都想到正解,两个半小时就写完后花两个小时想这题,结果T2和T3都挂分挂飞:/
一定要手造数据 一定要手造数据 一定要手造数据