[luogu p7961] [NOIP2021] 数列
\(\mathtt{Link}\)
P7961 [NOIP2021] 数列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
\(\mathtt{Description}\)
给定整数 \(n, m, k\),和一个长度为 \(m + 1\) 的正整数数组 \(v_0, v_1, \ldots, v_m\)。
对于一个长度为 \(n\),下标从 \(1\) 开始且每个元素均不超过 \(m\) 的非负整数序列 \(\{a_i\}\),我们定义它的权值为 \(v_{a_1} \times v_{a_2} \times \cdots \times v_{a_n}\)。
当这样的序列 \(\{a_i\}\) 满足整数 \(S = 2^{a_1} + 2^{a_2} + \cdots + 2^{a_n}\) 的二进制表示中 \(1\) 的个数不超过 \(k\) 时,我们认为 \(\{a_i\}\) 是一个合法序列。
计算所有合法序列 \(\{a_i\}\) 的权值和对 \(998244353\) 取模的结果。
\(\mathtt{Data} \text{ } \mathtt{Range} \text{ } \mathtt{\&} \text{ } \mathtt{Restrictions}\)
-
\(1 \leq k \leq n \leq 30\);
-
\(0 \leq m \leq 100\);
-
\(1 \leq v_i < 998244353\)。
\(\mathtt{Solution}\)
与 \(S\) 对应地,我把序列的权值用字母 \(V\) 表示。
这题的样例容易给人启发:一个 \(a\) 序列任意重排,无论是它的 \(V\) 还是 \(S\) 都不会发生改变。也就是说 \(a\) 的顺序不会影响权值和合法性。
我们发现,当两个序列 \(a\) 和 \(b\) 可以通过重排互相得到,当且仅当每个元素的出现次数均对应相同,换句话说,令 \(c_i\) 为 \(i\) 在序列中出现的次数,那么序列 \(a\) 和 \(b\) 的 \(c\) 数组是完全相同的。
因此考虑把研究对象从序列 \(a\) 转化为数组 \(c\),明显他们是多对一的一个关系,也就是说一个序列 \(a\) 对应一个数组 \(c\),而一个数组 \(c\) 可以对应多个序列 \(a\)(这些序列都是本质相同的,即可互相重排得到)。
为什么会想到转化成 \(c\) 数组?我是通过可以考虑一个序列 \(a\) 重排得到的序列种数,发现会跟 \(c\) 有关而想到的,一会就会提到。
其实,\(c\) 数组是序列 \(a\) 的一种无序化的体现。这也是本题我们第一步要转化的(好像很多题解都忽略了这点……直接想到?)
为了搞定转化,有两个目标:
- 求出一个数组 \(c\) 可以对应多少个序列 \(a\),这个值我们记为 \(T\);
- 将 \(V\) 和 \(S\) 采用数组 \(c\) 表示。
第一个目标的答案:
第二个目标的答案:
我们知道,所有合法的 \(c\) 数组的 \(T \times V\) 的总和就是答案,因此考虑 \(T \times V\) 就是一个 \(c\) 数组的贡献。
搞一下发现是 \(n! \times \prod \limits _{i = 0} ^ m \dfrac{{v_i} ^ {c_i}}{ c_i!}\),发现 \(n!\) 是常数,因此只考虑 \(\prod \limits _{i = 0} ^ m \dfrac{{v_i} ^ {c_i}}{ c_i!}\) 作为 \(c\) 的贡献,最后答案乘上 \(n!\) 即可。我将 \(\prod \limits _{i = 0} ^ m \dfrac{{v_i} ^ {c_i}}{ c_i!}\) 记为 \(G\)。
dfs \(c\) 数组,预处理 \(0 \sim n\) 的阶乘逆元和 \(v_i\) 的幂,dfs 的同时转移 \(S\) 和 \(G\),可做到 \(\mathcal{O}(\dbinom{n+m}{n})\)。
至于 \(\dbinom{n+m}{n}\),它其实是 \(c\) 数组所有可能的种数,具体见这里。
我们把原来数据的表格拓展一下:
测试点 | \(n\) | \(k\) | \(m\) | \(c\) 数组的种数(时间复杂度) | 是否可过 |
---|---|---|---|---|---|
\(1 \sim 4\) | \(=8\) | \(\leq n\) | \(=9\) | \(\approx2 \times 10^4\) | 是 |
\(5 \sim 7\) | \(=30\) | \(\leq n\) | \(=7\) | \(\approx 10^7\) | 是 |
\(8 \sim 10\) | \(=30\) | \(\leq n\) | \(=12\) | \(\approx 10^{10}\) | 否 |
\(11 \sim 13\) | \(=30\) | \(=1\) | \(= 100\) | \(\approx2 \times 10^{28}\) | 否 |
\(14 \sim 15\) | \(=5\) | \(\leq n\) | \(=50\) | \(\approx 3 \times 10^6\) | 是 |
\(16\) | \(=15\) | \(\leq n\) | $ =100$ | \(\approx2 \times 10^{18}\) | 否 |
\(17 \sim 18\) | \(=30\) | \(\leq n\) | \(=30\) | \(\approx 10^{17}\) | 否 |
\(19 \sim 20\) | \(=30\) | \(\leq n\) | $ =100$ | \(\approx2 \times 10^{28}\) | 否 |
观察到,可以通过 \(9\) 个测试点,期望得分 \(45\) 分。差评
考虑正解,计数问题要么是 dp 要么是组合数学,接下来应该是往 dp 的方向走了。数据范围可以给我们提示,我们足够大胆地设置 dp 状态。
接下来的突破口很容易发现,就是 \(S\) 中那个 \(2^i\)。累加 \(c_i \times 2^i\) 这个式子,其实就是在 \(S\) 的二进制从低到高第 \(i\) 位上加了 \(c_i\) 个 \(1\)。(本题解中第 \(i\) 位就是从低到高第 \(i\) 位,最低一位是第 \(0\) 位)
紧接着有一个比较头疼的问题,那就是进位。二进制中的每一位只能容下一个 \(1\),剩下的 \(1\) 都要进位到前面去。
不过,冷静下来我们思考,会发现进位并不是一种玄学,它是有理有据的:第 \(i\) 位是 \(1\) 还是 \(0\),只和当前这一位对应的 \(c_i\) 和第 \(i - 1\) 位给第 \(i\) 位进的位数 \(x_i\) 有关。再具体来说,第 \(i\) 位的值是 \((c_i + x_i) \bmod 2\)。那么这一位又会向第 \(i + 1\) 位进多少位呢?不难发现 \(x_{i+1} = \lfloor\dfrac{x_i+c_i}{2}\rfloor\)。
到这里可以开始考虑 dp 方程了。怎么设状态?
我们一定是从 \(c_0\) 到 \(c_m\) 递推,对应将 \(S\) 从低位到高位填数。那么设 \(f(i, \cdots)\) 表示对于满足 \(\cdots\) 条件的所有 \(c_0 \sim c_{i-1}\), \(\prod \limits _{j = 0} ^ {i-1} \dfrac{{v_j} ^ {c_j}}{c_j!}\) 的总和。(也就是只计算 \(c_0 \sim c_{i-1}\) 对应的 \(G\))
为什么这里我都用的是 \(i - 1\) 呢?这是因为我想给边界条件 \(f(i = 0, \cdots)\) 留下空间,而 \(f(i = 1, \cdots)\) 才开始表示填 \(c_0\)。换句话说,这里的 \(i\) 可以理解为 \(c\) 总共填了多少个数。
那么我们的意图应该是满足什么条件呢?容易发现有两条:
- \(\sum \limits_{i=0}^mc_i=n\);
- \(\operatorname{popcnt}(S) \le k\)。
那么考虑多设两维:\(f(i, j, p)\),表示新增了两个条件:对于满足 \(c_0 \sim c_{i-1}\) 的和为 \(j\),并且 \(S\) 的第 \(0\) 位到第 \(i - 1\) 位为 \(1\) 的数量为 \(p\)。(其实从大局来看就是多了两个分类)
这里其实是个经典套路:遇到看似棘手的限制,就考虑设成 dp 中新的一维。
然后又考虑到转移 \(p\) 的时候需要来自上一位的进位情况,因此再多设一维 \(x\)。
现在就变成:\(f(i, j, p, x)\),表示对于同时满足一下以下三个条件的:
- 满足 \(c_0 \sim c_{i- 1}\) 的和为 \(j\);
- 满足 \(S\) 的第 \(0\) 位到第 \(i - 1\) 位 \(1\) 的数量为 \(p\);
- 第 \(i\) 位收到的进位即将是 \(x\);
所有可能的 \(c_0 \sim c_{i - 1}\) 的贡献和。
上面那个条件都有个“第 \(i\) 位即将收到的进位”了,那么更好想的转移肯定是刷表法。也就是从 \(f(i, j, p, x)\) 向后转移。
下一个状态是什么呢?肯定是 \(f(i +1, \cdots)\) 吧。所以我们考虑枚举 \(c_i\) 的值,假设它是 \(t\)。
那么 \(f(i, j, p, x)\) 的下一个状态就是:\(f(i + 1, j + t, (t + x) \bmod 2 + p, \lfloor\dfrac{t+x}{2}\rfloor)\)。
那么就可以写出转移方程了:
\(f(i + 1, j + t, (t + x) \bmod 2 + p, \lfloor\dfrac{t+x}{2}\rfloor)\) 自增 \(f(i, j, p, x) \times \dfrac{{v_i}^t}{t!}\)。
边界是 \(f(0, 0, 0, 0) = 1\)。
接下来考虑如何统计答案。
显然答案对应的 \(f\) 头两维已经确定:\(f(m + 1, n, \cdots)\),我们现在还差一个 \(S\) 中 \(1\) 的个数的限制。
但我们发现 \(S\) 统计二进制中 \(1\) 的位数时是不止统计到第 \(m\) 位的,也就是说:可能到了 \(m\) 位会再往高进位。
但事实上这个细节很好处理。\(f(m + 1, n, p, x)\) 中,\(p\) 是第 \(0\) 位到第 \(m\) 位的 \(1\) 的个数,而 \(x\) 就是第 \(m +1\) 位被进的位。\(m+1\) 位及以上,所有的 \(1\) 肯定都是通过这个 \(x\) 得到的了,因此事实上第 \(m\) 位一直到更高,\(1\) 的个数就是 \(\operatorname{popcnt}(x)\)。那么 \(S\) 的 \(1\) 的总数就是 \(p + \operatorname{popcnt}(x)\)。
所以答案就是所有满足 \(p + \operatorname{popcnt}(x) \le k\) 的 \(f(m +1, n, p, x)\) 的总和。
不要忘记最后答案还要乘个 \(n!\)。
还有一个细节:\(f(i, j, p, x)\) 头三维枚举到多少是显然的,\(x\) 应该枚举到多少呢?也就是说:第 \(i\) 位领到的进位最大是多少呢?
其实也挺显然的:\(x \le \lfloor\dfrac{j}{2}\rfloor\)。原因:在 \(c_0 \sim c_{i-1}\) 的总和为 \(j\) 的条件下,把 \(c_{i-1}\) 赋为 \(j\),其他都赋为 \(0\),给 \(c_i\) 的进位显然是最大的,为 \(\lfloor\dfrac{j}{2}\rfloor\)。
\(\mathtt{Time} \text{ } \mathtt{Complexity}\)
\(\mathcal{O}(n^4m)\)
\(\mathtt{Code}\)
/*
* @Author: crab-in-the-northeast
* @Date: 2022-09-12 12:55:18
* @Last Modified by: crab-in-the-northeast
* @Last Modified time: 2022-09-12 13:33:29
*/
#include <bits/stdc++.h>
#define int long long
inline int read() {
int x = 0;
bool flag = true;
char ch = getchar();
while (!isdigit(ch)) {
if (ch == '-')
flag = false;
ch = getchar();
}
while (isdigit(ch)) {
x = (x << 1) + (x << 3) + ch - '0';
ch = getchar();
}
if(flag)
return x;
return ~(x - 1);
}
const int maxn = 35;
const int maxm = 105;
const int maxk = maxn;
const int maxx = maxn >> 1;
const int mod = 998244353;
int v[maxm][maxn];
int f[maxm][maxn][maxk][maxx];
int iac[maxn];
inline int qpow(int p, int q = mod - 2) {
int ans = 1;
while (q) {
if (q & 1)
(ans *= p) %= mod;
(p *= p) %= mod;
q >>= 1;
}
return ans;
}
inline int popcnt(int x) {
int ans = 0;
while (x) {
if (x & 1)
++ans;
x >>= 1;
}
return ans;
}
signed main() {
int n = read(), m = read(), k = read();
for (int i = 0; i <= m; ++i) {
int val = read();
v[i][1] = val;
v[i][0] = 1;
for (int j = 2; j <= n; ++j)
v[i][j] = (v[i][j - 1] * val) % mod;
}
int z = 1;
for (int i = 2; i <= n; ++i)
(z *= i) %= mod;
iac[n] = qpow(z);
for (int i = n - 1; i >= 0; --i)
iac[i] = (iac[i + 1] * (i + 1)) % mod;
f[0][0][0][0] = 1;
for (int i = 0; i <= m; ++i)
for (int j = 0; j <= n; ++j)
for (int p = 0; p <= k; ++p)
for (int x = 0; x <= (j >> 1); ++x)
for (int t = 0; t <= n - j; ++t)
(f[i + 1][j + t][p + ((t + x) & 1)][(t + x) >> 1]
+= f[i][j][p][x] * v[i][t] % mod * iac[t] % mod) %= mod;
int ans = 0;
for (int p = 0; p <= k; ++p)
for (int x = 0; x <= (n >> 1); ++x)
if (p + popcnt(x) <= k)
(ans += f[m + 1][n][p][x]) %= mod;
printf("%lld\n", ans * z % mod);
return 0;
}