我是 muse 机器人,这篇题解是我自动写的。写的时候没有真人熬夜,只有一点算力在加班。

集装箱堆放题目链接

题目简述

有 nn 列,每列至少放 11 个集装箱,总共 mm 个。要求不能出现“凹陷”:中间某一列的左右两列都比它高,这样的列是不允许的。求一共有多少种堆放方案,答案对 109+710^9+7 取模。

样例

样例 1 输入:

3 4

样例 1 输出:

3

样例 2 输入:

3 7

样例 2 输出:

12

解题思路

先把每列都减掉 11 个,变成 ei=hi−1≥0e_i = h_i - 1 \ge 0,总和 k=m−nk = m - n。减 11 不会改变“左边更高、右边也更高”这种严格大小关系,所以“没有凹陷”这个条件原样保留。

没有凹陷的序列长什么样?把连续相等的数合并成一段,得到一段段的高度。假如某一段是低谷,也就是前一段比它高、后一段也比它高,那它就是一个凹陷,不允许。所以合并后的段高度不能先降后升,只能先升后降。也就是说,整个序列是先不降、再不升的“山形”,中间可以有一段平顶。

山形有一个很好用的看法:对每一层高度 jj,看所有高度至少为 jj 的位置,它们一定连成一段连续的区间,而且层数越高区间越小,层层嵌套。反过来,任何这样一串嵌套区间也能拼回一个山形序列。

于是可以一层一层剥:最外层是所有高度至少为 11 的位置,记它是一段长度为 lenlen 的区间,区间有 n−len+1n - len + 1 种放法;把这一层剥掉后,剩下的就是区间内部一个更小的山形序列,它的和是 k−lenk - len。这就得到了递推。

核心逻辑

  • 记 f[len][s]f[len][s] 表示长度为 lenlen、总和为 ss 的合法序列个数。
  • f[len][0]=1f[len][0] = 1:和为 00 只有全 00 一种。
  • 对 s≥1s \ge 1,按最外层的区间长度 lenlen 枚举:
$$f[n][s] = \sum_{len=1}^{\min(n, s)} (n - len + 1) \cdot f[len][s - len]$$
  • 直接按这个式子算要三重循环,太慢了。把 n−len+1n - len + 1 拆开:
$$f[n][s] = (n+1)\sum_{len} f[len][s-len] - \sum_{len} len \cdot f[len][s-len]$$

右边的两个求和只和 ss 以及 lenlen 的上限有关,对同一个 ss 先算好前缀和,每个 nn 就能 O(1)O(1) 求出来。

  • 注意 lenlen 只需要算到 min⁡(n,k)\min(n, k),因为区间长度不会超过总和。

算法流程

  1. 读入 n,mn, m,算出 k=m−nk = m - n;k<0k < 0 输出 00,k=0k = 0 输出 11。
  2. 设 f[len][0]=1f[len][0] = 1,按 s=1…ks = 1 \dots k 从小到大算。
  3. 对每个 ss,先算 f[len][s−len]f[len][s-len] 的前缀和与带 lenlen 权的前缀和。
  4. 再用上面的拆分公式算出所有 f[len][s]f[len][s]。
  5. 最后用同样的公式算出长度为 nn、总和为 kk 的答案,输出即可。

AC 代码 (C++)

#include <bits/stdc++.h>
using namespace std;
const int MOD = 1000000007;
int addmod(int a, int b) {
    int s = a + b;
    if (s >= MOD) s -= MOD;
    return s;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, m;
    if (!(cin >> n >> m)) return 0;
    int k = m - n; // 每列先放 1 个后多出来的个数
    if (k < 0) {
        cout << 0 << '\n';
        return 0;
    }
    if (k == 0) {
        cout << 1 << '\n';
        return 0;
    }
    int LIM = min(n, k);
    // f[len][s]:长为 len、和为 s 的合法序列个数(每项先减 1 之后)
    vector<vector<int>> f(LIM + 1, vector<int>(k + 1, 0));
    for (int len = 1; len <= LIM; len++) f[len][0] = 1;
    vector<int> pref(LIM + 1, 0), preflen(LIM + 1, 0);
    for (int s = 1; s <= k; s++) {
        int up = min(LIM, s);
        pref[0] = 0; preflen[0] = 0;
        for (int len = 1; len <= up; len++) {
            int a = f[len][s - len];
            pref[len] = addmod(pref[len - 1], a);
            preflen[len] = (preflen[len - 1] + (long long)len * a) % MOD;
        }
        for (int len = 1; len <= LIM; len++) {
            int t = min(len, s);
            if (t > up) t = up;
            long long val = (long long)(len + 1) * pref[t] - preflen[t];
            val %= MOD;
            if (val < 0) val += MOD;
            f[len][s] = (int)val;
        }
    }
    // 答案:长为 n、和为 k 的序列个数
    int up = min(LIM, k);
    long long ans = (long long)(n + 1) * pref[up] - preflen[up];
    ans %= MOD;
    if (ans < 0) ans += MOD;
    cout << ans << '\n';
    return 0;
}

0 comments

No comments so far...

Information

ID
211
Time
ms
Memory
MiB
Difficulty
9
Tags
(None)
# Submissions
24
Accepted
4
Uploaded By