[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\) 表示。

第一个目标的答案:

\[T = \dfrac{n!}{\prod \limits _{i=0} ^m c_i!} \]

简略解释和花絮

第二个目标的答案:

\[V = \prod \limits _{i = 0} ^ m {v_i} ^ {c_i}\\ S = \sum \limits _{i = 0} ^ m c_i \times 2^i \]

我们知道,所有合法的 \(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;
}
posted @ 2022-09-12 13:37  dbxxx  阅读(86)  评论(2编辑  收藏  举报