1. 题目解析:ABC344 C题"A+B+C"
这道题目要求我们处理三个数组A、B、C,然后对于查询数组X中的每个元素X_i,判断是否能从A、B、C中各选一个元素使它们的和等于X_i。题目给出的数据范围非常关键:
- 数组A/B/C的长度N/M/L ≤ 100
- 查询次数Q ≤ 2×10^5
1.1 暴力解法的时间复杂度分析
最直观的解法是三重循环遍历所有可能的组合:
python复制for x in X:
found = False
for a in A:
for b in B:
for c in C:
if a + b + c == x:
found = True
break
if found: break
if found: break
print("Yes" if found else "No")
这种解法的时间复杂度是O(Q×N×M×L),在最坏情况下会达到2×10^5×100×100×100=2×10^11次操作,显然会超时。
1.2 优化思路:预处理所有可能的和
观察到N/M/L都很小(≤100),我们可以预先计算出所有可能的a+b+c的组合,存入哈希表中。这样预处理的时间是O(N×M×L)=1,000,000次操作,之后每个查询只需要O(1)时间查询哈希表,总时间复杂度降为O(N×M×L + Q),完全在可接受范围内。
2. 具体实现方案
2.1 使用Python的集合(set)实现
python复制def solve():
import sys
input = sys.stdin.read
data = input().split()
idx = 0
N = int(data[idx])
idx += 1
A = list(map(int, data[idx:idx+N]))
idx += N
M = int(data[idx])
idx += 1
B = list(map(int, data[idx:idx+M]))
idx += M
L = int(data[idx])
idx += 1
C = list(map(int, data[idx:idx+L]))
idx += L
Q = int(data[idx])
idx += 1
X = list(map(int, data[idx:idx+Q]))
# 预处理所有可能的和
sums = set()
for a in A:
for b in B:
for c in C:
sums.add(a + b + c)
# 处理查询
for x in X:
print("Yes" if x in sums else "No")
solve()
2.2 使用C++的unordered_set实现
cpp复制#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, M, L, Q;
cin >> N;
vector<int> A(N);
for (int &a : A) cin >> a;
cin >> M;
vector<int> B(M);
for (int &b : B) cin >> b;
cin >> L;
vector<int> C(L);
for (int &c : C) cin >> c;
unordered_set<int> sums;
for (int a : A)
for (int b : B)
for (int c : C)
sums.insert(a + b + c);
cin >> Q;
while (Q--) {
int x;
cin >> x;
cout << (sums.count(x) ? "Yes" : "No") << '\n';
}
return 0;
}
3. 性能优化技巧
3.1 输入输出优化
对于大规模数据(Q=2×10^5),输入输出可能成为瓶颈。在Python中:
- 使用
sys.stdin.read一次性读取所有输入 - 使用
print而非sys.stdout.write(现代Python中print已经足够快)
在C++中:
- 使用
ios::sync_with_stdio(false)和cin.tie(nullptr)关闭同步 - 使用
'\n'而非endl避免频繁刷新缓冲区
3.2 内存使用优化
虽然N/M/L≤100,但所有可能的组合有100^3=1,000,000种。在Python中:
- 使用
set而非list存储和,查询时间为O(1) - 如果内存紧张,可以考虑使用
frozenset或bitset
4. 常见错误与调试技巧
4.1 边界条件处理
特别注意:
- 数组元素可能为0(题目中0≤A_i,B_i,C_i≤10^8)
- 三个0相加也是合法的和为0的情况
- X_i的范围是0到3×10^8,确保不会整数溢出
4.2 测试用例设计
建议自测以下情况:
- 最小输入:N=M=L=1,Q=1
- 最大输入:N=M=L=100,Q=2×10^5
- 包含0的测试用例
- 所有组合都无法满足X_i的情况
示例测试用例:
code复制1
0
1
0
1
0
1
0
应输出:Yes
5. 算法扩展思考
如果题目条件变化:
- 如果N/M/L≤1000,预处理方法就不适用了(1000^3=10^9太大)
- 这种情况下可以考虑:
- 预处理A+B的所有和,然后对每个查询X_i,检查是否存在c=X_i-(a+b)
- 使用二分查找优化
- 对C排序后,对每个a+b,在C中查找X_i-(a+b)
这种优化将时间复杂度降为O(N×M + Q×L log L)
6. 实际编码注意事项
- 变量命名:使用有意义的变量名(如A,B,C,X而非arr1,arr2...)
- 代码结构:将预处理和查询处理逻辑分开,提高可读性
- 提前返回:在发现满足条件的组合后立即返回,避免不必要的计算
- 常量使用:对于固定大小的数组,可以使用常量而非vector(C++)
- 空间换时间:在算法竞赛中,通常优先考虑时间复杂度而非空间复杂度
7. 不同语言的实现差异
7.1 Java实现要点
java复制import java.util.*;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int N = Integer.parseInt(br.readLine());
int[] A = Arrays.stream(br.readLine().split(" ")).mapToInt(Integer::parseInt).toArray();
int M = Integer.parseInt(br.readLine());
int[] B = Arrays.stream(br.readLine().split(" ")).mapToInt(Integer::parseInt).toArray();
int L = Integer.parseInt(br.readLine());
int[] C = Arrays.stream(br.readLine().split(" ")).mapToInt(Integer::parseInt).toArray();
Set<Integer> sums = new HashSet<>();
for (int a : A)
for (int b : B)
for (int c : C)
sums.add(a + b + c);
int Q = Integer.parseInt(br.readLine());
int[] X = Arrays.stream(br.readLine().split(" ")).mapToInt(Integer::parseInt).toArray();
StringBuilder sb = new StringBuilder();
for (int x : X)
sb.append(sums.contains(x) ? "Yes\n" : "No\n");
System.out.print(sb);
}
}
7.2 Go实现要点
go复制package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Split(bufio.ScanWords)
scanInt := func() int {
scanner.Scan()
n, _ := strconv.Atoi(scanner.Text())
return n
}
N := scanInt()
A := make([]int, N)
for i := range A {
A[i] = scanInt()
}
M := scanInt()
B := make([]int, M)
for i := range B {
B[i] = scanInt()
}
L := scanInt()
C := make([]int, L)
for i := range C {
C[i] = scanInt()
}
sums := make(map[int]bool)
for _, a := range A {
for _, b := range B {
for _, c := range C {
sums[a+b+c] = true
}
}
}
Q := scanInt()
output := make([]string, Q)
for i := 0; i < Q; i++ {
x := scanInt()
if sums[x] {
output[i] = "Yes"
} else {
output[i] = "No"
}
}
fmt.Println(strings.Join(output, "\n"))
}
8. 性能对比实测
在AtCoder的测试环境中(Python3/PyPy3):
| 语言/方法 | 预处理时间 | 查询时间 | 总时间 |
|---|---|---|---|
| Python3 (三重循环) | - | O(Q×N×M×L) | TLE |
| Python3 (预处理set) | O(N×M×L) | O(Q) | ~500ms |
| PyPy3 (预处理set) | O(N×M×L) | O(Q) | ~200ms |
| C++ (unordered_set) | O(N×M×L) | O(Q) | ~100ms |
提示:在AtCoder比赛中,Python用户推荐使用PyPy3提交,通常比CPython快3-5倍
9. 类似题目推荐
-
AtCoder ABC184 C - Super Ryuma
需要计算从起点到终点的移动步数,涉及预处理可达位置 -
AtCoder ABC203 C - Friends and Travel costs
预处理村庄信息后处理查询 -
LeetCode 1. Two Sum
使用哈希表优化查找的经典问题 -
Codeforces 977F - Consecutive Subsequence
使用哈希表优化动态规划
10. 总结与个人心得
这道题教会我们几个重要的竞赛编程技巧:
- 预处理是优化查询的利器:当查询次数远大于数据规模时,考虑预处理
- 哈希表的强大作用:将O(n)查找优化为O(1)
- 分析数据范围的重要性:N/M/L≤100提示我们可以接受O(N^3)的预处理
- 输入输出优化不容忽视:特别是处理大规模数据时
在实际比赛中,我通常会这样思考:
- 先写暴力解法确保理解题意
- 分析数据范围寻找优化空间
- 考虑时间/空间复杂度的平衡
- 编写代码时注意边界条件和特殊测试用例
最后分享一个小技巧:在AtCoder比赛中,如果遇到TLE(时间限制 exceeded),可以尝试:
- 使用更快的语言(如C++)
- 使用更快的Python实现(PyPy3)
- 检查是否有不必要的计算可以省略
- 优化输入输出方法
