题解:洛谷 P14360 多边形

【题目来源】

洛谷:P14360 [CSP-J 2025] 多边形 / polygon(官方数据) - 洛谷

【题目描述】

小 R 喜欢玩小木棍。小 R 有 \(n\) 根小木棍,第 \(i\) (\(1 \leq i \leq n\))根小木棍的长度为 \(a_i\)

小 X 希望小 R 从这 \(n\) 根小木棍中选出若干根小木棍,将它们按任意顺序首尾相连拼成一个多边形。小 R 并不知道小木棍能拼成多边形的条件,于是小 X 直接将条件告诉了他:对于长度分别为 \(l_1, l_2, \dots, l_m\)\(m\) 根小木棍,这 \(m\) 根小木棍能拼成一个多边形当且仅当 \(m \geq 3\) 且所有小木棍的长度之和大于所有小木棍的长度最大值的两倍,即 \(\sum_{i=1}^{m} l_i > 2 \times \max_{i=1}^{m} l_i\)

由于小 R 知道了小木棍能拼成多边形的条件,小 X 提出了一个更难的问题:有多少种选择小木棍的方案,使得选出的小木棍能够拼成一个多边形?你需要帮助小 R 求出选出的小木棍能够拼成一个多边形的方案数。两种方案不同当且仅当选择的小木棍的下标集合不同,即存在 \(1 \leq i \leq n\),使得其中一种方案选择了第 \(i\) 根小木棍,但另一种方案未选择。由于答案可能较大,你只需要求出答案对 \(998,244,353\) 取模后的结果。

【输入】

输入的第一行包含一个正整数 \(n\),表示小 R 的小木棍的数量。

输入的第二行包含 \(n\) 个正整数 \(a_1, a_2, \dots, a_n\),表示小 R 的小木棍的长度。

【输出】

输出一行一个非负整数,表示小 R 选出的小木棍能够拼成一个多边形的方案数对 \(998,244,353\) 取模后的结果。

【输入样例】

5
1 2 3 4 5

【输出样例】

9

【核心思想】

  1. 问题分析:给定 \(n\) 根小木棍的长度 \(a_i\),求能拼成多边形的子集数量(对 \(998244353\) 取模)。多边形条件为 \(m \geq 3\)\(\sum l_i > 2 \cdot \max l_i\)。本质上是补集转化 + 01 背包 DP问题:总方案数减去不满足条件的方案数,不满足条件即"和 \(\leq 2 \cdot\) 最大值"或"元素个数 \(< 3\)"。

  2. 算法选择

    • 补集思想:答案 = 所有非空子集数 \(-\) 不满足多边形条件的子集数 \(-\) 空集
    • 排序后 DP:将 \(a\) 升序排序,对每个 \(a_i\) 作为子集最大值时,统计前 \(i-1\) 个元素中选出若干个数使其和 \(\leq a_i\) 的方案数
    • 01 背包\(dp[i][j]\) 表示前 \(i\) 个数中选若干个数使其和为 \(j\) 的方案数
  3. 关键步骤

    • 读取数据:读入 \(n\)\(a[1..n]\),计算总子集数 \(ans = 2^n \bmod mod\)
    • 排序sort(a+1, a+n+1) 升序排列
    • 初始化 DP\(dp[0][0] = 1\)(空集和为 \(0\)
    • 01 背包递推\(i = 1\)\(n\)\(j = 0\)\(5000\)):
      • \(dp[i][j] = dp[i-1][j]\)(不选 \(a_i\)
      • \(j \geq a_i\)\(dp[i][j] = (dp[i][j] + dp[i-1][j-a_i]) \bmod mod\)(选 \(a_i\)
      • \(j \leq a_i\)\(bad\_ans = (bad\_ans + dp[i-1][j]) \bmod mod\)(以 \(a_i\) 为最大值的非法子集)
    • 输出\((ans - bad\_ans - 1 + mod) \bmod mod\)(减去非法子集和空集)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \cdot S)\)\(S = 5000\) 为最大和值
    • 空间复杂度:\(O(n \cdot S)\),DP 数组
  5. 补集 + 01 背包的核心思想

    • 多边形条件的反面\(\sum l_i \leq 2 \cdot \max l_i\) 等价于 \(\sum_{j \neq max} l_j \leq \max l_i\),即除最大值外其余元素之和不超过最大值
    • 排序后的单调性:升序排序后,\(a_i\) 作为子集最大值时,子集中其他元素只能从 \(a_1, \dots, a_{i-1}\) 中选取
    • DP 统计非法方案\(dp[i-1][j]\)\(j \leq a_i\))表示以 \(a_i\) 为最大值且其余元素和 \(\leq a_i\) 的非法子集数
    • 空集与单/双元素集的处理\(bad\_ans\) 自动包含单元素和双元素子集(因为 \(j \leq a_i\)\(dp[i-1][0] = 1\) 对应只选 \(a_i\) 的情况),最后统一减去空集即可
    • 适用于组合计数、补集转化、背包 DP 类问题

【算法标签】

普及 #01背包

【代码详解】

// 40分解法
#include <bits/stdc++.h>
using namespace std;

const int N = 5005;        // 定义最大数组长度
const int mod = 998244353;  // 模数(未使用)

int n;                      // 数组元素个数
int a[N];                   // 存储输入的数字数组
int ans;                    // 统计满足条件的子集数量

/**
 * 深度优先搜索枚举所有子集
 * @param step 当前处理的元素索引
 * @param tmx 当前子集中的最大值
 * @param sum 当前子集的元素和
 * @param cnt 当前子集的元素个数
 */
