P14360 [CSP-J 2025] 多边形

从“能拼成多边形”到“固定最长木棍,再用 01 背包数方案”。

排序 固定关键对象 01 背包计数 CSP-J 2025

一、先回答问题

课堂结论

把长度排序后,枚举当前木棍 x 作为选中集合里“最后一根最长木棍”。

这时只要数:前面的木棍能选出多少种方案,使长度和 > x。这个数量用 01 背包计数维护。

学习路径

本题训练的不是一个单独技巧,而是一条建模链:

条件变形固定最长木棍排序避免重复背包计数

二、相关知识点讲解

知识点 1:多边形条件

题目给出的条件是:

总长度 > 2 × 最长木棍

设最长木棍是 mx,其他木棍长度和是 rest,那么:

rest + mx > 2mx,因此 rest > mx

所以判断重点变成:其他木棍加起来,能不能超过最长木棍。

知识点 2:固定关键对象

直接数一个集合很难,因为每个集合大小不同、最长木棍也不同。

我们固定“最长木棍”后,剩下的选择就只看长度和。这是很多计数题常用的切入点。

固定最大值 x → 统计前面若干个数的和是否 > x
第 1 步

排序。

让“前面的木棍都不超过当前木棍”。

第 2 步

枚举当前木棍 x。

它负责统计以自己为最后最长木棍的方案。

第 3 步

查背包。

前面木棍的选法里,长度和 > x 的都能和 x 拼成多边形。

第 4 步

加入当前木棍。

它会成为后面更长木棍的“可选前缀”。

三、为什么不会重复统计

关键规定

每个合法方案,只交给它里面“排序后下标最大的最长木棍”来统计。

如果最长长度出现多次,比如长度都是 2,也没关系。排序后它们仍然有先后位置,一个方案只由选中的最后那个最大值负责。

10

例子:方案 `{3, 8, 10}` 会在枚举 `10` 时被统计;方案 `{2, 2, 3}` 会在枚举排序后那根 `3` 时被统计。

四、互动演示:样例 1 怎么算出 9

样例木棍

样例 1 排序后仍是:

当前最长木棍 x -
累计答案 0
前面可选木棍 -
本轮新增 0
背包状态

点击“下一步”,观察每次枚举最长木棍时,哪些长度和会被计入答案。

绿色格子表示:前面木棍能凑出的和已经大于当前最长木棍 x,所以可以和 x 组成合法多边形。

五、手推计算过程

当前 x 前面木棍 前面能凑出的长度和 大于 x 的方案数 累计答案
1 0 0 0
2 1 0, 1 0 0
3 1, 2 0, 1, 2, 3 0 0
4 1, 2, 3 0, 1, 2, 3×2, 4, 5, 6 2 2
5 1, 2, 3, 4 0, 1, 2, 3×2, 4×2, 5×2, 6×2, 7×2, 8, 9, 10 7 9
复习提示

表里的 `3×2` 表示长度和为 3 的选法有 2 种:选 `{3}` 或选 `{1,2}`。

本题数的是“下标集合”的方案数,所以相同长度的木棍如果下标不同,也要当作不同选择。

六、自己选答案

问题 1

如果当前最长木棍长度是 10,其他木棍长度和至少要满足什么?

问题 2

为什么背包更新要倒序循环?

问题 3

为什么可以把超过 5000 的长度和都记到 5001?

七、C++14 参考代码

读代码抓三处
  • `dp[0] = 1`:表示前面什么都不选,也是一种方案。
  • 先统计答案,再加入当前木棍:因为当前木棍要当“最长木棍”,不能又跑进“其他木棍”。
  • `min(LIM, s + x)`:把超过 5000 的和统一封顶到 5001。
常见错误
  • 把 `rest > mx` 写成 `rest >= mx`。
  • 没有排序,导致“最长木棍”这个角色不稳定。
  • 正序更新背包,让一根木棍被重复使用。
P14360_solution.cpp
#include <bits/stdc++.h>
using namespace std;

const int MOD = 998244353;
const int LIM = 5001; // 5001 表示长度和已经超过 5000

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    vector<int> a(n);
    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }

    sort(a.begin(), a.end());

    vector<int> dp(LIM + 1, 0);
    dp[0] = 1;

    int ans = 0;
    for (int x : a) {
        // 当前木棍作为最长木棍,前面选出的总长度必须大于 x。
        for (int s = x + 1; s <= LIM; s++) {
            ans += dp[s];
            if (ans >= MOD) ans -= MOD;
        }

        // 01 背包:把当前木棍加入后续可选集合。
        for (int s = LIM; s >= 0; s--) {
            int to = min(LIM, s + x);
            dp[to] += dp[s];
            if (dp[to] >= MOD) dp[to] -= MOD;
        }
    }

    cout << ans << '\n';
    return 0;
}

八、课后复习路线

先练模型

P1048 采药

练 01 背包的倒序更新。

再练计数

P1164 小A点菜

练“不是求最大值,而是数方案数”。

最后迁移

P14360 多边形

练排序后固定最大值,再用背包统计前缀方案。