A 判断闰年
解题思路
这道题的目标是:给你一个年份,判断它是不是闰年。
那什么是闰年呢?我们平时说的一年是365天,但地球绕太阳一圈其实多出一点点时间(大约365.2422天)。为了补上这个“零头”,每过几年就要多加一天——这一天就是2月29日,而有2月29日的年份就叫闰年。
根据公历规定,判断闰年的规则如下:
- 如果年份能被 4 整除,并且不能被 100 整除 → 是闰年;
- 或者,年份能被 400 整除 → 也是闰年。
举几个例子:
- 2000 年:能被 400 整除 → 闰年 ✅
- 1900 年:能被 100 整除但不能被 400 整除 → 不是闰年 ❌
- 2024 年:能被 4 整除,且不能被 100 整除 → 闰年 ✅
- 2023 年:不能被 4 整除 → 不是闰年 ❌
所以,我们只需要把上面两条规则写成 if 条件判断就可以了!
💡 小提示:在编程中,
%是“取余”运算符。比如2024 % 4 == 0表示 2024 除以 4 没有余数,也就是能被 4 整除。
参考代码
- C++
#include <iostream>
using namespace std;
int main() {
int year;
cin >> year;
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
cout << "Y" << endl;
} else {
cout << "N" << endl;
}
return 0;
}
- Java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int year = sc.nextInt();
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
System.out.println("Y");
} else {
System.out.println("N");
}
}
}
- Python
year = int(input())
if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0):
print("Y")
else:
print("N")
B 判断素数
解题思路
什么是素数?
素数(也叫质数)是指:大于1,并且除了1和它自己之外,不能被其他任何正整数整除的数。
比如:
- 2:只能被 1 和 2 整除 → 是素数 ✅
- 3:只能被 1 和 3 整除 → 是素数 ✅
- 4:能被 2 整除 → 不是素数 ❌
- 1:不符合“大于1”的条件 → 不是素数 ❌
最简单的做法(暴力法)
我们可以从 2 开始,一直试到 n-1,看看有没有哪个数能整除 n。如果找到了,就说明不是素数;如果一个都没找到,那就是素数。
但这样效率很低,特别是当 n 很大的时候(比如 100000),程序会跑很久。
- C/C++, Java也差不多
bool isPrime(int n) {
if (n <= 1) return false;
for (int i = 2; i < n; i++) {
if (n % i == 0) return false;
}
return true;
}
- python
def is_prime(n):
if n <= 1:
return False
for i in range(2, n):
if n % i == 0:
return False
return True
优化一:只检查到 √n
其实我们不需要检查到 n-1!
原因:如果 n 能被某个大于 √n 的数整除,那一定也能被一个小于 √n 的数整除(因为两个因子相乘等于 n,不可能两个都大于 √n)。
举个例子:
n = 36,√36 = 6。
如果我们发现 36 能被 9 整除(9 > 6),那它也一定能被 4 整除(因为 36 ÷ 9 = 4,而 4 < 6)。
所以只要检查到 6 就够了!
因此,循环条件可以改成:i * i <= n(等价于 i <= √n)。
优化二:跳过偶数
除了 2 以外,所有偶数都不是素数(因为都能被 2 整除)。所以我们可以:
- 先单独判断
n == 2; - 再判断
n是不是偶数(n % 2 == 0),如果是,直接返回false; - 然后只检查奇数(从 3 开始,每次加 2)。
- C++
bool isPrime(int n) {
if (n <= 1) return false;
if (n == 2) return true;
if (n % 2 == 0) return false; // 排除所有偶数
for (int i = 3; 1LL * i * i <= n; i += 2) { // i*i 可能溢出,用 1LL 强制 long long
if (n % i == 0) return false;
}
return true;
}
- python
def is_prime(n):
if n <= 1:
return False
if n == 2:
return True
if n % 2 == 0:
return False
i = 3
while i * i <= n:
if n % i == 0:
return False
i += 2
return True
终极优化:6k±1 法(推荐掌握)
数学上有一个结论:所有大于 3 的素数,都可以写成 6k ± 1 的形式(比如 5=6×1−1,7=6×1+1,11=6×2−1……)。
反过来,不是 6k±1 形式的数,一定不是素数(除了 2 和 3)。
所以我们可以:
- 先处理小于等于 3 的情况;
- 排除 2 和 3 的倍数;
- 然后从 5 开始,每次增加 6,检查
i和i+2(即6k−1和6k+1)是否能整除n。
这样能跳过更多不必要的检查,速度更快!
✅ 这个写法既高效又简洁,建议大家记住这个模板!
参考代码
- C++
#include <iostream>
using namespace std;
bool isPrime(int n) {
if (n <= 1) return false;
if (n <= 3) return true;
if (n % 2 == 0 || n % 3 == 0) return false;
for (int i = 5; 1LL * i * i <= n; i += 6) {
if (n % i == 0 || n % (i + 2) == 0)
return false;
}
return true;
}
int main() {
int n;
cin >> n;
if (isPrime(n)) {
cout << "Yes" << endl;
} else {
cout << "No" << endl;
}
return 0;
}
- java
import java.util.Scanner;
public class Main {
private static boolean isPrime(int n) {
if (n <= 1) return false; // 小于1不是素数
if (n <= 3) return true; // 2, 3是素数
if (n % 2 == 0 || n % 3 == 0) return false; // 2, 3的倍数不是素数
// 从5开始检查,每次增加6,检查是否能被i或i+2整除
for (int i = 5; (long) i * i <= n; i += 6) {
if (n % i == 0 || n % (i + 2) == 0) return false;
}
return true;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
if (isPrime(n)) {
System.out.println("Yes");
} else {
System.out.println("No");
}
}
}
- python
def is_prime(n):
if n <= 1:
return False
if n <= 3:
return True
if n % 2 == 0 or n % 3 == 0:
return False
i = 5
while i * i <= n:
if n % i == 0 or n % (i + 2) == 0:
return False
i += 6
return True
n = int(input())
print("Yes" if is_prime(n) else "No")
C 求和
解题思路
题目意思
给定 n 个整数,要求计算:所有两两不同位置的数相乘,再把结果全部加起来。
例如:输入 [1, 2, 3],那么要算:
1×2 + 1×3 + 2×3 = 2 + 3 + 6 = 11
方法一:暴力双重循环(适合小数据)
最直观的想法就是:用两个 for 循环,外层遍历每个数,内层遍历它后面的所有数,相乘后累加。
但要注意:
- 结果可能很大!比如 1000 个 1000 相乘再相加,结果远超
int范围(最大约 20 亿)。 - 所以要用
long类型来存总和(long能存到约 9×10¹⁸)。
这种方法虽然简单,但如果 n 很大(比如 10⁵),循环次数会达到 50 亿次(≈ n²/2),远远超过 1 秒能处理的范围(一般最多 1 亿次操作),就会“超时”。
📌 本次测试数据比较小,所以暴力也能过。但在正式比赛中,通常不会让你这么轻松过关!
方法二:用前缀和优化(推荐学习)
我们换个角度看问题:
原式子可以拆成:
a₁×(a₂ + a₃ + ... + aₙ)
+ a₂×(a₃ + a₄ + ... + aₙ)
+ ...
+ aₙ₋₁×aₙ
你会发现,括号里的部分其实是从某个位置到末尾的和。
这时候就可以用一个叫 前缀和 的技巧!
什么是前缀和?
前缀和就是一个数组 prefix,其中:
prefix[i] = a₁ + a₂ + ... + aᵢ
那么,从第 i+1 项到第 n 项的和就是:
prefix[n] - prefix[i]
这样,我们只需要一次循环预处理前缀和,然后再一次循环计算答案,总共只用 O(n) 时间!
🎥 如果你还不太懂前缀和,强烈推荐看这个入门视频:前缀和及其变种,十道题检验你是不是真的学会了
💡 提示:前缀和版本虽然多了一点代码,但当 n 很大时(比如 10⁵),它比暴力快 上万倍!
参考代码
方法一: 暴力枚举(语法基础必会)
- java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = sc.nextInt();
}
long sum = 0;
// 双重循环:i 从 0 到 n-2,j 从 i+1 到 n-1
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
sum += (long) nums[i] * nums[j]; // 强制转 long 防止溢出
}
}
System.out.println(sum);
}
}
- c++
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
long long sum = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
sum += 1LL * a[i] * a[j]; // 1LL 防止乘法溢出
}
}
cout << sum << endl;
return 0;
}
- python
n = int(input())
a = list(map(int, input().split()))
total = 0
for i in range(n):
for j in range(i + 1, n):
total += a[i] * a[j]
print(total)
方法二:前缀和(基础算法必会)
💡 小技巧:为了让代码更清晰,我们让数组下标从 1 开始(而不是默认的 0),这样
prefix[i]就正好表示前i个数的和,不容易出错。
- java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
// 为了让下标从1开始,方便计算前缀和,数组开大一点
int[] nums = new int[n + 1];
long[] prefix = new long[n + 1]; // 前缀和用 long,防止中间结果溢出
for (int i = 1; i <= n; i++) {
nums[i] = sc.nextInt();
prefix[i] = prefix[i - 1] + nums[i]; // prefix[i] = 前i个数的和
}
long sum = 0;
// 对每个 i(从1到n-1),计算 nums[i] * (后面所有数的和)
for (int i = 1; i < n; i++) {
long suffixSum = prefix[n] - prefix[i]; // 从 i+1 到 n 的和
sum += (long) nums[i] * suffixSum;
}
System.out.println(sum);
}
}
- c++
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n;
cin >> n;
vector<long long> a(n + 1), prefix(n + 1, 0);
for (int i = 1; i <= n; i++) {
cin >> a[i];
prefix[i] = prefix[i - 1] + a[i];
}
long long sum = 0;
for (int i = 1; i < n; i++) {
sum += a[i] * (prefix[n] - prefix[i]);
}
cout << sum << endl;
return 0;
}
- python
n = int(input())
a = [0] + list(map(int, input().split())) # 下标从1开始
prefix = [0] * (n + 1)
for i in range(1, n + 1):
prefix[i] = prefix[i - 1] + a[i]
total = 0
for i in range(1, n):
total += a[i] * (prefix[n] - prefix[i])
print(total)
J 数列求和
解题思路
这是一道典型的“签到题”——看起来在最后一题,其实非常简单。
题目要求计算 1 + 2 + 3 + \cdots + n。虽然题目说“不能用高斯公式”,但这只是为了让你练习 for 循环累加。实际上,在正式比赛中,只要输出正确,怎么算都行!
因此我们提供两种写法:
- 方法一:用循环从 1 加到 n(符合题目“练习循环”的意图);
- 方法二:直接用等差数列求和公式 (1 + n) \times n / 2(更快更简洁,比赛中推荐)。
💡 小提示:即使本题 n \le 100 不会溢出,养成用
long存求和结果的习惯是好做法!
参考代码
方法一:循环累加
- C++
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
long long sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
cout << sum << endl;
return 0;
}
- Java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
long sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
System.out.println(sum);
}
}
- Python
n = int(input())
total = 0
for i in range(1, n + 1):
total += i
print(total)
方法二:公式法(推荐)
- C++
#include <iostream>
using namespace std;
int main() {
long long n;
cin >> n;
cout << (1 + n) * n / 2 << endl;
return 0;
}
- Java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
long n = sc.nextLong();
System.out.println((1 + n) * n / 2);
}
}
- Python
n = int(input())
print((1 + n) * n // 2)
✅ 公式法不仅代码短,而且时间复杂度是 O(1),无论 n 多大都能瞬间出结果!
I 小鱼比可爱
解题思路
每只小鱼只能看到它左边的鱼,要统计左边有多少只鱼的可爱值严格小于自己。
我们可以对每只鱼 i(从左到右),遍历它左边的所有鱼 j(j < i),如果 a[j] < a[i],就计数加一。
这就是经典的 双重循环枚举,时间复杂度 O(n²)。由于题目没给数据范围,但作为基础题,通常 n \le 1000,完全不会超时。
💡 编程建议:采用“输入 → 处理 → 输出”三步走,逻辑清晰,便于调试和理解。
参考代码
- C++
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
// 记录每个小鱼的结果
vector<int> ans(n, 0);
// 外层枚举每个小鱼
for (int i = 0; i < n; i++) {
// 内层枚举它左边的所有小鱼
for (int j = 0; j < i; j++) {
if (a[j] < a[i]) {
ans[i]++;
}
}
}
for (int i = 0; i < n; i++) {
cout << ans[i] << " ";
}
cout << endl;
return 0;
}
- Java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = sc.nextInt();
}
// 记录每个小鱼的结果
int[] result = new int[n];
// 外层枚举每个小鱼
for (int i = 0; i < n; i++) {
// 内层枚举它左边的所有小鱼
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
result[i]++;
}
}
}
// 输出结果
for (int i = 0; i < n; i++) {
System.out.print(result[i] + " ");
}
}
}
- Python
n = int(input())
a = list(map(int, input().split()))
ans = []
for i in range(n):
cnt = 0
for j in range(i):
if a[j] < a[i]:
cnt += 1
ans.append(cnt)
print(" ".join(map(str, ans)))
E 优秀的拆分
解题思路
题目要求把一个正整数 n 拆成若干个 不同的 2 的正整数次幂(即 2^1=2, 2^2=4, 2^3=8, \dots),不能包含 2^0 = 1。
关键观察:
- 所有 2 的正整数次幂都是 偶数;
- 若干个偶数相加,结果一定是 偶数;
- 所以 奇数不可能被拆成这样的形式 → 直接输出
-1。
对于偶数 n,我们可以把它看成 二进制表示中去掉最低位(即去掉 2⁰ 位)后的部分。
例如:
- n = 10,二进制是
1010,其中:- 第 1 位(2¹)是 1 → 包含 2;
- 第 3 位(2³)是 1 → 包含 8;
- 所以拆分为
8 2(从大到小输出)。
✅ 因此,只需检查二进制中 第 1 位及以上(即 k ≥ 1)哪些位是 1,对应的 2^k 就是拆分项。
参考代码
- C++
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
// 奇数无法拆分(因为不能用1)
if (n % 2 != 0) {
cout << -1 << endl;
return 0;
}
vector<int> parts;
// 从 k=1 开始(对应 2^1 = 2)
for (int k = 1; (1 << k) <= n; k++) {
if (n & (1 << k)) {
parts.push_back(1 << k);
}
}
// 从大到小输出
reverse(parts.begin(), parts.end());
for (int i = 0; i < parts.size(); i++) {
cout << parts[i] << " ";
}
cout << endl;
return 0;
}
- Java
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
if (n % 2 != 0) {
System.out.println(-1);
return;
}
List<Integer> parts = new ArrayList<>();
for (int k = 1; (1 << k) <= n; k++) {
if ((n & (1 << k)) != 0) {
parts.add(1 << k);
}
}
Collections.reverse(parts);
for (int i = 0; i < parts.size(); i++) {
System.out.print(parts.get(i) + " ");
}
System.out.println();
}
}
- Python
n = int(input())
if n % 2 == 1:
print(-1)
else:
parts = []
k = 1
while (1 << k) <= n:
if n & (1 << k):
parts.append(1 << k)
k += 1
# 从大到小输出
parts.reverse()
print(" ".join(map(str, parts)))
💡 补充说明:这种拆分本质上就是 n 的二进制表示中,去掉最低位后剩下的 1 所在的位置对应的 2 的幂,且题目保证方案唯一。
D 最大通过数
题目大意
洛洛从左边打关卡(共 n 关,第 i 关耗电 a[i]),
晶晶从右边打关卡(共 m 关,第 i 关耗电 b[i])。
两人必须从第一关开始连续打(不能跳关),总能量为 k。问:两人最多一共能通过多少关?
✅ 示例:
n=2, m=2, k=10
a = [1, 2],b = [3, 5]
最优方案:洛洛打 2 关(耗 3),晶晶打 1 关(耗 3),共 3 关。
💡 初学者建议
前缀和在前面的题目中介绍了,但如果你还不熟悉二分查找,强烈推荐先看以下视频建立直观理解:
- 灵茶山艾府:二分查找,三种写法,四种类型,一网打尽(简洁高效)
- 【红蓝染色法】二分查找为什么总是写错?(五点七边的写法,形象好记,我愿意称之为邪道,但好用的邪门)
- 手把手撕出正确的二分法(代码随想录,讲边界控制)
学完再回来看本题,你会觉得“原来二分这么自然”!
解题思路
两种正确解法概览
| 方法 | 核心思想 | 时间复杂度 | 特点 |
|---|---|---|---|
| 方法一:枚举 + 二分 | 枚举洛洛打多少关 i,用二分快速找晶晶最多能打多少关 j |
O(n log m) | ✅ 逻辑清晰,适合初学者 |
| 方法二:双指针 | 利用单调性:i 增大 ⇒ j 不增,用两个指针滑动扫描 |
O(n + m) | ⚡ 更快更省空间,适合进阶 |
💡 建议学习顺序:先掌握方法一,再理解方法二。
方法一:枚举 + 二分(推荐初学者)
核心思想
-
预处理前缀和:
prefixA[i] = a[0] + ... + a[i-1]→ 洛洛打前i关的总耗电prefixB[j] = b[0] + ... + b[j-1]→ 晶晶打前j关的总耗电- 特别地:
prefixA[0] = prefixB[0] = 0(打0关不耗能)
-
枚举洛洛打多少关
i(0 ≤ i ≤ n):- 如果
prefixA[i] > k,说说明洛洛自己就超了,后面不用看了(因为前缀和递增) - 否则,晶晶最多能用
rest = k - prefixA[i]的能量 - 在
prefixB中找最大的j,使得prefixB[j] ≤ rest
- 如果
-
因为
prefixB是非递减数组,可以用 二分查找 快速找到这个j -
答案就是所有
i + j中的最大值
🎯 为什么正确?
我们穷举了洛洛所有可能的打法,对每种打法都让晶晶打得尽可能多——这一定是全局最优!
方法二:双指针(进阶优化)
我们不枚举所有组合,而是利用一个重要的性质:
当洛洛打得越来越多(
i增大),他消耗的能量sumA增加,
那么留给晶晶的能量k - sumA就越来越少,
所以晶晶能打的关数j不会增加,只会减少或不变。
这意味着:j 是随着 i 增加而单调不增的!
于是我们可以:
- 先让晶晶尽可能多打(
j最大) - 然后让洛洛从
i = 0开始逐步多打 - 每次洛洛多打一关,如果总能量超了,就让晶晶退关(
j--) - 每一步都记录最大的
i + j
整个过程,i 从 0 → n,j 从 m → 0,每个指针只走一遍,时间复杂度 O(n + m)!
✅ 为什么不会漏解?
对于每个i,我们都找到了当前能量下晶晶能打的最大j,且利用单调性避免重复计算。
参考代码
方法一:枚举 + 二分
- C++
#include <iostream>
#include <vector>
using namespace std;
// 在非递减数组 arr 中,找最大的下标 idx,使得 arr[idx] <= target
int binarySearch(const vector<long long>& arr, long long target) {
int left = 0;
int right = (int)arr.size() - 1; // arr.size() = m+1
int result = 0; // 至少可以打0关(arr[0]=0)
while (left <= right) {
int mid = (left + right) / 2;
if (arr[mid] <= target) {
result = mid; // mid 是一个合法答案
left = mid + 1; // 尝试找更大的 j(往右)
} else {
right = mid - 1; // 太大了,往左找
}
}
return result;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
long long k;
cin >> n >> m >> k;
// 构建 prefixA[0..n]:prefixA[i] = 前 i 关总耗电
vector<long long> prefixA(n + 1, 0);
for (int i = 1; i <= n; i++) {
long long x;
cin >> x;
prefixA[i] = prefixA[i - 1] + x;
}
// 构建 prefixB[0..m]
vector<long long> prefixB(m + 1, 0);
for (int i = 1; i <= m; i++) {
long long x;
cin >> x;
prefixB[i] = prefixB[i - 1] + x;
}
int ans = 0;
// 枚举洛洛打前 i 关(i 从 0 到 n)
for (int i = 0; i <= n; i++) {
if (prefixA[i] > k) {
break; // 后面 i 更大,肯定也超,提前退出
}
long long rest = k - prefixA[i]; // 剩给晶晶的能量
int j = binarySearch(prefixB, rest); // 晶晶最多打 j 关
ans = max(ans, i + j);
}
cout << ans << '\n';
return 0;
}
- Java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
long k = sc.nextLong();
long[] prefixA = new long[n + 1];
for (int i = 1; i <= n; i++) {
prefixA[i] = prefixA[i - 1] + sc.nextLong();
}
long[] prefixB = new long[m + 1];
for (int i = 1; i <= m; i++) {
prefixB[i] = prefixB[i - 1] + sc.nextLong();
}
int ans = 0;
for (int i = 0; i <= n; i++) {
if (prefixA[i] > k) break;
long rest = k - prefixA[i];
int j = binarySearch(prefixB, rest);
ans = Math.max(ans, i + j);
}
System.out.println(ans);
sc.close();
}
public static int binarySearch(long[] arr, long target) {
int left = 0, right = arr.length - 1, res = 0;
while (left <= right) {
int mid = (left + right) / 2;
if (arr[mid] <= target) {
res = mid;
left = mid + 1;
} else {
right = mid - 1;
}
}
return res;
}
}
- Python(利用 bisect 模块)
import bisect
n, m, k = map(int, input().split())
a = list(map(int, input().split()))
b = list(map(int, input().split()))
# 构建前缀和(含0)
prefixA = [0]
for x in a:
prefixA.append(prefixA[-1] + x)
prefixB = [0]
for x in b:
prefixB.append(prefixB[-1] + x)
ans = 0
for i in range(len(prefixA)):
if prefixA[i] > k:
break
rest = k - prefixA[i]
# bisect_right 返回第一个 > rest 的位置,即最大合法 j
j = bisect.bisect_right(prefixB, rest)
ans = max(ans, i + j - 1) # 因为 prefixB[0] 对应 j=0
print(ans)
方法二:双指针(进阶优化)
- C++
#include <iostream>
#include <vector>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
long long k;
cin >> n >> m >> k;
vector<int> a(n), b(m);
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0; i < m; i++) cin >> b[i];
long long sumA = 0; // 洛洛当前总耗电
long long sumB = 0; // 晶晶当前总耗电
int j = 0; // 晶晶当前打了多少关
// 第一步:让晶晶尽可能多打(贪心打满)
while (j < m && sumB + b[j] <= k) {
sumB += b[j];
j++;
}
int ans = j; // 此时洛洛打0关,晶晶打j关
// 第二步:枚举洛洛打多少关(i 从 1 到 n)
for (int i = 1; i <= n; i++) {
sumA += a[i - 1]; // 洛洛打第 i 关(下标 i-1)
if (sumA > k) break; // 洛洛自己就超了,后面不可能更优
// 能量超了?让晶晶不断退关,直到总能量 ≤ k
while (j > 0 && sumA + sumB > k) {
j--; // 退掉最后一关
sumB -= b[j]; // 减去该关耗电
}
// 现在 sumA + sumB <= k,是合法方案
ans = max(ans, i + j);
}
cout << ans << '\n';
return 0;
}
- Java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
long k = sc.nextLong();
int[] a = new int[n];
int[] b = new int[m];
for (int i = 0; i < n; i++) a[i] = sc.nextInt();
for (int i = 0; i < m; i++) b[i] = sc.nextInt();
long sumA = 0, sumB = 0;
int j = 0;
// 晶晶先打满
while (j < m && sumB + b[j] <= k) {
sumB += b[j];
j++;
}
int ans = j;
// 洛洛逐步多打
for (int i = 1; i <= n; i++) {
sumA += a[i - 1];
if (sumA > k) break;
while (j > 0 && sumA + sumB > k) {
j--;
sumB -= b[j];
}
ans = Math.max(ans, i + j);
}
System.out.println(ans);
sc.close();
}
}
- Python
n, m, k = map(int, input().split())
a = list(map(int, input().split()))
b = list(map(int, input().split()))
sumA = 0
sumB = 0
j = 0
# 晶晶先打满
while j < m and sumB + b[j] <= k:
sumB += b[j]
j += 1
ans = j
# 洛洛逐步多打
for i in range(1, n + 1):
sumA += a[i - 1]
if sumA > k:
break
while j > 0 and sumA + sumB > k:
j -= 1
sumB -= b[j]
ans = max(ans, i + j)
print(ans)
⏱️ 复杂度分析(方法二)
- 时间:O(n + m)(每个指针最多走一遍)
- 空间:O(1)(除输入外,只用几个变量)
总结
| 方法 | 思路 | 时间 |
|---|---|---|
| 枚举 + 二分 | 枚举一方,二分另一方 | O(n log m) |
| 双指针 | 利用单调性滑动窗口 | O(n + m) |
H 并查集
解题思路
题目初始时有 n 个集合,每个集合只包含一个数字:{1}, {2}, ..., {n}。随后进行 m 次操作,分为两类:
M a b:把a和b所在的集合合并;Q a b:查询a和b是否在同一个集合中。
这正是 并查集(Union-Find / Disjoint Set Union, DSU) 的经典应用场景!
🧩 类比理解:想象每个数字是一个人,一开始每个人都自成一派(自己是老大)。
M a b就是让a和b所在的帮派合并,选一个新老大;Q a b就是问:“你们两个的老大是不是同一个人?”
核心操作
并查集只需两个函数:
find(x):找到x所在集合的“代表”(根节点);union(a, b):将a和b所在集合合并。
为了高效处理最多 10^5 次操作,我们需要两种优化:
- ✅ 路径压缩:在
find时,把沿途所有节点直接连到根上,下次查找更快; - ✅ 按秩合并(可选):合并时把“矮树”接到“高树”下,避免树退化成链。
💡 实际竞赛中,只写路径压缩通常就足够快了!因为路径压缩本身已经让均摊复杂度接近常数。按秩合并更多是为了理论严谨性,且在某些变形题中有用。
💡 给初学者的学习建议
如果你还没学过并查集,强烈推荐先看以下视频建立直观理解:
- 【动画讲解】并查集 - 基本操作、路径压缩、按秩合并(系统教学,含代码)
- 超简单60秒看懂并查集(这个不是教程,是整活的())
看完再回来看代码,你会觉得“原来如此”!
参考代码
方法:带路径压缩 + 按秩合并(标准高效版)
- C++
#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 100010; // 最大点数:题目中 n <= 1e5,留一点余量
// parent[i] 表示节点 i 的父节点,初始时每个点自己是自己的根
int parent[MAXN];
// rank[i] 表示以 i 为根的树的高度(用于按秩合并优化)
int rank_[MAXN]; // 注意:不能叫 rank,因为 C++ 标准库中有同名函数
// 查找 x 所在集合的根节点,并进行路径压缩
int find(int x) {
// 如果 x 的父节点不是自己,说明还没到根
if (parent[x] != x) {
// 递归找到根,同时把 x 直接连到根上(这就是“路径压缩”)
parent[x] = find(parent[x]);
}
return parent[x]; // 返回根节点
}
// 合并 a 和 b 所在的两个集合
void unite(int a, int b) {
int rootA = find(a); // 找到 a 的根
int rootB = find(b); // 找到 b 的根
// 如果根相同,说明已经在同一个集合,无需合并
if (rootA == rootB) {
return;
}
// 按秩合并:尽量把矮的树接到高的树下面,避免树太高
if (rank_[rootA] < rank_[rootB]) {
parent[rootA] = rootB; // A 树更矮,接到 B 的根下
} else if (rank_[rootA] > rank_[rootB]) {
parent[rootB] = rootA; // B 树更矮,接到 A 的根下
} else {
// 两棵树高度一样,随便接一个(这里把 B 接到 A 下)
parent[rootB] = rootA;
rank_[rootA]++; // 此时新树的高度增加 1
}
}
// 判断 a 和 b 是否在同一集合
bool same(int a, int b) {
return find(a) == find(b);
}
int main() {
ios::sync_with_stdio(false); // 加速输入输出(可选但推荐)
cin.tie(nullptr);
int n, m;
cin >> n >> m;
// 初始化:每个数字 i 自成一个集合,父节点是自己,高度为 0
for (int i = 1; i <= n; i++) {
parent[i] = i;
rank_[i] = 0;
}
// 处理 m 次操作
while (m--) {
char op;
int a, b;
cin >> op >> a >> b;
if (op == 'M') {
unite(a, b); // 合并 a 和 b 所在集合
} else if (op == 'Q') {
if (same(a, b)) {
cout << "Yes\n";
} else {
cout << "No\n";
}
}
}
return 0;
}
- Java
import java.util.Scanner;
public class Main {
static int[] parent;
static int[] rank;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
parent = new int[n + 1];
rank = new int[n + 1];
for (int i = 1; i <= n; i++) {
parent[i] = i;
rank[i] = 0;
}
for (int i = 0; i < m; i++) {
String op = sc.next();
int a = sc.nextInt();
int b = sc.nextInt();
if (op.equals("M")) {
union(a, b);
} else if (op.equals("Q")) {
System.out.println(find(a) == find(b) ? "Yes" : "No");
}
}
sc.close();
}
static int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
static void union(int a, int b) {
int rootA = find(a);
int rootB = find(b);
if (rootA == rootB) return;
if (rank[rootA] < rank[rootB]) {
parent[rootA] = rootB;
} else if (rank[rootA] > rank[rootB]) {
parent[rootB] = rootA;
} else {
parent[rootB] = rootA;
rank[rootA]++;
}
}
}
- Python
import sys
sys.setrecursionlimit(200000)
def main():
data = sys.stdin.read().split()
it = iter(data)
n = int(next(it))
m = int(next(it))
parent = list(range(n + 1))
rank = [0] * (n + 1)
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # 路径压缩
return parent[x]
def union(a, b):
ra, rb = find(a), find(b)
if ra == rb:
return
# 按秩合并
if rank[ra] < rank[rb]:
parent[ra] = rb
elif rank[ra] > rank[rb]:
parent[rb] = ra
else:
parent[rb] = ra
rank[ra] += 1
out_lines = []
for _ in range(m):
op = next(it)
a = int(next(it))
b = int(next(it))
if op == 'M':
union(a, b)
else: # op == 'Q'
out_lines.append("Yes" if find(a) == find(b) else "No")
print("\n".join(out_lines))
if __name__ == "__main__":
main()
简化版提示(竞赛常用)
如果你觉得按秩合并麻烦,只保留路径压缩也完全可以通过本题!例如 find 函数不变,union 可简化为:
void unite(int a, int b) {
parent[find(a)] = find(b);
}
虽然最坏情况树可能变高,但由于路径压缩的存在,实际运行速度依然很快,且代码更短。
✅ 结论:对于大多数 OJ 题目(包括本题),路径压缩已经足够!
复杂度分析
- 时间复杂度:每次操作均摊 O(\alpha(n)),其中 \alpha 是反阿克曼函数,对 n \le 10^5 来说 \alpha(n) \le 5,可视为常数;
- 空间复杂度:O(n)。
完全满足 n, m \le 10^5 的要求。
F 孤岛计数
解题思路
题目给你一个由 0(水)和 1(陆地)组成的 n×n 矩阵,要求统计 岛屿的数量。
🌊 什么是岛屿?
相邻(上下左右)的1连在一起,就算作同一个岛屿。
比如:1 0 1 1 0 0 0 1 0有 3 个互不相连的
1块 → 所以答案是 3。
这个问题本质上是:在二维网格中统计连通块的数量,是图论中的经典问题。
核心思想:遍历 + “淹没”岛屿
我们可以这样做:
- 从左到右、从上到下扫描整个矩阵;
- 每当遇到一个
1(且没被访问过),说明发现了一个新岛屿; - 然后用 DFS(深度优先搜索) 或 BFS(广度优先搜索) 把这个岛屿“全部标记为已访问”,防止重复计数;
- 继续扫描,直到结束。
💡 就像你在地图上看到一块陆地,就派无人机飞过去把整片岛都拍一遍(标记),下次再看到就知道是同一块了。
关于是否修改原矩阵
有些写法会直接把 grid[i][j] = 1 改成 0 来表示“已访问”。虽然节省空间,但破坏了原始数据。
更规范的做法是:额外开一个 visited 布尔数组,只读原矩阵,这样更安全、更通用。
下面两种方法我们都采用 不修改原矩阵 的方式。
💡 给初学者的学习建议
如果你还没学过 递归、DFS(深度优先搜索)或 BFS(广度优先搜索),直接看代码可能会感到困惑。别担心!推荐按以下顺序观看配套教学视频,循序渐进掌握核心思想:
- 如果你还不懂「递归」:• 【递归1】递归中的逆向思维• 【递归2】如何治疗“晕”递归• 打破天赋壁垒:快速掌握递归
- 如果你已会递归,想学 DFS/BFS:
• 第09期:DFS 基础|迷宫行走问题
• 第10期:DFS 进阶|回溯与所有路径
• 第13期:BFS 超详细讲解|迷宫最短路径
学完后再回来看本题解,你会豁然开朗!
方法一:DFS(深度优先搜索)
DFS 的思路是:“从当前点出发,能走多远就走多远,一条路走到黑”。
- 使用递归实现;
- 每次向四个方向(上下左右)尝试扩展;
- 遇到边界、水或已访问的格子就返回。
参考代码
- C++
#include <iostream>
#include <vector>
using namespace std;
void dfs(const vector<vector<int>>& grid, vector<vector<bool>>& visited, int i, int j, int n) {
// 越界 or 是水 or 已访问 → 停止
if (i < 0 || j < 0 || i >= n || j >= n || grid[i][j] == 0 || visited[i][j]) {
return;
}
visited[i][j] = true; // 标记为已访问
// 递归四个方向
dfs(grid, visited, i - 1, j, n); // 上
dfs(grid, visited, i + 1, j, n); // 下
dfs(grid, visited, i, j - 1, n); // 左
dfs(grid, visited, i, j + 1, n); // 右
}
int main() {
int n;
cin >> n;
vector<vector<int>> grid(n, vector<int>(n));
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cin >> grid[i][j];
}
}
vector<vector<bool>> visited(n, vector<bool>(n, false));
int count = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1 && !visited[i][j]) {
dfs(grid, visited, i, j, n);
count++;
}
}
}
cout << count << endl;
return 0;
}
- Java
import java.util.Scanner;
public class Main {
private static void dfs(int[][] grid, boolean[][] visited, int i, int j, int n) {
if (i < 0 || j < 0 || i >= n || j >= n || grid[i][j] == 0 || visited[i][j]) {
return;
}
visited[i][j] = true;
dfs(grid, visited, i - 1, j, n); // 上
dfs(grid, visited, i + 1, j, n); // 下
dfs(grid, visited, i, j - 1, n); // 左
dfs(grid, visited, i, j + 1, n); // 右
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[][] grid = new int[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
grid[i][j] = sc.nextInt();
}
}
boolean[][] visited = new boolean[n][n];
int count = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1 && !visited[i][j]) {
dfs(grid, visited, i, j, n);
count++;
}
}
}
System.out.println(count);
sc.close();
}
}
- Python
def dfs(grid, visited, i, j, n):
if i < 0 or j < 0 or i >= n or j >= n or grid[i][j] == 0 or visited[i][j]:
return
visited[i][j] = True
# 四个方向
dfs(grid, visited, i - 1, j, n)
dfs(grid, visited, i + 1, j, n)
dfs(grid, visited, i, j - 1, n)
dfs(grid, visited, i, j + 1, n)
n = int(input())
grid = []
for _ in range(n):
row = list(map(int, input().split()))
grid.append(row)
visited = [[False] * n for _ in range(n)]
count = 0
for i in range(n):
for j in range(n):
if grid[i][j] == 1 and not visited[i][j]:
dfs(grid, visited, i, j, n)
count += 1
print(count)
方法二:BFS(广度优先搜索)
BFS 的思路是:“一层一层往外扩散”,像水波一样慢慢覆盖整个岛屿。
- 使用队列(queue)实现;
- 先把起点加入队列;
- 每次取出一个点,检查它的四个邻居,如果是未访问的陆地,就加入队列并标记。
参考代码
- C++
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int main() {
int n;
cin >> n;
vector<vector<int>> grid(n, vector<int>(n));
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cin >> grid[i][j];
}
}
vector<vector<bool>> visited(n, vector<bool>(n, false));
// 方向数组:上、下、左、右
vector<pair<int, int>> dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
int count = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1 && !visited[i][j]) {
// 启动 BFS
queue<pair<int, int>> q;
q.push({i, j});
visited[i][j] = true;
while (!q.empty()) {
auto [x, y] = q.front();
q.pop();
for (auto& dir : dirs) {
int nx = x + dir.first;
int ny = y + dir.second;
if (nx >= 0 && nx < n && ny >= 0 && ny < n &&
grid[nx][ny] == 1 && !visited[nx][ny]) {
visited[nx][ny] = true;
q.push({nx, ny});
}
}
}
count++;
}
}
}
cout << count << endl;
return 0;
}
- Java
import java.util.*;
public class Main {
private static final int[][] DIRECTIONS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[][] grid = new int[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
grid[i][j] = sc.nextInt();
}
}
boolean[][] visited = new boolean[n][n];
int count = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1 && !visited[i][j]) {
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{i, j});
visited[i][j] = true;
while (!queue.isEmpty()) {
int[] cell = queue.poll();
int x = cell[0], y = cell[1];
for (int[] dir : DIRECTIONS) {
int nx = x + dir[0];
int ny = y + dir[1];
if (nx >= 0 && nx < n && ny >= 0 && ny < n &&
grid[nx][ny] == 1 && !visited[nx][ny]) {
visited[nx][ny] = true;
queue.offer(new int[]{nx, ny});
}
}
}
count++;
}
}
}
System.out.println(count);
sc.close();
}
}
- Python
from collections import deque
n = int(input())
grid = []
for _ in range(n):
row = list(map(int, input().split()))
grid.append(row)
visited = [[False] * n for _ in range(n)]
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
count = 0
for i in range(n):
for j in range(n):
if grid[i][j] == 1 and not visited[i][j]:
# BFS 开始
q = deque()
q.append((i, j))
visited[i][j] = True
while q:
x, y = q.popleft()
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < n and 0 <= ny < n and grid[nx][ny] == 1 and not visited[nx][ny]:
visited[nx][ny] = True
q.append((nx, ny))
count += 1
print(count)
总结建议
| 方法 | 优点 | 适合场景 |
|---|---|---|
| DFS | 代码简洁,递归直观 | 连通性判断、路径探索 |
| BFS | 过程像“洪水填充”,更易想象 | 最短路径、层序遍历 |
🎯 本题两种方法都能 AC,时间复杂度都是 O(n²),因为每个格子最多访问一次。
初学者建议先掌握 DFS,再学 BFS,它们是算法竞赛的基石!
G 0-1 背包问题(模板题)
题目大意
你有一个容量为 M 的背包,和 N 件物品。第 i 件物品:
- 体积为
v[i] - 价值为
w[i] - 每件只能选一次
问:在总体积不超过 M 的前提下,最大总价值是多少?
✅ 示例:
N=4, M=5
物品:(1,2), (2,4), (3,4), (4,5)
最优方案:选第2件(2,4)和第3件(3,4)→ 体积=5,价值=8
💡 给初学者的学习建议
如果你没有学过01背包可以参考下面的视频讲解
- 【自制】01背包问题算法动画讲解 直观理解“选 or 不选”
- 带你学透0-1背包问题!(代码随想录) 讲清
dp数组含义、初始化、遍历顺序 - 0-1背包 完全背包【基础算法精讲 18】(灵茶山艾府)学会“恰好装满”、“方案数”等变种
💡 建议:先看动画建立直觉,再动手写代码,最后看系统讲解巩固。
如果你没学过动态规划,现在觉得难,完全正常! 这属于动态规划算法较为进阶的部分
先学前面的基础算法,等到学动态规划时,先把“斐波那契”、“爬楼梯”这些简单 DP 搞懂,再回来看背包,会有“顿悟”感。
不要怕慢,理解比速度重要!
解题思路
想象你去超市抢购,背包只能装 M 升东西。
货架上有 N 个商品,每个商品有体积和价值(比如“1升牛奶值2元”)。
你只能拿每个商品最多一次,怎么拿才能让总价值最高?
这就是 0-1 背包:每个物品“拿(1)”或“不拿(0)”。
动态规划解法
1. 状态定义
设 dp[i][j] 表示:
从前
i个物品中选,总体积不超过j时,能获得的最大价值
i范围:0 ~ Nj范围:0 ~ M
2. 状态转移
对于第 i 个物品(体积 v,价值 w),有两种选择:
- 不选它:
dp[i][j] = dp[i-1][j] - 选它(前提是
j >= v):dp[i][j] = dp[i-1][j - v] + w
取两者最大值:
dp[i][j] = max(dp[i-1][j], dp[i-1][j - v] + w) (如果 j >= v)
dp[i][j] = dp[i-1][j] (如果 j < v)
3. 初始条件
dp[0][j] = 0(没有物品可选,价值为0)dp[i][0] = 0(背包容量为0,装不下任何东西)
4. 最终答案
dp[N][M] —— 从前 N 个物品中选,容量不超过 M 的最大价值。
空间优化(滚动数组)
注意到 dp[i][...] 只依赖 dp[i-1][...],所以可以用一维数组优化:
for (int i = 0; i < N; i++) {
for (int j = M; j >= v[i]; j--) { // ⚠️ 必须倒序!
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
🔥 为什么倒序?
如果正序,dp[j - v[i]]可能已经被当前i更新过(相当于用了两次物品),就变成“完全背包”了!
倒序保证每次用的是上一轮(i-1)的状态。
参考代码(空间优化版,推荐)
- C++
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int N, M;
cin >> N >> M;
// dp[j] 表示:背包容量为 j 时,能获得的最大价值
vector<int> dp(M + 1, 0);
for (int i = 0; i < N; i++) {
int v, w;
cin >> v >> w;
// 从大到小遍历容量(防止重复使用物品)
for (int j = M; j >= v; j--) {
// 要么不选(dp[j] 不变),要么选(dp[j - v] + w)
dp[j] = max(dp[j], dp[j - v] + w);
}
}
cout << dp[M] << endl;
return 0;
}
- Java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
int M = sc.nextInt();
int[] dp = new int[M + 1];
for (int i = 0; i < N; i++) {
int v = sc.nextInt();
int w = sc.nextInt();
// 倒序遍历容量
for (int j = M; j >= v; j--) {
dp[j] = Math.max(dp[j], dp[j - v] + w);
}
}
System.out.println(dp[M]);
sc.close();
}
}
- Python
N, M = map(int, input().split())
dp = [0] * (M + 1)
for _ in range(N):
v, w = map(int, input().split())
# 倒序更新
for j in range(M, v - 1, -1):
dp[j] = max(dp[j], dp[j - v] + w)
print(dp[M])
⏱️ 复杂度分析
- 时间复杂度:O(N × M)
- 空间复杂度:O(M)(优化后)
对于 N, M ≤ 1000,最多 1e6 次操作,完全可以通过。
总结
- 0-1 背包是动态规划的入门必学模板;
- 核心思想:对每个物品,决策“选”或“不选”;
- 空间优化时必须倒序遍历容量;
- 熟练掌握后,可拓展到“分割等和子集”、“目标和”等经典问题。