统计各长度频率,按边长枚举两根整边和一对短棒;Python 用对齐切片降低双重枚举的循环常数。
OJ: luogu
题目 ID: P3799
难度:普及-
标签:组合计数枚举python
日期: 2026-07-15 21:50
目录
题意
从
即使两根木棒长度相同,只要编号不同,它们仍是不同的选择。数据范围为
思路
四根木棒必须是什么结构
正三角形有三条等长边,而题目要求恰好选择四根木棒。因此三条边中必然有一条由两根木棒拼成,另外两条边各由一根木棒组成。
将选出的四根木棒长度排序为:
它们能够拼成正三角形,当且仅当:
这里
朴素枚举
最直接的方法是枚举任意四根木棒,排序后检查
import sys
from itertools import combinations
MOD = 1_000_000_007
data = list(map(int, sys.stdin.buffer.read().split()))
n = data[0]
sticks = data[1:1 + n]
answer = 0
for chosen in combinations(sticks, 4):
a, b, c, d = sorted(chosen)
if a + b == c == d:
answer += 1
print(answer % MOD)这个程序与题意完全一致,适合作为随机对拍的可信基线,但时间复杂度为
把枚举对象从木棒变成长度
木棒数量很大,但长度只有
count[x] = 长度为 x 的木棒数量枚举正三角形边长
第三条边需要一对满足
- 当
时,选法数为 。 - 当
时,必须从同一长度中选两根不同木棒,选法数为 。
因此,令 pair_choices(s) 表示两根短棒拼出边长
最终答案为:
其中
例如枚举
为什么不会选到同一根木棒
短棒长度
当
原 Python 为什么容易只有部分分
原代码的组合计数公式是正确的,问题主要在运行常数。它用 Python 双重循环预处理所有 left + right,最坏情况下大约执行 625 万次解释器层的乘法、分支和列表访问;在紧时限下,这种写法容易超时。
新代码仍然是 pair_sum,而是只在当前边长确实有两根整边时计算短棒对。更重要的是,它把左右频率切片对齐,再使用:
sum(map(mul, left_counts, right_counts))完成逐项乘积求和。切片、map 和 sum 的主要遍历在 C 层执行,减少了 Python 最内层循环开销。本机最坏频率分布测试中,完整程序由约 2.05s 降到约 0.32s;具体时间会随解释器和评测机变化。
C++ 实现
C++ 可以直接写双重循环。对每个边长 side,枚举满足 left * 2 < side 的 left,令 right = side - left,累加:
count[left] * count[right]若 side 为偶数,再加入中点的组合数。两个组合数都可能接近 long long 溢出。
Python 切片如何对应 C++ 循环
Python 中令:
middle = (side + 1) // 2
left_counts = count[1:middle]
right_counts = count[side - 1:side - middle:-1]当 side = 5 时,两个切片分别对应长度 [1, 2] 和 [4, 3],逐项相乘正好得到:
偶数边长的中点没有放进切片,继续单独使用组合数处理。这样既与 C++ 枚举完全等价,又避免在 Python 中手写最内层循环。
正确性说明
任取一个合法方案,排序后必有 pair_choices(s) 中选到
反过来,程序统计的每一项都包含两根长度为
Python 知识
固定值域使用列表频率表
长度上限明确为 5000,使用 count = [0] * 5001 比字典或 Counter 更直接,下标就是木棒长度,访问开销也更小。
map 可以并行消费多个序列
map(mul, left_counts, right_counts) 每次从两个切片各取一个数,调用一次 operator.mul。它返回迭代器,再由 sum 完成聚合,不需要创建乘积列表。
C++ 到 Python 对照
- C++ 的全局计数数组对应 Python 的整数列表。
- C++ 的
for (left...)对应两个方向相反的对齐切片。 - C++ 使用
long long并防止乘法溢出;Python 整数不会固定宽度溢出,但仍要按题意取模。 - 两种语言中的组合数都写成
x * (x - 1) // 2对应的整数运算。
模仿清单
- 值域较小时,用列表建立频率表。
- 无序二元组按
x < y与x == y分类计数。 - 两段序列逐项运算时,考虑
map(现成二元函数, left, right)。 - 用小数据
combinations暴力验证组合计数公式。
相关 Python 笔记:
/home/rainboy/mycode/hugo-blog/content/program_language/python/collections_toolkit.md:频率统计容器的选择。/home/rainboy/mycode/hugo-blog/content/program_language/python/map_reduce_filter.md:map的多序列映射和惰性迭代。/home/rainboy/mycode/hugo-blog/content/program_language/python/itertools_recipes.md:operator.mul与组合枚举工具。/home/rainboy/mycode/hugo-blog/content/program_language/python/cpp_to_python_pitfalls.md:Python 竞赛代码中的性能常数意识。
代码
C++17 正解
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-19 09:21
* update_at: 2026-07-19 09:29
*/
#include <bits/stdc++.h>
using namespace std;
const int MAX_LENGTH = 5000;
const long long MOD = 1000000007;
int n;
int max_length;
long long count_length[MAX_LENGTH + 1]; // count_length[x] 表示长度 x 的木棒数量
long long choose_two(long long x) {
return x * (x - 1) / 2;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
int length;
cin >> length;
count_length[length]++;
if (max_length < length) max_length = length;
}
long long answer = 0;
// side 是三角形边长,需要两根长度为 side 的整边木棒。
for (int side = 2; side <= max_length; side++) {
if (count_length[side] < 2) continue;
long long pair_choices = 0;
// left < right,避免把同一对短棒按顺序重复计算。
for (int left = 1; left * 2 < side; left++) {
int right = side - left;
pair_choices += count_length[left] * count_length[right];
}
// side 为偶数时,补上 left == right == side / 2 的情况。
if (side % 2 == 0) {
pair_choices += choose_two(count_length[side / 2]);
}
long long same_side_choices = choose_two(count_length[side]);
answer += (same_side_choices % MOD) * (pair_choices % MOD) % MOD;
answer %= MOD;
}
cout << answer << '\n';
return 0;
}Python 正解
import sys
from operator import mul
MOD = 1_000_000_007
MAX_LENGTH = 5000
data = list(map(int, sys.stdin.buffer.read().split()))
n = data[0]
sticks = data[1:1 + n]
count = [0] * (MAX_LENGTH + 1)
for length in sticks:
count[length] += 1
answer = 0
max_length = max(sticks)
for side in range(2, max_length + 1):
side_count = count[side]
if side_count < 2:
continue
# 左端点只取到中点之前,因此每对不同长度的短棒只统计一次。
middle = (side + 1) // 2
left_counts = count[1:middle]
right_counts = count[side - 1:side - middle:-1]
pair_choices = sum(map(mul, left_counts, right_counts))
# side 为偶数时,补上两根短棒长度都为 side // 2 的情况。
if side % 2 == 0:
middle_count = count[side // 2]
pair_choices += middle_count * (middle_count - 1) // 2
same_side_choices = side_count * (side_count - 1) // 2
answer = (answer + same_side_choices * pair_choices) % MOD
print(answer)复杂度
设最大木棒长度为
- 统计频率需要
时间。 - 枚举所有边长和短棒对共需要
时间。 - C++ 和 Python 正解的总时间复杂度均为
,空间复杂度均为 。 brute.py的时间复杂度为,只用于小数据验证。
Python 每轮创建的两个切片最多占用
总结
本题的决定性观察是:四根木棒拼正三角形,排序后一定满足
C++ 直接使用双循环即可;Python 在算法正确的基础上还要控制解释器循环常数,通过对齐切片、map(operator.mul, ...) 和 sum 批量完成乘积求和,才能更稳妥地通过紧时限测试。