void dfs(int step, int tmx, int sum, int cnt)
{
    // 递归终止条件:已处理完所有元素
    if (step > n)
    {
        // 检查子集是否满足条件:
        // 1. 子集元素和 > 2 * 最大值
        // 2. 子集元素个数 >= 3
        if (sum > 2 * tmx && cnt >= 3)
        {
            ans++;  // 满足条件,计数加1
        }
        return;
    }

    // 分支1:选择当前元素加入子集
    dfs(step + 1, max(tmx, a[step]), sum + a[step], cnt + 1);

    // 分支2:不选择当前元素
    dfs(step + 1, tmx, sum, cnt);
}

int main()
{
    // 输入数组长度
    cin >> n;

    // 输入数组元素
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }

    // 初始化DFS搜索
    // 参数说明:
    // step=1: 从第一个元素开始
    // tmx=-1: 初始最大值为-1(表示没有选择任何元素)
    // sum=0: 初始和为0
    // cnt=0: 初始元素个数为0
    dfs(1, -1, 0, 0);

    // 输出满足条件的子集数量
    cout << ans << endl;

    return 0;
}
// 64分版本
#include <bits/stdc++.h>
using namespace std;

const int N = 5005;        // 定义最大数组长度
const int mod = 998244353;  // 模数

int n;                      // 数组元素个数
int a[N];                   // 存储输入的数字数组
int ans;                    // 统计满足条件的子集数量
int c[N][N];                // 组合数数组,存储C(n,k)的值

/**
 * 计算组合数表(杨辉三角)
 */
void calc()
{
    // 初始化组合数表:C(i,0)=1
    for (int i = 0; i <= n; i++)
    {
        c[i][0] = 1;
    }

    // 使用递推公式计算组合数:C(i,j) = C(i-1,j) + C(i-1,j-1)
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= i; j++)
        {
            c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
        }
    }
}

/**
 * 深度优先搜索枚举所有子集
 * @param step 当前处理的元素索引
 * @param tmx 当前子集中的最大值
 * @param sum 当前子集的元素和
 * @param cnt 当前子集的元素个数
 */
void dfs(int step, int tmx, int sum, int cnt)
{
    // 递归终止条件:已处理完所有元素
    if (step > n)
    {
        // 检查子集是否满足条件:
        // 1. 子集元素和 > 2 * 最大值
        // 2. 子集元素个数 >= 3
        if (sum > 2 * tmx && cnt >= 3)
        {
            ans++;  // 满足条件,计数加1
        }
        return;
    }

    // 分支1:选择当前元素加入子集
    dfs(step + 1, max(tmx, a[step]), sum + a[step], cnt + 1);

    // 分支2:不选择当前元素
    dfs(step + 1, tmx, sum, cnt);
}

int main()
{
    // 输入数组长度
    cin >> n;

    // 输入数组元素
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }

    // 根据数据规模选择不同算法
    if (n <= 20)
    {
        // 小规模数据:使用DFS暴力枚举
        dfs(1, -1, 0, 0);
        cout << ans << endl;
    }
    else
    {
        // 大规模数据:使用组合数近似计算
        calc();  // 计算组合数表

        // 统计所有大小>=3的子集数量(近似解)
        for (int i = 3; i <= n; i++)
        {
            ans = (ans + c[n][i]) % mod;
        }
        cout << ans << endl;
    }

    return 0;
}
// 100分版本
#include <bits/stdc++.h>
using namespace std;

#define int long long  // 使用长整型防止溢出
const int N = 5005;     // 定义最大数组长度
const int mod = 998244353;  // 模数

int n;                  // 数组元素个数
int a[N];               // 存储输入的数字数组
int dp[N][N];           // 动态规划数组,dp[i][j]表示前i个元素和为j的子集数
int bad_ans;            // 统计不满足条件的子集数量
int ans = 1;            // 总子集数(2^n),初始化为1

signed main()
{
    // 输入数组长度
    cin >> n;

    // 输入数组元素并计算总子集数(2^n)
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        ans *= 2;        // 计算2的n次方
        ans %= mod;      // 取模防止溢出
    }

    // 对数组进行升序排序,保证从小到大排序
    sort(a + 1, a + n + 1);

    // 初始化动态规划数组:空集的和为0,有1种方式
    dp[0][0] = 1;

    // 动态规划:计算子集和分布
    for (int i = 1; i <= n; i++)
    {
        for (int j = 0; j <= 5000; j++)  // 遍历所有可能的和(0到5000)
        {
            // dp[i][j]表示选到前i个数,所选数字的和为j的方案数,那么dp方程就是dp[i][j] = dp[i-1][j] + dp[i-1][j-a[i]]
            dp[i][j] = dp[i - 1][j];

            // 选择当前元素a[i](需要j>=a[i])
            if (j >= a[i])
            {
                dp[i][j] = (dp[i][j] + dp[i - 1][j - a[i]]) % mod;
            }

            // 统计不满足条件的子集:和j <= 当前元素a[i],
            if (j <= a[i])
            {
                // 对于第i个数,它作为最大值的方案数是前i-1个数,选出来的总和 ≤ a[i]的方案数,对齐进行累加
                bad_ans = (bad_ans + dp[i - 1][j]) % mod;
            }
        }
    }

    // 计算最终答案:总子集数 - 不满足条件的子集数 - 空集
    // 公式:ans = 2^n - bad_ans - 1
    cout << (ans - bad_ans - 1 + mod) % mod << endl;

    return 0;
}

【运行结果】

5
1 2 3 4 5
9

【算法真传】

从一道 CSP-J 真题出发:聊聊补集转化与 01 背包计数

posted @ 2026-08-17 09:45  团爸讲算法  阅读(18)  评论(0)    收藏  举报