洛谷 P14360 [CSP-J 2025] 多边形 题解
洛谷 P14360 [CSP-J 2025] 多边形 题解
题目大意
有 \(n\) 根小木棍,第 \(i\) (\(1 \leq i \leq n\)) 根小木棍的长度为 \(a_i\)。已知对于长度分别为 \(l_1, l_2, \dots, l_m\) 的 \(m\) 根小木棍,这 \(m\) 根小木棍能拼成一个多边形当且仅当 \(m \geq 3\) 且所有小木棍的长度之和大于所有小木棍的长度最大值的两倍,即 \(\displaystyle \sum_{i=1}^{m} l_i > 2 \times \max_{i=1}^{m} l_i\)。求出选出的小木棍能够拼成一个多边形的方案数对 \(998,244,353\) 取模后的结果。两种方案不同当且仅当选择的小木棍的下标集合不同,即存在 \(1 \leq i \leq n\),使得其中一种方案选择了第 \(i\) 根小木棍,但另一种方案未选择。
题意分析
判断条件转化
当有 \(m\) 根长度分别为 \(l_1, l_2, \dots, l_m\) 的木棍时,能拼成多边形的条件是:
设最长的木棍长度为 \(x\),其他木棍长度之和为 \(s\)。所以其他木棍长度都 \(\leq x\)。则条件变成:
即:
所以问题转化为:
对于每一根木棍,将其作为最长的木棍,统计在不大于它的木棍中选取若干根,使得长度和 \(s>x\) 的方案数。
解题思路
因为要在不大于最长木棍的其余木棍中选取符合条件的木棍,所以首先要对数组进行升序排序,即:
枚举 \(i\) 从 \(1\) 到 \(n\),将 \(a_i\) 当作最长木棍,则需要统计从 \(a_1, a_2, \cdots a_{i-1}\) 中选若干根,长度 \(s>a_i\) 的方案数。
因为前面每根木棍长度都 \(\leq a_i\),如果只选 \(0\) 或 \(1\) 根,则一定有 \(s \leq a_i\)。若满足 \(s>a_i\),则前面至少选 \(2\) 根,再加上当前最长木棍,总数至少为 \(3\) 根,自动满足 \(m \geq 3\)。
DP 实现
可以开一个 \(dp\) 数组,其中,\(dp_{i,j}\) 表示从前 \(i\) 根木棍中选出若干根,长度和恰好为 \(j\) 的方案数。初始 \(dp_{i,0}=1\),表示在和为 \(0\) 的情况下,只能选择空集(有一种方案)。状态转移方程为:\(dp_{i,j} = dp_{i-1,j} + dp_{i-1,j-a_i}\)(非常经典的 01 背包),答案 \(\displaystyle ans= \sum_{i=1}^{n} \sum_{j>a_i} dp_{i-1,j}\)(\(i\) 枚举多边形边数,\(j\) 枚举边数为 \(i\) 时的合法长度和)。
这里有一个问题,\(j\) 到底要枚举到多少。可以设 \(\displaystyle sum = \sum_{i=1}^{n} a_i\),则 \(j\) 枚举到 \(sum\) 即可。
代码实现
#include <cstdio>
#include <algorithm>
using namespace std;
#define mod 998244353
int n, a[5005], dp[5005][25000100], sum;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
sum += a[i];
}
sort(a + 1, a + n + 1);
for (int i = 0; i <= n; i++) dp[i][0] = 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= sum; j++) {
if (j >= a[i]) dp[i][j] = (dp[i - 1][j] + dp[i - 1][j - a[i]]) % mod;
else dp[i][j] = dp[i - 1][j];
}
int ans = 0;
for (int i = 1; i <= n; i++)
for (int j = a[i] + 1; j <= sum; j++)
(ans += dp[i - 1][j]) %= mod;
printf("%d\n", ans);
return 0;
}
算法复杂度分析
考虑到边界情况,\(a_1 = a_2 = \cdots = a_n\),此时,\(\displaystyle sum = \sum_{i=1}^{n} a_i = n \cdot \max_{i=1}^{n} a_i\),内层循环执行 \(\displaystyle n \cdot \max_{i=1}^{n} a_i\) 次,外层循环执行 \(n\) 次,所以时间复杂度为 \(\displaystyle\mathcal O(n^2 \cdot \max_{i=1}^{n} a_i)\)。显然,空间复杂度也是 \(\displaystyle\mathcal O(n^2 \cdot \max_{i=1}^{n} a_i)\)。时空复杂度均无法接受。
优化一:溢出桶
实际上,并不需要考虑 \(\displaystyle \max_{i=1}^{n} a_i\) 到底是多少,直接认为其值为 \(5000\) 即可。若长度和 \(\displaystyle s> \max_{i=1}^{n} a_i\),在这里并不关心 \(s\) 是多少,统计到溢出桶 \(dp_{5001}\) 中。这种优化方式叫做状态压缩。这样内层循环由 \(\displaystyle n \cdot \max_{i=1}^{n} a_i\) 次,降至 \(\displaystyle \max_{i=1}^{n} a_i\) 次,时空复杂度也降至 \(\displaystyle \mathcal O(n \cdot \max_{i=1}^{n} a_i)\),可以接受。
代码实现
#include <cstdio>
#include <algorithm>
using namespace std;
#define mod 998244353
int n, a[5005], dp[5005][5005];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
sort(a + 1, a + n + 1);
for (int i = 0; i <= n; i++) dp[i][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= 5000; j++) {
if (j >= a[i]) dp[i][j] = (dp[i - 1][j] + dp[i - 1][j - a[i]]) % mod;
else dp[i][j] = dp[i - 1][j];
}
dp[i][5001] = dp[i - 1][5001];
for (int j = 5001; j <= 5001 + a[i]; j++) (dp[i][5001] += dp[i - 1][j - a[i]]) %= mod; // 一种偷懒的写法
}
int ans = 0;
for (int i = 1; i <= n; i++)
for (int j = a[i] + 1; j <= 5001; j++)
(ans += dp[i - 1][j]) %= mod;
printf("%d\n", ans);
return 0;
}
优化二:二维化一维
和 01 背包二维数组转化为一维数组类似,这道题也可以采用滚动数组和逆序转移的方法将二维数组压缩成一维。此时,状态转移方程变成了 \(dp_j = dp_j + dp_{j-a_i}\)。
注意:因为统计溢出桶时,需要使用上一轮枚举的数据,所以统计溢出桶要放在状态转移的前面。
#include <cstdio>
#include <algorithm>
using namespace std;
#define mod 998244353
int n, a[5005], dp[5005];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
sort(a + 1, a + n + 1);
int ans = 0;dp[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = a[i] + 1; j <= 5001; j++) (ans += dp[j]) %= mod; // 记录答案
for (int j = 5001; j <= 5001 + a[i]; j++) (dp[5001] += dp[j - a[i]]) %= mod; // 统计溢出桶
for (int j = 5000; j >= a[i]; j--) (dp[j] += dp[j - a[i]]) % mod; // 状态转移
}
printf("%d\n", ans);
return 0;
}

浙公网安备 33010602011771号