题解:洛谷 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
【核心思想】
-
问题分析:给定 \(n\) 根小木棍的长度 \(a_i\),求能拼成多边形的子集数量(对 \(998244353\) 取模)。多边形条件为 \(m \geq 3\) 且 \(\sum l_i > 2 \cdot \max l_i\)。本质上是补集转化 + 01 背包 DP问题:总方案数减去不满足条件的方案数,不满足条件即"和 \(\leq 2 \cdot\) 最大值"或"元素个数 \(< 3\)"。
-
算法选择:
- 补集思想:答案 = 所有非空子集数 \(-\) 不满足多边形条件的子集数 \(-\) 空集
- 排序后 DP:将 \(a\) 升序排序,对每个 \(a_i\) 作为子集最大值时,统计前 \(i-1\) 个元素中选出若干个数使其和 \(\leq a_i\) 的方案数
- 01 背包:\(dp[i][j]\) 表示前 \(i\) 个数中选若干个数使其和为 \(j\) 的方案数
-
关键步骤:
- 读取数据:读入 \(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\)(减去非法子集和空集)
-
时间/空间复杂度:
- 时间复杂度:\(O(n \cdot S)\),\(S = 5000\) 为最大和值
- 空间复杂度:\(O(n \cdot S)\),DP 数组
-
补集 + 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
【算法真传】
浙公网安备 33010602011771号