1318.或运算的最小翻转次数
/**
* 解决方案类:解决 LeetCode 1318. 或运算的最小翻转次数问题
* 问题描述:给定三个正整数 a、b、c,你可以对 a 或 b 的二进制位进行翻转操作(0 变 1,1 变 0),
* 要求使得 a | b == c 成立,计算最少需要多少次翻转操作。
* 核心思路:
* 逐位分析二进制位的要求,根据 c 的当前位是 0 或 1,分别计算 a、b 对应位需要的翻转次数,累加得到总次数;
* 时间复杂度 O(1)(固定遍历 31 位二进制位,常数级操作),
* 空间复杂度 O(1)(仅使用常数级变量)。
*/
public class Solution {
/**
* 计算使 a | b == c 成立的最小翻转次数
* @param a 第一个整数
* @param b 第二个整数
* @param c 目标结果整数
* @return 最少翻转次数
*/
public int minFlips(int a, int b, int c) {
int ans = 0; // 累计翻转次数
// 遍历 32 位整数的低 31 位(题目中数值为正整数,最高位为0,无需处理)
for (int i = 0; i < 31; ++i) {
// 提取 a 的第 i 位二进制值(0 或 1):右移 i 位后与 1 按位与,仅保留第 i 位
int bitA = (a >> i) & 1;
// 提取 b 的第 i 位二进制值
int bitB = (b >> i) & 1;
// 提取 c 的第 i 位二进制值
int bitC = (c >> i) & 1;
if (bitC == 0) {
// 情况1:c 的第 i 位为 0
// 要求 a | b 的第 i 位为 0 → a 和 b 的第 i 位必须都为 0
// 翻转次数 = a的第i位(1则需翻转,0则无需) + b的第i位(同理)
ans += bitA + bitB;
} else {
// 情况2:c 的第 i 位为 1
// 要求 a | b 的第 i 位为 1 → a 或 b 的第 i 位至少一个为 1
// 若两者都为 0,需翻转其中一个(次数+1);否则无需翻转(次数+0)
ans += (bitA + bitB == 0) ? 1 : 0;
}
}
return ans;
}
}
1268.搜索推荐系统
/**
* 解决方案类:解决 LeetCode 1268. 搜索推荐系统问题
* 问题描述:给定产品数组 products 和搜索词 searchWord,
* 对于 searchWord 的每个前缀(第1个字符、前2个字符...前n个字符),
* 返回最多3个按字典序排序的、以该前缀开头的产品推荐列表;
* 核心思路:
* 字典树(Trie)+ 最大堆:
* 1. 字典树存储所有产品的字符前缀,每个节点维护一个最多3个元素的最大堆;
* 2. 插入产品时,每个字符节点的堆存储以该前缀开头的产品,超出3个则移除最大的(保证堆中是最小的3个);
* 3. 查询时遍历搜索词前缀,从对应节点的堆中取出元素并反转(转为升序);
* 时间复杂度 O(m*L + n*L)(m=产品数,L=产品平均长度;n=搜索词长度),
* 空间复杂度 O(m*L)(字典树存储所有产品的字符)。
*/
// 字典树节点类:每个节点包含子节点映射和存储最多3个单词的最大堆
class TrieNode {
// 子节点映射:key=字符,value=该字符对应的子节点
Map<Character, TrieNode> child = new HashMap<>();
// 最大堆:存储以当前前缀开头的单词,自定义比较器实现「字典序降序」(Java默认最小堆)
// 堆中最多保留3个元素,保证是字典序最小的3个(移除最大的)
PriorityQueue<String> words = new PriorityQueue<>((a, b) -> b.compareTo(a));
}
public class Solution {
/**
* 向字典树中插入单个单词
* @param root 字典树根节点
* @param word 待插入的产品单词
*/
private void addWord(TrieNode root, String word) {
TrieNode cur = root; // 从根节点开始遍历
// 遍历单词的每个字符,构建字典树路径
for (char ch : word.toCharArray()) {
// 若当前字符的子节点不存在,创建新节点
if (!cur.child.containsKey(ch)) {
cur.child.put(ch, new TrieNode());
}
// 移动到当前字符的子节点
cur = cur.child.get(ch);
// 将单词加入当前节点的最大堆
cur.words.offer(word);
// 堆中元素超过3个时,移除字典序最大的元素(保证堆中仅保留最小的3个)
if (cur.words.size() > 3) {
cur.words.poll();
}
}
}
/**
* 生成搜索词每个前缀的推荐产品列表
* @param products 产品数组
* @param searchWord 搜索词
* @return 每个前缀对应的推荐列表(最多3个,升序)
*/
public List<List<String>> suggestedProducts(String[] products, String searchWord) {
// 初始化字典树根节点
TrieNode root = new TrieNode();
// 将所有产品插入字典树
for (String word : products) {
addWord(root, word);
}
List<List<String>> ans = new ArrayList<>(); // 最终结果列表
TrieNode cur = root; // 从根节点开始遍历搜索词
boolean flag = false; // 标记:是否已无匹配的前缀(后续前缀直接返回空列表)
// 遍历搜索词的每个字符(逐个构建前缀)
for (char ch : searchWord.toCharArray()) {
// 情况1:已无匹配前缀 或 当前字符无对应子节点 → 该前缀无推荐,添加空列表
if (flag || !cur.child.containsKey(ch)) {
ans.add(new ArrayList<>());
flag = true; // 标记后续前缀均无匹配
} else {
// 情况2:当前字符有对应子节点 → 移动到该子节点
cur = cur.child.get(ch);
List<String> selects = new ArrayList<>();
// 取出堆中所有元素(最多3个,字典序降序)
while (!cur.words.isEmpty()) {
selects.add(cur.words.poll());
}
// 反转列表:将降序转为升序(符合题目要求的字典序)
Collections.reverse(selects);
// 将该前缀的推荐列表加入结果
ans.add(selects);
}
}
return ans;
}
}
435.无重叠区间
class Solution {
/**
* 题目:无重叠区间(最少移除多少区间使剩余区间无重叠)
* 核心思路(贪心解法,最优解):
* 1. 贪心策略:优先选择「结束时间最早」的区间,尽可能为后续区间留出更多空间
* 2. 问题转化:最少移除数 = 总区间数 - 最大不重叠区间数(与动态规划思路一致,但贪心效率更高)
* 3. 排序规则:按区间右端点升序排序(核心!保证每次选的是结束最早的区间)
* 时间复杂度:O(n log n)(排序 O(n log n) + 一次遍历 O(n))
* 空间复杂度:O(log n)(排序的系统栈空间)
*
* @param intervals 二维数组,每个元素为 [左端点, 右端点] 的区间
* @return 最少需要移除的区间数量
*/
public int eraseOverlapIntervals(int[][] intervals) {
// 边界条件:无区间时,无需移除任何区间
if (intervals.length == 0) {
return 0;
}
// 步骤1:按区间「右端点」升序排序(贪心策略的核心前提)
// 排序后,前面的区间结束更早,优先选这类区间能最大化不重叠区间的数量
Arrays.sort(intervals, new Comparator<int[]>() {
public int compare(int[] interval1, int[] interval2) {
// 右端点升序:interval1[1] - interval2[1] 为正则 interval1 在后,负则在前
return interval1[1] - interval2[1];
}
});
int n = intervals.length;
// 步骤2:初始化贪心遍历的关键变量
int right = intervals[0][1]; // 记录当前选中区间的右端点(初始为第一个区间的右端点)
int ans = 1; // 记录最大不重叠区间数(初始为1,因为至少选第一个区间)
// 步骤3:遍历剩余区间,统计最大不重叠区间数
for (int i = 1; i < n; ++i) {
// 关键判断:当前区间的左端点 >= 上一个选中区间的右端点 → 无重叠,可选中
if (intervals[i][0] >= right) {
++ans; // 不重叠区间数+1
right = intervals[i][1];// 更新选中区间的右端点为当前区间的右端点
}
// 若当前区间与上一个选中区间重叠 → 直接跳过(贪心策略:不选这个重叠区间,保留结束更早的那个)
}
// 步骤4:计算最少移除数 = 总区间数 - 最大不重叠区间数
return n - ans;
}
}
452.用最少数量的箭引爆气球
class Solution {
/**
* 题目:用最少数量的箭引爆气球
* 核心思路(贪心解法):
* 1. 贪心策略:优先选择「右边界最早」的气球,在其右边界射箭,尽可能引爆更多重叠气球
* 2. 问题本质:找"最多的不重叠区间数"(每个不重叠区间需要一支箭,与"无重叠区间"问题同源)
* 3. 排序规则:按气球右边界升序排序(核心!保证每次射箭位置是当前最靠左的有效位置)
* 时间复杂度:O(n log n)(排序 O(n log n) + 一次遍历 O(n))
* 空间复杂度:O(log n)(排序的系统栈空间)
*
* @param points 二维数组,每个元素为 [气球左边界, 气球右边界]
* @return 引爆所有气球所需的最少箭数量
*/
public int findMinArrowShots(int[][] points) {
// 边界条件:无气球时,无需射箭
if (points.length == 0) {
return 0;
}
// 步骤1:按气球「右边界」升序排序(贪心策略的核心前提)
// 注意:这里不用 point1[1] - point2[1] 直接相减,避免整数溢出(官方题解写法更安全)
// 排序后,前面的气球右边界更早,在其右边界射箭能覆盖更多后续重叠气球
Arrays.sort(points, new Comparator<int[]>() {
public int compare(int[] point1, int[] point2) {
if (point1[1] > point2[1]) {
return 1; // point1 右边界更大,排后面
} else if (point1[1] < point2[1]) {
return -1; // point1 右边界更小,排前面
} else {
return 0; // 右边界相等,顺序无关
}
}
});
// 步骤2:初始化贪心遍历的关键变量
int pos = points[0][1]; // 记录当前射箭的位置(初始为第一个气球的右边界)
int ans = 1; // 初始需要1支箭(至少引爆第一个气球)
// 步骤3:遍历剩余气球,判断是否需要新增箭
for (int[] balloon : points) {
// 关键判断:当前气球的左边界 > 上一次射箭位置 → 无重叠,需要新增箭
// 说明当前气球无法被上一支箭引爆,必须在其右边界重新射箭
if (balloon[0] > pos) {
pos = balloon[1]; // 更新射箭位置为当前气球的右边界
++ans; // 箭的数量+1
}
// 若当前气球左边界 ≤ 射箭位置 → 与当前箭的覆盖范围重叠,无需新增箭
}
return ans;
}
}