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,检查 ii+2(即 6k−16k+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(从左到右),遍历它左边的所有鱼 jj < 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) ⚡ 更快更省空间,适合进阶

💡 建议学习顺序:先掌握方法一,再理解方法二。

方法一:枚举 + 二分(推荐初学者)

核心思想

  1. 预处理前缀和

    • prefixA[i] = a[0] + ... + a[i-1] → 洛洛打前 i 关的总耗电
    • prefixB[j] = b[0] + ... + b[j-1] → 晶晶打前 j 关的总耗电
    • 特别地:prefixA[0] = prefixB[0] = 0(打0关不耗能)
  2. 枚举洛洛打多少关 i(0 ≤ i ≤ n)

    • 如果 prefixA[i] > k,说说明洛洛自己就超了,后面不用看了(因为前缀和递增)
    • 否则,晶晶最多能用 rest = k - prefixA[i] 的能量
    • prefixB 中找最大的 j,使得 prefixB[j] ≤ rest
  3. 因为 prefixB 是非递减数组,可以用 二分查找 快速找到这个 j

  4. 答案就是所有 i + j 中的最大值

🎯 为什么正确?
我们穷举了洛洛所有可能的打法,对每种打法都让晶晶打得尽可能多——这一定是全局最优!

方法二:双指针(进阶优化)

我们不枚举所有组合,而是利用一个重要的性质:

当洛洛打得越来越多(i 增大),他消耗的能量 sumA 增加,
那么留给晶晶的能量 k - sumA越来越少
所以晶晶能打的关数 j 不会增加,只会减少或不变

这意味着:j 是随着 i 增加而单调不增的!

于是我们可以:

  1. 先让晶晶尽可能多打(j 最大)
  2. 然后让洛洛从 i = 0 开始逐步多打
  3. 每次洛洛多打一关,如果总能量超了,就让晶晶退关j--
  4. 每一步都记录最大的 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:把 ab 所在的集合合并
  • Q a b查询 ab 是否在同一个集合中。

这正是 并查集(Union-Find / Disjoint Set Union, DSU) 的经典应用场景!

🧩 类比理解:想象每个数字是一个人,一开始每个人都自成一派(自己是老大)。

  • M a b 就是让 ab 所在的帮派合并,选一个新老大;
  • Q a b 就是问:“你们两个的老大是不是同一个人?”

核心操作

并查集只需两个函数:

  1. find(x):找到 x 所在集合的“代表”(根节点);
  2. union(a, b):将 ab 所在集合合并。

为了高效处理最多 ​10^5 次操作,我们需要两种优化:

  • 路径压缩:在 find 时,把沿途所有节点直接连到根上,下次查找更快;
  • 按秩合并(可选):合并时把“矮树”接到“高树”下,避免树退化成链。

💡 实际竞赛中,只写路径压缩通常就足够快了!因为路径压缩本身已经让均摊复杂度接近常数。按秩合并更多是为了理论严谨性,且在某些变形题中有用。


💡 给初学者的学习建议

如果你还没学过并查集,强烈推荐先看以下视频建立直观理解:

看完再回来看代码,你会觉得“原来如此”!


参考代码

方法:带路径压缩 + 按秩合并(标准高效版)

  • 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. 从左到右、从上到下扫描整个矩阵;
  2. 每当遇到一个 1(且没被访问过),说明发现了一个新岛屿
  3. 然后用 DFS(深度优先搜索)BFS(广度优先搜索) 把这个岛屿“全部标记为已访问”,防止重复计数;
  4. 继续扫描,直到结束。

💡 就像你在地图上看到一块陆地,就派无人机飞过去把整片岛都拍一遍(标记),下次再看到就知道是同一块了。

关于是否修改原矩阵

有些写法会直接把 grid[i][j] = 1 改成 0 来表示“已访问”。虽然节省空间,但破坏了原始数据

更规范的做法是:额外开一个 visited 布尔数组,只读原矩阵,这样更安全、更通用。

下面两种方法我们都采用 不修改原矩阵 的方式。

💡 给初学者的学习建议

如果你还没学过 递归、DFS(深度优先搜索)或 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背包可以参考下面的视频讲解

💡 建议:先看动画建立直觉,再动手写代码,最后看系统讲解巩固。

如果你没学过动态规划,现在觉得难,完全正常! 这属于动态规划算法较为进阶的部分

先学前面的基础算法,等到学动态规划时,先把“斐波那契”、“爬楼梯”这些简单 DP 搞懂,再回来看背包,会有“顿悟”感。
不要怕慢,理解比速度重要

解题思路

想象你去超市抢购,背包只能装 M 升东西。
货架上有 N 个商品,每个商品有体积价值(比如“1升牛奶值2元”)。
你只能拿每个商品最多一次,怎么拿才能让总价值最高?

这就是 0-1 背包:每个物品“拿(1)”或“不拿(0)”。

动态规划解法

1. 状态定义

dp[i][j] 表示:

从前 i 个物品中选,总体积不超过 j 时,能获得的最大价值

  • i 范围:0 ~ N
  • j 范围: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 背包是动态规划的入门必学模板
  • 核心思想:对每个物品,决策“选”或“不选”
  • 空间优化时必须倒序遍历容量
  • 熟练掌握后,可拓展到“分割等和子集”、“目标和”等经典问题。

一个敲代码的