LOJ6089. 小 Y 的背包计数问题 题解

LOJ6089. 小 Y 的背包计数问题

本题是一个背包计数问题,发现 \(n \leq 10^5\),显然普通的多重背包计数不可取,需要优化。

本着多重背包计数时间复杂度成本较高,我们总是希望尽量不做多重背包计数,发现对于编号 \(i > \lfloor \sqrt{n} \rfloor\) 的物品,由于要装满背包,它们永远不可能用完,所以我们可以对其使用其它方法计数。故考虑一种类似于根号分治的思想,对编号不超过 \(\lfloor \sqrt{n} \rfloor\) 和超过该值的物品分类讨论。

  • 对于编号 \(i \leq \lfloor \sqrt{n} \rfloor\) 的物品:

由于 \(\sqrt{n}\) 数量级在我们接受范围内,故考虑直接使用多重背包计数,设 \(f(i, j)\) 表明考虑只从前 \(i\) 个物品中取物品,取出的物品总体积为 \(j\) 的方案数,则不难列出方程:

\[f(i, j) = \sum_{k = 0}^{i} f(i - 1, j - k \times i) \quad (f(0, 0) = 1) \]

可以使用滚动数组优化掉 \(i\) 那一维,又发现每次转移都是取 \(f(i - 1, j - i \times i), f(i - 1, j - i \times(i - 1)), \cdots, f(i - 1, j)\) 的值,可见每次取值都是各相邻 \(i\) 个位置取一次值,这样我们可以使用类似于前缀和优化的方式进行优化转移。(其实这里如果不使用滚动数组的话也可以,毕竟 \(i\) 那一维的大小已经被我们控制在了 \(O(\sqrt{n})\),而加上 \(j\) 那一维,空间复杂度为 \(O(n \sqrt{n})\) 也不会爆空间)

  • 对于编号 \(i > \lfloor \sqrt{n} \rfloor\) 的物品:

由于此类物品无法取完,故不用考虑每种物品取了多少个。我们发现这类物品我们最多只能取 \(\lfloor \sqrt{n} \rfloor\) 个,故不妨设 \(g(i, j)\) 为取 \(i\) 个此类物品,取出的这 \(i\) 个物品总体积为 \(j\) 的方案数。我们考虑如何列递推式,要想知道 \(g(i, j)\),就需要知道其可以由哪些状态变化而来,注意这里提到的状态必须保证计数不重(前驱状态和后续状态一一对应)不漏(不会少考虑情况) 。考虑到这两点后我们就来寻找这些状态吧 qwq

首先容易想到的是取 \(i\) 个物品的状态一定是可以由取 \(i - 1\) 个物品的状态推出来的,但这时我们似乎需要枚举 \(g(i - 1, (i - 1) \times (\lfloor \sqrt{n} \rfloor - 1))\)\(g(i - 1, j - \lfloor \sqrt{n} \rfloor - 1)\) 所有值,这显然还是会超时,但我们发现一个有趣的事情是,如果我们取了一个体积大于 \(\lfloor \sqrt{n} \rfloor + 1\) 的物品,我们可以将其看成先取了一个体积为 \(\lfloor \sqrt{n} \rfloor + 1\) 的物品,再通过某种方式使物品的总体积“增长”到我们要的目标 \(j\),考虑寻找增长的方式,由于前后状态必须一一对应,所以每次所有已选的物品的体积增长量必须相同,由于不能跳过某些状态,所以这个增长量只能为 1,所以我们就找到了一种增长的方法,使得这样计数不重不漏。

所以可以推出 \(g(i, j)\) 的状态有两种:一种是已经取了 \(i - 1\) 个物品,又取了一个体积为 \(\lfloor \sqrt{n} \rfloor + 1\) 的物品,达到了状态 \(g(i, j)\),即 \(g(i - 1, j - \lfloor \sqrt{n} \rfloor - 1)\);一种是我们上文中所述的,将取体积大于 \(\lfloor \sqrt{n} \rfloor + 1\) 的物品的情况进行分解,将其分解为第一种状态与“增长”这两步,由于第一步在前面的状态中肯定会被当成第一种状态算进答案中,故我们只需考虑如何利用“增长”统计这部分答案,而正如上文所述,增长是要求每一个已选物品体积均增加 1,所以总体积增加 \(i\),而已选物品数量是不会通过增长改变的,所以这一种可以推出 \(g(i, j)\) 的状态为 \(g(i, j - i)\)

综上,状态转移方程为:

\[g(i, j) = g(i - 1, j - \lfloor \sqrt{n} \rfloor - 1) + g(i, j - i) \quad (g(0, 0) = 1) \]

由于我们将物品按编号分类讨论,所以我们最终要使用乘法原理合并答案,我们设 \(F(i) = f(\lfloor \sqrt{n} \rfloor, i), G(i) = \sum_{j = 0}^{\lfloor \sqrt{n} \rfloor} \limits g(j, i)\),则答案为:

\[\sum_{i = 0}^{n} F(i)G(n - i) \]

两部分时间复杂度均为 \(O(n \sqrt{n})\),故总时间复杂度也为 \(O(n \sqrt{n})\)

下面是简短的代码qwq:

Code
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>

const int maxn = 1e5 + 10;
const int p = 23333333;

int n, m, f[maxn], sum[maxn], g[320][maxn];

int main()
{
    scanf("%d", &n);
    m = sqrt(n);
    f[0] = 1;
    for (int i = 1; i <= m; ++i)
    {
        for (int j = 0; j < i; ++j)
            sum[j] = f[j];
        for (int j = i; j <= n; ++j)
            sum[j] = (f[j] + sum[j - i]) % p;
        for (int j = 0; j <= n; ++j)
        {
            if (j >= i * (i + 1))
                f[j] = (sum[j] - sum[j - i * (i + 1)] + p) % p;
            else
                f[j] = sum[j];
        }
    }
    g[0][0] = 1;
    int ans = f[n];
    for (int i = 1; i <= m; ++i)
    {
        for (int j = i * (m + 1); j <= n; ++j)//根据 g 的定义 g[i][i * (m + 1)] 之前数组的值都应为 0,所以直接跳过
        {
            g[i][j] = (g[i - 1][j - (m + 1)] + g[i][j - i]) % p;
            ans = (ans + 1ll * f[n - j] * g[i][j] % p) % p;//这里直接统计答案,但需注意 f[n] 无法在循环中统计到
        }
    }
    printf("%d\n", ans);
    return 0;
}
posted @ 2021-08-18 22:38  Nickel_Angel  阅读(621)  评论(0)    收藏  举报