二分法的算法思路
創建於:2026年1月4日
創建於:2026年1月4日
二分法的算法思路
二分法(Binary Search)的算法思路如下:
二分法是一种在有序数据中查找目标值的高效算法。
核心思想是:
每次将搜索区间一分为二,通过比较中间元素与目标值,迅速缩小查找范围。
确定初始区间
left = 0right = n - 1循环查找
left <= right 时:
mid = left + (right - left) / 2arr[mid] == target → 查找成功arr[mid] < target → 目标在右半区left = mid + 1arr[mid] > target → 目标在左半区right = mid - 1结束条件
left > right)仍未找到 → 查找失败例如在数组:
[1, 3, 5, 7, 9, 11] 中查找 7
5 → 小了 → 查右边9 → 大了 → 查左边7 → 找到二分法通过不断“对半排除不可能区间”,在有序数据中实现高效查找。
如果你愿意,我也可以:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取第一行输入:升级影响的容忍值
long count = 0;
if (scanner.hasNextLong()) {
count = scanner.nextLong();
}
text// 读取第二行输入:7*24小时的用户访问量数据,按空格分隔 // 题目明确给长度为168 int[] nums = new int[168]; for (int i = 0; i < 168; i++) { if (scanner.hasNextInt()) { nums[i] = scanner.nextInt(); } } // 初始化滑动窗口的左右指针 int start = 0; int end = 0; // 初始化窗口内的和,先放入第一个元素 // 注意:数据范围可能较大,使用 long 类型防止溢出 long sum_val = nums[start]; // 用于记录找到的最大窗口长度(即x) int max_window_size = -1; // 初始化结果下标 int res_start = -1; int res_end = -1; // 外层循环:遍历所有可能的起始时间 start while (true) { // 终止条件:当 start 遍历完一个完整周期(168小时)后结束 if (start >= 168) { System.out.println(res_start + " " + res_end); break; } else { // 内层循环:在 sum_val 小于等于容忍值的情况下,尽可能向右扩展 end while (sum_val <= count) { // 如果当前有效窗口长度 (end - start) 大于已记录的最大长度 // 更新最佳结果 if (end - start > max_window_size) { max_window_size = end - start; res_start = start; // 结束时间可能跨周期,所以取模 res_end = end % 168; } // 尝试扩大窗口:右指针右移 end++; // 累加新进入窗口的值,使用取模实现循环数组的效果 sum_val += nums[end % 168]; // 边界检查:如果窗口长度已经达到最大周期 168 if (end - start >= 168) { // 减去当前 start 位置的值(配合原本逻辑)并跳出内层循环 sum_val -= nums[start]; break; } } // 当内层循环结束(sum_val > count 或 达到最大长度) // 窗口左边界 start 即将右移,先从 sum_val 中减去移出的 nums[start] sum_val -= nums[start]; } // 左指针右移,开始尝试下一个起始位置 start++; } scanner.close(); }
} 解析下算法思路,以及总结下滑动窗口的解题思路
下面我分两块来讲:(1) 你这段代码在做什么(算法思路)、(2) 滑动窗口题的通用解题套路总结。另外我会顺带指出你代码里几个关键边界/细节问题,方便你对齐题意和避免 WA。
nums,表示一周每小时访问量。sum <= count),并且让时间窗长度尽量大;若多个,取起点最小。你代码的目标:找“满足
sum_val <= count的最长窗口”。
start:窗口左端点(起始小时)end:窗口右端点(你代码把它当作“不断往右扩”的指针)sum_val:当前窗口内元素之和(用 long 防溢出)end % 168:实现环形访问(跨周期)并且你把 end 往右推进时,用 %168 去取数组,等价于把数组“拼接成无限长”:
nums[0..167], nums[0..167], nums[0..167]...
初始化:
start=0, end=0sum_val = nums[0]外层循环:枚举每个 start(从 0 到 167)
内层循环:当 sum_val <= count 时,不断扩展 end
res_start=start, res_end=end%168)end++,并把新的元素 nums[end%168] 加进来当窗口不再满足条件(sum_val > count)或者长度达到 168 后:
start(缩小窗口),即从 sum_val 中减去 nums[start]直觉上:
sum <= count 还是 sum >= count?你截图里写的是:
“时间窗内累计用户访问量必须 大于 给定的容忍值”
但你的代码条件是:
javawhile (sum_val <= count) { ... }
也就是“累计访问量 不超过 容忍值”才算合法。
这两者含义完全相反:
sum <= count(窗口内访问量别太大)。sum >= count(累计访问量至少达到某阈值),那窗口扩展/收缩的方向、比较符号都要反过来设计。✅ 你需要先确认题目真正约束是哪一个。
(从“减少升级影响、找最佳升级时间窗”这种语义上,我更倾向是 sum <= count 才合理。)
你用的是:
javaif (end - start > max_window_size)
但你又把 sum_val 初始化为 nums[start],说明窗口里已经有 1 个元素了。
如果窗口是“左右都包含”的 [start, end],那么长度应该是:
len = end - start + 1如果窗口是“右开区间”的 [start, end),那么初始化和更新方式又得完全配套。
你现在的写法更像是“sum_val 对应 [start, end](含 end)”,但长度却用 end-start,因此很可能短 1。
✅ 最稳的做法:明确窗口定义(建议用左闭右闭或左闭右开),所有地方统一。
你内层里有一段:
javaif (end - start >= 168) { sum_val -= nums[start]; break; }
跳出内层后,外层又会执行一次:
javasum_val -= nums[start];
这会导致当触发长度==168时,nums[start] 被减了两次,sum_val 错掉。
✅ 应该保证“每次 start++ 只减一次”。
滑动窗口适合这类题的核心原因是:
数组非负(访问量范围是 [0, 2^31]),所以右扩 sum 不会变小,左缩 sum 不会变大,具备“单调性”,双指针就能线性推进。
[l, r] 或 [l, r)sum)r++,更新状态l++,更新状态关键点就三句:
常见两种方式:
把 nums 变成 nums2,长度 336:
nums2[i] = nums[i % 168]
然后在 nums2 上做滑动窗口,但要求窗口长度 <= 168 且起点 < 168。% N 取值(你用的方式)用一个“逻辑上的 r”不断增加:
nums[r % N]
并限制窗口长度 r - l + 1 <= N两种都可以,复制版更直观、边界更少坑。
如果你愿意,我可以在你确认“题目到底是 sum<=count 还是 sum>=count”之后,把这段 Java 代码**改成边界正确、不会差 1、不会重复减、并满足“多解取最小 start”**的标准版本。你只要告诉我:题目约束到底是哪一个符号即可。
import java.util.Scanner;
import java.util.*;
import java.util.stream.Stream;
import java.util.HashMap;
import java.util.regex.Pattern;
public class Main {
public static void main(String[] args) {
// 处理输入
Scanner in = new Scanner(System.in);
String input_str = in.nextLine();
String[] tmp2 = input_str.split(" ");
int[] nums = new int[tmp2.length];
for (int i = 0; i < tmp2.length; i++) {
nums[i] = Integer.parseInt(tmp2[i]);
}
int m = nums[0];
int n = nums[1];
textString input_str1 = in.nextLine(); String[] tmp21 = input_str1.split(" "); Integer[] times = new Integer[tmp21.length]; for (int i = 0; i < tmp21.length; i++) { times[i] = Integer.parseInt(tmp21[i]); } Arrays.sort(times, new Comparator<Integer>() { @Override //在这里主要是重写了 Comparator类的compare方法, //sort方法可能也用了这个方法进行排序,然后在这里被重写了。 public int compare(Integer o1, Integer o2) { return o1 - o2; } }); int[] temp = new int[m]; for (int i = 0; i < n; i++) { temp[i % m] += times[i]; } System.out.println(Arrays.stream(temp).max().getAsInt()); return; }
} 解析下每个部分的作用,标上注释并输出
下面我按代码执行顺序,把你这段程序逐行解析清楚,并给出一份**“加好注释的完整版本”,最后再总结一下这类题的正确建模思路(以及你这份代码在算法层面的含义)**。
这是一个并行调度问题:
永远优先选择「处理时间最短」的作业
你的代码做了三件核心事情:
i % m 的方式,把作业“轮流”分配到 m 条流水线👉 本质上是一个 “贪心 + 简化调度” 的实现。
javaimport java.util.Scanner; import java.util.*; import java.util.stream.Stream; import java.util.HashMap; import java.util.regex.Pattern;
👉 实际只用到了:
ScannerArrays其余可以删掉(面试/考试不扣分,但工程上不推荐)。
javapublic class Main { public static void main(String[] args) {
javaScanner in = new Scanner(System.in); String input_str = in.nextLine(); String[] tmp2 = input_str.split(" ");
"3 5"javaint[] nums = new int[tmp2.length]; for (int i = 0; i < tmp2.length; i++) { nums[i] = Integer.parseInt(tmp2[i]); }
javaint m = nums[0]; int n = nums[1];
m:流水线数量n:作业数量javaString input_str1 = in.nextLine(); String[] tmp21 = input_str1.split(" ");
例如:
8 4 3 2 10
javaInteger[] times = new Integer[tmp21.length]; for (int i = 0; i < tmp21.length; i++) { times[i] = Integer.parseInt(tmp21[i]); }
Integer[] 是为了后面 Comparator 排序(虽然 int[] 也能排)javaArrays.sort(times, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1 - o2; } });
👉 排序后:
[2, 3, 4, 8, 10]
含义:
后续调度始终“优先处理时间短的作业”
这是题目明确给出的规则。
javaint[] temp = new int[m];
temp[i]:第 i 条流水线目前总共干了多久初始化时,所有流水线耗时都是 0
javafor (int i = 0; i < n; i++) { temp[i % m] += times[i]; }
times[i]:第 i 个最短作业i % m:轮流分配给第 0、1、2、...、m-1 条流水线以示例为例:
textm = 3 times = [2, 3, 4, 8, 10]
分配过程:
| 作业 | 时间 | i % m | 分配到流水线 | 流水线耗时 |
|---|---|---|---|---|
| 2 | 2 | 0 | 0 | [2,0,0] |
| 3 | 3 | 1 | 1 | [2,3,0] |
| 4 | 4 | 2 | 2 | [2,3,4] |
| 8 | 8 | 0 | 0 | [10,3,4] |
| 10 | 10 | 1 | 1 | [10,13,4] |
javaSystem.out.println(Arrays.stream(temp).max().getAsInt());
13javaimport java.util.*; public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); // ===== 1. 读取 m 和 n ===== String[] firstLine = in.nextLine().split(" "); int m = Integer.parseInt(firstLine[0]); // 流水线数量 int n = Integer.parseInt(firstLine[1]); // 作业数量 // ===== 2. 读取每个作业的处理时间 ===== String[] secondLine = in.nextLine().split(" "); Integer[] times = new Integer[n]; for (int i = 0; i < n; i++) { times[i] = Integer.parseInt(secondLine[i]); } // ===== 3. 按作业时间从小到大排序(贪心)===== Arrays.sort(times); // ===== 4. temp[i] 表示第 i 条流水线的累计耗时 ===== int[] temp = new int[m]; // ===== 5. 轮流分配最短作业给流水线 ===== for (int i = 0; i < n; i++) { temp[i % m] += times[i]; } // ===== 6. 总完成时间 = 所有流水线中的最大耗时 ===== int result = Arrays.stream(temp).max().getAsInt(); System.out.println(result); } }
✅ 思路简单
✅ 能通过大部分基础测试
⚠️ 不是严格最优调度算法(真正严格的是用「最小堆 + 当前最空闲流水线」)
但在 m,n ≤ 100 的限制下,这种实现通常是出题人默认答案。
如果你愿意,下一步我可以帮你:
你可以直接说:
👉「帮我改成最小堆版」或「讲一下为什么这个方法可行」
代码里为什么对排序方法进行了重写
你代码里对排序方法 重写 Comparator 的原因,本质上是为了告诉 Arrays.sort():** “我想按什么规则排序”**。
不过在你这段代码里,重写其实不是必须的,更多是“写法习惯/为了显式表达排序规则”。
你的数组是:
javaInteger[] times
当你写:
javaArrays.sort(times, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1 - o2; } });
等价于告诉 Java:
按照从小到大(升序)排序,因为如果 o1 < o2 就返回负数,让 o1 排在前面。
compare 返回值含义< 0 :o1 排在 o2 前面= 0 :两者位置无所谓> 0 :o1 排在 o2 后面因为 Integer 本身就实现了 Comparable 接口,默认就是升序排序。
所以你完全可以直接写:
javaArrays.sort(times);
效果和你那段重写 Comparator 的升序版本一模一样。
下面这些场景,重写才有意义 / 才需要:
javaArrays.sort(times, (a, b) -> b - a);
例如按作业时间的“个位数”排序:
javaArrays.sort(times, (a, b) -> (a % 10) - (b % 10));
比如 Job { int time; int id; }
你要按 time 排序就必须提供 Comparator。
你写的是:
javareturn o1 - o2;
如果数很大,o1 - o2 可能溢出(例如接近 Integer.MAX_VALUE)。
更安全的写法是:
javareturn Integer.compare(o1, o2);
或者 lambda:
javaArrays.sort(times, Integer::compare);
不过你这题 t < 100,所以不会溢出,只是写法习惯上建议更规范。
Integer[] 默认排序就是升序。Arrays.sort(times); 或用 Integer.compare 的安全写法。如果你愿意,我也可以帮你把这段代码改成更简洁、更规范的版本(包括输入读取更鲁棒)。
import java.util.Scanner;
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
String inputStr = in.next();
int length = in.nextInt();
text// 统计输入字符串中各个字符的频率 Map<Character, Integer> targetFreq = new HashMap<>(); for (char c : inputStr.toCharArray()) { targetFreq.put(c, targetFreq.getOrDefault(c, 0) + 1); } // 限制搜索范围 int maxSearch = 1000 - length + 1; for (int start = 1; start <= maxSearch; start++) { // 统计当前窗口中的字符频率 Map<Character, Integer> windowFreq = new HashMap<>(); for (int i = 0; i < length; i++) { String numStr = String.valueOf(start + i); for (char c : numStr.toCharArray()) { windowFreq.put(c, windowFreq.getOrDefault(c, 0) + 1); } } // 比较字符频率 boolean isMatch = true; for (Map.Entry<Character, Integer> entry : targetFreq.entrySet()) { char c = entry.getKey(); int freq = entry.getValue(); if (!windowFreq.containsKey(c) || !windowFreq.get(c).equals(freq)) { isMatch = false; break; } } if (isMatch && windowFreq.size() == targetFreq.size()) { System.out.println(start); return; } } }
} 解析下代码思路,并注释
下面按“题意 → 代码做法 → 复杂度/注意点 → 带注释代码”给你讲清楚。
start, start+1, ..., start+length-18 9 10 11 12 → "89101112")inputStr(字符总数不变、每个数字字符出现次数不变)inputStr 和 length,保证能还原唯一的连续序列start)关键:因为只是“打乱字符”,所以我们只需要比较字符频次(0~9 各出现多少次),不需要关心顺序。
targetFreqjavaMap<Character, Integer> targetFreq = new HashMap<>(); for (char c : inputStr.toCharArray()) { targetFreq.put(c, targetFreq.getOrDefault(c, 0) + 1); }
含义:把输入的打乱字符串当作“字符多重集合”,例如 "19801211":
javaint maxSearch = 1000 - length + 1; for (int start = 1; start <= maxSearch; start++) { ... }
因为序列长度为 length,且数字不超过 1000,所以起点最大只能到 1000-length+1。
windowFreqjavaMap<Character, Integer> windowFreq = new HashMap<>(); for (int i = 0; i < length; i++) { String numStr = String.valueOf(start + i); for (char c : numStr.toCharArray()) { windowFreq.put(c, windowFreq.getOrDefault(c, 0) + 1); } }
含义:把 start..start+length-1 每个数字转成字符串,逐字符统计频次。
windowFreq 是否与 targetFreq 完全一致你做了两步保证“完全一致”:
targetFreq 中的每个字符,看 windowFreq 里是否频次相等windowFreq.size() == targetFreq.size() 防止多出别的字符键匹配则输出 start 并结束。
length 个数,每个数最多 4 位(<=1000)✅ 可以优化:不用 HashMap<Character,Integer>,因为字符只可能是 '0'..'9',用 int[10] 更快更简洁(但你现在这样也能过)。
javaimport java.util.*; public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); // inputStr:被打乱后的拼接字符串(只打乱顺序,不改变字符数量) String inputStr = in.next(); // length:连续正整数序列的长度 int length = in.nextInt(); // ========= 1) 统计目标字符串中每个字符(0~9)的出现次数 ========= Map<Character, Integer> targetFreq = new HashMap<>(); for (char c : inputStr.toCharArray()) { targetFreq.put(c, targetFreq.getOrDefault(c, 0) + 1); } // ========= 2) 枚举可能的起始数字 start ========= // 题目限制“正整数不超过1000”,因此 start+length-1 <= 1000 int maxSearch = 1000 - length + 1; for (int start = 1; start <= maxSearch; start++) { // ===== 3) 构造当前候选序列 start..start+length-1 的字符频次 ===== Map<Character, Integer> windowFreq = new HashMap<>(); for (int i = 0; i < length; i++) { // 当前数字转为字符串,逐字符计数 String numStr = String.valueOf(start + i); for (char c : numStr.toCharArray()) { windowFreq.put(c, windowFreq.getOrDefault(c, 0) + 1); } } // ===== 4) 判断 windowFreq 是否与 targetFreq 完全一致 ===== boolean isMatch = true; // 先检查:targetFreq 中的每个字符在 windowFreq 中频次必须相同 for (Map.Entry<Character, Integer> entry : targetFreq.entrySet()) { char c = entry.getKey(); int freq = entry.getValue(); if (!windowFreq.containsKey(c) || !windowFreq.get(c).equals(freq)) { isMatch = false; break; } } // 再检查:windowFreq 不应包含 targetFreq 没有的额外字符 if (isMatch && windowFreq.size() == targetFreq.size()) { // 找到唯一匹配的起点(题目保证唯一) System.out.println(start); return; } } } }
如果你想让我把它改成更“标准高效”的版本(用 int[10],比较数组即可,代码更短更快),我也可以直接给你一份。
getOrDefault()
getOrDefault() 是 Java Map 接口里一个非常常用、也非常好用的方法,用来安全地取值并提供默认值,避免写一堆 if 判断。
下面从「是什么 → 为什么用 → 怎么用 → 和传统写法对比 → 在你代码中的作用」一步步讲清楚。
getOrDefault() 是什么?方法签名:
javaV getOrDefault(Object key, V defaultValue)
含义:
如果
Map中存在key,返回对应的 value;
如果不存在,返回你给的defaultValue。
在使用 Map 统计次数时,最常见的问题是:
javamap.get(key) + 1 // ❌ 可能空指针
如果 key 之前没出现过,map.get(key) 会返回 null,直接 +1 就会抛 NullPointerException。
javaif (map.containsKey(c)) { map.put(c, map.get(c) + 1); } else { map.put(c, 1); }
✅ 正确
❌ 冗长、啰嗦
getOrDefault() 的写法(推荐)javamap.put(c, map.getOrDefault(c, 0) + 1);
一步完成:
0+1javaMap<Character, Integer> map = new HashMap<>();
'1':javamap.getOrDefault('1', 0) // 返回 0 map.put('1', 0 + 1); // 存成 1
'1':javamap.getOrDefault('1', 0) // 返回 1 map.put('1', 1 + 1); // 存成 2
你写的是:
javatargetFreq.put(c, targetFreq.getOrDefault(c, 0) + 1);
这行代码的含义是:
统计字符串中每个字符出现的次数
同样的逻辑也用在:
javawindowFreq.put(c, windowFreq.getOrDefault(c, 0) + 1);
getOrDefault?当你必须区分:
null这种情况就不适合用(大多数计数题不涉及)。
getOrDefault(key, defaultValue)用来在 Map 中安全取值并提供默认值,
特别适合计数、频率统计、滑动窗口等场景,可以避免空指针并大幅简化代码。
如果你愿意,我也可以:
getOrDefault vs mergeMap.compute() / computeIfAbsent() 的区别和使用场景import java.util.Scanner;
import java.util.*;
import java.util.stream.Collectors;
class _57热点网站统计1 {
public static void main(String[] args) {
// 处理输入
Scanner in = new Scanner(System.in);
HashMap<String, Integer> map = new HashMap<>();
StringBuffer result = new StringBuffer();
textwhile (in.hasNext()) { String input_str = in.nextLine(); //用抛异常的方式判断此行是否为int try { int num = Integer.parseInt(input_str); List<String> list = new ArrayList<>(); map.forEach((k, v) -> { list.add(v + " " + k); }); //自定义排序方法 list.sort((o1, o2) -> { int value1 = Integer.parseInt(o1.split(" ")[0]); int value2 = Integer.parseInt(o2.split(" ")[0]); if (value1 == value2) { String key1 = o1.split(" ")[1]; String key2 = o2.split(" ")[1]; return key1.compareTo(key2); } else { return value2 - value1; } }); for (int i = 0; i < num; i++) { result.append(list.get(i).split(" ")[1]).append(","); } result.delete(result.length() - 1, result.length()).append("\n"); } catch (Exception e) { if (map.containsKey(input_str)) { int value = map.get(input_str); map.put(input_str, value + 1); } else { map.put(input_str, 1); } } } System.out.println(result); }
} 加上详细注释并输出,同时总结下自定义排序怎么实现
下面先把你这段代码逐行加上详细注释(保持原逻辑不改),再总结一下自定义排序(Comparator / lambda)怎么实现。
javaimport java.util.*; import java.util.stream.Collectors; class _57热点网站统计1 { public static void main(String[] args) { // Scanner 用于从标准输入读取多行 Scanner in = new Scanner(System.in); // map:统计每个 URL 的访问次数 // key = URL 字符串, value = 访问次数 HashMap<String, Integer> map = new HashMap<>(); // result:拼接所有“查询 N”时的输出结果(每次查询输出一行) StringBuffer result = new StringBuffer(); // hasNext(): 只要输入流中还有 token 就继续 // 注意:你这里用 nextLine() 读整行,但 while 用 hasNext() 判断 token,有时会有细微不一致 // 不过一般测试数据是规整的,仍可工作 while (in.hasNext()) { // 读取一整行:要么是 URL,要么是数字 N String input_str = in.nextLine(); // 用“尝试转 int + 捕获异常”的方式判断:该行到底是不是数字 // 如果能 parse 成 int -> 说明这一行是查询 N(需要输出 Top N) // 如果 parse 抛异常 -> 说明这一行是 URL(需要计数 +1) try { int num = Integer.parseInt(input_str); // list:把 map 中的统计结果搬到列表里,便于排序取 Top N // 你这里把每项拼成 "count url" 这样的字符串,后面排序时再 split 拆开 // (能跑,但效率一般;更推荐用 Entry 或自定义对象) List<String> list = new ArrayList<>(); // 遍历 map,把每个 (url, count) 转成 "count url" 存入 list map.forEach((k, v) -> { list.add(v + " " + k); }); // ===== 自定义排序:先按访问次数降序;次数相同按 URL 字典序升序 ===== list.sort((o1, o2) -> { // o1/o2 形如 "count url" // split(" ") 后 [0] 是count, [1] 是url int value1 = Integer.parseInt(o1.split(" ")[0]); int value2 = Integer.parseInt(o2.split(" ")[0]); if (value1 == value2) { // 次数相同:按 URL 字典序升序(compareTo 默认升序) String key1 = o1.split(" ")[1]; String key2 = o2.split(" ")[1]; return key1.compareTo(key2); } else { // 次数不同:按次数降序(value2 - value1) return value2 - value1; } }); // 取排序后前 num 个 URL,拼接输出:用逗号分隔 for (int i = 0; i < num; i++) { result.append(list.get(i).split(" ")[1]).append(","); } // 删除最后一个多余的逗号,然后加换行 result.delete(result.length() - 1, result.length()).append("\n"); } catch (Exception e) { // 进入 catch:说明这一行不是数字 -> 当作 URL 访问记录 // 如果已经存在该 URL,次数 +1;否则初始化为 1 if (map.containsKey(input_str)) { int value = map.get(input_str); map.put(input_str, value + 1); } else { map.put(input_str, 1); } } } // 输出所有查询的结果 System.out.println(result); } }
你这里用的是:
javalist.sort((o1, o2) -> { ... });
这本质上就是传了一个 Comparator(比较器),规则由你定义。
< 0:o1 排在 o2 前= 0:顺序无所谓> 0:o1 排在 o2 后伪逻辑:
textif count1 != count2: count 大的排前 -> 降序 else: url 字典序小的排前 -> 升序
你用 lambda 写出来就是:
javaif (value1 == value2) return key1.compareTo(key2); // 升序 else return value2 - value1; // 降序
不要用 "count url" 这种字符串拼接再 split,直接对 map.entrySet() 排序更合理:
javaList<Map.Entry<String, Integer>> entries = new ArrayList<>(map.entrySet()); entries.sort((e1, e2) -> { int c = Integer.compare(e2.getValue(), e1.getValue()); // 次数降序 if (c != 0) return c; return e1.getKey().compareTo(e2.getKey()); // URL 升序 });
这就是“自定义排序”的标准实现方式:先比主关键字,再比次关键字。
如果你想,我也可以顺手把你整段程序改成“Entry 排序版 + 输出 TopN”,同时修一下 hasNext() 搭配 nextLine() 的小不一致点,让代码更稳。
自定义排序的格式
在 Java 里,“自定义排序”本质就是:给排序方法传一个 Comparator(比较器),告诉它“两个元素该怎么比”。
下面把常见格式按从“最标准”到“最常用”整理给你(你写题基本就用这些)。
javaCollections.sort(list, new Comparator<T>() { @Override public int compare(T a, T b) { // a < b 返回负数,a == b 返回0,a > b 返回正数 return ...; } });
数组也一样:
javaArrays.sort(arr, new Comparator<T>() { @Override public int compare(T a, T b) { return ...; } });
javalist.sort((a, b) -> { return ...; });
或者直接:
javaCollections.sort(list, (a, b) -> ...); Arrays.sort(arr, (a, b) -> ...);
在 compare(a, b) 里:
javalist.sort((a, b) -> Integer.compare(a, b));
javalist.sort((a, b) -> Integer.compare(b, a));
⚠️ 不推荐写 b - a,可能溢出(虽然很多题范围小也能过)。
javalist.sort((o1, o2) -> { int c = Integer.compare(o2.count, o1.count); // 主条件:count 降序 if (c != 0) return c; return o1.url.compareTo(o2.url); // 次条件:url 升序 });
这就是“自定义排序”的标准模板:
javaint c = compare(主关键字); if (c != 0) return c; return compare(次关键字);
如果你排序的是对象或 Entry:
javalist.sort( Comparator.comparingInt((Node x) -> x.count).reversed() // count 降序 .thenComparing(x -> x.url) // url 升序 );
如果是 Map.Entry<String,Integer>:
javaentries.sort( Comparator.<Map.Entry<String,Integer>>comparingInt(e -> e.getValue()).reversed() .thenComparing(e -> e.getKey()) );
你写的是这种“写死的规则”格式:
javalist.sort((o1, o2) -> { int v1 = ...; int v2 = ...; if (v1 == v2) return key1.compareTo(key2); return v2 - v1; // 降序 });
它属于 Lambda Comparator + 多条件排序。
自定义排序格式 = sort(Comparator);Comparator.compare = 先比主关键字,若相等再比次关键字。
如果你愿意,我可以按你“热点网站TopN”那题,给你写一份最标准的 Entry+Comparator 链式排序版本(更短、更快、不用 split)。
总结下java里map各种常用操作
下面给你把 Java 里 Map(重点是 HashMap)最常用操作系统整理一遍:包括 增删改查、遍历、计数统计、排序、常见坑,并配上典型代码模板,刷题/面试都够用 ✅
HashMap:无序,查找最快(O(1)平均)LinkedHashMap:按插入顺序TreeMap:按 key 自动排序(红黑树)javamap.put("a", 1); map.put("a", 2); // 覆盖原来的 value
✅ 作用:插入或更新 key 对应的值。
javaInteger v = map.get("a");
⚠️ 如果 key 不存在:返回 null
javaint v = map.getOrDefault("a", 0);
✅ 统计次数 / 频率题必用。
javaif(map.containsKey("a")) {}
javamap.remove("a");
javaint sz = map.size();
javamap.clear();
javaboolean empty = map.isEmpty();
javafor (Map.Entry<String, Integer> e : map.entrySet()) { System.out.println(e.getKey() + " " + e.getValue()); }
✅ 推荐原因:同时拿 key/value,不额外查找。
javafor (String k : map.keySet()) { System.out.println(k + " " + map.get(k)); }
⚠️ 这里 map.get(k) 会二次查表,性能略差。
javafor (Integer v : map.values()) { System.out.println(v); }
javamap.forEach((k, v) -> System.out.println(k + ":" + v));
javamap.put(x, map.getOrDefault(x, 0) + 1);
javamap.merge(x, 1, Integer::sum);
等价于:
javamap.computeIfAbsent(key, k -> new ArrayList<>()).add(val);
典型应用:分组
Map 本身无序,排序一般是:
1)转成 List<Entry<K,V>>
2)自定义 Comparator 排序
javaList<Map.Entry<String, Integer>> list = new ArrayList<>(map.entrySet()); list.sort((e1, e2) -> Integer.compare(e2.getValue(), e1.getValue()));
javalist.sort((e1, e2) -> { int c = Integer.compare(e2.getValue(), e1.getValue()); if (c != 0) return c; return e1.getKey().compareTo(e2.getKey()); });
javalist.sort( Comparator.<Map.Entry<String,Integer>>comparingInt(e -> e.getValue()).reversed() .thenComparing(e -> e.getKey()) );
javamap.putIfAbsent(key, 0);
javamap.replace(key, newValue);
javamap.replaceAll((k, v) -> v + 1);
javamap1.equals(map2);
javamap.put(k, map.get(k) + 1); // ❌ 可能空指针
✅ 用 getOrDefault 或 merge
输出顺序不固定,必须排序才能确定 TopN。
javareturn b - a; // ❌ 可能溢出
✅ 用:
javaInteger.compare(b, a)
javamap.put(x, map.getOrDefault(x, 0) + 1);
javaList<Map.Entry<String,Integer>> list = new ArrayList<>(map.entrySet()); list.sort((e1,e2)->{ int c = Integer.compare(e2.getValue(), e1.getValue()); if(c!=0) return c; return e1.getKey().compareTo(e2.getKey()); });
javamap.computeIfAbsent(key, k -> new ArrayList<>()).add(val);
Map 最常用就是:put/get/getOrDefault/containsKey/entrySet遍历/转List排序/merge计数/computeIfAbsent分组
如果你愿意我还可以进一步整理:
✅ HashMap vs TreeMap vs LinkedHashMap 区别 + 适用场景
✅ ConcurrentHashMap 在并发下的常用操作
✅ TopN 题的 Map + 小根堆 通用模板
import java.util.*;
import java.util.stream.Collectors;
public class _56统计射击比赛成绩1 {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
textint n = Integer.parseInt(in.nextLine().trim()); List<Integer> ids = Arrays.stream(in.nextLine().trim().split(",")) .map(Integer::parseInt) .collect(Collectors.toList()); List<Integer> scores = Arrays.stream(in.nextLine().trim().split(",")) .map(Integer::parseInt) .collect(Collectors.toList()); // 存储选手成绩信息 HashMap<Integer, List<Integer>> map = new HashMap<>(); for (int i = 0; i < n; i++) { map.computeIfAbsent(ids.get(i), k -> new ArrayList<>()).add(scores.get(i)); } // 预处理:计算符合条件的选手的最高3个成绩之和 // 这样避免在排序比较器中重复计算 List<PlayerStats> validPlayers = new ArrayList<>(); for (Map.Entry<Integer, List<Integer>> entry : map.entrySet()) { List<Integer> s = entry.getValue(); // 题目条件2:如果一个选手成绩少于3个,则认为选手的所有成绩无效,排名忽略该选手 if (s.size() >= 3) { // 降序排序,取前3个 s.sort(Collections.reverseOrder()); int sum = 0; for (int i = 0; i < 3; i++) { sum += s.get(i); } validPlayers.add(new PlayerStats(entry.getKey(), sum)); } } // 排序条件3:如果选手的成绩之和相等,则成绩之和相等的选手按照其ID降序排列 validPlayers.sort((o1, o2) -> { if (o1.sum != o2.sum) { return o2.sum - o1.sum; // 总分降序 } else { return o2.id - o1.id; // ID降序 } }); // 输出结果,使用 Collectors.joining 自动处理逗号,且能够处理空结果的情况 String result = validPlayers.stream() .map(p -> String.valueOf(p.id)) .collect(Collectors.joining(",")); System.out.println(result); } // 辅助类用于存储计算好的总分 static class PlayerStats { int id; int sum; public PlayerStats(int id, int sum) { this.id = id; this.sum = sum; } }
} 详细解析下
下面按“输入解析 → 数据结构建模 → 预处理计算 → 排序规则 → 输出格式 → 复杂度/注意点”把这段代码完整拆开讲一遍,并指出一些容易忽略的细节。
n 条成绩记录,每条记录包含:选手ID、成绩javaint n = Integer.parseInt(in.nextLine().trim()); List<Integer> ids = Arrays.stream(in.nextLine().trim().split(",")) .map(Integer::parseInt) .collect(Collectors.toList()); List<Integer> scores = Arrays.stream(in.nextLine().trim().split(",")) .map(Integer::parseInt) .collect(Collectors.toList());
n(成绩条数)这里用了 Stream 把 "1,2,3" 变成 List<Integer>:
split(",") 切成字符串数组map(Integer::parseInt) 转 intcollect(toList()) 收集成 ListjavaHashMap<Integer, List<Integer>> map = new HashMap<>(); for (int i = 0; i < n; i++) { map.computeIfAbsent(ids.get(i), k -> new ArrayList<>()).add(scores.get(i)); }
map 的含义:选手ID -> 该选手所有成绩列表computeIfAbsent(key, ...):
ArrayList 放进去并返回.add(scores.get(i)) 把成绩追加进去这是一种非常常见的“分组写法”,比 containsKey + put 更简洁。
javaList<PlayerStats> validPlayers = new ArrayList<>(); for (Map.Entry<Integer, List<Integer>> entry : map.entrySet()) { List<Integer> s = entry.getValue(); if (s.size() >= 3) { s.sort(Collections.reverseOrder()); int sum = 0; for (int i = 0; i < 3; i++) { sum += s.get(i); } validPlayers.add(new PlayerStats(entry.getKey(), sum)); } }
这一段做了两件事:
javaif (s.size() >= 3)
成绩少于 3 条的选手被忽略。
s.sort(Collections.reverseOrder()):把该选手成绩列表按降序排(高分在前)sumvalidPlayers(一个只存 id 和 sum 的列表)注释里说“避免在排序比较器中重复计算”,这个思路很对:
先把每个人的 sum 算好,排序时只比 sum 和 id,比较器就会很轻量。
javavalidPlayers.sort((o1, o2) -> { if (o1.sum != o2.sum) { return o2.sum - o1.sum; // 总分降序 } else { return o2.id - o1.id; // ID降序 } });
排序规则对应:
o2.sum - o1.sum:让 sum 大的排前(降序)o2.id - o1.id:让 id 大的排前(降序)更稳妥的写法通常用 Integer.compare 防止溢出(虽然一般成绩不会大到溢出):
javaint c = Integer.compare(o2.sum, o1.sum); return (c != 0) ? c : Integer.compare(o2.id, o1.id);
javaString result = validPlayers.stream() .map(p -> String.valueOf(p.id)) .collect(Collectors.joining(",")); System.out.println(result);
map(p -> String.valueOf(p.id)):把 PlayerStats 转成字符串 idCollectors.joining(","):用逗号连接validPlayers 为空,joining 会返回 "",打印就是空行(符合“无有效选手”的常见输出习惯)javastatic class PlayerStats { int id; int sum; public PlayerStats(int id, int sum) { this.id = id; this.sum = sum; } }
作用:把“选手 id + 计算好的总分 sum”绑在一起,方便排序和输出。
设:
Pk_i,总成绩条数为 n主要开销:
n 通常不大时完全没问题。
还能优化:不必把每个选手所有成绩都整体排序,只要维护 top3(比如用长度为3的小结构或小根堆),能把每个选手处理到 O(k_i)。但当前写法更直观。
o2.sum - o1.sum 这种减法比较有溢出风险(更规范用 Integer.compare)。s.sort(reverseOrder()) 会直接修改 map 里存的 list(就地排序)。这题没问题,但如果后面还要用原顺序就要复制一份再排。如果你把“题目原文的规则”也贴出来(尤其是“取最高3个成绩是否要去重 / 是否允许负分 / n 的范围”等),我可以顺便帮你确认这份实现是否完全贴合题意,以及给你一个“top3 不用排序”的更优版本。
text// 存储选手成绩信息 HashMap<Integer, List<Integer>> map = new HashMap<>(); for (int i = 0; i < n; i++) { map.computeIfAbsent(ids.get(i), k -> new ArrayList<>()).add(scores.get(i)); } 解析下这段的写法
当然可以!你这段代码是 “按选手 ID 分组收集成绩” 的经典写法,核心是 Map + computeIfAbsent。我把它拆开解释得更细一点,让你能一眼看懂每个部分在做什么、为什么这样写、等价写法是什么。
java// 存储选手成绩信息 HashMap<Integer, List<Integer>> map = new HashMap<>(); for (int i = 0; i < n; i++) { map.computeIfAbsent(ids.get(i), k -> new ArrayList<>()).add(scores.get(i)); }
你有两份列表:
ids[i]:第 i 条记录的选手 IDscores[i]:第 i 条记录的成绩比如:
| i | ids[i] | scores[i] |
|---|---|---|
| 0 | 1 | 9 |
| 1 | 2 | 8 |
| 2 | 1 | 10 |
| 3 | 1 | 7 |
| 4 | 2 | 6 |
最终希望得到:
textmap = { 1 -> [9, 10, 7] 2 -> [8, 6] }
也就是说:
✅ 每个选手 ID 对应一个成绩列表
✅ 同一个选手出现多次,就把成绩不断加进列表里
javaHashMap<Integer, List<Integer>> map = new HashMap<>();
map 的结构是:
javafor (int i = 0; i < n; i++) { ... }
遍历第 i 条成绩记录,准备把 scores[i] 放进对应选手的 list 中。
javamap.computeIfAbsent(ids.get(i), k -> new ArrayList<>()).add(scores.get(i));
这一句可以拆成 2 个步骤理解:
computeIfAbsent(key, mappingFunction) 的含义:如果 key 已存在,返回已有的 value
如果 key 不存在,就创建一个新 value 放进去,然后返回它
在这里:
ids.get(i)(选手 ID)k -> new ArrayList<>()也就是说:
最后 .add(scores.get(i)) 就把当前成绩加进去。
这行代码:
javamap.computeIfAbsent(ids.get(i), k -> new ArrayList<>()).add(scores.get(i));
等价于下面这种传统写法(更容易看懂):
javaint id = ids.get(i); int score = scores.get(i); if (!map.containsKey(id)) { map.put(id, new ArrayList<>()); } map.get(id).add(score);
✅ computeIfAbsent 就是把这几行缩成一行,并且更优雅、更不容易写错。
一行就完成了“判断存在 → 创建 → add”。
传统写法很容易写漏 map.put(id, new ArrayList<>()) 导致 NullPointer。
这种 “key -> list” 的结构是刷题里非常常见的分组统计结构。
假设:
javaids = [1, 2, 1] scores= [9, 8, 10]
map:
1 -> [9]
map:
text1 -> [9] 2 -> [8]
map:
text1 -> [9,10] 2 -> [8]
这段代码的作用是:把每条成绩记录按照选手 ID 分组,把成绩加入该选手的成绩列表中。
computeIfAbsent的作用是:如果 key 没有对应的 list 就创建一个,否则直接用已有的 list。
如果你愿意我还能继续补充:
✅ computeIfAbsent vs putIfAbsent 区别
✅ merge() 用于计数统计的写法
✅ Map 分组的 3 种常用模板(List/Set/Map 作为 value)
import java.util.*;
class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()) {
int num = Integer.parseInt(in.nextLine());
List<Integer> weights = new ArrayList<>();
for (int i = 0 ; i < num ; i++) {
weights.add(in.nextInt());
}
textint aWeight = 0; int bWeight = 0; int min = Integer.MAX_VALUE; for (int i = 0 ; i < num ; i++) { //不进位的二进制加法,等同于二进制的异或操作。 aWeight ^= weights.get(i); bWeight += weights.get(i); min = Math.min(weights.get(i), min); } if (aWeight == 0) { System.out.println(bWeight - min); } else { System.out.println(-1); } } }
} 给代码加上详细注释并输出
javaimport java.util.*; class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); // 题目可能会有多组输入,所以用 while 循环持续读取 while (in.hasNext()) { // 读取苹果数量 num(一行一个整数) int num = Integer.parseInt(in.nextLine()); // 读取每个苹果的重量 // 注意:这里用 nextInt() 按空格读 num 个整数 List<Integer> weights = new ArrayList<>(); for (int i = 0; i < num; i++) { weights.add(in.nextInt()); } // aWeight:A 的“二进制加法但不进位”规则下的总和 // 这种“不进位加法”在二进制里等价于按位异或 XOR int aWeight = 0; // bWeight:所有苹果重量的正常十进制求和(包含进位的普通加法) int bWeight = 0; // min:记录最小的苹果重量(用于后面让 B 拿最多) int min = Integer.MAX_VALUE; // 遍历所有苹果,做三件事: // 1) aWeight 用 XOR 累积(不进位的二进制加法) // 2) bWeight 做普通求和 // 3) 统计最小重量 min for (int i = 0; i < num; i++) { int w = weights.get(i); // 不进位的二进制加法 == XOR aWeight ^= w; // 普通加法求总和 bWeight += w; // 更新最小值 min = Math.min(min, w); } /* * 关键结论: * A 的规则是“不进位二进制加法”,即 XOR。 * 若要把苹果分成两堆,让 A 觉得两堆“相等”,则: * A看到的(堆1) == A看到的(堆2) * 在 XOR 语义下,等价于: * (堆1的XOR) == (堆2的XOR) * 两边再 XOR 一下可得: * (堆1的XOR) XOR (堆2的XOR) == 0 * 而 (堆1 XOR 堆2) 就是“所有苹果的 XOR”,也就是 aWeight * 所以必须满足:aWeight == 0,才存在可行分法。 */ if (aWeight == 0) { /* * 在可行的前提下(总 XOR 为 0),题目要求 B 在满足 A 的情况下拿最多(按普通重量和最大)。 * 最优策略是:让 A 拿最轻的那个苹果(min),其余都给 B。 * 因为只要总 XOR 为 0,就一定能构造满足 A 规则相等的划分, * 且为了让 B 最大,尽量把最小重量给 A。 * * 因此 B 的最大总重量 = 总和 bWeight - 最小值 min */ System.out.println(bWeight - min); } else { // 总 XOR 不为 0,无法分成两堆让 A 的“不进位加法”相等 System.out.println(-1); } } } }
暴力枚举的算法思路
下面用通俗 + 模板化的方式,给你系统总结一下 「暴力枚举」的算法思路。这是算法入门和面试中非常基础但非常重要的一类思想。
定义一句话版:
把所有“可能的情况”一个不漏地列出来,逐一检查,找出满足条件的解。
它不依赖复杂技巧,靠“穷举 + 判断”解决问题。
👉 找“可枚举的变量”
for 循环javafor (int i = 0; i < n; i++) { if (满足条件) { // 记录 / 返回 } }
[i, j]javafor (int i = 0; i < n; i++) { for (int j = i; j < n; j++) { // 检查区间 [i, j] } }
javafor (int i = 0; i < n; i++) for (int j = i+1; j < n; j++) for (int k = j+1; k < n; k++) // 判断
这是很多题的“隐形暴力”。
javafor (int start = 1; start <= max; start++) { // 假设从 start 开始,验证是否成立 }
👉 你之前那道「打乱字符串还原连续整数序列」就是这个模型。
javafor (int mask = 0; mask < (1 << n); mask++) { // mask 表示一个子集 }
java// 通常用回溯 dfs(当前排列)
经验判断(非常重要)👇
| n 大小 | 可接受算法 |
|---|---|
| n ≤ 100 | O(n³) |
| n ≤ 1,000 | O(n²) |
| n ≤ 100,000 | O(n log n) |
| n ≤ 1,000,000 | O(n) |
👉 如果题目数据范围小 + 没要求极致效率,优先想暴力。
不是“无脑枚举”,而是:
枚举过程中,发现不可能成功就提前退出
javafor (int i = 0; i < n; i++) { int sum = 0; for (int j = i; j < n; j++) { sum += a[j]; if (sum > target) break; // 剪枝 } }
| 当前 | 下一步 |
|---|---|
| 暴力枚举 | 前缀和 |
| 两层循环 | 滑动窗口 |
| 枚举区间 | 双指针 |
| 枚举子集 | 位运算 / 状态压缩 |
| 枚举组合 | 回溯 + 剪枝 |
👉 90% 的高效算法,都是从暴力枚举“优化”来的。
暴力枚举就是:把所有可能的情况列出来逐一验证,简单直接,但需要结合数据范围判断是否可行。
如果你愿意,我可以下一步帮你:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
public class Main {
static int n, x, y;
static int[] a;
static int count = 0;
textpublic static void main(String[] args) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); String line1 = reader.readLine(); if (line1 == null || line1.trim().isEmpty()) { return; } StringTokenizer st1 = new StringTokenizer(line1); n = Integer.parseInt(st1.nextToken()); x = Integer.parseInt(st1.nextToken()); y = Integer.parseInt(st1.nextToken()); String line2 = reader.readLine(); a = new int[n]; if (line2 != null && !line2.trim().isEmpty()) { StringTokenizer st2 = new StringTokenizer(line2); for (int i = 0; i < n; i++) { a[i] = Integer.parseInt(st2.nextToken()); } } // 题目要求选择 x 颗宝石,计算乘积等于 y 的组合数 // 数据范围 n < 20,比较小,可以直接用回溯法暴力枚举组合 combine(0, 0, 1); System.out.println(count); } // 递归函数:startIndex为当前遍历到的数组索引,currentCount为已选宝石数量,currentProduct为当前乘积 private static void combine(int startIndex, int currentCount, long currentProduct) { // 如果选够了x个宝石 if (currentCount == x) { if (currentProduct >= y) { count++; } return; } // 遍历所有可能的组合 for (int i = startIndex; i < n; i++) { // 剪枝优化:如果剩余的元素不足以填满 x 个,则不再继续 // n - i : 剩余可选元素个数 // x - currentCount : 还需要选的元素个数 if (n - i < x - currentCount) { break; } combine(i + 1, currentCount + 1, currentProduct * a[i]); } }
} 用例子拆分下这个暴力枚举的过程
下面我用你给的示例一步一步“跑”这段回溯/暴力枚举代码,把每一次递归、每一种组合、为什么是 3 种结果都拆清楚。
看完你就能真正理解:暴力枚举 + 组合回溯是怎么工作的。
textn = 4 x = 2 y = 8 宝石属性数组 a = [2, -3, 4, 5]
含义:
从
[2, -3, 4, 5]中
任选 2 个不同的元素组合
计算它们的乘积,判断是否 ≥ 8
下标用 (i, j) 表示:
| 组合 | 宝石 | 乘积 |
|---|---|---|
| (0,1) | 2 × -3 | -6 ❌ |
| (0,2) | 2 × 4 | 8 ✅ |
| (0,3) | 2 × 5 | 10 ✅ |
| (1,2) | -3 × 4 | -12 ❌ |
| (1,3) | -3 × 5 | -15 ❌ |
| (2,3) | 4 × 5 | 20 ✅ |
✅ 一共 3 种 满足条件
👉 所以输出是 3
javacombine(startIndex, currentCount, currentProduct)
| 参数 | 含义 |
|---|---|
| startIndex | 下一步从数组哪个下标开始选 |
| currentCount | 当前已经选了多少颗宝石 |
| currentProduct | 当前选中宝石的乘积 |
javacombine(0, 0, 1)
textcombine(1, 1, 2)
现在已经选了 1 颗,还要再选 1 颗。
选 a[1] = -3
textcombine(2, 2, -6)
已选够 2 颗
👉 -6 < 8 ❌ 不计数
选 a[2] = 4
textcombine(3, 2, 8)
已选够 2 颗
👉 8 ≥ 8 ✅ count++
选 a[3] = 5
textcombine(4, 2, 10)
已选够 2 颗
👉 10 ≥ 8 ✅ count++
textcombine(2, 1, -3)
选 a[2] = 4
textcombine(3, 2, -12)
❌ 不计数
选 a[3] = 5
textcombine(4, 2, -15)
❌ 不计数
textcombine(3, 1, 4)
👉 20 ≥ 8 ✅ count++textcombine(4, 2, 20)
textcombine(4, 1, 5)
⚠️ 剩余元素不足再选 1 个
触发剪枝:
javaif (n - i < x - currentCount) break;
直接返回,不再递归。
满足条件的组合是:
👉 count = 3
输出:
text3
n < 20这段代码是一个标准的组合型暴力枚举(回溯):
用递归枚举所有“选 x 个”的组合,用参数保存“当前状态”,在选满时判断并计数。
如果你愿意,下一步我可以:
画一个递归树
下面我用你给的示例 n=4, x=2, y=8, a=[2,-3,4,5],把递归过程画成一棵 递归树(组合回溯树)。
(每一条路径表示一次“选宝石”的过程,走到选满 2 个就叶子结算。)
初始调用:
combine(0, 0, 1)
textcombine(0,0,1) ├── 选 a[0]=2 → combine(1,1,2) │ ├── 再选 a[1]=-3 → combine(2,2,-6) → 叶子:-6 < 8 ❌ │ ├── 再选 a[2]=4 → combine(3,2,8) → 叶子: 8 ≥ 8 ✅ count++ │ └── 再选 a[3]=5 → combine(4,2,10) → 叶子:10 ≥ 8 ✅ count++ │ ├── 选 a[1]=-3 → combine(2,1,-3) │ ├── 再选 a[2]=4 → combine(3,2,-12) → 叶子:-12 < 8 ❌ │ └── 再选 a[3]=5 → combine(4,2,-15) → 叶子:-15 < 8 ❌ │ ├── 选 a[2]=4 → combine(3,1,4) │ └── 再选 a[3]=5 → combine(4,2,20) → 叶子:20 ≥ 8 ✅ count++ │ └── 选 a[3]=5 → combine(4,1,5) └── ⚠️ 剪枝:剩余宝石不足再选 1 个 → return
满足条件(乘积 ≥ 8)的路径一共有:
2 × 4 = 8 ✅2 × 5 = 10 ✅4 × 5 = 20 ✅因此 count = 3,输出:
3
如果你愿意,我还可以把这棵递归树画成更直观的 “选择路径表格” 或者再画一个更大的例子(比如 n=5,x=3)帮助你练熟这种题。
textList<Long> task = Arrays.stream(in.nextLine().split(" ")) .map(Long::parseLong) .collect(Collectors.toList()); 这个还可以换成什么写法
这行代码的作用是:把一行以空格分隔的数字字符串,转换成 List<Long>。
当然可以有多种等价写法,下面我按【常见 / 简洁 / 高效 / 传统】分类给你总结,方便你在不同场景下选择。
javaList<Long> task = Arrays.stream(in.nextLine().split(" ")) .map(Long::parseLong) .collect(Collectors.toList());
javaList<Long> task = Arrays.stream(in.nextLine().split(" ")) .map(s -> Long.parseLong(s)) .collect(Collectors.toList());
功能完全一样,只是把 Long::parseLong 写展开。
long[] 而不是 Listjavalong[] task = Arrays.stream(in.nextLine().split(" ")) .mapToLong(Long::parseLong) .toArray();
适合数值计算 / 性能更好的场景。
javaString[] arr = in.nextLine().split(" "); List<Long> task = new ArrayList<>(); for (String s : arr) { task.add(Long.parseLong(s)); }
👉 面试、初学、调试时非常推荐。
javaString[] arr = in.nextLine().split(" "); List<Long> task = new ArrayList<>(arr.length); for (int i = 0; i < arr.length; i++) { task.add(Long.parseLong(arr[i])); }
javaStringTokenizer st = new StringTokenizer(in.nextLine()); List<Long> task = new ArrayList<>(); while (st.hasMoreTokens()) { task.add(Long.parseLong(st.nextToken())); }
优点:
split 生成数组javaList<Long> task = new ArrayList<>(); for (int i = 0; i < n; i++) { task.add(in.nextLong()); }
⚠️ 前提:你知道要读多少个数 n。
javaBufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer st = new StringTokenizer(br.readLine()); List<Long> task = new ArrayList<>(); while (st.hasMoreTokens()) { task.add(Long.parseLong(st.nextToken())); }
👉 大数据 / 卡时间时首选。
| 场景 | 推荐写法 |
|---|---|
| 日常刷题 / 可读性 | Stream 或 for |
| 面试现场 | for 循环 |
| OJ / ACM / 大输入 | BufferedReader + StringTokenizer |
| 需要数组 | mapToLong().toArray() |
这一行代码本质是“字符串 → long → 容器”,
你可以用 Stream / for 循环 / StringTokenizer / Scanner 任意一种方式实现,
区别只在可读性和性能,不在功能。
如果你愿意,我也可以帮你把整套输入模板(Scanner版 / BufferedReader版)整理成一份“通用模板”。
import java.util.Scanner;
import java.util.*;
import java.util.stream.Collectors;
import java.math.BigInteger;
class Main {
public static void main(String[] args) {
// --- 1. 输入处理 ---
Scanner in = new Scanner(System.in);
// 读取一行以空格分隔的数字,并将其转换为Integer类型的数组
Integer[] height = Arrays.stream(in.nextLine().split(" ")).map(Integer::parseInt).toArray(Integer[]::new);
int n = height.length;
text// --- 2. 预计算每个位置左右两侧的最大高度 --- // `left[i]` 存储的是 height[0...i-1] 中的最大值 int[] left = new int[n]; for (int i = 1; i < n; i++) { left[i] = Math.max(left[i - 1], height[i - 1]); } // `right[i]` 存储的是 height[i+1...n-1] 中的最大值 int[] right = new int[n]; for (int i = n - 2; i >= 0; i--) { right[i] = Math.max(right[i + 1], height[i + 1]); } // --- 3. 计算每个位置可能的储水高度,并收集所有可能的水位 --- // `water_height[i]` 代表了如果水能漫到i位置时,理论上可能达到的最高水位。 // 这个水位由该点左右两侧最高山峰的较小者决定(经典的“接雨水”问题思路)。 int[] water_height = new int[n]; // `water_heights` 使用HashSet存储所有不重复的可能水位,作为后续计算的候选水位。 HashSet<Integer> water_heights = new HashSet<>(); for (int i = 1; i < n - 1; i++) { // 计算i位置上方的储水量 int water = Math.max(0, Math.min(left[i], right[i]) - height[i]); if (water != 0) { // `water_height[i]` 记录的是山峰高度 + 水深,即该点的总水位 water_height[i] = water + height[i]; water_heights.add(water_height[i]); } } // --- 4. 遍历所有可能的候选水位,寻找最佳边界和最大蓄水量 --- // result数组用于存储最终结果: [左边界下标, 右边界下标, 最大蓄水量] int[] result = new int[3]; // 遍历每一个在上一阶段收集到的、独一无二的候选水位 for (int single_water_height : water_heights) { // --- 4a. 确定当前水位下的有效蓄水区间 [l, r] --- // 寻找当前水位下,最左侧的蓄水点 l int l = 0; // 条件解读: // 1. water_height[l] < single_water_height: l点的理论水位低于当前候选水位,说明它不是这个水库的一部分。 // 2. height[l] >= single_water_height: l点本身的山峰就高于或等于当前水位,它不能储水,只能是边界。 // 只要满足任意一个条件,就继续向右寻找区间的左端点。 while (l < n && (water_height[l] < single_water_height || height[l] >= single_water_height)) { l++; } // 寻找当前水位下,最右侧的蓄水点 r int r = n - 1; // 逻辑同上,从右向左寻找区间的右端点。 while (r >= 0 && (water_height[r] < single_water_height || height[r] >= single_water_height)) { r--; } // --- 4b. 计算该水位和区间下的总蓄水量 --- if (l < r) { // 必须构成一个有效的区间 int total_water = 0; // 遍历[l, r]区间,累加每个点的蓄水量 for (int i = l; i <= r; i++) { total_water += Math.max(0, single_water_height - height[i]); } // --- 4c. 更新最优解 --- // 如果当前计算的总水量更大,则更新结果 if (total_water > result[2]) { // 这里的 l-1 和 r+1 是为了找到形成这个蓄水区间的实际山峰边界 result[0] = l - 1; result[1] = r + 1; result[2] = total_water; } // 如果水量相等,则根据题目要求,选择距离更近的一组边界 else if (total_water == result[2]) { int dis1 = (r + 1) - (l - 1); // 当前边界的距离 int dis2 = result[1] - result[0]; // 已记录的最佳边界的距离 if (dis1 < dis2) { result[0] = l - 1; result[1] = r + 1; } } } } // --- 5. 输出最终结果 --- if (result[2] == 0) { System.out.println(0); } else { System.out.print(result[0]); System.out.print(" "); System.out.print(result[1]); System.out.print(":"); System.out.print(result[2]); } }
} 总结代码思路,并加上详细注释(包括用到的java基础知识),然后输出
下面我先把这段代码的整体思路讲清楚(它在做什么、每一步为什么这么做、对应什么经典问题),然后给出一份带“详细注释 + 涉及的 Java 基础知识点说明”的完整代码(保持你原有逻辑不改,只增强可读性),最后给出一些实现上的注意点。
这题看起来像“建水库最大蓄水量”,本质上是把山脉数组当作高度图,寻找一对边界,使得中间可蓄水量最大;水位由“两边界较小高度”决定,蓄水量是区间内 min(hL, hR) - height[i] 的累加(负数按 0 计)。
你的代码采用了一个“候选水位枚举”的做法:
height[]。left[i] = height[0..i-1] 的最大值(i 左侧最高山)right[i] = height[i+1..n-1] 的最大值(i 右侧最高山)min(left[i], right[i])(代码里等价存为 water_height[i] = height[i] + water = min(left[i], right[i]))。HashSet 去重,得到一组候选水位。l 和最右位置 r[l..r] 区间在该水位下总蓄水量:sum(max(0, waterLevel - height[i]))0leftIndex rightIndex:waterSum注意:这份代码的策略是“枚举水位 + 找有效区间 + 计算面积”,不是最常见的双指针最大容器/接雨水写法,但在数据量不大时能工作。
javaimport java.util.*; // Scanner / Arrays / HashSet 等常用工具类 class Main { public static void main(String[] args) { // ========== 1) 输入处理 ========== // Scanner:用于从标准输入读取数据(控制台/OJ输入) Scanner in = new Scanner(System.in); // 读取一整行字符串,如:"1 8 6 2 5 4 8 3 7" String line = in.nextLine(); // split(" "):按空格分割成字符串数组 // Arrays.stream(...):把数组转成 Stream 流,便于链式处理 // map(Integer::parseInt):把每个字符串转为 int // toArray(Integer[]::new):收集成 Integer[] 数组(装箱类型) Integer[] height = Arrays.stream(line.split(" ")) .map(Integer::parseInt) .toArray(Integer[]::new); int n = height.length; // ========== 2) 预处理 left[] / right[] ========== // left[i] 表示:i 左侧(0..i-1)最高的山峰高度 // Java 基础:数组默认初始化为 0 int[] left = new int[n]; for (int i = 1; i < n; i++) { // Math.max:取两数最大值 // left[i-1] 是更左边的最高峰,height[i-1] 是紧邻左侧的峰 left[i] = Math.max(left[i - 1], height[i - 1]); } // right[i] 表示:i 右侧(i+1..n-1)最高的山峰高度 int[] right = new int[n]; for (int i = n - 2; i >= 0; i--) { right[i] = Math.max(right[i + 1], height[i + 1]); } // ========== 3) 计算每个点的“可达最高水位”,并收集候选水位 ========== // water_height[i]:如果 i 位置能蓄水,那么该位置最高水位(山高+水深) // 实际上它等于 min(left[i], right[i])(接雨水的结论) int[] water_height = new int[n]; // HashSet:集合,自动去重 // 用来收集“可能出现的水位高度”,减少后续枚举次数 HashSet<Integer> water_heights = new HashSet<>(); // 边界位置 i=0 和 i=n-1 不可能蓄水(没有两侧边界),从 1 到 n-2 for (int i = 1; i < n - 1; i++) { // min(left[i], right[i]) 是 i 能达到的最高水位 // 蓄水深度 = min(...) - height[i] int water = Math.max(0, Math.min(left[i], right[i]) - height[i]); if (water != 0) { // water_height[i] = height[i] + water // 等价于 min(left[i], right[i]) water_height[i] = water + height[i]; water_heights.add(water_height[i]); } } // ========== 4) 枚举候选水位,计算最大蓄水量及对应边界 ========== // result[0]:左边界下标 // result[1]:右边界下标 // result[2]:最大蓄水量 int[] result = new int[3]; // for-each 遍历 HashSet 中每个候选水位 for (int single_water_height : water_heights) { // ---- 4a) 找到该水位下的有效蓄水区间 [l, r] ---- // l:从左往右找到第一个“确实能在该水位下蓄水的位置” int l = 0; while (l < n && ( // water_height[l] < 当前水位:说明 l 位置最高也到不了这个水位,不能算这座“水库”的蓄水区 water_height[l] < single_water_height // height[l] >= 当前水位:这个位置本身就是山峰挡住/高于水位,不是“水面之下的洼地” || height[l] >= single_water_height )) { l++; } // r:从右往左找到第一个“能在该水位下蓄水的位置” int r = n - 1; while (r >= 0 && ( water_height[r] < single_water_height || height[r] >= single_water_height )) { r--; } // ---- 4b) 在 [l, r] 内计算总蓄水量 ---- if (l < r) { // 至少要有一个有效区间 int total_water = 0; for (int i = l; i <= r; i++) { // 每个点能蓄的水 = max(0, 水位 - 山高) total_water += Math.max(0, single_water_height - height[i]); } // ---- 4c) 更新答案:水量优先;水量相同选“边界更近” ---- if (total_water > result[2]) { // 这里用 l-1, r+1 作为“边界下标”的意图: // l 和 r 是“真正有水的内部点”,它们外侧才是挡水的山峰边界 result[0] = l - 1; result[1] = r + 1; result[2] = total_water; } else if (total_water == result[2]) { // 边界距离:右边界 - 左边界 int dis1 = (r + 1) - (l - 1); int dis2 = result[1] - result[0]; // 题意:如果多个满足条件的边界,选择距离更近的一组 if (dis1 < dis2) { result[0] = l - 1; result[1] = r + 1; } } } } // ========== 5) 输出 ========== if (result[2] == 0) { // 无法蓄水:输出 0(题意:此时不返回边界) System.out.println(0); } else { // 输出格式:leftIndex rightIndex:waterSum System.out.print(result[0]); System.out.print(" "); System.out.print(result[1]); System.out.print(":"); System.out.print(result[2]); } } }
left[]/right[] 的思路来自“接雨水”:
单点能蓄水高度取决于左右最高峰的较小值。
为什么要用 HashSet 收集水位?
因为可能出现很多重复水位,去重后再枚举可以减少重复计算。
输出边界使用 l-1、r+1
你代码的语义是:l..r 是“真正有水的内部区间”,外侧才是“挡水边界”。
这和题目“边界是山脉上的两个点”的描述是对齐思路的一种实现方式(但要小心极端情况下 l=0 或 r=n-1 会出现 -1 / n 的边界风险——当前代码靠 while 条件通常能规避,但严格来说需要更稳的边界保护)。
如果你希望我再做一步:用 示例 1 8 6 2 5 4 8 3 7 带你把 left/right/water_heights 具体算一遍,并解释为什么输出 1 6:15,我也可以继续按数组逐项展开。
mysql里的覆盖索引
下面用一句话 → 原理 → 例子 → 怎么用 → 什么时候用 → 注意点的结构,帮你把 **MySQL 覆盖索引(Covering Index)**讲透,面试和实战都够用。
覆盖索引:查询需要的所有字段,都能直接从索引中拿到,不用回表查数据行。
关键词:不回表(Using index)
索引列 + 主键sqlSELECT name FROM user WHERE age = 20;
这一步就叫:回表
如果查询的字段 全部都在索引里:
sqlSELECT id, age FROM user WHERE age = 20;
age + id👉 这就是 覆盖索引
EXPLAINsqlEXPLAIN SELECT age FROM user WHERE age = 20;
如果 Extra 列出现:
textUsing index
✅ 说明用了 覆盖索引
⚠️ 如果是:
textUsing where
或
textUsing index condition
说明还可能回表。
sqlCREATE TABLE user ( id INT PRIMARY KEY, age INT, name VARCHAR(50), INDEX idx_age_name (age, name) );
sqlSELECT name FROM user WHERE age = 20;
索引中有:
(age, name, id)
其实这是覆盖索引(很多人这里容易误判)
👉 因为 name 已经在联合索引里了
➡️ 不回表
sqlSELECT address FROM user WHERE age = 20;
address 不在索引中
➡️ 回表
sqlSELECT age, name FROM user WHERE age = 20;
查询字段完全在 (age, name) 索引中
➡️ Using index
sqlINDEX idx_age_name (age, name)
sqlSELECT age FROM user WHERE age = 20; SELECT age, name FROM user WHERE age = 20;
sqlSELECT name FROM user WHERE name = 'Tom'; -- 不满足最左前缀 SELECT age, address FROM user WHERE age = 20; -- address 不在索引中
sqlSELECT name FROM user WHERE age = 20;
sqlCREATE INDEX idx_age_name ON user(age, name);
👉 让查询字段 被索引覆盖
WHERE 仍然会用索引过滤,只是不回表。
sqlSELECT id FROM user WHERE id = 10;
是的,因为主键索引本身就是数据。
不一定:
👉 空间 + 写性能换查询性能
| 特性 | 覆盖索引 | 索引下推 |
|---|---|---|
| 是否回表 | ❌ 不回表 | ✅ 可能回表 |
| Extra | Using index | Using index condition |
| 目的 | 减少 I/O | 减少回表次数 |
覆盖索引是指查询所需的所有字段都包含在索引中,MySQL 可以直接从索引返回结果而不回表,从而减少 I/O、提升查询性能。
如果你愿意,我可以下一步帮你:
mysql的最左匹配原则
下面把 MySQL 最左匹配原则(Leftmost Prefix Rule) 用通俗 + 规则 + 例子 + 易错点 + 面试总结一次讲清楚。
联合索引必须从最左边的列开始使用,不能跳过前面的列。
假设你建了一个 联合索引:
sqlCREATE INDEX idx_abc ON t(a, b, c);
索引的顺序是:
👉 a → b → c
这棵索引在逻辑上相当于按 (a, b, c) 排好序。
sqlWHERE a = 1 WHERE a = 1 AND b = 2 WHERE a = 1 AND b = 2 AND c = 3
👉 从 a 开始,没有跳过前面的列
sqlWHERE b = 2 WHERE c = 3 WHERE b = 2 AND c = 3
👉 跳过了 a,直接从 b / c 开始
联合索引在 B+Tree 中的排序方式是:
text(a1,b1,c1) (a1,b1,c2) (a1,b2,c1) (a2,b1,c1) ...
👉 先按 a 排,再按 b 排,再按 c 排
所以:
a,就没法快速定位 bsqlINDEX idx_abc(a, b, c)
sqlWHERE a = 1 AND b > 10 AND c = 3
a = 1 ✅b > 10 ✅(到这里为止)c = 3 ❌ 用不到索引👉 一旦出现范围查询(>, <, BETWEEN, LIKE %)
后面的列无法继续使用索引
sqlWHERE name LIKE 'Tom%'
sqlWHERE name LIKE '%Tom'
原因:
'Tom%' 是确定前缀'%Tom' 前缀不确定sqlWHERE b = 2 AND a = 1
仍然可以使用 (a,b) 索引
👉 SQL 中条件顺序不重要,优化器会自动调整
👉 关键是:条件里有没有“最左列”
sqlSELECT c, b FROM t WHERE a = 1 AND b = 2;
(a,b,c) 实际能支持:| 查询条件 | 是否使用索引 |
|---|---|
| a | ✅ |
| a,b | ✅ |
| a,b,c | ✅ |
| a,c | ❌(跳过 b) |
| b | ❌ |
| b,c | ❌ |
sqlINDEX idx_abc(a,b,c)
sqlSELECT a,b FROM t WHERE a = 1 AND b = 2;
👉 覆盖索引 + 不回表 + 性能最好
❌ 错
✅ MySQL 会自动优化
❌ 错
✅ 必须从最左列开始
❌ 错
✅ 范围会“截断”索引使用
把“区分度高、最常作为查询条件”的列放在联合索引最左边
例如:
sqlWHERE user_id = ? AND status = ? AND create_time > ?
👉 索引设计:
sql(user_id, status, create_time)
MySQL 联合索引遵循最左匹配原则,查询条件必须从索引的最左列开始,不能跳过;一旦遇到范围查询,后续列将无法继续使用索引。
如果你愿意,我可以下一步:
总结一下mysql索引相关面试高频问题
下面给你一份 MySQL 索引相关的面试高频问题「系统总结版」,我按 “必背结论 + 原理理解 + 面试答法” 来整理,基本覆盖 80%~90% 的索引面试题 👇
(你可以当成一份 索引速查表 + 面试背诵稿)
答法(标准):
索引是数据库中用于加快数据查询速度的数据结构,在 MySQL InnoDB 中主要是 B+Tree,通过减少磁盘 I/O 来提升查询性能。
作用:
SELECTWHERE / ORDER BY / GROUP BY| 类型 | 说明 |
|---|---|
| 主键索引 | 聚簇索引,数据本身 |
| 普通索引 | 最基本索引 |
| 唯一索引 | 不允许重复 |
| 联合索引 | 多列索引 |
| 覆盖索引 | 查询字段全在索引中 |
| 全文索引 | 文本搜索(InnoDB 支持) |
高频题,重点记:
B+Tree:
Hash:
👉 MySQL 需要“范围 + 排序 + 稳定 IO”
一句话:
InnoDB 中,数据行本身存储在主键索引的叶子节点中,这就是聚簇索引。
通过二级索引找到主键,再根据主键去聚簇索引查完整数据的过程
❌ 性能开销点
✅ 覆盖索引可以避免
联合索引必须从最左列开始使用,不能跳过
sqlINDEX (a, b, c)
| 查询 | 是否用索引 |
|---|---|
| a | ✅ |
| a,b | ✅ |
| a,b,c | ✅ |
| b | ❌ |
| a,c | ❌ |
sqlWHERE a = 1 AND b > 10 AND c = 3
a、b 用索引c ❌ 不能再用索引👉 范围查询会截断索引
| 条件 | 是否走索引 |
|---|---|
LIKE 'abc%' | ✅ |
LIKE '%abc' | ❌ |
LIKE '%abc%' | ❌ |
查询字段全部在索引中,不需要回表
EXPLAIN 显示:
textUsing index
❌ 对索引列使用函数
sqlWHERE YEAR(create_time) = 2024
❌ 隐式类型转换
sqlWHERE phone = 123456 -- phone 是 varchar
❌ 使用 != / <>
❌ 使用 OR(部分情况)
❌ LIKE '%xxx'
❌ 联合索引跳过最左列
OR 一定会导致索引失效吗?❌ 不一定
OR 两边 都有索引,可能走索引❌ 不一定
要用索引必须:
否则:
textUsing temporary; Using filesort
黄金原则:
等值查询列在前,范围查询列在后,区分度高的列靠前
❌ 不是
👉 索引是“读写权衡”
| 字段 | 含义 |
|---|---|
| type | 扫描方式(重要) |
| key | 使用的索引 |
| rows | 预估扫描行数 |
| Extra | Using index / filesort |
textsystem > const > eq_ref > ref > range > index > ALL
👉 ALL = 全表扫描(危险)
MySQL InnoDB 使用 B+Tree 作为索引结构,主键索引是聚簇索引,数据存储在叶子节点;二级索引存储索引列和主键值,查询时可能需要回表。联合索引遵循最左匹配原则,范围查询会中断索引使用。覆盖索引可以避免回表、提升性能。索引并非越多越好,需要结合查询场景合理设计。
如果你愿意,我可以下一步帮你:
贪心算法和动态规划的算法思路,给个java框架
下面把贪心算法和**动态规划(DP)**的核心思路讲清楚,并给你一套可直接套用的 Java 框架模板(含常见变体)。
贪心的核心是:每一步都做当前看起来最优的选择,并希望整体也最优。
通常包含 3 步:
✅ 贪心能成立的关键:问题满足
面试/刷题里,贪心题往往能用“排序 + 一次遍历/堆”搞定。
javaimport java.util.*; public class GreedyTemplate { static class Item { int a, b; // 你可以理解为 start/end, weight/value 等 Item(int a, int b) { this.a = a; this.b = b; } } public static int solve(List<Item> items) { // 1) 根据贪心规则排序(示例:按 b 升序) items.sort((x, y) -> Integer.compare(x.b, y.b)); // 2) 维护答案/状态 int ans = 0; // 3) 扫描做局部最优决策 int last = Integer.MIN_VALUE; // 示例:上一次选择的结束位置/时间 for (Item it : items) { // 示例判断:能选则选 if (it.a >= last) { ans++; last = it.b; } } return ans; } public static void main(String[] args) { // TODO: 读入数据,构造 items,调用 solve } }
javaimport java.util.*; public class GreedyWithHeapTemplate { public static int solve(int[][] arr) { // 示例:按某字段排序 Arrays.sort(arr, (x, y) -> Integer.compare(x[0], y[0])); PriorityQueue<Integer> pq = new PriorityQueue<>(); // 小根堆/大根堆按需 int ans = 0; for (int[] e : arr) { // 示例:把候选加入堆 pq.offer(e[1]); // 示例:如果违反约束,就弹出最差的 // if (violated) pq.poll(); ans = Math.max(ans, pq.size()); } return ans; } }
DP 的核心是:把问题拆成子问题,保存子问题答案避免重复计算。
通常步骤:
dp[...]:dp 表示什么?dp[new] 如何从 dp[old] 推来?DP 能成立的关键:
javaimport java.util.*; public class DP1DTemplate { public static int solve(int[] nums) { int n = nums.length; int[] dp = new int[n]; // dp[i] 的含义自己定义 // 1) 初始化 Arrays.fill(dp, 1); // 2) 状态转移(示例:LIS O(n^2)) for (int i = 0; i < n; i++) { for (int j = 0; j < i; j++) { if (nums[j] < nums[i]) { dp[i] = Math.max(dp[i], dp[j] + 1); } } } // 3) 汇总答案 int ans = 0; for (int v : dp) ans = Math.max(ans, v); return ans; } }
javaimport java.util.*; public class DP2DTemplate { public static int solve(String a, String b) { int n = a.length(), m = b.length(); int[][] dp = new int[n + 1][m + 1]; // dp[i][j]:前 i 个与前 j 个的最优值 // 初始化(示例:编辑距离) for (int i = 0; i <= n; i++) dp[i][0] = i; for (int j = 0; j <= m; j++) dp[0][j] = j; // 转移 for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { if (a.charAt(i - 1) == b.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], // 替换 Math.min(dp[i - 1][j], // 删除 dp[i][j - 1])); // 插入 } } } return dp[n][m]; } }
javaimport java.util.*; public class MemoDFSTemplate { static int[] memo; // -1 表示没算过 static int[] arr; static int dfs(int i) { if (i < 0) return 0; // 边界 if (memo[i] != -1) return memo[i]; // 示例:打家劫舍 int take = dfs(i - 2) + arr[i]; int skip = dfs(i - 1); return memo[i] = Math.max(take, skip); } public static int solve(int[] nums) { arr = nums; memo = new int[nums.length]; Arrays.fill(memo, -1); return dfs(nums.length - 1); } }
如果你给我一个你正在做的具体题(或你截图里某一题),我可以直接告诉你:它更像贪心还是 DP,并把上面的框架替换成“这道题的专用模板”。
import java.util.Scanner;
import java.util.*;
import java.util.stream.Stream;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = Integer.parseInt(in.nextLine());
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = Integer.parseInt(in.nextLine());
}
text//二维DP已经转成1维DP, 审核大佬请注意。。。。 int[] dp = new int[3000]; for (int num : nums) { for (int j = 2880; j >= (int) Math.ceil(num / 512.0); j--) { dp[j] = Math.max(dp[j], dp[j - (int) Math.ceil(num / 512.0)] + num); } } System.out.println(dp[2880]); return; }
}
这段代码本质是一个 0/1 背包(Knapsack):
n 个任务/文件/数据块(用 num 表示“收益”,也就是你想尽量装进去的总大小)。num 同时还有一个“代价/占用”——用 ceil(num / 512.0) 表示,占用的单位是 512 的块数(向上取整)。2880 个块(也就是最多 2880 个“512块”单位)。num 总和最大。dp[2880]:容量为 2880 时能得到的最大总 num。javaint n = Integer.parseInt(in.nextLine()); int[] nums = new int[n]; for (int i = 0; i < n; i++) { nums[i] = Integer.parseInt(in.nextLine()); }
nums[]dp[i][j]:考虑前 i 个物品、容量为 j 时的最大收益dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] + v)dp[j]:当前处理到某个阶段时,容量为 j 的最大收益javaint[] dp = new int[3000];
dp[j] 表示:占用块数(容量)不超过 j 时,能获得的最大 num 总和2880,所以开 3000 足够。对每个 num:
num(你想最大化的东西)ceil(num / 512.0)(占用多少个 512 块)代码里重复算了好几次:
java(int) Math.ceil(num / 512.0)
这就是该物品的 weight。
javafor (int j = 2880; j >= weight; j--) { dp[j] = Math.max(dp[j], dp[j - weight] + num); }
倒序是 0/1 背包的关键:
dp[j - weight] 还是“上一轮(未用当前物品)”的状态javadp[j] = Math.max(dp[j], dp[j - weight] + num);
含义:
dp[j]:不选当前物品(保持原来最优)dp[j - weight] + num:选当前物品(容量减少 weight,收益增加 num)javaSystem.out.println(dp[2880]);
表示容量为 2880 时的最大收益(最大总 num)。
O(n * 2880),很常规的背包复杂度O(2880)把 weight 提前算出来可读性更强:
javafor (int num : nums) { int w = (int) Math.ceil(num / 512.0); // 占用块数 for (int j = 2880; j >= w; j--) { dp[j] = Math.max(dp[j], dp[j - w] + num); } }
如果你把题目背景贴一下(2880、512 分别代表什么,比如分钟/存储块/带宽单位),我还能帮你把“状态含义、为什么这样建模”用更贴近题意的语言讲一遍,并指出有没有边界坑(例如 num 很大时 dp 值是否会溢出、是否需要 long)。
这题本质就是你前面那段代码实现的 0/1 背包问题。我按题意把“为什么能这么建模、每个常量从哪来、DP 怎么转移”讲清楚。
对每个文件大小 Si:
价值(value):Si(文件内容大小,题目要最大化的东西)
重量(weight):该文件会占用多少个 512 字节块
因为哪怕只多 1 字节也得多占一整块。
背包容量(capacity):软盘有多少块
所以就是:从 n 个文件中选一些(每个最多选 1 次),使得块数和 ≤ 2880,且文件大小和最大。
ceil(num / 512.0) 和 2880你代码这句:
java(int) Math.ceil(num / 512.0)
就是把 Si 变成 wi:该文件要占用的块数。
你代码里的容量上限:
javafor (int j = 2880; j >= weight; j--)
对应软盘总块数 2880。
你用的是 0/1 背包的一维优化版:
dp[j]:当可用块数不超过 j 时,最多能拷贝的“文件内容总大小”是多少初始化默认全是 0,表示什么都不选时内容总大小为 0。
对每个文件(size=num,weight=w):
javadp[j] = Math.max(dp[j], dp[j - w] + num);
含义:
dp[j]:不选这个文件dp[j - w] + num:选这个文件,占用 w 块,内容大小增加 num你代码:
javafor (int j = 2880; j >= w; j--)
倒序的意义:保证每个文件只被选一次。
dp[j-w] 还是“上一轮(不含当前文件)”的结果。dp[2880]dp[2880] 就是:在软盘最多 2880 块限制下,能拷贝的最大文件内容总大小。
示例文件大小:
对应占用块数(向上取整):
容量 2880 块:
所以答案是 1474542,正好吻合题目输出。
把每个文件看成一个物品:价值=文件大小,重量=ceil(文件大小/512),背包容量=1474560/512=2880,然后用 0/1 背包一维 DP(倒序遍历容量) 求最大价值,最终输出
dp[2880]。
如果你想,我也可以把你的代码按题意变量名重写一版(比如 capacityBlocks、blocksNeeded),更贴合题目,更容易记忆。
画图解释下这个过程
下面我用**“图 + 具体数字”**把这道题(软盘拷贝文件)对应的 0/1 背包 DP 过程画出来,帮你直观看到:
dp[j] = max(dp[j], dp[j-w]+v) 在做什么dp[2880]软盘容量:1474560 bytes
每块:512 bytes
所以总块数:
2880 blocks = 1474560 / 512
每个文件大小 Si 放进软盘需要占多少块(向上取整):
textweight = ceil(Si / 512) value = Si
✅ 背包容量 = 2880
✅ 物品 = 每个文件
✅ 目标 = 价值(内容大小)最大
示例输入(你图里):
text3 个文件 S1 = 737270 S2 = 737272 S3 = 737288
计算每个文件占用块数:
| 文件 | 大小Si(value) | 块数wi(weight)=ceil(Si/512) | 实际占空间 |
|---|---|---|---|
| 1 | 737270 | 1440 | 737280 |
| 2 | 737272 | 1440 | 737280 |
| 3 | 737288 | 1441 | 737792 |
背包容量 C = 2880 块。
你的代码里:
javadp[j] = 容量不超过 j 块时,能放进去的最大文件内容总大小
我们只画关键容量点(1440、1441、2880):
text容量 j(块) : 0 1439 1440 1441 ... 2879 2880 dp[j] : 0 0 ? ? ? ?
转移公式:
dp[j] = max(dp[j], dp[j-1440] + 737270)
因为 dp[0]=0,所以 dp[1440] 能更新:
dp[1440] = max(0, dp[0]+737270) = 737270
图示:
text加入文件1后: 容量 j(块) : 0 1439 1440 1441 ... 2879 2880 dp[j] : 0 0 737270 737270 737270 737270
(因为只要 j>=1440,都可以放文件1,所以都能达到 737270)
同样公式:
dp[j] = max(dp[j], dp[j-1440] + 737272)
关键更新看 j=2880:
所以:
textdp[2880] = max(737270, 737270 + 737272) = 1474542
图示(重点):
text加入文件2后: 容量 j(块) : 0 1439 1440 1441 ... 2879 2880 dp[j] : 0 0 737272 737272 737272 1474542 ↑ ↑ ↑ (只放文件2更大) (还能放2个?) (放文件1+2)
你会看到:
✅ 此时已经得到题目答案!
看看能不能让 dp[2880] 更大:
如果想放文件3进 dp[2880]:
textdp[2880-1441] = dp[1439] dp[1439] = 0 (容量不够放任何一个1440或1441块文件)
所以:
dp[2880] = max(1474542, 0 + 737288) = 1474542
即文件3无法和另一个文件组合塞进 2880(因为 1441+1440=2881 超了)。
最终:
dp[2880] = 1474542
✅ 输出就是 1474542
你的代码:
javafor (j = 2880; j >= w; j--)
保证 每个文件只用一次(0/1 背包)
假设只有一个文件 w=2 v=5,容量=4
正序 j 从小到大:
textj=2: dp[2] = dp[0] + 5 = 5 (用了一次) j=4: dp[4] = dp[2] + 5 = 10 (dp[2]刚更新,等于用同一个文件两次!)
这就变成了“完全背包”(一个文件能无限次放)——题目不允许!
倒序时:
textj=4: dp[4] 用的是 dp[2](上一轮状态,未放当前文件) j=2: dp[2] 才更新
所以同一个文件不会被重复用。
text文件 -> (weight=ceil(size/512), value=size) 容量 -> 2880 blocks dp[j] -> 容量≤j时最大总内容大小 对每个文件: for j 从 2880 倒序到 weight: dp[j] = max(dp[j], dp[j-weight] + value) 答案:dp[2880]
如果你愿意,我还可以给你画一张更“网格状”的 DP 表(二维 dp[i][j])并展示如何压缩成一维 dp[j],这样你对“二维→一维”的理解会更彻底。
总结下动态规划算法的解题思路以及java模板
下面给你一份 动态规划(Dynamic Programming, DP)解题思路的系统总结 + Java 通用模板。
目标是:看到题就知道怎么想、怎么写、怎么套模板。
把一个大问题拆成若干个会重复出现的小问题,用数组保存小问题的答案,避免重复计算。
关键词:
先想清楚:
dp[i]/dp[i][j]表示什么?
好的状态定义:
❌ 错误示例:
dp[i] = 最优解(太模糊)
✅ 正确示例:
dp[i]:以第 i 个元素结尾的最长递增子序列长度dp[i][j]:前 i 个物品、容量为 j 时的最大价值回答:
当前状态怎么从“更小的状态”得到?
常见形式:
textdp[i] = max / min / sum(dp[i-1], dp[i-2], ...) dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] + v)
这是 DP 的“灵魂”。
边界条件一定要对:
dp[0] 是什么?很多 DP bug 都死在初始化。
必须保证:
在算 dp[i] 时,所依赖的 dp 状态已经算好
dp[n]?max(dp[i])?dp[0][n-1]?当前状态只依赖前 1~2 个状态。
javapublic int solve(int[] nums) { int n = nums.length; int[] dp = new int[n]; // 初始化 dp[0] = nums[0]; for (int i = 1; i < n; i++) { dp[i] = Math.max(dp[i - 1], nums[i]); // 示例转移 } return dp[n - 1]; }
dp[j]:容量 ≤ j 时的最大价值javapublic int knapsack(int[] w, int[] v, int capacity) { int[] dp = new int[capacity + 1]; for (int i = 0; i < w.length; i++) { for (int j = capacity; j >= w[i]; j--) { dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]); } } return dp[capacity]; }
🔥 倒序遍历 = 0/1 背包(每个物品只能用一次)
javafor (int i = 0; i < w.length; i++) { for (int j = w[i]; j <= capacity; j++) { dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]); } }
🔥 正序遍历 = 完全背包(物品可无限用)
dp[i][j]:前 i 个字符 & 前 j 个字符的最优解
javapublic int minDistance(String a, String b) { int n = a.length(), m = b.length(); int[][] dp = new int[n + 1][m + 1]; for (int i = 0; i <= n; i++) dp[i][0] = i; for (int j = 0; j <= m; j++) dp[0][j] = j; for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { if (a.charAt(i - 1) == b.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = 1 + Math.min( dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1]) ); } } } return dp[n][m]; }
dp[l][r] 表示区间 [l, r] 的最优解javafor (len = 2; len <= n; len++) for (l = 0; l + len - 1 < n; l++) r = l + len - 1
javaint[] memo; int dfs(int i) { if (i < 0) return 0; if (memo[i] != -1) return memo[i]; memo[i] = Math.max(dfs(i - 1), dfs(i - 2) + value[i]); return memo[i]; }
出现以下关键词,优先想 DP:
| 特征 | 贪心 | DP |
|---|---|---|
| 是否回头 | 不回头 | 会 |
| 是否考虑全局 | 否 | 是 |
| 是否有后效性 | 无 | 有 |
| 是否存状态 | 否 | 是 |
动态规划通过定义状态、建立状态转移方程、保存子问题结果,将指数级问题优化为多项式时间,适用于具有最优子结构和重复子问题的场景。
如果你愿意,我可以下一步帮你:
贪心算法的模板
下面给你一份 「贪心算法 Java 模板大全」,按 最常见使用场景来整理。
目标是:看到题 → 立刻知道用哪一类贪心 → 直接套模板改条件。
贪心的核心不是代码,而是:
👉 “我现在选这个,一定不吃亏”
javaimport java.util.*; public class GreedyInterval { static class Interval { int start, end; Interval(int s, int e) { start = s; end = e; } } public static int solve(List<Interval> list) { // 1. 按结束时间升序 list.sort((a, b) -> Integer.compare(a.end, b.end)); int count = 0; int lastEnd = Integer.MIN_VALUE; // 2. 线性扫描 for (Interval it : list) { if (it.start >= lastEnd) { count++; lastEnd = it.end; } } return count; } }
javaimport java.util.*; public class GreedyHeap { public static int solve(int[][] tasks) { // tasks[i] = {deadline, duration} Arrays.sort(tasks, (a, b) -> Integer.compare(a[0], b[0])); PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a); int time = 0; for (int[] t : tasks) { time += t[1]; maxHeap.offer(t[1]); // 如果超时,反悔:移除耗时最大的任务 if (time > t[0]) { time -= maxHeap.poll(); } } return maxHeap.size(); } }
javaimport java.util.*; public class GreedyTwoPointers { public static int solve(int[] a, int[] b) { Arrays.sort(a); Arrays.sort(b); int i = 0, j = 0, count = 0; while (i < a.length && j < b.length) { if (a[i] <= b[j]) { count++; i++; j++; } else { j++; } } return count; } }
javaimport java.util.*; public class GreedyRatio { static class Item { double value, weight; Item(double v, double w) { value = v; weight = w; } } public static double solve(List<Item> items, double cap) { items.sort((a, b) -> Double.compare(b.value / b.weight, a.value / a.weight) ); double ans = 0; for (Item it : items) { if (cap >= it.weight) { cap -= it.weight; ans += it.value; } else { ans += it.value * (cap / it.weight); break; } } return ans; } }
javapublic class GreedyLocal { public static int maxSubArray(int[] nums) { int cur = nums[0]; int ans = nums[0]; for (int i = 1; i < nums.length; i++) { cur = Math.max(nums[i], cur + nums[i]); ans = Math.max(ans, cur); } return ans; } }
| 特征 | 贪心 | 动态规划 |
|---|---|---|
| 是否回头 | 不回头 | 会 |
| 是否存历史状态 | 否 | 是 |
| 是否考虑所有可能 | 否 | 是 |
| 是否能局部决定 | 能 | 不能 |
👉 如果当前选择不会影响未来最优性 → 贪心
👉 如果“现在选错会影响后面” → DP
❌ 没有排序直接贪
❌ 局部规则选错(比如选最早开始而不是最早结束)
❌ 忘了“反悔机制”(堆)
❌ 把 DP 题硬套贪心
贪心算法通过每一步选择当前最优解,并期望其能导出全局最优解,通常结合排序、堆或双指针使用,前提是问题满足贪心选择性质。
如果你愿意,我可以下一步:
暴力枚举算法模板以及bfs的算法模板
暴力枚举算法模板以及bfs的算法模板
下面给你一份 「暴力枚举算法模板 + BFS(广度优先搜索)算法模板」 的系统总结版,都是 Java 常用模板,可以直接套用到题目里。
我会按:思路 → 模板 → 适用场景 → 易错点 来讲,方便你真正用得上。
把所有可能情况一个不漏地枚举出来,逐一判断是否满足条件,取最优或计数。
关键词:
for / 递归 枚举所有可能javapublic static int bruteForce1(int[] arr) { int ans = Integer.MIN_VALUE; for (int i = 0; i < arr.length; i++) { ans = Math.max(ans, arr[i]); } return ans; }
javapublic static int bruteForce2(int[] arr) { int ans = 0; int n = arr.length; for (int i = 0; i < n; i++) { for (int j = i; j < n; j++) { // 枚举区间 [i, j] // 判断条件 } } return ans; }
javafor (int i = 0; i < n; i++) for (int j = i + 1; j < n; j++) for (int k = j + 1; k < n; k++) { // 判断三元组 }
javapublic static void subsetEnum(int[] arr) { int n = arr.length; for (int mask = 0; mask < (1 << n); mask++) { for (int i = 0; i < n; i++) { if ((mask & (1 << i)) != 0) { // arr[i] 被选中 } } } }
javastatic int n, k; static int[] arr; static void dfs(int idx, int count) { if (count == k) { // 处理一个组合 return; } if (idx == n) return; // 选当前 dfs(idx + 1, count + 1); // 不选当前 dfs(idx + 1, count); }
经验判断:
| 数据规模 | 可接受 |
|---|---|
| n ≤ 100 | O(n³) |
| n ≤ 1e3 | O(n²) |
| n ≤ 20 | 2ⁿ 枚举 |
从起点开始,一层一层向外扩展,保证最先到达的一定是最短路径。
关键词:
Queuevisitedjavaimport java.util.*; public class BFSBasic { static void bfs(int start, List<Integer>[] graph) { boolean[] visited = new boolean[graph.length]; Queue<Integer> queue = new LinkedList<>(); queue.offer(start); visited[start] = true; while (!queue.isEmpty()) { int cur = queue.poll(); for (int next : graph[cur]) { if (!visited[next]) { visited[next] = true; queue.offer(next); } } } } }
javastatic int bfs(int start, int target, List<Integer>[] graph) { boolean[] visited = new boolean[graph.length]; Queue<Integer> queue = new LinkedList<>(); queue.offer(start); visited[start] = true; int step = 0; while (!queue.isEmpty()) { int size = queue.size(); // 当前层 for (int i = 0; i < size; i++) { int cur = queue.poll(); if (cur == target) return step; for (int next : graph[cur]) { if (!visited[next]) { visited[next] = true; queue.offer(next); } } } step++; } return -1; }
javastatic int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}}; static int bfs(char[][] grid, int sx, int sy) { int m = grid.length, n = grid[0].length; boolean[][] visited = new boolean[m][n]; Queue<int[]> queue = new LinkedList<>(); queue.offer(new int[]{sx, sy}); visited[sx][sy] = true; int step = 0; while (!queue.isEmpty()) { int size = queue.size(); for (int i = 0; i < size; i++) { int[] cur = queue.poll(); int x = cur[0], y = cur[1]; for (int[] d : dirs) { int nx = x + d[0], ny = y + d[1]; if (nx>=0 && nx<m && ny>=0 && ny<n && !visited[nx][ny] && grid[nx][ny] != '#') { visited[nx][ny] = true; queue.offer(new int[]{nx, ny}); } } } step++; } return step; }
❌ 忘记 visited → 死循环
❌ 入队时不标记 visited → 重复入队
❌ BFS 却用递归(那是 DFS)
❌ 需要最短路径却用了 DFS
| 问题特征 | 用法 |
|---|---|
| 所有情况都要试 | 暴力枚举 |
| 最少步数 / 最近 | BFS |
| n 很小 | 暴力 |
| 状态转移 | BFS |
暴力枚举通过穷举所有可能情况并逐一判断,适用于数据规模较小的问题,是所有优化算法的基础。
BFS 使用队列按层遍历状态空间,保证首次到达目标即为最短路径,常用于无权图和最少步数问题。
如果你愿意,我可以下一步帮你:
import java.util.*;
import java.lang.Math;
// POItem 类用于表示一条采购订单记录,并实现Comparable接口以便排序
class POItem implements Comparable<POItem> {
int itemID;
int quantity;
int price; // 使用int存储最终价格
textpublic POItem(int itemID, int quantity, int price) { this.itemID = itemID; this.quantity = quantity; this.price = price; } // 自定义排序逻辑 @Override public int compareTo(POItem other) { // 规则4: 先按商品ID升序排列 if (this.itemID != other.itemID) { return Integer.compare(this.itemID, other.itemID); } // 如果商品ID相同,则按数量降序排列 return Integer.compare(other.quantity, this.quantity); }
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
text// 读取第一行的PR数量N int n = scanner.nextInt(); // highPricePOs 存储单价高于100的商品,这些商品单独生成PO List<POItem> highPricePOs = new ArrayList<>(); // lowPriceProducts 使用 Map 来按商品ID合并数量和存储单价 // Map 的 key 是商品ID, value 是一个包含数量和价格的临时对象数组 Map<Integer, int[]> lowPriceProducts = new HashMap<>(); // 循环读取N行PR数据 for (int i = 0; i < n; i++) { int itemID = scanner.nextInt(); int quantity = scanner.nextInt(); int price = scanner.nextInt(); int status = scanner.nextInt(); // 规则1: 只处理状态为 "审批通过" (0) 的PR if (status != 0) { continue; } // 规则2: 根据单价将商品分类 if (price > 100) { // 单价高于100元的商品,直接添加到列表 highPricePOs.add(new POItem(itemID, quantity, price)); } else { // 单价低于或等于100元的商品,进行合并 // Map的getOrDefault方法可以简化代码 int[] currentData = lowPriceProducts.getOrDefault(itemID, new int[]{0, price}); currentData[0] += quantity; // 累加数量 lowPriceProducts.put(itemID, currentData); } } scanner.close(); // --- 开始处理生成PO --- // allPOs 列表用于存储所有最终的PO记录 List<POItem> allPOs = new ArrayList<>(highPricePOs); // 遍历合并后的低价商品map,并应用折扣规则 for (Map.Entry<Integer, int[]> entry : lowPriceProducts.entrySet()) { int itemID = entry.getKey(); int totalQuantity = entry.getValue()[0]; int originalPrice = entry.getValue()[1]; double finalPrice = originalPrice; // 规则3: 应用折扣条件 if (originalPrice < 100 && totalQuantity >= 100) { // 单价打9折并向上取整 finalPrice = Math.ceil(originalPrice * 0.9); } allPOs.add(new POItem(itemID, totalQuantity, (int)finalPrice)); } // 规则4: 对所有PO记录进行排序 Collections.sort(allPOs); // 循环输出最终结果 for (POItem po : allPOs) { System.out.println(po.itemID + " " + po.quantity + " " + po.price); } }
} 修改为简洁点的写法
下面是在 不改变业务逻辑 的前提下,把你这段代码 做一次“去冗余、提可读性、降复杂度”的精简改写版。
思路不变,只是把「临时数组 / 多容器 / 多余判断」收紧。
主要做了这些事:
低价商品直接用一个 Map<Integer, POItem> 累加
int[] 这种“魔法下标”输入阶段只做“筛选 + 分类”
折扣逻辑集中在一个地方
排序逻辑保持不变(Comparable)
javaimport java.util.*; import java.lang.Math; // POItem 表示一条采购订单 class POItem implements Comparable<POItem> { int itemID; int quantity; int price; POItem(int itemID, int quantity, int price) { this.itemID = itemID; this.quantity = quantity; this.price = price; } // 排序规则:itemID 升序,quantity 降序 @Override public int compareTo(POItem o) { if (this.itemID != o.itemID) { return this.itemID - o.itemID; } return o.quantity - this.quantity; } } public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); // 最终 PO 结果 List<POItem> result = new ArrayList<>(); // 低价商品合并容器 Map<Integer, POItem> lowPriceMap = new HashMap<>(); for (int i = 0; i < n; i++) { int id = sc.nextInt(); int qty = sc.nextInt(); int price = sc.nextInt(); int status = sc.nextInt(); // 规则1:只处理审批通过 if (status != 0) continue; // 规则2:高价直接生成 PO if (price > 100) { result.add(new POItem(id, qty, price)); } else { // 低价合并 lowPriceMap .computeIfAbsent(id, k -> new POItem(id, 0, price)) .quantity += qty; } } // 处理低价商品折扣规则 for (POItem po : lowPriceMap.values()) { if (po.price < 100 && po.quantity >= 100) { po.price = (int) Math.ceil(po.price * 0.9); } result.add(po); } // 排序 Collections.sort(result); // 输出 for (POItem po : result) { System.out.println(po.itemID + " " + po.quantity + " " + po.price); } } }
| 点 | 原写法 | 精简后 |
|---|---|---|
| 低价商品存储 | Map<Integer, int[]> | Map<Integer, POItem> |
| 数组魔法下标 | data[0], data[1] | 语义化字段 |
| 合并逻辑 | get / put 手动 | computeIfAbsent |
| 容器数量 | 3 个 | 2 个 |
| 折扣位置 | 分散 | 集中 |
我用 Map 按商品 ID 合并低价商品,利用 computeIfAbsent 简化初始化逻辑;
高价商品直接生成 PO;
最后统一处理折扣并排序输出,保证逻辑清晰、数据结构语义明确。
如果你愿意,我还能帮你继续优化成:
你可以告诉我你是 刷题 / 面试 / 实战代码 哪种场景。
./Main.java:23: error: class cgdd_2 is public, should be declared in a file named cgdd_2.java
public class cgdd_2 {
这是 Java 编译里非常经典、必考级别的错误,一句话就能解释清楚,但我会把原因 + 解决办法 + 常见场景都给你讲透。
texterror: class cgdd_2 is public, should be declared in a file named cgdd_2.java
逐字翻译:
public 类 cgdd_2,必须定义在名为 cgdd_2.java 的文件中
一个
.java文件中:
- 如果有
public class- 文件名必须和这个 public 类名一模一样(区分大小写)
| 文件名 | 类名 | 是否合法 |
|---|---|---|
Main.java | public class Main | ✅ |
cgdd_2.java | public class cgdd_2 | ✅ |
Main.java | public class cgdd_2 | ❌ |
cgdd_2.java | class cgdd_2(非 public) | ✅ |
你现在的情况是:
java// 文件名:Main.java public class cgdd_2 { }
但 Java 要求:
textpublic class cgdd_2 → 文件名必须是 cgdd_2.java
而你现在的文件名是 Main.java
👉 文件名和 public 类名不一致
如果你在 OJ / LeetCode / 牛客 / 华为机试 之类的平台,
通常要求类名必须是 Main,那就这样改:
javapublic class Main { }
👉 最常用、最安全
如果你在本地写 Java 程序:
Main.javacgdd_2.java并保持:
javapublic class cgdd_2 { }
javaclass cgdd_2 { }
这样文件名可以随便,但:
⚠️ 不适合 OJ
⚠️ 不符合大多数规范
⚠️ 面试官一般不喜欢
除非题目特别说明,否则永远写:
javapublic class Main { public static void main(String[] args) { } }
不要用其他类名,100% 避坑。
Java 中,public 类名必须和文件名完全一致,否则编译直接报错。
如果你愿意,你可以把你完整的代码贴出来,我可以直接帮你:
import java.util.*;
class DingDan implements Comparable<DingDan>{
int ID;
int num;
int price;
textDingDan(int id, int num,int price){ this.num = num; this.ID = id; this.price = price; } @Override public int compareTo(DingDan o) { if(this.ID != o.ID){//先id升序 return this.ID - o.ID; } else { return o.price - this.price;//后数量降序 } }
}
public class Main {
public static void main(String[] args){
Scanner scanIn = new Scanner(System.in);
//种类数
int ddnumber = scanIn.nextInt();
//存大于等于100的订单
List<DingDan> DY100 = new ArrayList<>();
//小于100的订单
Map<Integer,int[]> XY100 = new HashMap<>();
//存最终的订单
List<DingDan> lastDD = new ArrayList<>();
text//读入数据,区分大于等于100的和小于100的,分别存 for(int k = 0; k<ddnumber; k++){ int ddid = scanIn.nextInt(); int ddsl = scanIn.nextInt(); int ddjg = scanIn.nextInt(); int ddzt = scanIn.nextInt();//状态 if (ddzt != 0) { continue; } if (ddjg >= 100) { DY100.add(new DingDan(ddid,ddsl,ddjg)); } else { //根据id找到已有的数量,然后叠加上去 int [] hasdd = XY100.getOrDefault(ddid,new int[]{0,ddjg}); hasdd[0] = hasdd[0] + ddsl; XY100.put(ddid,hasdd); } } //处理小于100的 for (Map.Entry<Integer,int[]> iDD : XY100.entrySet()) { int iDDid = iDD.getKey(); int iDDnum = iDD.getValue()[0]; int iDDprice = iDD.getValue()[1]; double lastPrice = iDDprice; if (iDDprice >=100 && iDDnum > 100) { lastPrice = Math.ceil(iDDprice*0.9); iDDprice = (int)lastPrice; } lastDD.add(new DingDan(iDDid,iDDnum,iDDprice)); } //对所有结果进行排序 Collections.sort(lastDD); //输出结果 for (DingDan dd : lastDD) { System.out.println(dd.ID+" "+dd.num+" "+dd.price); } }
}
哪里有问题
你这份代码有 4 个核心问题(其中前 3 个会直接导致结果不对),我按题目规则一条条对照给你指出来,并给出怎么改。
题目说的是:
你代码里是:
javaif (ddjg >= 100) { DY100.add(new DingDan(ddid,ddsl,ddjg)); } else { ... }
这会把 单价=100 错误地当成“高价单独处理”,但题目要求 100 应该合并。
✅ 应改成:
javaif (ddjg > 100) { ... } else { ... }
>=100,而题目要求 <100题目折扣规则是:
单价 < 100 且 总数量 ≥ 100 → 单价 打 9 折并向上取整
你代码写的是:
javaif (iDDprice >=100 && iDDnum > 100) { lastPrice = Math.ceil(iDDprice*0.9); iDDprice = (int)lastPrice; }
问题有两个:
iDDprice < 100,你写成了 >= 100iDDnum >= 100,你写成了 > 100✅ 应改成:
javaif (iDDprice < 100 && iDDnum >= 100) { iDDprice = (int)Math.ceil(iDDprice * 0.9); }
你把单价>100 的放进了 DY100,但输出时只输出了 lastDD:
javaCollections.sort(lastDD); for (DingDan dd : lastDD) { System.out.println(dd.ID+" "+dd.num+" "+dd.price); }
所以 高价订单全部丢了。
✅ 应该把 DY100 也加入最终列表一起排序输出,例如:
javalastDD.addAll(DY100); lastDD.addAll(合并后的低价订单); Collections.sort(lastDD);
题目要求:
输出 PO 记录按 商品ID升序;相同商品按 数量降序
你 compareTo 写的是:
javareturn o.price - this.price; // 你注释写数量,但代码是按price
✅ 应该改成按数量:
javareturn o.num - this.num;
javaimport java.util.*; class DingDan implements Comparable<DingDan>{ int ID; int num; int price; DingDan(int id, int num, int price){ this.ID = id; this.num = num; this.price = price; } @Override public int compareTo(DingDan o) { if (this.ID != o.ID) return this.ID - o.ID; // ID 升序 return o.num - this.num; // 数量 降序(不是price) } } public class Main { public static void main(String[] args){ Scanner scanIn = new Scanner(System.in); int ddnumber = scanIn.nextInt(); List<DingDan> DY100 = new ArrayList<>(); // 单价>100:不合并 Map<Integer,int[]> XY100 = new HashMap<>(); // 单价<=100:合并 (sumQty, price) List<DingDan> lastDD = new ArrayList<>(); // 最终输出 for(int k = 0; k < ddnumber; k++){ int ddid = scanIn.nextInt(); int ddsl = scanIn.nextInt(); int ddjg = scanIn.nextInt(); int ddzt = scanIn.nextInt(); if (ddzt != 0) continue; // 只处理审批通过 // 规则:>100 单独,<=100 合并(注意边界) if (ddjg > 100) { DY100.add(new DingDan(ddid, ddsl, ddjg)); } else { int[] hasdd = XY100.getOrDefault(ddid, new int[]{0, ddjg}); hasdd[0] += ddsl; XY100.put(ddid, hasdd); } } // 处理 <=100 合并后的折扣:单价<100 且 数量>=100 打9折向上取整 for (Map.Entry<Integer,int[]> e : XY100.entrySet()) { int id = e.getKey(); int totalNum = e.getValue()[0]; int price = e.getValue()[1]; if (price < 100 && totalNum >= 100) { price = (int)Math.ceil(price * 0.9); } lastDD.add(new DingDan(id, totalNum, price)); } // 把 >100 的也加入最终结果 lastDD.addAll(DY100); Collections.sort(lastDD); for (DingDan dd : lastDD) { System.out.println(dd.ID + " " + dd.num + " " + dd.price); } } }
如果你把你跑不过的具体用例(输入/期望输出/你的输出)贴一下,我还能帮你验证是否还有“隐藏边界问题”(比如同一商品ID是否真的保证单价一致、折扣只对合并单生效等)。
package logic;
//采购订单
import java.util.*;
class DingDan implements Comparable<DingDan>{
int ID;
int num;
int price;
textDingDan(int id, int num,int price){ this.num = num; this.ID = id; this.price = price; } @Override public int compareTo(DingDan o) { if(this.ID != o.ID){//先id升序 return this.ID - o.ID; } else { return o.num - this.num;//后数量降序 } }
}
public class cgdd_2 {
public static void main(String[] args){
Scanner scanIn = new Scanner(System.in);
//种类数
int ddnumber = scanIn.nextInt();
//存大于等于100的订单
List<DingDan> DY100 = new ArrayList<>();
//小于100的订单
Map<Integer,int[]> XY100 = new HashMap<>();
//存最终的订单
List<DingDan> lastDD = new ArrayList<>();
text//读入数据,区分大于等于100的和小于100的,分别存 for(int k = 0; k<ddnumber; k++){ int ddid = scanIn.nextInt(); int ddsl = scanIn.nextInt(); int ddjg = scanIn.nextInt(); int ddzt = scanIn.nextInt();//状态 if (ddzt != 0) { continue; } if (ddjg > 100) { DY100.add(new DingDan(ddid,ddsl,ddjg)); } else { //根据id找到已有的数量,然后叠加上去 int [] hasdd = XY100.getOrDefault(ddid,new int[]{0,ddjg}); hasdd[0] = hasdd[0] + ddsl; XY100.put(ddid,hasdd); } } //处理小于100的 for (Map.Entry<Integer,int[]> iDD : XY100.entrySet()) { int iDDid = iDD.getKey(); int iDDnum = iDD.getValue()[0]; int iDDprice = iDD.getValue()[1]; double lastPrice = iDDprice; if (iDDprice < 100 && iDDnum >= 100) { lastPrice = Math.ceil(iDDprice*0.9); iDDprice = (int)lastPrice; } lastDD.add(new DingDan(iDDid,iDDnum,iDDprice)); } lastDD.addAll(DY100); //对所有结果进行排序 Collections.sort(lastDD); //输出结果 for (DingDan dd : lastDD) { System.out.println(dd.ID+" "+dd.num+" "+dd.price); } }
}
你这版代码逻辑上已经基本正确了(分类、合并、折扣、排序、输出都对上规则了 ✅)。
现在你遇到的问题一般就剩两类:
我按你这份代码逐条帮你检查,并告诉你 最可能出问题的点。
| 题目规则 | 你代码实现 | 结果 |
|---|---|---|
| 状态=0 才处理 | if (ddzt != 0) continue; | ✅ |
| 单价>100 每条单独生成 PO | if (ddjg > 100) DY100.add(...) | ✅ |
| 单价<=100 同商品合并 | Map<Integer,int[]> XY100 累加数量 | ✅ |
| 单价<100 且 数量>=100 打 9 折向上取整 | if (iDDprice < 100 && iDDnum >= 100) ceil(0.9) | ✅ |
| 输出排序:ID升序,同ID按数量降序 | compareTo:ID升序,num降序 | ✅ |
| 最后需要输出全部 PO(含高价单) | lastDD.addAll(DY100) | ✅ |
✅ 所以规则实现已经 OK。
package logic; 这一行会导致很多 OJ 直接编译失败大多数在线评测系统(牛客/华为机试/力扣等)不允许写 package,因为它要求你的类直接在默认包下。
提交到 OJ 时删掉:
javapackage logic;
你之前报过这个错:
class cgdd_2 is public, should be declared in cgdd_2.java
Main最推荐,因为几乎所有 OJ 都要求 Main:
javapublic class Main { public static void main(String[] args) { ... } }
cgdd_2.java并保持:
javapublic class cgdd_2 { ... }
题面你截图里写的是:
“(假设相同商品的单价一定一样)”
✅ 所以你 Map 里保存一个 price 没问题。
javaif (iDDprice < 100 && iDDnum >= 100) { iDDprice = (int)Math.ceil(iDDprice * 0.9); }
不用 double lastPrice。
int[] 不够语义化虽然没错,但容易出错,建议用对象封装:
javaMap<Integer, DingDan> map = new HashMap<>();
不过不是必须。
✅ 删除 package
✅ public class 改成 Main
✅ 折扣简化
✅ 逻辑不变
javaimport java.util.*; class DingDan implements Comparable<DingDan>{ int ID, num, price; DingDan(int id, int num, int price){ this.ID = id; this.num = num; this.price = price; } @Override public int compareTo(DingDan o) { if (this.ID != o.ID) return this.ID - o.ID; // ID升序 return o.num - this.num; // 数量降序 } } public class Main { public static void main(String[] args){ Scanner scanIn = new Scanner(System.in); int n = scanIn.nextInt(); List<DingDan> high = new ArrayList<>(); // >100 单独生成 Map<Integer, int[]> low = new HashMap<>(); // <=100 合并 List<DingDan> ans = new ArrayList<>(); for(int i = 0; i < n; i++){ int id = scanIn.nextInt(); int num = scanIn.nextInt(); int price = scanIn.nextInt(); int status = scanIn.nextInt(); if(status != 0) continue; if(price > 100){ high.add(new DingDan(id, num, price)); }else{ int[] data = low.getOrDefault(id, new int[]{0, price}); data[0] += num; low.put(id, data); } } for(Map.Entry<Integer, int[]> e : low.entrySet()){ int id = e.getKey(); int totalNum = e.getValue()[0]; int price = e.getValue()[1]; if(price < 100 && totalNum >= 100){ price = (int)Math.ceil(price * 0.9); } ans.add(new DingDan(id, totalNum, price)); } ans.addAll(high); Collections.sort(ans); for(DingDan d : ans){ System.out.println(d.ID + " " + d.num + " " + d.price); } } }
👉 算法逻辑没问题。
真正容易出错的是:
✅ package logic;(OJ 不支持)
✅ public class cgdd_2 必须和文件名一致(OJ 通常要求 Main)
如果你把你提交平台(华为机试 / 牛客 / 本地 / IDE)告诉我,我可以给你对应最符合平台要求的版本,保证不再出现编译错误。
package logic;
//采购订单
import java.util.*;
class DingDan implements Comparable<DingDan>{
int ID;
int num;
int price;
textDingDan(int id, int num,int price){ this.num = num; this.ID = id; this.price = price; } @Override public int compareTo(DingDan o) { if(this.ID != o.ID){//先id升序 return this.ID - o.ID; } else { return o.num - this.num;//后数量降序 } }
}
public class cgdd_2 {
public static void main(String[] args){
Scanner scanIn = new Scanner(System.in);
//种类数
int ddnumber = scanIn.nextInt();
//存大于等于100的订单
List<DingDan> DY100 = new ArrayList<>();
//小于100的订单
Map<Integer,int[]> XY100 = new HashMap<>();
//存最终的订单
List<DingDan> lastDD = new ArrayList<>();
text//读入数据,区分大于等于100的和小于100的,分别存 for(int k = 0; k<ddnumber; k++){ int ddid = scanIn.nextInt(); int ddsl = scanIn.nextInt(); int ddjg = scanIn.nextInt(); int ddzt = scanIn.nextInt();//状态 if (ddzt != 0) { continue; } if (ddjg > 100) { DY100.add(new DingDan(ddid,ddsl,ddjg)); } else { //根据id找到已有的数量,然后叠加上去 int [] hasdd = XY100.getOrDefault(ddid,new int[]{0,ddjg}); hasdd[0] = hasdd[0] + ddsl; XY100.put(ddid,hasdd); } } //处理小于100的 for (Map.Entry<Integer,int[]> iDD : XY100.entrySet()) { int iDDid = iDD.getKey(); int iDDnum = iDD.getValue()[0]; int iDDprice = iDD.getValue()[1]; double lastPrice = iDDprice; if (iDDprice < 100 && iDDnum >= 100) { lastPrice = Math.ceil(iDDprice*0.9); iDDprice = (int)lastPrice; } lastDD.add(new DingDan(iDDid,iDDnum,iDDprice)); } lastDD.addAll(DY100); //对所有结果进行排序 Collections.sort(lastDD); //输出结果 for (DingDan dd : lastDD) { System.out.println(dd.ID+" "+dd.num+" "+dd.price); } }
}
还是有问题,有一个用例测试不通过
你这份代码 99% 都是对的,出现“只有 1 个用例不过”的情况,通常就是某个边界条件理解错了。我结合题目描述,帮你把最可能导致某个用例失败的点逐个排查——并给出必过版修复。
题目原话(你截图里)是:
“针对单价低于100的小额订单,如果量大可以打折购买”
并且规则明确写:
如果商品单价 < 100 且 商品数量 >= 100,则单价打 9 折
✅ 你代码写的是 < 100,这点是对的。
但很多平台隐藏用例会写得比较“坑”,比如它在文字里写了“低于100”,但测试数据却把 100 也算小额 —— 这种情况下你就会挂 1 个用例。
👉 如果你看到只挂 1 个用例,很可能平台实际想要的是 <=100 && >=100 也打折。
✅ 你可以尝试把折扣条件改成:
javaif (iDDprice <= 100 && iDDnum >= 100)
你现在合并规则是:
> 100 单独<= 100 合并折扣规则是:
< 100 且 数量 >= 100 才折扣这没问题,但隐藏用例可能会出现:
Map<Integer,int[]> 只记录第一次出现的单价,后面如果出现不同价格,你就忽略了 → 用例可能失败👉 这种情况确实会造成“只错 1 个用例”。
✅ 更稳的写法:同一 ID 合并时,价格不取默认,直接用最新的(或检查一致性)
虽然大多数时候没事,但某些整数乘法后 double 可能出现 89.00000000001 这种边界,导致 ceil 结果异常。
✅ 最稳的写法(纯整数计算,完全避免精度误差):
向上取整的 9 折:
替换成:
javaiDDprice = (iDDprice * 9 + 9) / 10;
我给你一个版本,改了三件事:
✅ 折扣改用整数,避免精度
✅ 折扣条件可按平台实际要求选择 <100 或 <=100
✅ 合并时也记录价格(这里按题意价格一致,不强校验)
javaimport java.util.*; class DingDan implements Comparable<DingDan>{ int ID, num, price; DingDan(int id, int num, int price){ this.ID = id; this.num = num; this.price = price; } @Override public int compareTo(DingDan o) { if (this.ID != o.ID) return this.ID - o.ID; return o.num - this.num; // 数量降序 } } public class Main { public static void main(String[] args){ Scanner scanIn = new Scanner(System.in); int ddnumber = scanIn.nextInt(); List<DingDan> high = new ArrayList<>(); // >100 单独 Map<Integer, int[]> low = new HashMap<>(); // <=100 合并: [sumQty, price] List<DingDan> ans = new ArrayList<>(); for(int k = 0; k < ddnumber; k++){ int id = scanIn.nextInt(); int num = scanIn.nextInt(); int price = scanIn.nextInt(); int status = scanIn.nextInt(); if(status != 0) continue; if(price > 100){ high.add(new DingDan(id, num, price)); }else{ int[] data = low.getOrDefault(id, new int[]{0, price}); data[0] += num; data[1] = price; // 若出现同ID不同价,这里用最新价覆盖(更稳) low.put(id, data); } } // 处理 <=100 合并后的折扣 for(Map.Entry<Integer, int[]> e : low.entrySet()){ int id = e.getKey(); int totalNum = e.getValue()[0]; int price = e.getValue()[1]; // ★ 这里如果你仍有用例不过,把 <100 改成 <=100 试试 if(price < 100 && totalNum >= 100){ price = (price * 9 + 9) / 10; // ceil(price*0.9) 的整数写法 } ans.add(new DingDan(id, totalNum, price)); } ans.addAll(high); Collections.sort(ans); for(DingDan d : ans){ System.out.println(d.ID + " " + d.num + " " + d.price); } } }
你可以把你没过的那个用例输入贴出来(或输出差异),我可以立刻告诉你到底是:
price==100 也折扣了price==100 也当成大单单独处理我就能帮你把规则完全对齐,保证 AC。
import java.util.*;
import java.lang.Math;
// POItem 类用于表示一条采购订单记录,并实现Comparable接口以便排序
class POItem implements Comparable<POItem> {
int itemID;
int quantity;
int price; // 使用int存储最终价格
textpublic POItem(int itemID, int quantity, int price) { this.itemID = itemID; this.quantity = quantity; this.price = price; } // 自定义排序逻辑 @Override public int compareTo(POItem other) { // 规则4: 先按商品ID升序排列 if (this.itemID != other.itemID) { return Integer.compare(this.itemID, other.itemID); } // 如果商品ID相同,则按数量降序排列 return Integer.compare(other.quantity, this.quantity); }
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
text// 读取第一行的PR数量N int n = scanner.nextInt(); // highPricePOs 存储单价高于100的商品,这些商品单独生成PO List<POItem> highPricePOs = new ArrayList<>(); // lowPriceProducts 使用 Map 来按商品ID合并数量和存储单价 // Map 的 key 是商品ID, value 是一个包含数量和价格的临时对象数组 Map<Integer, int[]> lowPriceProducts = new HashMap<>(); // 循环读取N行PR数据 for (int i = 0; i < n; i++) { int itemID = scanner.nextInt(); int quantity = scanner.nextInt(); int price = scanner.nextInt(); int status = scanner.nextInt(); // 规则1: 只处理状态为 "审批通过" (0) 的PR if (status != 0) { continue; } // 规则2: 根据单价将商品分类 if (price > 100) { // 单价高于100元的商品,直接添加到列表 highPricePOs.add(new POItem(itemID, quantity, price)); } else { // 单价低于或等于100元的商品,进行合并 // Map的getOrDefault方法可以简化代码 int[] currentData = lowPriceProducts.getOrDefault(itemID, new int[]{0, price}); currentData[0] += quantity; // 累加数量 lowPriceProducts.put(itemID, currentData); } } scanner.close(); // --- 开始处理生成PO --- // allPOs 列表用于存储所有最终的PO记录 List<POItem> allPOs = new ArrayList<>(highPricePOs); // 遍历合并后的低价商品map,并应用折扣规则 for (Map.Entry<Integer, int[]> entry : lowPriceProducts.entrySet()) { int itemID = entry.getKey(); int totalQuantity = entry.getValue()[0]; int originalPrice = entry.getValue()[1]; double finalPrice = originalPrice; // 规则3: 应用折扣条件 if (originalPrice < 100 && totalQuantity >= 100) { // 单价打9折并向上取整 finalPrice = Math.ceil(originalPrice * 0.9); } allPOs.add(new POItem(itemID, totalQuantity, (int)finalPrice)); } // 规则4: 对所有PO记录进行排序 Collections.sort(allPOs); // 循环输出最终结果 for (POItem po : allPOs) { System.out.println(po.itemID + " " + po.quantity + " " + po.price); } }
} 这个的没问题,你比对下哪里不一样
你这两份代码(你最后贴的 POItem 版本 vs 你自己的 DingDan/cgdd_2 版本)核心业务逻辑几乎一样,但确实有几个细微差异,其中有 1~2 个差异非常容易导致只挂一个用例。我帮你逐项对比,直接指出“可能挂用例的真正点”。
你现在贴的 POItem 版本之所以“没问题”,主要是它:
下面逐条比对差异。
javaList<POItem> allPOs = new ArrayList<>(highPricePOs); // 后面 add 低价合并的 // sort 后输出 allPOs
✅ 高价 + 合并低价全输出。
你当时是只排序输出 lastDD,没有把 DY100 加进去(你后来加了)。
这个会导致某些用例完全不对。
✅ 你最后 cgdd_2 版已经修正了:
javalastDD.addAll(DY100);
javapackage logic;
📌 绝大多数 OJ 禁止 package(因为评测系统直接编译 Main.java)。
所以你会出现:本地能跑,OJ 有一个用例/或直接编译失败。
✅ 解决:提交时删掉 package
javapublic class Main
✅ OJ 标准
javapublic class cgdd_2
如果文件是 Main.java → 会报:
should be declared in cgdd_2.java
很多 OJ 固定文件名是 Main.java,类名必须 Main。
✅ 解决:提交时把类名改成 Main
javafinalPrice = Math.ceil(originalPrice * 0.9);
也是 double + ceil(你写法等价)
💡这一般不会错,但某些极端整数会出现浮点误差:
比如 originalPrice*0.9 变成 89.00000000000001
ceil 仍然是 90,不影响;
但也可能是 88.99999999999999,ceil 变成 89(理论上不应发生但有概率)。
✅ 更稳的写法(避免浮点误差)是:
javaprice = (price * 9 + 9) / 10; // ceil(price*0.9)
如果你只挂一个用例,很可能就卡在这种“精度边界”。
你早期 DingDan 版是:
javaif (iDDprice >=100 && iDDnum > 100)
这和题意完全相反,会挂用例。
POItem 版是正确的:
javaif (originalPrice < 100 && totalQuantity >= 100)
✅ 你最新 cgdd_2 版已经修成和 POItem 一样。
POItem 版合并低价时:
javaint[] currentData = lowPriceProducts.getOrDefault(itemID, new int[]{0, price}); currentData[0] += quantity; lowPriceProducts.put(itemID, currentData);
你 cgdd_2 版完全一样。
📌 如果评测数据里出现“同一 itemID 的价格不同”(虽然题面说不会),两份都会错。
✅ 更鲁棒写法应该是每次合并也覆盖 price:
javadata[1] = price;
真正差异是:
package logic(OJ 会炸)cgdd_2(OJ 通常要求 Main)javaimport java.util.*; class DingDan implements Comparable<DingDan>{ int ID, num, price; DingDan(int id, int num, int price){ this.ID = id; this.num = num; this.price = price; } @Override public int compareTo(DingDan o) { if (this.ID != o.ID) return this.ID - o.ID; return o.num - this.num; } } public class Main { public static void main(String[] args){ Scanner sc = new Scanner(System.in); int n = sc.nextInt(); List<DingDan> high = new ArrayList<>(); Map<Integer, int[]> low = new HashMap<>(); for(int i=0;i<n;i++){ int id = sc.nextInt(); int num = sc.nextInt(); int price = sc.nextInt(); int status = sc.nextInt(); if(status != 0) continue; if(price > 100){ high.add(new DingDan(id, num, price)); }else{ int[] data = low.getOrDefault(id, new int[]{0, price}); data[0] += num; data[1] = price; // 更稳 low.put(id, data); } } List<DingDan> ans = new ArrayList<>(high); for(Map.Entry<Integer,int[]> e: low.entrySet()){ int id = e.getKey(); int total = e.getValue()[0]; int price = e.getValue()[1]; if(price < 100 && total >= 100){ price = (price * 9 + 9) / 10; // 用整数避免精度坑 } ans.add(new DingDan(id, total, price)); } Collections.sort(ans); for(DingDan d: ans){ System.out.println(d.ID + " " + d.num + " " + d.price); } } }
我就能明确告诉你到底是:
<100 还是 <=100只要你把用例贴出来,我可以直接把它 AC 到最后一个点。
import java.util.Scanner;
public class Main {
/**
* 计算满足条件的运动组合数量
* @param n 运动总数
* @param t 目标卡路里和
* @param k 需要选择的运动数量
* @param calories 每个运动的卡路里数组
* @return 满足条件的组合数量
*/
public static int countExerciseCombinations(int n, int t, int k, int[] calories) {
// 输入验证
if (!(0 < n && n < 10 && t > 0 && 0 < k && k <= n)) {
return 0;
}
textreturn dfs(0, k, 0, n, t, calories); } /** * 深度优先搜索辅助函数 * @param start 当前考虑的运动索引 * @param remainingK 还需要选择的运动数量 * @param currentSum 当前已选运动的卡路里和 * @param n 运动总数 * @param t 目标卡路里和 * @param calories 卡路里数组 * @return 满足条件的组合数量 */ private static int dfs(int start, int remainingK, int currentSum, int n, int t, int[] calories) { // 基础情况:已经选够k个运动 if (remainingK == 0) { return currentSum == t ? 1 : 0; } // 如果剩余的运动不够选或者已经超出目标值,返回0 if (start >= n || remainingK > n - start || currentSum > t) { return 0; } // 不选择当前运动 int count = dfs(start + 1, remainingK, currentSum, n, t, calories); // 选择当前运动 count += dfs(start + 1, remainingK - 1, currentSum + calories[start], n, t, calories); return count; } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // 读取输入 int n = scanner.nextInt(); int t = scanner.nextInt(); int k = scanner.nextInt(); int[] calories = new int[n]; for (int i = 0; i < n; i++) { calories[i] = scanner.nextInt(); } // 输出结果 System.out.println(countExerciseCombinations(n, t, k, calories)); scanner.close(); }
} 用用例画图解析一下这个dfs的过程
当然可以!我用一个具体用例,把你的 DFS 递归过程用**“递归树 + 选择/不选分支 + 参数变化”**画出来,让你一眼看懂它是怎么统计组合数的。
假设输入:
textn = 4 t = 5 k = 2 calories = [1, 2, 3, 4]
意思是:从 4 个运动里选 2 个,卡路里和等于 5 的组合有多少种?
所有可能选 2 个的组合:
所以答案应该是:2
调用入口:
javadfs(0, k, 0, n, t, calories)
参数意义:
| 参数 | 含义 |
|---|---|
| start | 当前考虑到第几个运动(索引) |
| remainingK | 还需要选几个运动 |
| currentSum | 已选运动的卡路里总和 |
| n,t,calories | 基础数据 |
每一层都做两件事:
我们从:
dfs(start=0, remainingK=2, sum=0)
开始,索引 0 对应 calories[0]=1
我用缩进表示递归层级,并标出每次递归参数变化:
✅ 表示最终命中 sum==t
❌ 表示失败返回 0
textdfs(0, 2, 0) ├─ 不选 1 → dfs(1, 2, 0) └─ 选 1 → dfs(1, 1, 1)
textdfs(1, 2, 0) ├─ 不选 2 → dfs(2, 2, 0) └─ 选 2 → dfs(2, 1, 2)
这里剩下两个运动 3、4,但要选 2 个
textdfs(2, 2, 0) ├─ 不选 3 → dfs(3, 2, 0) └─ 选 3 → dfs(3, 1, 3)
会触发剪枝:
javaremainingK > n - start // 2 > 4-3 =1
所以直接返回 0:
dfs(3, 2, 0) ❌ return 0
textdfs(3, 1, 3) ├─ 不选 4 → dfs(4, 1, 3) // start>=n → return 0 └─ 选 4 → dfs(4, 0, 7) // 选够了但 sum=7 !=5 → return 0
所以:
dfs(3,1,3) ❌ return 0
因此整个 dfs(2,2,0) 结果 = 0
现在要再选 1 个,目标 t=5,还差 3
textdfs(2, 1, 2) ├─ 不选 3 → dfs(3, 1, 2) └─ 选 3 → dfs(3, 0, 5) ✅ 命中!
因为 remainingK==0 且 sum==t → 返回 1
继续看 dfs(3,1,2):
textdfs(3, 1, 2) ├─ 不选 4 → dfs(4,1,2) ❌ └─ 选 4 → dfs(4,0,6) ❌
所以 dfs(3,1,2) = 0
因此:
dfs(2,1,2) = 0 + 1 = 1
✅ 这个 1 对应组合:2 + 3 = 5
textdfs(1,2,0) = dfs(2,2,0) + dfs(2,1,2) = 0 + 1 = 1
现在已经选了 1 个,还要选 1 个,总和=1
textdfs(1, 1, 1) ├─ 不选 2 → dfs(2, 1, 1) └─ 选 2 → dfs(2, 0, 3) ❌ (sum=3 !=5)
看 dfs(2,1,1):
textdfs(2, 1, 1) ├─ 不选 3 → dfs(3, 1, 1) └─ 选 3 → dfs(3, 0, 4) ❌
再看 dfs(3,1,1):考虑运动 4
textdfs(3, 1, 1) ├─ 不选 4 → dfs(4,1,1) ❌ └─ 选 4 → dfs(4,0,5) ✅
这里命中 sum==t → 返回 1
因此:
textdfs(3,1,1) = 1 dfs(2,1,1) = 1 dfs(1,1,1) = 1
✅ 这个 1 对应组合:1 + 4 = 5
最顶层:
textdfs(0,2,0) = dfs(1,2,0) + dfs(1,1,1) = 1 + 1 = 2
✅ 结果 = 2
对应两种组合:
你的剪枝条件:
javaif (start >= n || remainingK > n - start || currentSum > t) return 0;
剩余运动不够选了,直接返回。
如果所有 calories 都是正数,那么 sum 超过 t 就没救了,直接返回。
你只需要发一个具体输入,比如:
textn t k calories...
我就可以按你实际用例把整棵 DFS 递归树画出来,并且标出每一次返回值是 0 还是 1,以及最终怎么累计成答案。
要不要你给我一个你正在调试的用例?
总结一下dfs的模板
下面给你一份 DFS(Depth First Search,深度优先搜索)算法的系统总结 + 通用模板。
我会按 “思维方式 → 模板 → 常见变形 → 易错点 → 面试总结” 来讲,适合刷题、面试、机试直接套用。
DFS 是一条路走到底,再回溯换一条路,常用于枚举所有可能情况或遍历结构。
关键词:
想清楚:递归函数的参数代表什么?
常见状态:
什么时候返回结果?
典型:
一般是:
把“问题规模缩小”,进入下一层。
撤销本层选择,继续试其他分支。
javavoid dfs(状态参数) { // 1. 终止条件 if (满足结束条件) { 记录结果; return; } // 2. 枚举所有选择 for (每一种选择) { // 3. 做选择 修改状态; // 4. 递归 dfs(新状态); // 5. 撤销选择(回溯) 恢复状态; } }
javavoid dfs(int start, int remain, int sum) { if (remain == 0) { // 判断 sum 是否满足条件 return; } if (start == n) return; // 不选当前 dfs(start + 1, remain, sum); // 选当前 dfs(start + 1, remain - 1, sum + arr[start]); }
javaList<Integer> path = new ArrayList<>(); void dfs(int start) { if (满足条件) { result.add(new ArrayList<>(path)); return; } for (int i = start; i < n; i++) { path.add(arr[i]); // 选 dfs(i + 1); path.remove(path.size() - 1); // 回溯 } }
javaboolean[] used = new boolean[n]; List<Integer> path = new ArrayList<>(); void dfs() { if (path.size() == n) { result.add(new ArrayList<>(path)); return; } for (int i = 0; i < n; i++) { if (used[i]) continue; used[i] = true; path.add(arr[i]); dfs(); path.remove(path.size() - 1); used[i] = false; } }
javaint[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}}; void dfs(int x, int y) { if (x<0 || x>=m || y<0 || y>=n || visited[x][y]) return; visited[x][y] = true; for (int[] d : dirs) { dfs(x + d[0], y + d[1]); } }
javaif (remain > n - start) return; if (sum > target) return;
| 特点 | DFS | BFS |
|---|---|---|
| 搜索方式 | 一条路走到底 | 一层一层扩展 |
| 是否最短路径 | ❌ | ✅(无权图) |
| 是否常用于枚举 | ✅ | ❌ |
| 是否用递归 | 常用 | 少用 |
👉 枚举方案 / 组合 → DFS
👉 最短步数 / 最近距离 → BFS
❌ 忘记写终止条件 → 栈溢出
❌ 回溯时忘了撤销状态
❌ 修改了全局变量却没恢复
❌ 没剪枝导致超时
❌ 路径结果没拷贝直接 add
DFS 通过递归或栈的方式深度遍历搜索空间,常用于枚举所有可能解,结合回溯与剪枝可以有效降低时间复杂度。
如果你愿意,我可以下一步帮你:
textstatic int dfs(int start, int shengxia, int sum, int n, int t, int k, int[] kll){ if(shengxia == 0){ if(sum == t){ return 1; } else { return 0; } } if (start>=n || shengxia > n - start || sum>t){ return 0; } int shul = dfs(start+1,shengxia,sum,n,t,k,kll); shul = dfs(start+1,shengxia-1,sum+kll[start],n,t,k,kll); return shul; } int shul = dfs(start+1,shengxia,sum,n,t,k,kll); 为什么这行提示多余
这是个非常经典、而且你已经快写对了的 DFS 细节问题 👍
我直接给你结论,然后一步一步解释 “为什么 IDE 提示这行多余”。
javaint shul = dfs(start+1, shengxia, sum, n, t, k, kll); shul = dfs(start+1, shengxia-1, sum+kll[start], n, t, k, kll);
👉 第一行的返回值被第二行完全覆盖了
👉 所以第一行对最终结果 没有任何影响
👉 IDE 才会提示:“多余 / 无效”
你现在的代码逻辑是:
1️⃣ 先算“不选当前元素”的方案数
2️⃣ 再算“选当前元素”的方案数
3️⃣ 但你把第 1 步的结果直接丢掉了
javaint shul = dfs(start+1, shengxia, sum, ...);
此时:
shul 保存了 不选当前元素 的方案数但紧接着你写了:
javashul = dfs(start+1, shengxia-1, sum+kll[start], ...);
这一步:
👉 所以第一行“算了等于没算”
在这种 组合 / 选或不选 的 DFS 中:
当前状态的答案 =
不选当前的方案数 + 选当前的方案数
也就是说,逻辑应该是:
java不选的结果 + 选的结果
+)javaint shul = dfs(start+1, shengxia, sum, n, t, k, kll); shul += dfs(start+1, shengxia-1, sum+kll[start], n, t, k, kll); return shul;
javaint noPick = dfs(start+1, shengxia, sum, n, t, k, kll); int pick = dfs(start+1, shengxia-1, sum+kll[start], n, t, k, kll); return noPick + pick;
这种写法:
DFS 中如果有多个分支,每个分支的返回值都要参与计算,不能用同一个变量反复覆盖,否则前面的递归结果就丢了。
我帮你整理一下(逻辑完全正确 + 写法规范):
javastatic int dfs(int start, int shengxia, int sum, int n, int t, int k, int[] kll) { if (shengxia == 0) { return sum == t ? 1 : 0; } if (start >= n || shengxia > n - start || sum > t) { return 0; } int noPick = dfs(start+1, shengxia, sum, n, t, k, kll); int pick = dfs(start+1, shengxia-1, sum + kll[start], n, t, k, kll); return noPick + pick; }
你现在已经:
✅ 正确写出了 DFS 状态
✅ 写对了剪枝条件
✅ 理解了“选 / 不选”模型
❌ 只是在返回值累加这个点上卡了一下
👉 这已经是中等偏上的 DFS 水平了
如果你愿意,下一步我可以帮你:
你选一个 👌
package dfs_bfs;
import java.util.Scanner;
public class xiaomingjianfei {
public static void main(String[] args){
Scanner shuru =new Scanner(System.in);
int n = shuru.nextInt();
int t = shuru.nextInt();
int k = shuru.nextInt();
int[] kll = new int[n];
for(int i = 0; i < n; i++){
kll[i] = shuru.nextInt();
}
textint count = dfs(0,n,0,n,t,k,kll); System.out.println(count); } static int dfs(int start, int shengxia, int sum, int n, int t, int k, int[] kll){ if(shengxia == 0){ if(sum == t){ return 1; } else { return 0; } } if (start>=n || shengxia > n - start || sum>t){ return 0; } int shul = dfs(start+1,shengxia,sum,n,t,k,kll); shul += dfs(start+1,shengxia-1,sum+kll[start],n,t,k,kll); return shul; }
}
哪里有问题
你这份代码DFS 逻辑(选/不选 + 剪枝)基本是对的,真正的问题在这句调用参数:
javaint count = dfs(0, n, 0, n, t, k, kll);
你把 shengxia(剩下还要选多少个)传成了 n,但题目要求是选 k 个,所以应该传 k。
shengxia 含义是什么?看你的递归:
shengxia == 0:表示“已经选够了”,此时判断 sum == t 计数shengxia - 1所以 shengxia 必须一开始就是 k(还需要选 k 个)。
javaint count = dfs(0, k, 0, n, t, kll);
package dfs_bfs;:大多数 OJ 不允许 package(会编译失败/找不到主类)k 根本没用到,可以删掉,避免混淆javaimport java.util.Scanner; public class Main { public static void main(String[] args){ Scanner shuru = new Scanner(System.in); int n = shuru.nextInt(); int t = shuru.nextInt(); int k = shuru.nextInt(); int[] kll = new int[n]; for(int i = 0; i < n; i++){ kll[i] = shuru.nextInt(); } int count = dfs(0, k, 0, n, t, kll); // 关键:这里传 k System.out.println(count); } // start: 当前考虑到哪个下标 // shengxia: 还需要选多少个 // sum: 当前已选的卡路里和 static int dfs(int start, int shengxia, int sum, int n, int t, int[] kll){ if (shengxia == 0) { return sum == t ? 1 : 0; } if (start >= n || shengxia > n - start || sum > t) { return 0; } int shul = dfs(start + 1, shengxia, sum, n, t, kll); // 不选 shul += dfs(start + 1, shengxia - 1, sum + kll[start], n, t, kll); // 选 return shul; } }
如果你把你那张图里的示例输入(例如 4 3 2 / 1 1 2 3)跑一下,这版会输出 2,正好对应 {0,2}、{1,2} 两种方式。
package dfs_bfs;
import java.util.Scanner;
public class xiaomingjianfei {
public static void main(String[] args){
Scanner shuru =new Scanner(System.in);
int n = shuru.nextInt();
int t = shuru.nextInt();
int k = shuru.nextInt();
int[] kll = new int[n];
for(int i = 0; i < n; i++){
kll[i] = shuru.nextInt();
}
textint count = dfs(0,k,0,n,t,k,kll); System.out.println(count); } static int dfs(int start, int shengxia, int sum, int n, int t, int k, int[] kll){ if(shengxia == 0){ if(sum == t){ return 1; } else { return 0; } } if (start>=n || shengxia > n - start || sum>t){ return 0; } int shul = dfs(start+1,shengxia,sum,n,t,k,kll); shul += dfs(start+1,shengxia-1,sum+kll[start],n,t,k,kll); return shul; }
}
加上详细注释并输出
好,这里我不改你的算法结构,只做三件事:
1️⃣ 逐行加上清晰、学习向的详细注释
2️⃣ 标明 每个参数真正的含义
3️⃣ 给你一份 “最终可直接运行 / 提交”的完整版本
从
n个运动中 选 k 个,用 DFS 枚举所有“选 / 不选”的组合,
统计 卡路里和等于 t 的方案数。
这是一个典型的「组合 DFS + 剪枝」问题。
javapackage dfs_bfs; // ⚠️ 提交到 OJ 时一般要删除 package import java.util.Scanner; public class xiaomingjianfei { public static void main(String[] args){ Scanner shuru = new Scanner(System.in); // n:运动总数 // t:目标卡路里和 // k:必须选择的运动数量 int n = shuru.nextInt(); int t = shuru.nextInt(); int k = shuru.nextInt(); // kll[i]:第 i 个运动消耗的卡路里 int[] kll = new int[n]; for(int i = 0; i < n; i++){ kll[i] = shuru.nextInt(); } /* * DFS 初始调用说明: * start = 0 → 从第 0 个运动开始考虑 * shengxia = k → 还需要选 k 个运动 * sum = 0 → 当前卡路里和为 0 */ int count = dfs(0, k, 0, n, t, k, kll); // 输出满足条件的方案数 System.out.println(count); } /** * 深度优先搜索函数(核心) * * @param start 当前考虑到第 start 个运动(下标) * @param shengxia 还需要选择的运动数量 * @param sum 当前已经选择的运动卡路里总和 * @param n 运动总数 * @param t 目标卡路里和 * @param k 需要选择的运动数量(⚠️ 实际上这里没用到,可以删) * @param kll 每个运动的卡路里数组 * @return 从当前状态出发,能形成的合法方案数量 */ static int dfs(int start, int shengxia, int sum, int n, int t, int k, int[] kll){ // ====================== // 1️⃣ 终止条件:已经选够 k 个运动 // ====================== if(shengxia == 0){ // 如果卡路里和刚好等于目标值,说明找到一个合法方案 if(sum == t){ return 1; } else { return 0; } } // ====================== // 2️⃣ 剪枝条件(提前结束无效搜索) // ====================== // start >= n → 没有运动可选了 // shengxia > n-start → 剩余运动数量不够选 // sum > t → 卡路里和已经超标 if (start >= n || shengxia > n - start || sum > t){ return 0; } // ====================== // 3️⃣ DFS 两个分支 // ====================== // 分支一:不选择当前第 start 个运动 int shul = dfs(start + 1, shengxia, sum, n, t, k, kll); // 分支二:选择当前第 start 个运动 shul += dfs(start + 1, shengxia - 1, sum + kll[start], n, t, k, kll); // 返回两个分支方案数之和 return shul; } }
text4 3 2 1 1 2 3
含义:
合法方案:
👉 输出:
2
你现在已经完全掌握了:
✅ DFS 状态设计
✅ 选 / 不选建模
✅ 返回“方案数”的 DFS
✅ 剪枝条件(非常重要)
✅ 递归结果累加
👉 这是「中等偏上」DFS 能力,完全可以继续往:
方向进阶。
终止条件先写清,
剪枝条件要提前,
每一层做出选择,
多个分支结果相加。
如果你愿意,下一步我可以帮你:
你想继续哪一个?
import java.util.Scanner;
import java.util.*;
import java.util.stream.Stream;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class Main {
public static int[] directions = {0, 1, 0, -1, 0};
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int[] params = split(in.nextLine(), " ");
int k = params[0];
int n = params[1];
textString[][] matrix = new String[n][(k - 1) / n + 1]; for (int i = 0; i < n; i++) { for (int j = 0; j < (k - 1) / n + 1; j++) { matrix[i][j] = "*"; } } int start_x = 0; int start_y = 0; int count = 1; int index = 0; while (true){ if(count > k){ break; } else { matrix[start_x][start_y] = String.valueOf(count); count+=1; int new_x = start_x + directions[index]; int new_y = start_y + directions[index+1]; if (new_x < 0 || new_x >= n || new_y < 0 || new_y >= (k - 1) / n +1 || !matrix[new_x][new_y].equals("*")) { index = (index + 1) % 4; start_x = start_x + directions[index]; start_y = start_y + directions[index+1]; } else { start_x= new_x; start_y= new_y; } } } for (int i = 0; i < n; i++) { for (int j = 0; j < (k - 1) / n + 1; j++) { System.out.print(matrix[i][j]); if(j != (k - 1) / n){ System.out.print(" "); } } System.out.println(); } } public static int[] split(String input_str,String chars){ String[] tmp2 = input_str.split(chars); int[] counts = new int[tmp2.length]; for (int i = 0; i < tmp2.length; i++) { counts[i] = Integer.parseInt(tmp2[i]); } return counts; }
} 解析代码思路
这段代码实现的是:把 1~k 按“顺时针螺旋向内”的顺序填进一个矩阵里,矩阵有 n 行,并且为了满足“列尽可能少、每行元素一样多”,它把列数取成 ceil(k / n);如果格子不够填,就用 "*" 占位。
下面按代码结构把思路拆开讲。
javaint[] params = split(in.nextLine(), " "); int k = params[0]; int n = params[1]; String[][] matrix = new String[n][(k - 1) / n + 1];
k:要写的数字个数(写 1..k)n:行数(m 行矩阵里这里用 n 变量名)(k - 1) / n + 1这个公式等价于:
cols = ceil(k / n)举例:k=9, n=4
(9-1)/4+1 = 8/4+1 = 2+1 = 3 → 4 行 3 列,正好是示例输出的尺寸。
javafor (int i = 0; i < n; i++) { for (int j = 0; j < cols; j++) { matrix[i][j] = "*"; } }
这样后面判断“这个格子是否填过”就很简单:
matrix[x][y].equals("*")"*"javapublic static int[] directions = {0, 1, 0, -1, 0};
它的用法是:
index 取 0..3dx = directions[index]dy = directions[index+1]所以四个方向依次是:
| index | dx | dy | 方向 |
|---|---|---|---|
| 0 | 0 | 1 | 右 |
| 1 | 1 | 0 | 下 |
| 2 | 0 | -1 | 左 |
| 3 | -1 | 0 | 上 |
index = (index + 1) % 4 就能循环转向。
初始化:
javaint start_x = 0, start_y = 0; // 从左上角开始 int count = 1; // 要写的数字 int index = 0; // 当前方向(0=右)
主循环:
javawhile (true) { if (count > k) break; matrix[start_x][start_y] = String.valueOf(count); count++; int new_x = start_x + directions[index]; int new_y = start_y + directions[index+1]; if (new_x 越界 || new_y 越界 || 下一个格子不是"*") { index = (index + 1) % 4; // 右转 start_x += directions[index]; start_y += directions[index+1]; } else { start_x = new_x; // 继续直走 start_y = new_y; } }
这段逻辑为什么会形成“外圈→内圈”的螺旋?
这就是典型的“碰壁/碰已访问格子就转向”的螺旋写法。
javafor (i 行) { for (j 列) { print(matrix[i][j]); if (j != lastCol) print(" "); } println(); }
所以输出就是你截图那种格式:每行列之间一个空格。
题面一般写“输入 n m”,但你代码里是:
k = params[0](数字个数)n = params[1](行数)也就是说你的输入格式是:k 行数(不是 n m)。
你截图示例“9 4”能对上(k=9,行=4),所以没问题。
如果你想更稳一点(避免重复写 (k - 1) / n + 1),可以先算 cols 变量,提高可读性;或者我也可以把这段螺旋填充重写成更清晰的版本(同逻辑更少分支)。
详细拆解下主循环
下面把你代码里主循环(螺旋填充那段)按“每一步在干什么、为什么这么干、关键变量怎么变”彻底拆开讲清楚。为了读起来更直观,我会先把循环“抽象成伪代码”,再逐行解释,并用一个小例子跑几步。
javawhile (true){ if(count > k){ break; } else { matrix[start_x][start_y] = String.valueOf(count); count+=1; int new_x = start_x + directions[index]; int new_y = start_y + directions[index+1]; if (new_x < 0 || new_x >= n || new_y < 0 || new_y >= (k - 1) / n +1 || !matrix[new_x][new_y].equals("*")) { index = (index + 1) % 4; start_x = start_x + directions[index]; start_y = start_y + directions[index+1]; } else { start_x= new_x; start_y= new_y; } } }
每次循环把
count写到当前位置(start_x,start_y),然后尝试按当前方向走一步;
如果下一步会越界或撞到已填过的格子,就右转一次再走;否则就直走。
这套规则会自然形成“顺时针螺旋向内”。
start_x, start_y:当前要填的格子坐标count:当前要写的数字(从 1 递增到 k)index:当前方向编号(0右、1下、2左、3上)directions = {0,1,0,-1,0}:用来从 index 取 (dx,dy)方向对照:
| index | dx=directions[index] | dy=directions[index+1] | 方向 |
|---|---|---|---|
| 0 | 0 | 1 | 右 |
| 1 | 1 | 0 | 下 |
| 2 | 0 | -1 | 左 |
| 3 | -1 | 0 | 上 |
javaif(count > k){ break; }
count 已经变成 k+1,说明 1..k 都写完了,退出。你这里用
while(true)+break,等价于while (count <= k)。
javamatrix[start_x][start_y] = String.valueOf(count); count+=1;
countcount++,准备写下一个数javaint new_x = start_x + directions[index]; int new_y = start_y + directions[index+1];
new_x,new_y 是候选下一格(你还没真正走过去,只是在“试探”)javaif (new_x < 0 || new_x >= n || new_y < 0 || new_y >= cols || !matrix[new_x][new_y].equals("*"))
不能走的原因只有两种:
new_x/new_y 超出了矩阵范围matrix[new_x][new_y] 不是 "*",说明那格之前已经写过数字了这里用
"*"当作“未访问标记”。
javaindex = (index + 1) % 4; start_x = start_x + directions[index]; start_y = start_y + directions[index+1];
发生了什么?
index = (index+1)%4:方向顺时针转 90°(右→下→左→上→右…)(dx,dy) 让当前位置移动一步注意:这里不是“只转向不移动”,而是“转向并立刻走一步”。
javastart_x= new_x; start_y= new_y;
假设(跟截图示例一致):
k=9,n=4,列数 cols = (k-1)/n+1 = 3start=(0,0),index=0(向右),count=1矩阵初始全是 "*"。
1 到 (0,0),count=2*2 到 (0,1),count=33 到 (0,2),count=44 到 (1,2),count=55 到 (2,2),count=66 到 (3,2),count=7……继续下去就会形成螺旋:
关键在这一句:
java|| !matrix[new_x][new_y].equals("*")
当你走完一圈外圈后,再往前走会遇到“已经填过的格子”,于是触发转向,进入下一层内圈继续走。
这就是螺旋“向内”的来源。
你代码里多次写 (k - 1) / n + 1,建议提成变量 cols:
javaint cols = (k - 1) / n + 1; ... if (new_y >= cols) ...
这样主循环更好读也更不容易写错。
如果你愿意,我还可以用你截图的示例 k=9,n=4 把每一步填完后的矩阵状态也画出来(第几轮后矩阵长什么样),会更直观。
简化下,不要方向数组了
可以的 👍
方向数组 directions = {0,1,0,-1,0} 的作用只是把 右→下→左→上 四个方向压缩成一个数组来写。
如果你觉得不好理解,我们完全可以把它改成更直观的写法:
用两个变量 dx, dy 表示当前方向,遇到边界/已填就顺时针换方向:
dx=0, dy=1dx=1, dy=0dx=0, dy=-1dx=-1, dy=0"*"(0,0) 开始,方向初始向右"*",就换方向javaimport java.util.*; public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); int k = in.nextInt(); // 要填的数字个数 int n = in.nextInt(); // 行数 int cols = (k - 1) / n + 1; // 列数 = ceil(k / n) String[][] matrix = new String[n][cols]; for (int i = 0; i < n; i++) { Arrays.fill(matrix[i], "*"); } int x = 0, y = 0; // 当前坐标 int dx = 0, dy = 1; // 当前方向:初始向右 int num = 1; while (num <= k) { matrix[x][y] = String.valueOf(num++); // 试探下一格 int nx = x + dx; int ny = y + dy; // 如果下一格越界或已经填过,则顺时针转向 if (nx < 0 || nx >= n || ny < 0 || ny >= cols || !matrix[nx][ny].equals("*")) { // 顺时针旋转: (dx, dy) -> (dy, -dx) int tmp = dx; dx = dy; dy = -tmp; nx = x + dx; ny = y + dy; } // 真正移动 x = nx; y = ny; } // 输出矩阵 for (int i = 0; i < n; i++) { for (int j = 0; j < cols; j++) { System.out.print(matrix[i][j]); if (j != cols - 1) System.out.print(" "); } System.out.println(); } } }
(dx,dy) -> (dy,-dx))顺时针旋转规则:
| 原方向 | (dx,dy) | 转后方向 | (dy,-dx) |
|---|---|---|---|
| 右 | (0,1) | 下 | (1,0) |
| 下 | (1,0) | 左 | (0,-1) |
| 左 | (0,-1) | 上 | (-1,0) |
| 上 | (-1,0) | 右 | (0,1) |
所以只要一句:
javaint tmp = dx; dx = dy; dy = -tmp;
就能实现顺时针换方向,不需要方向数组,也不需要 index。
如果你愿意,我还可以进一步把它改成 “四个边界变量 top/bottom/left/right” 的写法(更像经典螺旋矩阵),那种也不用方向数组,且不需要判断是否填过格子。
依赖,注解,依赖注入,ioc,aop,bean 帮我通俗的总结下spring/spring mvc /spring boot 里的这些概念
下面把你列的这些词,用通俗但不失准确的方式,按 Spring / Spring MVC / Spring Boot 的语境一口气讲清楚。你可以把它当成面试/复习的“口述答案”。
一句话: 一个类要完成工作,需要用到另一个类,这个“需要用到的东西”就叫依赖。
OrderService 处理订单时需要 UserService 查询用户:
UserService 就是 OrderService 的依赖。传统写法:
new UserService()
Spring 写法:让 Spring 给你“提供”一个UserService
一句话: 给代码“贴标签”,让 Spring 能识别并自动做事情。
@Component:把这个类交给 Spring 管理(它会变成 Bean)@Service:语义上表示业务层(本质也是 @Component)@Controller / @RestController:控制层@Repository:数据层@Autowired:自动注入依赖@Configuration:配置类@Bean:手动声明一个 Bean@RequestMapping / @GetMapping / @PostMapping:MVC 路由你可以把注解理解成:“告诉 Spring:这块代码你来管!”
一句话: 依赖不自己 new,而是由外部(Spring)给你“注入”进来。
你只写:
java@Service public class OrderService { @Autowired private UserService userService; }
你没有 new UserService(),但 userService 依然能用
因为 Spring 会在运行时把合适的对象塞进来。
一句话: 以前是你控制对象的创建和关系,现在“控制权反转”给了 Spring。
DI 是 IoC 的实现方式之一(最常用)。
可以这样背:
一句话: 被 Spring 容器管理的对象,就叫 Bean。
@Component/@Service/@Controller 扫描出来的@Configuration + @Bean 手动创建的你没问,但 Bean/IoC/DI 必须靠它。
一句话: Spring 容器就是一个“超级对象工厂 + 管理中心”。
它负责:
一句话: 把“和业务无关但到处都要用的功能”,抽出来统一处理,不侵入业务代码。
如果没有 AOP,你的业务代码会变成:
java打印日志 开始事务 执行业务 提交事务 打印日志
到处重复、非常乱。
有 AOP 后:
下面是重点:很多人混乱就是因为分不清这三个框架范围。
Spring 的核心就是:
一句话总结 Spring:
Spring 是一个“管对象 + 管依赖 + 管增强”的框架。
Spring MVC 是 Spring 的 Web 层框架,它解决:
@GetMapping)@RequestParam/@RequestBody)@RestController)你可以把 Spring MVC 想成:
专门负责 Web 请求处理流程的模块(Controller 那一套)。
Spring Boot 并不是“新的框架”,它是:
一句话总结 Spring Boot:
让你不用到处写配置,直接跑起来。
你可以用下面这段话背下来(面试很好用):
Spring 核心是 IoC 和 DI,容器负责创建 Bean 并注入依赖;AOP 用动态代理在方法执行前后增强能力,比如事务和日志。Spring MVC 负责 Web 请求到 Controller 的映射与参数绑定;Spring Boot 提供 starter 和自动配置,让 Spring 项目几乎不用手写配置就能运行。
如果你想更实战一点,我也可以用一个小例子(用户下单接口)把:
starter
下面把 Spring Boot Starter 用非常通俗 + 面试可背的方式给你讲清楚 👌
Starter 就是一份“打包好的依赖清单 + 默认配置”,让你一句依赖就能用一整套功能。
你想写一个 Web 项目,需要:
只要:
xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
立刻拥有:
👉 Starter 的目的:开箱即用,避免依赖地狱
Starter 本质上就是一个“没有代码的 Maven 依赖包”
它里面:
你可以记成一个公式:
Starter = 一组依赖 + Auto Configuration(自动配置)
spring-boot-starter-web它帮你引入:
spring-webspring-webmvcspring-boot-starter-tomcatjackson-databindspring-boot-starter-logging并且自动帮你做:
| Starter | 作用 |
|---|---|
spring-boot-starter-web | Web / REST 接口 |
spring-boot-starter-test | 测试 |
spring-boot-starter-jdbc | JDBC |
spring-boot-starter-data-jpa | JPA |
spring-boot-starter-security | 安全 |
spring-boot-starter-validation | 参数校验 |
spring-boot-starter-logging | 日志(默认) |
spring-boot-starter-thymeleaf | 模板引擎 |
👉 看到需求,第一反应:找 Starter
spring.factories@ConditionalOnXXX)所以 Starter ≠ 自动配置
Starter 是 触发自动配置的“开关”
因为它的意义是:
“我想用某种能力,项目一启动,这些东西就已经准备好了。”
比如:
面试如果追问,你可以这么说 👇
1️⃣ xxx-spring-boot-starter
2️⃣ xxx-spring-boot-autoconfigure
@Configuration@ConditionalOnClass@ConditionalOnProperty👉 官方 starter 也是这么拆的
| 方式 | 特点 |
|---|---|
| 直接引依赖 | 需要自己配置,容易冲突 |
| Starter | 依赖整合好 + 默认配置 |
👉 Starter 是“约定大于配置”的典型体现
Spring Boot Starter 是对常用功能依赖的统一封装,通过引入 Starter 即可获得相关依赖和默认自动配置,从而实现开箱即用、减少配置和版本冲突。
Starter 就像“套餐”
你点了 Web 套餐,不用再单点 Tomcat、JSON、日志,厨师(Spring Boot)已经帮你配好了。
如果你愿意,我可以下一步帮你讲:
@EnableAutoConfiguration 的关系你想接着哪个?
springboot的启动过程
下面我用**“先整体 → 再细节 → 最后面试版总结”**的方式,把 Spring Boot 启动过程给你讲清楚。
目标是:你能自己在白板上从 main 讲到容器启动完成。
Spring Boot 启动的本质:
👉 创建 SpringApplication
👉 启动 Spring 容器
👉 自动配置生效
👉 内嵌服务器启动
👉 应用就绪
java@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
这里发生了两件事:
SpringApplication.run() → 真正启动我先给你一个完整流程图(文字版),后面逐步拆。
text1. new SpringApplication() 2. 推断应用类型(Web / 非 Web) 3. 加载 ApplicationContextInitializer 4. 加载 ApplicationListener 5. 运行 run() ├─ 创建 ApplicationContext ├─ 准备环境(Environment) ├─ 执行初始化器 ├─ 刷新容器 refresh() │ ├─ 扫描 Bean │ ├─ 创建 Bean │ ├─ 依赖注入 │ ├─ AOP 生效 │ ├─ 自动配置生效 ├─ 启动内嵌 Tomcat 6. 发布 ApplicationReadyEvent
javaSpringApplication app = new SpringApplication(Application.class);
此时会做这些事:
SERVLET(Web)REACTIVENONE📌 这一步还没创建 Spring 容器
Spring Boot 会判断:
ServletWebApplicationContext决定用哪种容器:
| 类型 | 容器 |
|---|---|
| 普通应用 | ApplicationContext |
| Web MVC | ServletWebServerApplicationContext |
| WebFlux | ReactiveWebServerApplicationContext |
javaapp.run(args);
这是最重要的一步。
Spring Boot 会加载所有配置:
优先级从高到低(面试常问):
application.properties / yml👉 最终合并成一个 Environment 对象
根据应用类型创建:
textServletWebServerApplicationContext
但此时:
refresh() 是 Spring 容器的灵魂
里面发生的关键事情:
@ComponentScan@Component / @Service / @Controller / @Configuration@Bean 方法变成 BeanDefinitionspring.factoriesxxxAutoConfiguration@ConditionalOnClass@ConditionalOnProperty@ConditionalOnMissingBean📌 Starter + AutoConfiguration 在这里生效
@Autowired@Value如果是 Web 项目:
📌 这一步之后,HTTP 请求就能进来了
javaApplicationReadyEvent
你可以监听这个事件做初始化工作。
| 名称 | 作用 |
|---|---|
| SpringApplication | 启动器 |
| ApplicationContext | 容器 |
| Environment | 配置管理 |
| AutoConfiguration | 自动装配 |
| BeanFactory | Bean 创建 |
| DispatcherServlet | MVC 核心 |
| Embedded Server | 内嵌容器 |
| 对比项 | Spring | Spring Boot |
|---|---|---|
| 配置方式 | XML / 手动 | 自动配置 |
| 容器启动 | 手动 | 自动 |
| Web 容器 | 外置 | 内嵌 |
| 启动难度 | 高 | 低 |
👉 Spring Boot = Spring + 自动配置 + 内嵌容器
Spring Boot 启动从 main 方法开始,通过 SpringApplication.run 创建并启动 ApplicationContext;在 run 过程中加载配置环境、执行初始化器、刷新容器;在 refresh 阶段完成 Bean 扫描、自动配置、生效 AOP;如果是 Web 项目则启动内嵌 Tomcat,最后发布应用就绪事件。
在容器 refresh 阶段,通过加载 AutoConfiguration 并结合 Conditional 条件注册 Bean。
starter 通过引入依赖触发自动配置,在 refresh 阶段生效。
在容器 refresh 之后,由 WebServerFactory 创建并启动。
如果你愿意,我可以继续帮你:
你想继续哪一个?
mysql分库分表
下面我用 “为什么要分 → 怎么分 → 怎么用 → 面试怎么答” 的方式,把 MySQL 分库分表 给你一次性讲清楚,通俗但不水,适合面试和实战理解。
分库分表是为了解决单库单表在数据量和并发变大时的性能和容量瓶颈。
ALTER TABLE、备份、恢复都很慢👉 核心问题:一个 MySQL 实例撑不住
把一张大表,拆成多张结构相同的小表
textorder → order_0 → order_1 → order_2
把数据拆到多个数据库实例
textorder_db_0 order_db_1 order_db_2
textorder_db_0.order_0 ~ order_7 order_db_1.order_8 ~ order_15
javatableIndex = userId % 16
不能再用自增 ID(会冲突)
你不能再:
sqlselect * from order;
而必须:
❌ MySQL 不支持跨库 JOIN
单库事务 OK
跨库事务 ❌(非常复杂)
| 场景 | 建议 |
|---|---|
| 单表 < 100 万 | 不用 |
| 单表 100 万 ~ 500 万 | 可以考虑 |
| 单表 > 1000 万 | 必须 |
| QPS > 几千 | 考虑分库 |
| 单库压力大 | 分库 |
分库分表是为了解决单库单表在大数据量和高并发场景下的性能瓶颈;分表解决单表过大问题,分库解决并发和资源瓶颈;常见拆分方式有按范围、按 Hash 和按业务维度;分库分表后需要解决主键生成、跨库查询、事务和分页等问题,通常借助 ShardingSphere 等中间件实现。
分库分表就像把一个超市拆成多家分店 + 多个货架
人多了就分流,货多了就分架,查起来快,但管理复杂。
如果你愿意,我可以下一步帮你:
你想继续哪一个?