2023-04-28 LeetCode精选题目20道附我的Java实现
LeetCode精选题目20道
1.56.合并区间
贪心
给出一个区间的集合,请合并所有重叠的区间。
示例 1:
输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入: intervals = [[1,4],[4,5]]
输出: [[1,5]]
解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
Java实现
// 考虑四个用例
// [[1,3],[2,6],[8,10],[15,18]]
// [[1,4],[4,5]]
// [[1,4],[2,3]]
// []
import java.util.*;
class Solution {
public int[][] merge(int[][] intervals) {
// 按照区间左端点进行排序
Arrays.sort(intervals, (o1, o2) -> o1[0] - o2[0]); // 按照区间左端点进行排序
List<int[]> result = new ArrayList<>();
int left = Integer.MIN_VALUE, right = Integer.MIN_VALUE;
for (int[] interval : intervals) {
if (right >= interval[0]) { // 当前区间的左端点 < right,则可以合并区间
if (right > interval[1]) continue; // [left, right]完全包含interval
right = interval[1]; // [left, right]没完全包含interval,需要更新右端点
} else { // 一旦right比新来的区间的左端点还小,那么肯定要新开一个区间了
if (left != Integer.MIN_VALUE) result.add(new int[]{left, right});
left = interval[0];
right = interval[1];
}
}
if (left != Integer.MIN_VALUE) result.add(new int[]{left, right}); // 最终的left和right记得还要加进去
int[][] res = new int[result.size()][2];
for (int i = 0; i < result.size(); i++) {
res[i] = result.get(i);
}
return res;
}
}
C++实现
#include <cmath>
class Solution {
public:
// 贪心算法,按照左端点升序排序
vector<vector<int>> merge(vector<vector<int>>& intervals) {
// intervals相当于二维数组,这里按照左端点进行升序排序
sort(intervals.begin(), intervals.end());
vector<vector<int>> res; // 内部的vector<int>代表区间,固定两个元素,外层vector表示存储所有合并后的区间
int left = INT_MIN, right = INT_MIN; // 初始化最左端点和最右端点
for(auto& interval : intervals) {
if (interval[0] <= right) {
// 当前区间的左端点 < 最右端点,则可以合并区间
if (interval[1] <= right) continue; // 之前的区间完全覆盖当前的区间,直接跳过,苟泽更新右端点
right = interval[1]; // [left, right]没有完全包括该区间,因为当前区间左端点interval[0]肯定小于left,所以更新右端点即可
} else { // 如果新来区间的左端点比目前最右端点还靠右,即[interval[0], interval[1]]完全在区间[left, right]右侧,显然需要新开辟一个区间放到结果中了
if (left != INT_MIN) res.push_back({left, right});
left = interval[0];
right = interval[1];
}
}
// 最后的left和right记得要处理
if (left != INT_MIN) res.push_back({left, right});
return res;
}
};
2.325.和为K的最长子数组长度
前缀和 + 哈希 + 贪心
给定一个数组 nums 和一个目标值 k,找到和等于 k 的最长子数组长度。如果不存在任意一个符合要求的子数组,则返回 0。
注意:
nums 数组的总和是一定在 32 位有符号整数范围之内的。
示例 1:
输入: nums = [1, -1, 5, -2, 3], k = 3
输出: 4
解释: 子数组 [1, -1, 5, -2] 和等于 3,且长度最长。
示例 2:
输入: nums = [-2, -1, 2, 1], k = 1
输出: 2
解释: 子数组 [-1, 2] 和等于 1,且长度最长。
暴力解法如下(用到了前缀数组):
class Solution {
public int maxSubArrayLen(int[] nums, int k) {
if (nums.length == 0) return 0;
int N = nums.length + 1;
int[] a = new int[N];
int[] S = new int[N];
for (int i = 1; i < N; i++) { // 对原数组重新赋值,下标 + 1才能方便前缀和计算
a[i] = nums[i - 1];
S[i] = S[i - 1] + a[i];
}
int res = 0;
for (int i = 1; i < N; i++) {
for (int j = i; j < N; j++) { // 一定注意这里是j = i而不是j = i + 1,
if (S[j] - S[i - 1] == k) {
res = Math.max(res, j - i + 1);
}
}
}
return res;
}
}
class Solution {
public:
int maxSubArrayLen(vector<int>& nums, int k) {
if (nums.size() == 0) return 0;
int N = nums.size() + 1;
int a[N]; // 下标从1开始的nums
a[0] = 0; // 下标为0的元素初始化为0
int S[N]; // 差分数组,S[i] = S[i - 1] + a[i];
for (int i = 1; i < N; i++) { // 对原来的数组重新赋值,下标 + 1才能方便前缀和计算
a[i] = nums[i - 1];
S[i] = S[i - 1] + a[i];
}
int res = 0;
for (int i = 1; i < N; i++) {
for (int j = i; j < N; j++) { // 一定注意这里是j = i而不是j = i + 1
if (S[j] - S[i - 1] == k) {
res = max(res, j - i + 1);
}
}
}
return res;
}
};
前缀和 + 哈希 + 贪心
class Solution {
// 注意不能用滑动窗口,滑动窗口一般是正数数组或者字符串才用(这两种情形当right++的时候,目标值增加,left++的时候,目标值减少。正数数组目标值是区间和,字符串时字符出现频率)
public int maxSubArrayLen(int[] nums, int k) {
if (nums.length == 0) return 0;
int N = nums.length + 1;
int[] a = new int[N];
int[] S = new int[N];
for (int i = 1; i < N; i++) {
a[i] = nums[i - 1];
S[i] = S[i - 1] + a[i];
}
// 遍历前缀和,同时构建map,键为前缀和1到i的前缀和即S[i] - S[0] = S[i],值为i
Map<Integer, Integer> mapSIndex = new HashMap<>();
int res = 0;
for (int i = 0; i < S.length; i++) {
// 需要在map中匹配的剩余值
int target = S[i] - k;
// 如果能匹配到剩余值对应的map的key,那么key对应的的值value就是要找的区间做端点(i是右端点)
if (mapSIndex.containsKey(target)) {
int index = mapSIndex.get(target); // [index, i)就是我们要找的区间
res = Math.max(res, i - index);
}
// 如果前缀和从未出现过,则加入当前前缀和的最优索引,注意如果这个前缀和的值之前出现过则不能覆盖(贪心思想:先加入的前缀和对应的索引肯定更小)
if (!mapSIndex.containsKey(S[i])) {
mapSIndex.put(S[i], i);
}
}
return res;
}
}
class Solution {
public:
// 注意不能用滑动窗口,滑动窗口一般是正数数组或者字符串才用(这两种情形当right++地时候目标值增加,当left++地时候,目标值减少。正数数组目标值是区间和,字符串时字符出现频率)
int maxSubArrayLen(vector<int>& nums, int k) {
if (nums.size() == 0) return 0;
int N = nums.size() + 1;
int a[N]; // 下标从1开始的nums
a[0] = 0; // 下标为0的元素初始化为0
int S[N]; // 差分数组,S[i] = S[i - 1] + a[i];
S[0] = 0;
for (int i = 1; i < N; i++) { // 对原来的数组重新赋值,下标 + 1才能方便前缀和计算
a[i] = nums[i - 1];
S[i] = S[i - 1] + a[i];
}
// 遍历前缀和,同时构建map,键为潜水和1到i的前缀和即S[i] - S[0] = S[i],值为i
map<int, int> m;
int res = 0;
for (int i = 0; i < N; i++) { // 必须从0开始,因为一个元素都没有也算一种情况哦
// 在map中匹配剩余的值
int target = S[i] - k;
// 如果能匹配到剩余值对应的map的key,那么key对应的value就是要找地区间的左端点,map[target]是右端点
if (m.find(target) != m.end()) { // 判断键值不能这么用,不存在这个键是会新建地。判断键是否存在的方法要记一下
res = max(res, i - m[target]); // [map[target], i)就是我们要找的区间
}
// 如果这个前缀和从来没有出现过,则更新当前前缀和的最优索引,注意如果这个前缀和的值之前出现过则不能覆盖(贪心算法:先加入的前缀和S[i]对应的索引i肯定更小)
if (m.find(S[i]) == m.end()) m[S[i]] = i;
}
return res;
}
};
3.739.每日温度
单调栈,找出每个元素右边第一个比它大的元素的下标
请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。
class Solution {
// 找出每个元素右边第一个比它大的元素的下标
int[] nextGreaterElement(int[] nums) {
Stack<Integer> st = new Stack<>();
int[] res = new int[nums.length];
for (int i = nums.length - 1; i >= 0; i--) {
// 只要栈不为空,且栈顶元素比当前元素小,则弹出栈顶元素。每个元素入栈一次出栈一次,所以时间复杂度为O(n)
while (!st.isEmpty() && nums[i] >= nums[st.peek()]) st.pop();
int nearestMax = st.isEmpty() ? -1 : st.peek();
res[i] = nearestMax == -1 ? 0 : (nearestMax - i);
st.push(i); // 插入x仍然保持住单调递减栈的特性
}
return res;
}
public int[] dailyTemperatures(int[] nums) {
return nextGreaterElement(nums);
}
}
C++实现如下:
class Solution {
public:
// 找出每个元素右侧第一个比它大的元素的下标,维护一个从栈底到栈顶递减的单调栈
vector<int> nextGreaterElement(vector<int> nums) {
stack<int> st;
int N = nums.size();
vector<int> res(N, 0); // 初始化长度为N值都为0的一个数组
for (int i = N - 1; i >= 0; i--) {
// 只要栈不为空,且栈顶元素比当前元素小,则弹出栈顶元素。每个元素入栈一次出栈一次,所以时间复杂度为O(n)
while(!st.empty() && nums[st.top()] <= nums[i]) st.pop();
int nearestMax = st.empty() ? -1 : st.top(); // 新元素入栈后一直到调整到了单调栈
res[i] = nearestMax == -1 ? 0 : (nearestMax - i);
st.push(i); // 插入新元素,因为上面while的调整,所以下面仍然可以保持单调性
}
return res;
}
vector<int> dailyTemperatures(vector<int>& T) {
return nextGreaterElement(T);
}
};
4.554.砖墙
计算每行所有缝隙位置(距离最左侧的距离),枚举所有缝隙位置去划线,通过二分法确定当前的线是否穿过砖
你的面前有一堵矩形的、由多行砖块组成的砖墙。 这些砖块高度相同但是宽度不同。你现在要画一条自顶向下的、穿过最少砖块的垂线。
砖墙由行的列表表示。 每一行都是一个代表从左至右每块砖的宽度的整数列表。
如果你画的线只是从砖块的边缘经过,就不算穿过这块砖。你需要找出怎样画才能使这条线穿过的砖块数量最少,并且返回穿过的砖块数量。
你不能沿着墙的两个垂直边缘之一画线,这样显然是没有穿过一块砖的。
// 需要重点关注的用例:
// [[1,2,2,1],[3,1,2],[1,3,2],[2,4],[3,1,2],[1,3,1,1]] 输出为2
// [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]] 结果为0
// [[1],[1],[1]] 结果为3
class Solution {
// 判断缝隙位置是否在当前行存在
private boolean binarySearch(List<Integer> rowCracks, int crack) {
int l = 0;
int r = rowCracks.size();
while (l < r) {
int mid = l + r >> 1;
if (rowCracks.get(mid) > crack) r = mid;
else if (rowCracks.get(mid) < crack) l = mid + 1;
else return true; // 找到缝隙缝隙了
}
return false; // 没找到缝隙
}
public int leastBricks(List<List<Integer>> wall) {
Set<Integer> crackAllSet = new HashSet<>(); // 所有行不重复的缝隙集合
List<List<Integer>> rowCracksList = new ArrayList<>();
for (int i = 0; i < wall.size(); i++) {
rowCracksList.add(new ArrayList<>());
int sum = 0; // 存储缝隙的位置
List<Integer> wallRow = wall.get(i); // 第i行墙的情况
for (int j = 0; j < wallRow.size() - 1; j++) {
sum += wallRow.get(j); // 获取当前的裂缝距最左端的距离
rowCracksList.get(i).add(sum);
crackAllSet.add(sum); // 累计所有的缝隙
}
}
int res = wall.size(); // 初始化为最多穿过的砖数(即为行数),不要初始化为其他值,否则可能找不到缝隙,比如用例 [[1],[1],[1]],结果应该是 3
// 遍历所有的缝隙,通过二分法判断缝隙能够穿过某一行
for (int crack : crackAllSet) {
int wallsThrough = wall.size(); // 初始化默认穿过所有的砖
for (List<Integer> rowCracks : rowCracksList) {
if (binarySearch(rowCracks, crack)) { // 找到缝隙了,没有穿过当前行的砖
wallsThrough--;
}
}
res = Math.min(res, wallsThrough);
}
return res;
}
}
C++实现
class Solution {
public:
int leastBricks(vector<vector<int>>& wall) {
set<int> crackAllSet; // 所有不重复的缝隙集合
vector<vector<int>> rowCracksList(wall.size()); // 存储每一行的所有缝隙位置(位置即距离最左侧的距离)
for (auto& wallRow : wall) { // 处理每一行,统计当前行的所有裂缝位置
vector<int> vec;
int sum = 0; // 存储缝隙的位置
for (int j = 0; j < wallRow.size() - 1; j++) { // wallRow.size() - 1表示最后一块砖不算,因为垂直边缘不能划线
sum += wallRow[j]; // 计算距离最左侧的距离
vec.push_back(sum); // 把当前砖块的位置加进去
crackAllSet.insert(sum); // 累计所有的缝隙位置并去重
}
rowCracksList.push_back(vec); // 把当前行的所有缝隙组成的列表加进去
}
int res = wall.size(); // 初始化为最多穿过的砖数(即为行数),不要初始化为其他值,否则可能找不到缝隙。
// 遍历所有的缝隙,通过二分法判断能否穿过某一行
for (auto& crack : crackAllSet) {
int wallsThrough = wall.size(); // 初始化默认穿过所有的墙
for (auto& rowCracks : rowCracksList) {
if (binary_search(rowCracks.begin(), rowCracks.end(), crack)) wallsThrough--; // 找到缝隙了,没有穿过当前的砖
}
res = min(res, wallsThrough);
}
return res;
}
};
5.76.最小子串覆盖
滑动窗口
给你一个字符串 S、一个字符串 T 。请你设计一种算法,可以在 O(n) 的时间复杂度内,从字符串 S 里面找出:包含 T 所有字符的最小子串。
示例:
输入:S = "ADOBECODEBANC", T = "ABC"
输出:"BANC"
class Solution {
public String minWindow(String s, String t) {
char[] sa = s.toCharArray();
char[] ta = t.toCharArray();
Map<Character, Integer> need = new HashMap<>(); // T中字符出现次数
Map<Character, Integer> window = new HashMap<>(); // 「窗口」中的相应字符的出现次数
for (char c : ta) {
if (need.get(c) == null) need.put(c, 0);
else need.put(c, need.get(c) + 1);
}
int left = 0, right = 0; // 使用left和right变量初始化窗口的两端,不要忘了,区间[left, right)是左闭右开的,所以初始情况下窗口没有包含任何元素:
int valid = 0; // 窗口中满足need条件的字符个数
int start = 0, len = Integer.MAX_VALUE; // 最小覆盖子串的起始索引及长度
while (right < s.length()) {
char c = sa[right]; // c是将移入窗口的字符
right++; // 右边界右移,扩大窗口
// Todo: 进行窗口内数据的一些列更新
if (need.containsKey(c)) {
if (window.get(c) == null) window.put(c, 0);
else window.put(c, window.get(c) + 1);
if (window.get(c).equals(need.get(c))) valid++; // 一个字符频率相等,说明当前字符c满足情况了。这里一定要用equals而不是==,否则会有大用例通过不了
}
// 判断左侧窗口是否要收缩
while (valid == need.size()) { // 所有字符都在滑动窗口内找到了
// Todo: 在这里更新最小覆盖字符串相关参数
if (right - left < len) {
start = left;
len = right - left;
}
char d = sa[left]; // d是即将移出窗口的字符
left++; // 左边界右移,窗口缩小
if (need.containsKey(d)) {
if (window.get(d).equals(need.get(d))) valid--; // 左边界字符正要满足滑动窗口包含目标值,删除一个左边界可能会影响。这里一定要用equals而不是==,否则会有大用例通过不了
window.put(d, window.get(d) - 1); // 更新当前点的字符d的频率
}
}
}
return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len); // 注意是开始索引和结束索引取字符串
}
}
C++实现
#include <cmath>
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> need; // T中字符出现的次数
unordered_map<char, int> window; // [窗口]中的相应字符出现的次数
for (auto& c : t) need[c]++; // 统计目标字符串中各字符的频率
int left = 0, right = 0; // 使用left和right变量初始化窗口的两端,不要忘了,区间[left, right)是左闭右开地,所以初始情况下窗口没有任何元素
int valid = 0; // 窗口中满足need条件的字符个数
int start = 0, len = INT_MAX; // 最小覆盖子串地起始索引及长度
while (right < s.size()) {
char c = s[right]; // c是将要移入窗口的字符
right++; // 右边界右移,扩大窗口
// 进行窗口内的一些更新
if (need.count(c)) { // 如果目标字符串中包含c,说明c是要找的
window[c]++; // 字符c移入窗口
if (window[c] == need[c]) valid++; // 如果窗口内字符c的频率和目标字符串中字符c的频率相等,则c已经达到了要求
}
// 判断窗口是否需要收缩
while (valid == need.size()) {
// 在这里更新最小覆盖字符串
if (right - left < len) { // 当前的窗口宽度小于前面得到的窗口宽度了
start = left;
len = right - left;
}
// d是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内的数据的一些列更新
if (need.count(d)) { // 目标字符串包含d,那么窗口中字符d的频率要变化
if (window[d] == need[d]) valid--; // 在移出之前d的字符频率在窗口和目标字符串中正好相等,那么移出d之后,达标的字符个数就要减少一个
window[d]--; // 字符d的频率也要减少
}
}
}
return len == INT_MAX ? "" : s.substr(start, len);
}
};
6.438.找到字符串中所有字母异位词
和上面的题几乎完全一样,就是变换了下要返回的数据,把start依次加入到结果列表中并返回即可
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
说明:
字母异位词指字母相同,但排列不同的字符串。
不考虑答案输出的顺序。
class Solution {
public List<Integer> findAnagrams(String s, String t) {
char[] sa = s.toCharArray();
char[] ta = t.toCharArray();
Map<Character, Integer> need = new HashMap<>(); // T中字符出现次数
Map<Character, Integer> window = new HashMap<>(); // 「窗口」中的相应字符的出现次数
for (char c : ta) {
if (need.get(c) == null) need.put(c, 0);
else need.put(c, need.get(c) + 1);
}
int left = 0, right = 0; // 使用left和right变量初始化窗口的两端,不要忘了,区间[left, right)是左闭右开的,所以初始情况下窗口没有包含任何元素:
int valid = 0; // 窗口中满足need条件的字符个数
int start = 0, len = t.length(); // 目标字符串的长度
List<Integer> res = new ArrayList<>(); // 存储满足调试的开始索引
while (right < s.length()) {
char c = sa[right]; // c是将移入窗口的字符
right++; // 右边界右移,扩大窗口
// Todo: 进行窗口内数据的一些列更新
if (need.containsKey(c)) {
if (window.get(c) == null) window.put(c, 0);
else window.put(c, window.get(c) + 1);
if (window.get(c).equals(need.get(c))) valid++; // 一个字符频率相等,说明当前字符c满足情况了。这里一定要用equals而不是==,否则会有大用例通过不了
}
// 判断左侧窗口是否要收缩
while (valid == need.size()) { // 所有字符都在滑动窗口内找到了
// Todo: 在这里更新最小覆盖字符串相关参数
if (right - left == len) {
start = left;
res.add(start);
}
char d = sa[left]; // d是即将移出窗口的字符
left++; // 左边界右移,窗口缩小
if (need.containsKey(d)) {
if (window.get(d).equals(need.get(d))) valid--; // 左边界字符正要满足滑动窗口包含目标值,删除一个左边界可能会影响。这里一定要用equals而不是==,否则会有大用例通过不了
window.put(d, window.get(d) - 1); // 更新当前点的字符d的频率
}
}
}
return res;
}
}
C++代码
class Solution {
public:
vector<int> findAnagrams(string s, string t) {
unordered_map<char, int> need; // T中字符出现的次数
unordered_map<char, int> window; // [窗口]中的相应字符出现的次数
for (auto& c : t) need[c]++; // 统计目标字符串中各字符的频率
int left = 0, right = 0; // 使用left和right变量初始化窗口的两端,不要忘了,区间[left, right)是左闭右开地,所以初始情况下窗口没有任何元素
int valid = 0; // 窗口中满足need条件的字符个数
int start = 0, len = t.size(); // 目标字符串的长度
vector<int> res;
while (right < s.size()) {
char c = s[right]; // c是将要移入窗口的字符
right++; // 右边界右移,扩大窗口
// 进行窗口内的一些更新
if (need.count(c)) { // 如果目标字符串中包含c,说明c是要找的
window[c]++; // 字符c移入窗口
if (window[c] == need[c]) valid++; // 如果窗口内字符c的频率和目标字符串中字符c的频率相等,则c已经达到了要求
}
// 判断窗口是否需要收缩
while (valid == need.size()) {
// 在这里更新最小覆盖字符串
if (right - left == len) { // 当前的窗口宽度等于前面得到的窗口宽度了,而且所有字符都在window中找到了
start = left;
res.push_back(start); // 把区间起点加入到结果中
}
// d是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内的数据的一些列更新
if (need.count(d)) { // 目标字符串包含d,那么窗口中字符d的频率要变化
if (window[d] == need[d]) valid--; // 在移出之前d的字符频率在窗口和目标字符串中正好相等,那么移出d之后,达标的字符个数就要减少一个
window[d]--; // 字符d的频率也要减少
}
}
}
return res;
}
};
7.200.岛屿数量
DFS求联通分量的个数
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
class Solution {
private int Rs;
private int Cs;
private char[][] grid;
private boolean[][] visited;
private final int[][] dirs = {{0, 1},{1, 0}, {0, -1}, {-1, 0}};
private int ccCount;
private boolean inGrid(int r, int c){
return r >= 0 && r < Rs && c >= 0 && c < Cs;
}
public int numIslands(char[][] grid) {
if(grid == null || grid.length == 0){
return 0;
}
Rs = grid.length;
Cs = grid[0].length;
this.grid = grid;
visited = new boolean[Rs][Cs];
ccCount = 0;
for(int r = 0; r < Rs; r++){
for(int c = 0; c < Cs; c++){
if(!visited[r][c] && grid[r][c] == '1'){
dfs(r, c);
ccCount++;
}
}
}
return ccCount;
}
private void dfs(int r, int c){
visited[r][c] = true;
for(int[] dir : dirs){
int rNext = r + dir[0];
int cNext = c + dir[1];
if(inGrid(rNext, cNext) && !visited[rNext][cNext] && grid[rNext][cNext] == '1'){
dfs(rNext, cNext);
}
}
}
}
C++实现
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int R{0}; // 行
int C{0}; // 列
int dirs[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 上下左右四个方向
vector<vector<bool>> visited;
vector<vector<char>> grid;
bool inGrid(int r, int c) {
return r >= 0 && r < R && c >= 0 && c < C;
}
void dfs(int r, int c) {
visited[r][c] = true;
for (auto &dir : dirs) {
int rNext = r + dir[0];
int cNext = c + dir[1];
if (inGrid(rNext, cNext) && !visited[rNext][cNext] && grid[rNext][cNext] == '1') {
dfs(rNext, cNext);
}
}
}
int numIslands(vector<vector<char>> &grid) {
R = grid.size();
C = grid[0].size();
this->grid = grid;
int ccCnt = 0;
// 初始化访问数组
for (int i = 0; i < R; i++) visited.emplace_back(C, false); // 初始化访问数组都为false
for (int r = 0; r < R; r++) {
for (int c = 0; c < C; c++) {
if (!visited[r][c] && grid[r][c] == '1') {
dfs(r, c);
ccCnt++;
}
}
}
return ccCnt;
}
};
8.1219.黄金矿工
DFS求最大值,很简单
你要开发一座金矿,地质勘测学家已经探明了这座金矿中的资源分布,并用大小为 m * n 的网格 grid 进行了标注。每个单元格中的整数就表示这一单元格中的黄金数量;如果该单元格是空的,那么就是 0。
为了使收益最大化,矿工需要按以下规则来开采黄金:
每当矿工进入一个单元,就会收集该单元格中的所有黄金。
矿工每次可以从当前位置向上下左右四个方向走。
每个单元格只能被开采(进入)一次。
不得开采(进入)黄金数目为 0 的单元格。
矿工可以从网格中 任意一个 有黄金的单元格出发或者是停止。
class Solution {
int R, C;
int[][] grid;
boolean[][] visited;
int res = 0; // 黄金的最大值
final int[][] dirs = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
public int getMaximumGold(int[][] grid) {
this.R = grid.length;
this.C = grid[0].length;
this.grid = grid;
for (int r = 0; r < R; r++) {
for (int c = 0; c < C; c++) {
if (grid[r][c] != 0) {
visited = new boolean[R][C];
dfs(r, c, grid[r][c]); // 每次DFS都刷新最大可以得到的黄金量
}
}
}
return res;
}
private void dfs(int rCur, int cCur, int goldTotal) {
visited[rCur][cCur] = true;
res = Math.max(res, goldTotal); // 一共的黄金量去更新最大的黄金量
for (int[] dir : dirs) {
int rNext = rCur + dir[0];
int cNext = cCur + dir[1];
if (inGrid(rNext, cNext) && !visited[rNext][cNext] && grid[rNext][cNext] != 0) {
dfs(rNext, cNext, goldTotal + grid[rNext][cNext]);
visited[rNext][cNext] = false;
}
}
}
private boolean inGrid(int r, int c) {
return r >= 0 && r < R && c >= 0 && c < C;
}
}
C++实现
class Solution {
public:
int R{0}; // 行
int C{0}; // 列
int res{0}; // 最大黄金量
int dirs[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 上下左右四个方向
vector<vector<bool>> visited;
vector<vector<int>> grid;
bool inGrid(int r, int c) {
return r >= 0 && r < R && c >= 0 && c < C;
}
int getMaximumGold(vector<vector<int>>& grid) {
R = grid.size();
C = grid[0].size();
this->grid = grid;
for (int r = 0; r < R; r++) {
for (int c = 0; c < C; c++) {
if (grid[r][c] != 0) {
visited.clear();
for (int i = 0; i < R; i++) visited.emplace_back(C, false); // 初始化访问数组都为false
dfs(r, c, grid[r][c]); // 每次DFS都刷新最大可以得到的黄金量
}
}
}
return res;
}
void dfs(int rCur, int cCur, int goldTotal) {
visited[rCur][cCur] = true;
res = max(res, goldTotal); // 一共的黄金量去更新最大的黄金量
for (auto& dir : dirs) {
int rNext = rCur + dir[0];
int cNext = cCur + dir[1];
if (inGrid(rNext, cNext) && !visited[rNext][cNext] && grid[rNext][cNext] != 0) {
dfs(rNext, cNext, goldTotal + grid[rNext][cNext]);
visited[rNext][cNext] = false;
}
}
}
};
9.505.迷宫II
这个题用DFS肯定会超时,见自己的DFS超时实现:https://leetcode-cn.com/submissions/detail/110787824/
此外,此题可以得到如下经验:
- 1.求最小值时,在邻接状态确定的情况下,优先选BFS,如LeetCode 505;在邻接状态不确定的情况下(随着到的点不同会动态变化),可是使用DFS找到所有达到目标点的情况,然后取这些方案中的最小值,如AcWing 1118.分成互质组 和 AcWing 165.小猫爬山
- 2.这个题目的特殊情况,一个点可以经过多次,不断更新某个点的最小值,尤其是终点,千万不要一到终点就提前退出!!
由空地和墙组成的迷宫中有一个球。球可以向上下左右四个方向滚动,但在遇到墙壁前不会停止滚动。当球停下时,可以选择下一个方向。
给定球的起始位置,目的地和迷宫,找出让球停在目的地的最短距离。距离的定义是球从起始位置(不包括)到目的地(包括)经过的空地个数。如果球无法停在目的地,返回 -1。
迷宫由一个0和1的二维数组表示。 1表示墙壁,0表示空地。你可以假定迷宫的边缘都是墙壁。起始位置和目的地的坐标通过行号和列号给出。
示例 1:
输入 1: 迷宫由以下二维数组表示
0 0 1 0 0
0 0 0 0 0
0 0 0 1 0
1 1 0 1 1
0 0 0 0 0
输入 2: 起始位置坐标 (rowStart, colStart) = (0, 4)
输入 3: 目的地坐标 (rowDest, colDest) = (4, 4)
输出: 12
解析: 一条最短路径 : left -> down -> left -> down -> right -> down -> right。 总距离为 1 + 1 + 3 + 1 + 2 + 2 + 2 = 12。
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Queue;
class Solution {
int[][] grid;
int R, C;
final int[][] dirs = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
// 当前位置必须在栅格内,而且必须不是铁壁,才能继续往下走
private boolean inGrid(int r, int c) {
return r >= 0 && r < R && c >= 0 && c < C;
}
// BFS原生就是求最短距离的,这里更好一些
private int bfs(int rStart, int cStart, int rEnd, int cEnd) {
int[][] dis = new int[R][C]; // dis[r]][c]表示位置[r, c]的点到起点的距离
for (int i = 0; i < R; i++) {
Arrays.fill(dis[i], Integer.MAX_VALUE); // 要求最小值,就要初始化为最大值,因为每个点可能会更新多次
}
Queue<Integer> rQueue = new ArrayDeque<>();
Queue<Integer> cQueue = new ArrayDeque<>();
rQueue.add(rStart);
cQueue.add(cStart);
dis[rStart][cStart] = 0; // 起点距离起点的步数为0
while (!rQueue.isEmpty() && !cQueue.isEmpty()) {
int rCur = rQueue.remove();
int cCur = cQueue.remove();
// if (rCur == rEnd && cCur == cEnd) return dis[rEnd][cEnd]; // 可能会有多种方案都能经过重点,所以这里一定不要提前退出!!
for (int[] dir : dirs) {
int rNext = rCur + dir[0];
int cNext = cCur + dir[1];
int stepDis = 0;
while (inGrid(rNext, cNext) && grid[rNext][cNext] == 0) { // 下一个方向是能走的条件:在栅格内且是空地
// 计算按照dir方向滚动最终能停在哪个点
// 2.尝试继续往下一个位置走
rNext = rNext + dir[0];
cNext = cNext + dir[1];
// 3.距离 + 1
stepDis++;
}
// 上面的while结束,[rNextFinal, cNextFinal]存储地是最后一个合法的位置的下一个位置,所以要再减回去,才是滚动后停下来的位置
rNext = rNext - dir[0];
cNext = cNext - dir[1];
// 注意在一般的BFS中,我们不会经过同一个点超过一次(用visited数组来控制),但是在这道题目中,只要从起始位置大当前位置的步数
// 小于之前走法得到的最小步数dis[rNext][cNext],我们就会把点(rNext, cNext)点加入到队列中,再次进行BFS
if (dis[rCur][cCur] + stepDis < dis[rNext][cNext]) {
dis[rNext][cNext] = dis[rCur][cCur] + stepDis;
rQueue.add(rNext);
cQueue.add(cNext);
}
}
}
return dis[rEnd][cEnd] == Integer.MAX_VALUE ? -1 : dis[rEnd][cEnd];
}
public int shortestDistance(int[][] maze, int[] start, int[] destination) {
R = maze.length;
C = maze[0].length;
grid = maze;
return bfs(start[0], start[1], destination[0], destination[1]);
}
/**
* [0,4] [4,4] true
* [4,3] [0,1] false
* [0,4] [1,2] true
*/
public static void main(String[] args) {
int[][] maze = {{0, 0, 1, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 1, 0}, {1, 1, 0, 1, 1}, {0, 0, 0, 0, 0}};
int[] start = {0, 4};
int[] destination = {4, 4};
System.out.println(new Solution().shortestDistance(maze, start, destination));
}
}
C++实现
#include <cmath>
#define MAX_VALUE 0x3f3f
class Solution {
public:
int R; // 行
int C; // 列
int dirs[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 上下左右四个方向
vector<vector<int>> grid;
bool inGrid(int r, int c) {
return r >= 0 && r < R && c >= 0 && c < C;
}
int bfs(int rStart, int cStart, int rEnd, int cEnd) {
int dis[R][C]; // dis[r][c]表示位置[r, c]到起点的局点
for (int r = 0; r < R; r++) {
for (int c = 0; c < C; c++) {
dis[r][c] = MAX_VALUE; // 要求最小值,就要初始化为最大值,因为每个点可能会更新多次
}
}
queue<int> rQueue;
queue<int> cQueue;
rQueue.push(rStart);
cQueue.push(cStart);
dis[rStart][cStart] = 0; // 起点距离起点的步数为0
while(!rQueue.empty() && !cQueue.empty()) {
int rCur = rQueue.front();
rQueue.pop();
int cCur = cQueue.front();
cQueue.pop();
for (auto& dir : dirs) {
int rNext = rCur + dir[0];
int cNext = cCur + dir[1];
int stepDis = 0;
while (inGrid(rNext, cNext) && grid[rNext][cNext] == 0) { // 下一个方向是能走的条件:在栅格内且是空地
// 计算按照dir方向滚动最终能停在哪个点
// 2.尝试继续往下一个位置走
rNext = rNext + dir[0];
cNext = cNext + dir[1];
// 3.距离 + 1
stepDis++;
}
// 上面的while结束,[rNextFinal, cNextFinal]存储地是最后一个合法位置的下一个位置,所以要再减回去,才是滚动后停下来地位置
rNext = rNext - dir[0];
cNext = cNext - dir[1];
// 注意在一般地BFS中,我们不会经过同一个点超过一次(用visited数组来控制),但是在这道题目中,只要从起始位置到当前位置的步数
// 小于之前走法得到的最小步数dis[rNext][cNext],我们就会把点(rNext, cNext)点加入到队列中,再次进行BFS
if (dis[rCur][cCur] + stepDis < dis[rNext][cNext]) {
dis[rNext][cNext] = dis[rCur][cCur] + stepDis;
rQueue.push(rNext);
cQueue.push(cNext);
}
}
}
return dis[rEnd][cEnd] == MAX_VALUE ? -1 : dis[rEnd][cEnd];
}
int shortestDistance(vector<vector<int>>& maze, vector<int>& start, vector<int>& dest) {
R = maze.size();
C = maze[0].size();
grid = maze;
return bfs(start[0], start[1], dest[0], dest[1]);
}
};
10.51.N皇后
也是AcWing 843,规则是
任意两个皇后都不能处于同一行、同一列或同一斜线上
,把check函数写好,剩下地就是不断尝试放皇后了
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
上图为 8 皇后问题的一种解法。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
import java.util.ArrayList;
import java.util.List;
class Solution {
int n;
boolean[][] visited; // 节点访问数组
List<List<String>> res;
/**
* 检查位置(r, c)处放置皇后是否和之前放地冲突
*/
private boolean check(int r, int c, List<int[]> points) {
for (int[] point : points) {
// 同一行 or 同一列则不能放,直接返回
if (point[0] == r || point[1] == c) return false;
// 斜率为1表明在一条斜线上,直接返回,不和上面的条件合并是为了防止point[1] - c值为0
double k = Math.abs((point[0] - r) * 1.0 / (point[1] - c)); // 注意斜率为负数的情况
if (k == 1.0) return false;
}
return true;
}
private void dfs(int r, int c, List<int[]> points) {
visited[r][c] = true;
points.add(new int[]{r, c});
if (points.size() == n) { // 找到了一个合适的放置方案
List<String> solution = new ArrayList<>();
for (int i = 0; i < n; i++) {
StringBuilder sb = new StringBuilder();
for (int j = 0; j < n; j++) {
if (visited[i][j]) sb.append("Q");
else sb.append(".");
}
solution.add(sb.toString());
}
res.add(solution);
return;
}
// 上面的点满足条件了,则下一个必须从下一行开始了
if (r + 1 < n) { // 行必须在合适的范围
for (int i = 0; i < n; i++) { // 固定行,遍历列
if (!visited[r + 1][i] && check(r + 1, i, points)) {
dfs(r + 1, i, points);
visited[r + 1][i] = false;
points.remove(points.size() - 1);
}
}
}
}
public List<List<String>> solveNQueens(int n) {
this.n = n;
res = new ArrayList<>();
for (int i = 0; i < n; i++) { // 第一个起始的元素肯定是在第1行
List<int[]> points = new ArrayList<>(); // 记录皇后放置的位置
visited = new boolean[n][n]; // 每次开始的位置不一样,所以要重置访问数组
dfs(0, i, points);
}
return res;
}
}
C++实现
class Solution {
public:
int n;
vector<vector<bool>> visited;
vector<vector<string>> res;
bool check(int r, int c, vector<vector<int>> points) {
for (auto& point : points) {
// 同一行 or 同一列 则不能放,直接返回
if (point[0] == r || point[1] == c) return false;
// 斜率为1表示在一条斜线上,直接返回,不和上面的条件合并是为了 point[1]-c 值为0
double k = abs((point[0] - r) * 1.0 / (point[1] - c));
if (k == 1.0) return false;
}
return true;
}
void dfs(int r, int c, vector<vector<int>> points) {
visited[r][c] = true;
points.push_back({r, c});
if (points.size() == n) { // 找到了一个合适的放置方案
vector<string> solution;
for (int i = 0; i < n; i++) {
string sb;
for (int j = 0; j < n; j++) {
if (visited[i][j]) sb += "Q";
else sb += ".";
}
solution.push_back(sb);
}
res.push_back(solution);
return;
}
// 上面的点满足条件了,则下一个必须从下一行开始了
if (r + 1 < n) { // 行必须在合适的范围
for (int i = 0; i < n; i++) { // 固定行,遍历列
if (!visited[r + 1][i] && check(r + 1, i, points)) {
dfs(r + 1, i, points);
visited[r + 1][i] = false;
points.pop_back(); // 回溯法,删除最后一个元素
}
}
}
}
vector<vector<string>> solveNQueens(int n) {
this->n = n;
for (int i = 0; i < n; i++) {
vector<vector<int>> points;
visited.clear(); // 清理之前的访问状态
for (int i = 0; i < n; i++) visited.emplace_back(vector<bool>(n, false)); // 初始化为false
dfs(0, i, points);
}
return res;
}
};
11.22.括号生成
栈 + 暴力DFS
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例:
输入:n = 3
输出:["((()))", "(()())", "(())()", "()(())", "()()()"]
import java.lang.reflect.Array;
import java.util.*;
class Solution {
int n;
List<String> res;
// 校验字符集合是否符合括号匹配的规则
boolean check(List<Character> chs) {
Stack<Character> st = new Stack<>();
int left = n;
int right = n;
for (char c : chs) {
if (c == '(') {
left--;
st.push(c);
}
if (c == ')') {
right--;
if (st.isEmpty()) return false;
st.pop();
}
}
return st.isEmpty() && left == 0 && right == 0;
}
public List<String> generateParenthesis(int n) {
this.n = n;
this.res = new ArrayList<>();
List<Character> chs = new ArrayList<>();
dfs(chs);
return res;
}
private void dfs (List<Character> chs) {
if (chs.size() == n * 2) {
if (!check(chs)) return;
StringBuilder sb = new StringBuilder();
for (Character ch : chs) {
sb.append(ch);
}
res.add(sb.toString());
return;
}
// 1.尝试放(
chs.add('(');
dfs(chs);
chs.remove(chs.size() - 1);
// 2.尝试放)
chs.add(')');
dfs(chs);
chs.remove(chs.size() - 1);
}
}
class Solution {
public:
int n;
vector<string> res;
// 检验字符集合是否符合括号匹配的原则
bool check(vector<char> chs) {
stack<char> st;
int left = n;
int right = n;
for (char c : chs) {
if (c == '(') {
left--;
st.push(c);
}
if (c == ')') {
right--;
if (st.empty()) return false;
st.pop();
}
}
return st.empty() && left == 0 && right == 0;
}
vector<string> generateParenthesis(int n) {
this->n = n;
vector<char> chs;
dfs(chs);
return res;
}
void dfs(vector<char> &chs) {
if (chs.size() == n * 2) {
if (!check(chs)) return;
string sb;
for (char c : chs) sb += c;
res.push_back(sb);
return;
}
// 1.尝试放(
chs.push_back('(');
dfs(chs);
chs.pop_back();
// 2.尝试放)
chs.push_back(')');
dfs(chs);
chs.pop_back();
}
};
12.207.课程表
拓扑排序,知识点见Part2BasicGraph/第13章_有向图及相关算法
你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]
给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?
示例 1:
输入: 2, [[1,0]]
输出: true
解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。
示例 2:
输入: 2, [[1,0],[0,1]]
输出: false
解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。
class Solution {
/**
* 拓扑排序结果
*/
private List<Integer> topoOrder;
/**
* 当前图是否有环
*/
private boolean hasCycle = false;
/**
* 所有顶点的入度
*/
private int[] inDegreesG;
/**
* 根据先决条件构造邻接矩阵,在DFS过程中判断是否存在环,存在环则不能修完所有课程
*/
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 不能为null
if (prerequisites == null || prerequisites.length ==0 || prerequisites[0].length == 0) {
return true;
}
// 记录学完一个课程后下面可以学的课程列表,即邻接表
Map<Integer, List<Integer>> mapAdj = new HashMap<>();
// 每个点的入度
inDegreesG = new int[numCourses];
for (int[] prerequisite : prerequisites) {
List<Integer> adj;
if (mapAdj.get(prerequisite[1]) == null) {
// 不存在这个键的邻接表则要新建
adj = new ArrayList<>();
// 把新建的列表加进到map中
mapAdj.put(prerequisite[1], adj);
} else {
// 之前存在这个键的邻接表,直接get
adj = mapAdj.get(prerequisite[1]);
}
// prerequisite[1] ==> prerequisite[0]
adj.add(prerequisite[0]);
inDegreesG[prerequisite[0]]++;
}
topoOrder = new ArrayList<>();
sort(numCourses, mapAdj);
// 没有环说明可以修完所有课程
return !hasCycle;
}
/**
* 拓扑排序核心
*/
public void sort(int numCourses, Map<Integer, List<Integer>> mapAdj) {
// 存储还未排序的入度为0的顶点
Queue<Integer> queue = new ArrayDeque<>();
int[] inDegrees = new int[numCourses];
for (int v = 0; v < numCourses; v++) {
inDegrees[v] = inDegreesG[v];
if (inDegrees[v] == 0) {
queue.add(v);
}
}
while (!queue.isEmpty()) {
int cur = queue.remove();
topoOrder.add(cur);
if (mapAdj.get(cur)!=null){
for (int next : mapAdj.get(cur)) {
// 更新cur点的邻接点的入度
inDegrees[next]--;
if (inDegrees[next] == 0) {
// 更新后入度为0的顶点加入到queue中
queue.add(next);
}
}
}
}
if (topoOrder.size() != numCourses) {
// 找不到入度为0的点但是还有点没被删除进行拓扑排序,说明图中有环
hasCycle = true;
// 没法进行拓扑排序就把已经加入的顶点清理掉
topoOrder.clear();
}
}
}
C++实现
class Solution {
public:
vector<int> topoOrder; // 存储拓扑排序的结果
bool hasCycle = false; // 默认起始图没有环
map<int, int> inDegreesG; // 所有顶点的入度
// 根据先决条件构造邻接矩阵,在DFS过程中判断是否存在环,存在环则不能修完所有课程
bool canFinish(int numCourses, vector<vector<int>> &prerequisites) {
map<int, vector<int>> mapAdj; // 记录学完一个课程后下面可以学的课程列表,即邻接表
for (auto &prerequisite : prerequisites) {
if (!mapAdj.count(prerequisite[1])) { // 不包含这个键,那么
vector<int> adj;
mapAdj[prerequisite[1]] = adj; // 不存在这个键那么要新建了
}
// prerequisite[1] ===> prerequisite[0]
mapAdj[prerequisite[1]].push_back(prerequisite[0]);
inDegreesG[prerequisite[0]]++;
}
topoSort(numCourses, mapAdj);
// 没有环说明可以修完所有课程
return !hasCycle;
}
// 拓扑排序核心
void topoSort(int numCourses, map<int, vector<int>> &mapAdj) {
// 存储还未排序的入度为0的顶点
queue<int> q;
int inDegrees[numCourses];
for (int v = 0; v < numCourses; v++) {
inDegrees[v] = inDegreesG[v];
if (inDegrees[v] == 0) q.push(v);
}
while (!q.empty()) {
int cur = q.front();
q.pop();
topoOrder.push_back(cur);
if (mapAdj.count(cur)) { // 含有当前的点才进行下一步
for (int next : mapAdj[cur]) {
// 更新cur点的邻接点的入度
inDegrees[next]--;
if (inDegrees[next] == 0) q.push(next); // 更新后入度为0的点加入到queue中
}
}
}
if (topoOrder.size() != numCourses) {
// 找不到入度为0的点但是还有点没被删除,说明图中有环
hasCycle = true;
topoOrder.clear();
}
}
};
13.562.矩阵中最长的连续1线段
暴力也能过,唉,计算机每秒执行10^7条指令,只要估算在题目限制时间内可以暴力过,就直接暴力!!哈哈
给定一个01矩阵 M,找到矩阵中最长的连续1线段。这条线段可以是水平的、垂直的、对角线的或者反对角线的。
示例:
输入:
[[0,1,1,0],
[0,1,1,0],
[0,0,0,1]]
输出: 3
// 考虑用例:
// [[1,1,1,1],[0,1,1,0],[0,0,0,1]]
// [[1,1,0,0,1,0,0,1,1,0],[1,0,0,1,0,1,1,1,1,1],[1,1,1,0,0,1,1,1,1,0],[0,1,1,1,0,1,1,1,1,1],[0,0,1,1,1,1,1,1,1,0],[1,1,1,1,1,1,0,1,1,1],[0,1,1,1,1,1,1,0,0,1],[1,1,1,1,1,0,0,1,1,1],[0,1,0,1,1,0,1,1,1,1],[1,1,1,0,1,0,1,1,1,1]]
class Solution {
int R, C;
int[][] grid;
// 计算[r, c]点所在的水平、垂直、对角线的最长连续1线段
private int getMax1Len(int r, int c) {
int max = 0;
// 1.判断水平方向
int len = 1;
for (int i = c + 1; i < C; i++) { // 只往右边找
if (grid[r][i] == 1) len++;
else break;
}
max = Math.max(max, len);
// 2.判断垂直方向
len = 1;
for (int i = r + 1; i < R; i++) {
if (grid[i][c] == 1) len++;
else break;
}
max = Math.max(max, len);
// 3.判断正对角线
len = 1;
for (int i = r + 1, j = c + 1; i < R && j < C; i++, j++) {
if (grid[i][j] == 1) len++;
else break;
}
max = Math.max(max, len);
len = 1;
// 4.判断反对角线
for (int i = r + 1, j = c - 1; i < R && j >= 0; i++, j--) {
if (grid[i][j] == 1) len++;
else break;
}
max = Math.max(max, len);
return max;
}
public int longestLine(int[][] M) {
if (M.length == 0 || M[0].length == 0) return 0;
R = M.length;
C = M[0].length;
grid = M;
int res = 0; // 求最大值,则初始化为最小值
for (int r = 0; r < R; r++) {
for (int c = 0; c < C; c++) {
if (grid[r][c] != 1) continue;
res = Math.max(res, getMax1Len(r, c));
}
}
return res;
}
public static void main(String[] args) {
int[][] M = {
{1, 1, 0, 0, 1, 0, 0, 1, 1, 0},
{1, 0, 0, 1, 0, 1, 1, 1, 1, 1},
{1, 1, 1, 0, 0, 1, 1, 1, 1, 0},
{0, 1, 1, 1, 0, 1, 1, 1, 1, 1},
{0, 0, 1, 1, 1, 1, 1, 1, 1, 0},
{1, 1, 1, 1, 1, 1, 0, 1, 1, 1},
{0, 1, 1, 1, 1, 1, 1, 0, 0, 1},
{1, 1, 1, 1, 1, 0, 0, 1, 1, 1},
{0, 1, 0, 1, 1, 0, 1, 1, 1, 1},
{1, 1, 1, 0, 1, 0, 1, 1, 1, 1}
};
new Solution().longestLine(M);
}
}
C++实现
class Solution {
public:
int R, C;
vector<vector<int>> grid;
// 计算[r, c]点所在的上水平、垂直、对角线、反对角线地最长连续1线段
int getMax1Len(int r, int c) {
int result = 0;
// 1.判断水平方向
int len = 1;
for (int i = c + 1; i < C; i++) { // 只向右侧找
if (grid[r][i] == 1) len++;
else break;
}
result = max(result, len);
// 2.判断垂直方向
len = 1;
for (int i = r + 1; i < R; i++) {
if (grid[i][c] == 1) len++;
else break;
}
result = max(result, len);
// 3.判断正对角线
len = 1;
for (int i = r + 1, j = c + 1; i < R && j < C; i++, j++) {
if (grid[i][j] == 1) len++;
else break;
}
result = max(result, len);
// 4.判断反对角线
len = 1;
for (int i = r + 1, j = c - 1; i < R && j >= 0; i++, j--) {
if (grid[i][j] == 1) len++;
else break;
}
result = max(result, len);
return result;
}
int longestLine(vector<vector<int>>& M) {
if (M.size() == 0 || M[0].size() == 0) return 0;
R = M.size();
C = M[0].size();
grid = M;
int res = 0; // 求最大值,则初始化最小值
for (int r = 0; r < R; r++) {
for (int c = 0; c < C; c++) {
if (grid[r][c] != 1) continue;
res = max(res, getMax1Len(r, c));
}
}
return res;
}
};
14.1477.找两个和为目标值且不重叠的子数组
前缀和 + 滑动窗口,类似上面的LeetCode 325.和为K的最长子数组长度
直接用暴力法,也过了
53/60
个用例,还行,后面再优化下
给你一个整数数组 arr 和一个整数值 target 。
请你在 arr 中找 两个互不重叠的子数组 且它们的和都等于 target 。可能会有多种方案,请你返回满足要求的两个子数组长度和的 最小值 。
请返回满足要求的最小长度和,如果无法找到这样的两个子数组,请返回 -1 。
class Solution {
// 前缀和 + 哈希,类似325.和为K的最长子数组长度
public int minSumOfLengths(int[] arr, int target) {
int[] s = new int[arr.length + 1]; // 前缀和数组
int[] a = new int[arr.length + 1]; // 数组arr统一往后移动一位
for (int i = 1; i <= arr.length; i++) {
a[i] = arr[i - 1];
s[i] = s[i - 1] + a[i];
}
List<int[]> intervalList = new ArrayList<>();
// 暴力吧,改成滑动窗口兴许能过(所有元素都是正数,right++和增加,left--和减少,因此可以用滑动窗口)
for (int i = 1; i < a.length; i++) {
for (int j = i; j < a.length; j++) {
if (s[j] - s[i - 1] != target) continue; // 遍历下一个区间
intervalList.add(new int[]{i, j});
}
}
// 再暴力遍历所有满足条件的集合
int res = Integer.MAX_VALUE;
for (int i = 0; i < intervalList.size(); i++) {
for (int j = i + 1; j < intervalList.size(); j++) {
if (notIntersect(intervalList.get(i), intervalList.get(j))) {
res = Math.min(res, getIntervalLenSum(intervalList.get(i), intervalList.get(j)));
}
}
}
return res == Integer.MAX_VALUE ? -1 : res;
}
// 判断两个区间是否不相交
private boolean notIntersect(int[] intervalLeft, int[] intervalRight) {
return intervalLeft[1] < intervalRight[0]; // 因为是闭区间,所以必须严格小于,区间才不会相交
}
// 获取两个区间的长度和
private int getIntervalLenSum(int[] intervalLeft, int[] intervalRight) {
return intervalLeft[1] - intervalLeft[0] + 1 + intervalRight[1] - intervalRight[0] + 1;
}
}
把暴力改成滑动窗口,又能多过4个用例 57/60
class Solution {
// 前缀和 + 哈希,类似325.和为K的最长子数组长度
public int minSumOfLengths(int[] arr, int target) {
int[] s = new int[arr.length + 1]; // 前缀和数组
int[] a = new int[arr.length + 1]; // 数组arr统一往后移动一位
for (int i = 1; i <= arr.length; i++) {
a[i] = arr[i - 1];
s[i] = s[i - 1] + a[i];
}
List<int[]> intervalList = new ArrayList<>();
// 暴力求所有合为target的子区间,改成滑动窗口兴许能过(所有元素都是正数,right++和增加,left--和减少,因此可以用滑动窗口)
int left = 0, right = 1;
while (right < a.length) {
if (s[right] - s[left] == target) {
intervalList.add(new int[]{left, right}); // 对于arr来说,区间是[left, right),左闭右开区间
}
while (s[right] - s[left] > target) {
left++;
if (s[right] - s[left] == target) {
intervalList.add(new int[]{left, right}); // 对于arr来说,区间是[left, right),左闭右开区间
}
}
right++;
}
// 再暴力遍历所有满足条件的集合
int res = Integer.MAX_VALUE;
for (int i = 0; i < intervalList.size(); i++) {
int[] iInterval = intervalList.get(i);
if (iInterval[1] - iInterval[0] >= res) continue; // 单个元素长度超出限制就提前退出
for (int j = i + 1; j < intervalList.size(); j++) {
int[] jInterval = intervalList.get(j);
if (iInterval[1] <= jInterval[0]) { // 两个区间不相交
res = Math.min(res, getIntervalLenSum(iInterval, jInterval));
}
}
}
return res == Integer.MAX_VALUE ? -1 : res;
}
// 获取两个区间的长度和
private int getIntervalLenSum(int[] intervalLeft, int[] intervalRight) {
return intervalLeft[1] - intervalLeft[0] + intervalRight[1] - intervalRight[0];
}
}
把前缀和去掉,直接在滑动窗口中维护sum,又多过了1个用例 58/60
class Solution {
// 前缀和 + 哈希,类似325.和为K的最长子数组长度
public int minSumOfLengths(int[] arr, int target) {
List<int[]> intervalList = new ArrayList<>(); // 存储所有和为target的区间左右端点
// 暴力求所有合为target的子区间,改成滑动窗口兴许能过(所有元素都是正数,right++和增加,left--和减少,因此可以用滑动窗口)
int left = 0, right = 0, sum = 0;
while (right < arr.length) { // 注意滑动窗口都是增删元素,然后再移动left或right指针
sum += arr[right]; // 右侧移入新的元素
right++;
if (sum == target) {
intervalList.add(new int[]{left, right}); // 对于arr来说,区间是[left, right),左闭右开区间
}
while (sum > target) {
sum -= arr[left]; // 左侧移走一个元素
left++;
if (sum == target) { // 不断的减也可能减
intervalList.add(new int[]{left, right}); // 对于arr来说,区间是[left, right),左闭右开区间
}
}
}
// 再暴力遍历所有满足条件的集合
int res = Integer.MAX_VALUE;
for (int i = 0; i < intervalList.size(); i++) {
int[] iInterval = intervalList.get(i);
if (iInterval[1] - iInterval[0] >= res) continue; // 单个元素长度超出限制就提前退出
for (int j = i + 1; j < intervalList.size(); j++) {
int[] jInterval = intervalList.get(j);
if (iInterval[1] <= jInterval[0]) { // 两个区间不相交
res = Math.min(res, getIntervalLenSum(iInterval, jInterval));
}
}
}
return res == Integer.MAX_VALUE ? -1 : res;
}
// 获取两个区间的长度和
private int getIntervalLenSum(int[] intervalLeft, int[] intervalRight) {
return intervalLeft[1] - intervalLeft[0] + intervalRight[1] - intervalRight[0];
}
}
对得到的和为target的区间进行排序,这样一旦满足条件的res,后面的都不需要遍历了,可以提前break,性能大大提高
import java.util.*;
// 参考评论区:https://leetcode-cn.com/problems/find-two-non-overlapping-sub-arrays-each-with-target-sum/solution/find-two-by-ikaruga/
class Solution {
// 前缀和 + 哈希,类似325.和为K的最长子数组长度
public int minSumOfLengths(int[] arr, int target) {
List<int[]> intervalList = new ArrayList<>();
// 完全按照labuladong总结的滑动窗口模板来做的:(所有元素都是正数,right++则sum增加,left++则sum减少,因此可以用滑动窗口)
int left = 0, right = 0, sum = 0;
while (right < arr.length) { // 注意滑动窗口都是增删元素,然后再移动left或right指针
sum += arr[right]; // 右侧移入新的元素
right++;
if (sum == target) {
intervalList.add(new int[]{right - left, left}); // 对于arr来说,区间是[left, right),左闭右开区间,所以区间长度正好是right - left
}
while (sum > target) {
sum -= arr[left]; // 左侧移走一个元素
left++;
if (sum == target) { // 不断的减也可能减到目标值
intervalList.add(new int[]{right - left, left}); // 对于arr来说,区间是[left, right),左闭右开区间,所以区间长度正好是right - left
}
}
}
intervalList.sort(((o1, o2) -> o1[0] - o2[0])); // 按照区间长度排序,这个用法很好,我们不用单独建PII类了
// 再暴力遍历所有满足条件的集合
int res = Integer.MAX_VALUE;
for (int i = 0; i < intervalList.size(); i++) {
int[] interval1 = intervalList.get(i);
if (interval1[0] * 2 >= res) break;// ans是两个长度之和,如果遍历到有超过这个长度的,后面的肯定更大了,就无需遍历了
for (int j = i + 1; j < intervalList.size(); j++) {
int[] interval2 = intervalList.get(j); // 获得另一个满足条件的区间
if (interval1[1] < interval2[1] && interval1[1] + interval1[0] > interval2[1]) continue; // 区间1在左,和区间2相交
if (interval2[1] < interval1[1] && interval2[1] + interval2[0] > interval1[1]) continue; // 区间2在左,和区间1相交
res = Math.min(res, interval1[0] + interval2[0]); // 区间不相交才可以更新区间长度之和
break; // 长度经过排序之后,后面的区间长度肯定更大,即一定比res大了,因此不需要遍历了
}
}
return res == Integer.MAX_VALUE ? -1 : res;
}
}
C++实现
class Solution {
public:
int minSumOfLengths(vector<int> &arr, int target) {
int INT_M = 0x3f3f3f;
vector<vector<int>> intervalList; // 存储区间左右端点对象的列表
// 完全按照labuladong总结的滑动窗口模板来做地:所有元素都是正数,right++则sum增加,left++则sum减少,因此可以用滑动窗口
int left = 0, right = 0, sum = 0;
while (right < arr.size()) {
sum += arr[right]; // 右侧移入新的元素
right++;
if (sum == target) {
// 对于arr来说,区间是[left, right),左闭右开区间,所以区间长度刚好是right - left
intervalList.push_back({right - left, left});
}
while (sum > target) {
sum -= arr[left]; // 左侧移走一个元素
left++; // 不断的减也可能减到目标值
// 对于arr来说,区间是[left, right),左闭右开区间,所以区间长度刚好是right - left
if (sum == target) intervalList.push_back({right - left, left});
}
}
// 自定义,按照区间长度排序
sort(intervalList.begin(), intervalList.end(), [](const auto &p1, const auto &p2) {
return p1[0] < p2[0];
});
// 再暴力遍历所有满足条件的集合
int res = INT_M;
for (int i = 0; i < intervalList.size(); i++) {
auto &interval1 = intervalList[i];
if (interval1[0] * 2 >= res) break; // ans是两个长度之和,如果遍历到有超过这个长度的,后面的肯定更大了,就无需遍历了
for (int j = i + 1; j < intervalList.size(); j++) {
auto &interval2 = intervalList[j]; // 获得另外一个
// 区间1在左,和区间2相交
if (interval1[1] < interval2[1] && interval1[1] + interval1[0] > interval2[1]) continue;
// 区间2在左,和区间1相交
if (interval2[1] < interval1[1] && interval2[1] + interval2[0] > interval1[1]) continue;
res = min(res, interval1[0] + interval2[0]); // 区间不相交才能更新区间长度之和
break; // 长度经过排序之后,后面的区间长度肯定更大,即一定比res大了,因此不需要遍历了
}
}
return res == INT_M ? -1 : res;
}
};
15.1405.最长快乐字符串
考察点 字符串, 贪心算法
如果字符串中不含有任何 'aaa','bbb' 或 'ccc' 这样的字符串作为子串,那么该字符串就是一个「快乐字符串」。
给你三个整数 a,b ,c,请你返回 任意一个 满足下列全部条件的字符串 s:
s 是一个尽可能长的快乐字符串。
s 中 最多 有a 个字母 'a'、b 个字母 'b'、c 个字母 'c' 。
s 中只含有 'a'、'b' 、'c' 三种字母。
如果不存在这样的字符串 s ,请返回一个空字符串 ""。
示例 1:
输入:a = 1, b = 1, c = 7
输出:"ccaccbcc"
解释:"ccbccacc" 也是一种正确答案。
示例 2:
输入:a = 2, b = 2, c = 1
输出:"aabbc"
示例 3:
输入:a = 7, b = 1, c = 0
输出:"aabaa"
解释:这是该测试用例的唯一正确答案。
先DFS暴力做了一下,过了1/3的用例
class Solution {
int aMax, bMax, cMax;
int aCnt = 0, bCnt = 0, cCnt = 0;
String result = "";
private void dfs(String cur, List<String> list) {
if (cur.equals("a")) aCnt++;
if (cur.equals("b")) bCnt++;
if (cur.equals("c")) cCnt++;
list.add(cur);
String str = String.join("", list);
if (aCnt > aMax || bCnt > bMax || cCnt > cMax ||
str.contains("aaa") || str.contains("bbb") || str.contains("ccc")) {
return; // 一旦不满足,直接退出
}
if (str.length() > result.length()) result = str; // 更新最大值
dfs("a", list);
list.remove(list.size() - 1);
aCnt--;
dfs("b", list);
list.remove(list.size() - 1);
bCnt--;
dfs("c", list);
list.remove(list.size() - 1);
cCnt--;
}
public String longestDiverseString(int a, int b, int c) {
aMax = a;
bMax = b;
cMax = c;
// 先暴力
List<String> cList = new ArrayList<>();
dfs("", cList);
return result; // 得到一个最大值即可
}
}
贪心:每轮放置字符时优先先放剩余次数最多的, 如果上次放的2个字符和剩余个数最多的字符相同,则放置次多的字符
import java.util.Arrays;
class Solution {
class PII {
char c;
int cnt;
public PII(char c, int cnt) {
this.c = c;
this.cnt = cnt;
}
}
public String longestDiverseString(int a, int b, int c) {
PII[] piis = new PII[3];
piis[0] = new PII('a', a);
piis[1] = new PII('b', b);
piis[2] = new PII('c', c);
StringBuilder sb = new StringBuilder();
while (true) {
Arrays.sort(piis, (o1, o2) -> o2.cnt - o1.cnt); // 按照使用次数降序排序列,每次使用后都要重新排队
// 先放最多的,如果前面放的两个字符和剩余个数最多的字符相同,则防止次多的字符
if (sb.length() >= 2 && sb.charAt(sb.length() - 1) == piis[0].c && sb.charAt(sb.length() - 2) == piis[0].c) {
if (piis[1].cnt > 0) {
sb.append(piis[1].c);
piis[1].cnt--;
} else {
break;
}
} else {
if (piis[0].cnt > 0) {
sb.append(piis[0].c);
piis[0].cnt--;
} else {
break;
}
}
}
return sb.toString();
}
}
C++实现
class Solution {
public:
string longestDiverseString(int a, int b, int c) {
vector<pair<char, int>> piis(3);
piis[0] = make_pair('a', a);
piis[1] = make_pair('b', b);
piis[2] = make_pair('c', c);
string sb;
while (true) {
// 按照使用次数降序排列,每次使用后都要重新排队
sort(piis.begin(), piis.end(), [](const pair<char, int> &p1, const pair<char, int> &p2) {
return p2.second < p1.second;
});
// 先放最多的,如果前面放的两个字符和剩余个数最多的字符相同,则放置次多的字符
if (sb.length() >= 2 && sb[sb.length() - 1] == piis[0].first && sb[sb.length() - 2] == piis[0].first) {
if (piis[1].second > 0) {
sb += piis[1].first;
piis[1].second--;
} else {
break;
}
} else {
if (piis[0].second > 0) {
sb += piis[0].first;
piis[0].second--;
} else {
break;
}
}
}
return sb;
}
};
16.990.等式方程的可满足性
考察点:并查集/图,借助了自己总结的并查集类
给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:"a==b" 或 "a!=b"。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。
示例 1:
输入:["a==b","b!=a"]
输出:false
解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量同时满足这两个方程。
示例 2:
输入:["b==a","a==b"]
输出:true
解释:我们可以指定 a = 1 且 b = 1 以满足满足这两个方程。
示例 3:
输入:["a==b","b==c","a==c"]
输出:true
示例 4:
输入:["a==b","b!=c","c==a"]
输出:false
示例 5:
输入:["c==c","b==d","x!=z"]
输出:true
提示:
1 <= equations.length <= 500
equations[i].length == 4
equations[i][0] 和 equations[i][3] 是小写字母
equations[i][1] 要么是 '=',要么是 '!'
equations[i][2] 是 '='
class UnionFind {
/**
* 记录每个节点在联通分量中的父节点
*/
private int[] parent;
/**
* rank[i]表示节点i所在的联通分量树的层数/高度/深度
*/
private int[] rank;
public UnionFind(int size) {
this.parent = new int[size];
this.rank = new int[size];
for (int i = 0; i < parent.length; i++) {
// 初始化时每个顶点的父节点都认为是自己
parent[i] = i;
// 初始时所有元素都是互不相连地,所以每个元素都是一个并查集,每个并查集只有一个元素,也就是一层
rank[i] = 1;
}
}
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
// p和q在一个联通分量内,不需要union了,直接退出
return;
}
// 不在一个并查集内的话,只需要把两个根节点连接起来即可
// 第5节:根据层数优化。下面按照两个并并查集的层数(rank[i])的大小决定谁连接谁(层数少地连接层数多地)
if (rank[pRoot] < rank[qRoot]) { // p所在的并查集层数小于q所在的并查集层数,p指向q
// p所在的并查集连接q所在的并查集,rank[root]取两者中层数较大地,并不需要维护rank
parent[pRoot] = qRoot;
} else if (rank[pRoot] > rank[qRoot]) { // p所在的并查集层数大于q所在的并查集层数,q指向p
// p所在的并查集连接q所在的并查集,rank[root]取两者中层数较大地,并不需要维护rank
// q所在的并查集连接p所在的并查集
parent[qRoot] = pRoot;
} else { // p所在的并查集层数等于q所在的并查集层数,谁指向谁都行,这里选p指向q
//当 rank[pRoot] = rank[qRoot];
parent[pRoot] = qRoot;
// 两个层级相等的并查集树根节点相连,层数一定增长1,所以把新的并查集层数+1
rank[qRoot] += 1;
}
}
public int getSize() {
return parent.length;
}
/**
* 获取元素i所属的联通分量的根节点,因为是树,所以查找的时间复杂度是O(logn)
*
* @param i 元素,即parent数组的下标,用来唯一标识一个元素,即parent数组的下标既是索引又是元素
* @return i所属的联通分量的根节点
*/
private int find(int i) {
if (i < 0 || i >= parent.length) {
throw new IllegalArgumentException("传入的索引超出了数组范围!");
}
// 当i的父节点是自己时说明达到了根节点
while (parent[i] != i) {
// 第6节:路径压缩
parent[i] = parent[parent[i]];
i = parent[i];
}
return i;
}
}
class Solution {
public boolean equationsPossible(String[] equations) {
UnionFind uf = new UnionFind(equations.length * 2); // 最多的联通分量的个数
int cnt = 0;
Map<Character, Integer> map = new HashMap<>(); // 记录字符对应的整数映射(不用的字符用0~cnt来表示)
for (String equation : equations) {
char left = equation.charAt(0); // 左操作符
char right = equation.charAt(3); // 右操作符
if (map.get(left) == null) map.put(left, cnt++);
if (map.get(right) == null) map.put(right, cnt++);
String operator = equation.substring(1, 3); // 获取操作符
if (operator.equals("==")) uf.unionElements(map.get(left), map.get(right));
}
// 再判断所有不等式
for (String equation : equations) {
char left = equation.charAt(0); // 左操作符
char right = equation.charAt(3); // 右操作符
String operator = equation.substring(1, 3); // 获取操作符
if (operator.equals("!=")) {
if (uf.isConnected(map.get(left), map.get(right))) return false;
}
}
return true;
}
}
17.1129.颜色交替的最短路径
考察点:广度优先搜索/图
import java.util.*;
class Solution {
final int RED = 0;
final int BLUE = 1;
TreeMap<Integer, Integer>[] adj;
int[] dis;
int n;
int MAX;
public int[] shortestAlternatingPaths(int n, int[][] red_edges, int[][] blue_edges) {
this.n = n;
dis = new int[n]; // 结果数组
Arrays.fill(dis, Integer.MAX_VALUE / 2); // 求最小值,所以要初始化为最大值
adj = new TreeMap[n];
MAX = red_edges.length + blue_edges.length; // 最长的路径可能是多少,用于剪枝,防止自环
for (int i = 0; i < n; i++) {
adj[i] = new TreeMap<>();
}
for (int[] red_edge : red_edges) {
adj[red_edge[0]].put(red_edge[1], RED);
}
for (int[] blue_edge : blue_edges) {
adj[blue_edge[0]].put(blue_edge[1], BLUE);
}
dis[0] = 0; // 0到0的节点为0
// 类似求解二分图吧,需要把父遍历点传进去
dfs(0, 0); // 起点的parent认为就是自己
// 把所有的Integer.MAX_VALUE改成-1
for (int i = 0; i < n; i++) {
if (dis[i] == Integer.MAX_VALUE / 2) dis[i] = -1;
}
return dis;
}
private void dfs(int v, int parent) {
if (dis[v] > MAX) return; // 防止自环边或者平行边
for (int w : adj[v].keySet()) { // 获取v的所有邻接边
if (v == parent) { // parent是root地话,直接遍历所有root的邻接点都可以
if ((dis[v] + 1) < dis[w]) {
dis[w] = dis[v] + 1;
dfs(w, v);
}
} else {
if ((adj[parent].get(v) + adj[v].get(w)) == 1 && (dis[v] + 1) < dis[w]) { // 颜色必须交替
dis[w] = dis[v] + 1;
dfs(w, v);
dis[w] = dis[v] + 1;
}
}
}
}
public static void main(String[] args) {
int n = 3;
int[][] red_edges = {{0, 1}, {1, 2}};
int[][] blue_edges = {};
new Solution().shortestAlternatingPaths(n, red_edges, blue_edges);
}
}
参考的代码
class Solution {
public int[] shortestAlternatingPaths(int n, int[][] A, int[][] B) {
boolean graph[][][] = new boolean[2][n][n];
for(int t[]: A) {
graph[0][t[0]][t[1]] = true;
}
for(int t[]: B) {
graph[1][t[0]][t[1]] = true;
}
int distance1[] = new int[n], distance2[] = new int[n], res[] = new int[n], d = 1;
Queue <Integer> queue1 = new LinkedList<> (), queue2 = new LinkedList<> (), queue = queue2;
queue1.offer(0);
queue2.offer(0);
boolean flag = false, color = true;
while(!queue1.isEmpty() || !queue2.isEmpty()) {
queue = (queue == queue1) ? queue2 : queue1;
int size = queue.size();
for(int i = 0; i < size; i++) {
int cur = queue.poll(), distance[] = color ? distance1 : distance2;
for(int nxt = 0; nxt < n; nxt++) {
if(distance[nxt] != 0 || !graph[color ? 0 : 1][cur][nxt])
continue;
graph[color ? 0 : 1][cur][nxt] = false;
distance[nxt] = d;
queue.offer(nxt);
}
}
if(flag) d++;
else color = !color;
flag = !flag;
}
for(int i = 1; i < n; i++) {
if(distance1[i] == 0 && distance2[i] == 0) res[i] = -1;
else if(distance1[i] == 0) res[i] = distance2[i];
else if(distance2[i] == 0) res[i] = distance1[i];
else res[i] = Math.min(distance1[i], distance2[i]);
}
return res;
}
}
18.1190.反转每对括号间的子串
考察点 字符串/栈
给出一个字符串 s(仅含有小写英文字母和括号)。
请你按照从括号内到外的顺序,逐层反转每对匹配括号中的字符串,并返回最终的结果。
注意,您的结果中 不应 包含任何括号。
示例 1:
输入:s = "(abcd)"
输出:"dcba"
示例 2:
输入:s = "(u(love)i)"
输出:"iloveu"
示例 3:
输入:s = "(ed(et(oc))el)"
输出:"leetcode"
示例 4:
输入:s = "a(bcdefghijkl(mno)p)q"
输出:"apmnolkjihgfedcbq"
提示:
0 <= s.length <= 2000
s 中只有小写英文字母和括号
我们确保所有括号都是成对出现的
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reverse-substrings-between-each-pair-of-parentheses
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public String reverseParentheses(String s) {
StringBuilder sb = new StringBuilder();
char[] arr = s.toCharArray();
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < arr.length; i++) {
if (arr[i] == '(') stack.push(i);
if (arr[i] == ')')reverse(arr, stack.pop() + 1, i - 1);
}
for (int i = 0; i < arr.length; i++) {
if (arr[i] != ')' && arr[i] != '(') {
sb.append(arr[i]);
}
}
return sb.toString();
}
public void reverse(char[] arr, int left, int right) {
while (right > left) {
char tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
right--;
left++;
}
}
}
19.LCP12.小张刷题计划
考察点 数组/二分法
为了提高自己的代码能力,小张制定了 LeetCode 刷题计划,他选中了 LeetCode 题库中的 n 道题,编号从 0 到 n-1,并计划在 m 天内按照题目编号顺序刷完所有的题目(注意,小张不能用多天完成同一题)。
在小张刷题计划中,小张需要用 time[i] 的时间完成编号 i 的题目。此外,小张还可以使用场外求助功能,通过询问他的好朋友小杨题目的解法,可以省去该题的做题时间。为了防止“小张刷题计划”变成“小杨刷题计划”,小张每天最多使用一次求助。
我们定义 m 天中做题时间最多的一天耗时为 T(小杨完成的题目不计入做题总时间)。请你帮小张求出最小的 T是多少。
示例 1:
输入:time = [1,2,3,3], m = 2
输出:3
解释:第一天小张完成前三题,其中第三题找小杨帮忙;第二天完成第四题,并且找小杨帮忙。这样做题时间最多的一天花费了 3 的时间,并且这个值是最小的。
示例 2:
输入:time = [999,999,999], m = 4
输出:0
解释:在前三天中,小张每天求助小杨一次,这样他可以在三天内完成所有的题目并不花任何时间。
限制:
1 <= time.length <= 10^5
1 <= time[i] <= 10000
1 <= m <= 1000
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/xiao-zhang-shua-ti-ji-hua
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public int minTime(int[] time, int m) {
int len = time.length, lt = Integer.MAX_VALUE, rt = 0, mid, res = 0;
if (m >= len)
return 0;
for (int i = 0; i < len; i++) {
lt = Math.min(lt, time[i]);
rt += time[i];
}
while (lt <= rt) {
mid = (lt + rt) >> 1;
if (check(time, len, mid, m)) { // 切割得多了,调大下界
lt = mid + 1;
} else { // 切割得可能少了,调小上界
rt = mid - 1;
res = mid; // 同时记录夹逼值
}
}
return res;
}
private boolean check(int[] time, int len, int limit, int m) {
int cnt = 1, sum = time[0], maxVal = time[0]; // 初始化为1,且当前子数组最大值初始化为第一个元素
for (int i = 1; i < len; i++) { // 从第二个元素开始遍历
sum += time[i];
maxVal = Math.max(maxVal, time[i]);
if (sum - maxVal > limit) { // 划分 第 cnt + 1 个子数组(新的子数组的第一个元素)
cnt++;
maxVal = sum = time[i];
}
if (cnt > m) // 当划分的子数组个数超过m时,直接返回true
return true;
}
return false; // 找到一种可能的分割方案
}
}
20.1419.数青蛙
中等;考察点 字符串
给你一个字符串 croakOfFrogs,它表示不同青蛙发出的蛙鸣声(字符串 "croak" )的组合。由于同一时间可以有多只青蛙呱呱作响,所以 croakOfFrogs 中会混合多个 “croak” 。请你返回模拟字符串中所有蛙鸣所需不同青蛙的最少数目。
注意:要想发出蛙鸣 "croak",青蛙必须 依序 输出 ‘c’, ’r’, ’o’, ’a’, ’k’ 这 5 个字母。如果没有输出全部五个字母,那么它就不会发出声音。
如果字符串 croakOfFrogs 不是由若干有效的 "croak" 字符混合而成,请返回 -1 。
示例 1:
输入:croakOfFrogs = "croakcroak"
输出:1
解释:一只青蛙 “呱呱” 两次
示例 2:
输入:croakOfFrogs = "crcoakroak"
输出:2
解释:最少需要两只青蛙,“呱呱” 声用黑体标注
第一只青蛙 "crcoakroak"
第二只青蛙 "crcoakroak"
示例 3:
输入:croakOfFrogs = "croakcrook"
输出:-1
解释:给出的字符串不是 "croak" 的有效组合。
示例 4:
输入:croakOfFrogs = "croakcroa"
输出:-1
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-number-of-frogs-croaking
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public int minNumberOfFrogs(String croakOfFrogs) {
int len = croakOfFrogs.length();
if (len % 5 != 0 || croakOfFrogs.charAt(0) != 'c' || croakOfFrogs.charAt(len - 1) != 'k') return -1;
char[] ch = croakOfFrogs.toCharArray();
int[] num = new int[127];
int count = 1;
for (char c: ch) {
num[c]++;
//如果数组不是非递增的,那么就是无效数据
if (!(num['c'] >= num['r'] && num['r'] >= num['o'] && num['o'] >= num['a'] && num['a'] >= num['k']))
return -1;
//显然,当字符串为c时,青蛙数量才会增加
if (c == 'c') {
count = Math.max(count, num['c'] - num['k']);
}
}
return count;
}
}