42. 接雨水
单调递减栈。对于出栈的元素,栈顶元素是左边第一个比它高的,待入栈的元素是右边第一个比它高的。这样的话以左右两个柱子之间的长度为长,以左右两个柱子较矮者减去出栈的柱子的高度为宽,这个长方体的面积接到的雨水量就可以确定了。
class Solution {
public int trap(int[] height) {
Deque<Integer> stack = new ArrayDeque<>();
int res = 0;
for (int i = 0; i < height.length; i++) {
while (!stack.isEmpty() && height[stack.peek()] < height[i]) {
int cur = stack.pop();
if (stack.isEmpty()) {
break;
}
int h = Math.min(height[stack.peek()], height[i]) - height[cur];
int w = i - stack.peek() - 1;
res += h * w;
}
stack.push(i);
}
return res;
}
}
4. 寻找两个正序数组的中位数
时间复杂度O(m+n),空间复杂度O(m+n)
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int len = nums1.length + nums2.length;
int[] nums = new int[len];
int i = 0, j = 0, pos = 0;
while (i < nums1.length && j < nums2.length) {
nums[pos++] = nums1[i] < nums2[j] ? nums1[i++] : nums2[j++];
}
while (i < nums1.length) {
nums[pos++] = nums1[i++];
}
while (j < nums2.length) {
nums[pos++] = nums2[j++];
}
return (nums[len / 2] + nums[(len - 1) / 2]) / 2.0;
}
}
二分,时间复杂度Olog(m+n)
我们分别找第 (m+n+1) / 2 个,和 (m+n+2) / 2 个数,然后求其平均值,就是中位数。
这里我们需要定义一个函数来在两个有序数组中找到第K个元素,下面重点来看如何实现找到第K个元素。首先,为了避免产生新的数组从而增加时间复杂度,我们使用两个变量i和j分别来标记数组nums1和nums2的起始位置。然后来处理一些边界问题,比如当某一个数组的起始位置大于等于其数组长度时,说明其所有数字均已经被淘汰了,相当于一个空数组了,那么实际上就变成了在另一个数组中找数字,直接就可以找出来了。还有就是如果K=1的话,那么我们只要比较nums1和nums2的起始位置i和j上的数字就可以了。难点就在于一般的情况怎么处理?因为我们需要在两个有序数组中找到第K个元素,为了加快搜索的速度,我们要使用二分法,对K二分,意思是我们需要分别在nums1和nums2中查找第K/2个元素,注意这里由于两个数组的长度不定,所以有可能某个数组没有第K/2个数字,所以我们需要先检查一下,数组中到底存不存在第K/2个数字,如果存在就取出来,否则就赋值上一个整型最大值。如果某个数组没有第K/2个数字,那么我们就淘汰另一个数字的前K/2个数字即可。有没有可能两个数组都不存在第K/2个数字呢,这道题里是不可能的,因为我们的K不是任意给的,而是给的m+n的中间值,所以必定至少会有一个数组是存在第K/2个数字的。最后就是二分法的核心啦,比较这两个数组的第K/2小的数字midVal1和midVal2的大小,如果第一个数组的第K/2个数字小的话,那么说明我们要找的数字肯定不在nums1中的前K/2个数字,所以我们可以将其淘汰,将nums1的起始位置向后移动K/2个,并且此时的K也自减去K/2,调用递归。反之,我们淘汰nums2中的前K/2个数字,并将nums2的起始位置向后移动K/2个,并且此时的K也自减去K/2,调用递归即可。
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length, n = nums2.length;
int left = (m + n + 1) / 2;
int right = (m + n + 2) / 2;
return (findKth(nums1, 0, nums2, 0, left) + findKth(nums1, 0, nums2, 0, right)) / 2.0;
}
private double findKth(int[] nums1, int begin1, int[] nums2, int begin2, int k) {
if (begin1 >= nums1.length) {
return nums2[begin2 + k - 1];
}
if (begin2 >= nums2.length) {
return nums1[begin1 + k - 1];
}
if (k == 1) {
return Math.min(nums1[begin1], nums2[begin2]);
}
int mid1 = begin1 + k / 2 - 1 < nums1.length ? nums1[begin1 + k / 2 - 1] : Integer.MAX_VALUE;
int mid2 = begin2 + k / 2 - 1 < nums2.length ? nums2[begin2 + k / 2 - 1] : Integer.MAX_VALUE;
if (mid1 < mid2) {
return findKth(nums1, begin1 + k / 2, nums2, begin2, k - k / 2);
} else {
return findKth(nums1, begin1, nums2, begin2 + k / 2, k - k / 2);
}
}
}
25. K 个一组翻转链表
先找到每一个组的开头和结尾,然后记录上一组的结尾和下一组的开头,把这个组翻转之后,再将头尾结点和上一组和下一组连好。
class Solution {
private ListNode reverse(ListNode head, int k) {
ListNode dummy = new ListNode(0);
dummy.next = head;
for (int i = 0; i < k; i++) {
ListNode temp = head.next;
head.next = dummy.next;
dummy.next = head;
head = temp;
}
return dummy.next;
}
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode pre = dummy;
while (head != null) {
ListNode tail = pre;
for (int i = 0; i < k; i++) {
tail = tail.next;
if (tail == null) {
return dummy.next;
}
}
ListNode nextGroupHead = tail.next;
tail = head;
head = reverse(head, k);
pre.next = head;
tail.next = nextGroupHead;
head = nextGroupHead;
pre = tail;
}
return dummy.next;
}
}
23. 合并K个升序链表
使用最小堆,每次将最小的结点弹出,如果这个结点后面还有链表,将后面的部分继续入队,直到队空为止。
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
Queue<ListNode> pq = new PriorityQueue<>((o1, o2) -> o1.val - o2.val);
for (ListNode node : lists) {
if (node != null) {
pq.offer(node);
}
}
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
while (!pq.isEmpty()) {
ListNode node = pq.poll();
cur.next = node;
if (node.next != null) {
pq.offer(node.next);
}
cur = cur.next;
}
return dummy.next;
}
}
124. 二叉树中的最大路径和
dfs(root)代表以root结点出发,到达某个结点为止最大路径和。
root结点左侧贡献的最大路径和为dfs(root.left)与0的较大值,右侧同理。
后序遍历每一个结点,当遍历到根结点时,左子树和右子树的最大路径和已经求出来了。找到某个结点左侧贡献的最大值+右侧贡献的最大值+这个结点的值的最大值即为答案。
class Solution {
int res = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
dfs(root);
return res;
}
private int dfs(TreeNode root) {
if (root == null) {
return 0;
}
int left = Math.max(0, dfs(root.left));
int right = Math.max(0, dfs(root.right));
int sum = left + right + root.val;
res = Math.max(sum, res);
return root.val + Math.max(left, right);
}
}
72. 编辑距离
dp[i][j]代表word1的前i个字符与word2的前j个字符的编辑距离。
边界:一个空字符串到另一个字符串的编辑距离就是另一个字符串的长度。
dp[i][0] = i , dp[0][j] = j
转移方程:
对于word1的第i个字符与word2的第j个字符,如果相等,那么dp[i][j] = dp[i-1][j-1]。
如果不相等:
1.可以把word1加一个字符之后与word2相等,加的这个字符就是word2的第j个字符 dp[i][j] = dp[i][j-1] + 1
2.可以把word1删一个字符之后与word2相等,删第i个字符
dp[i][j] = dp[i-1][j] + 1
3.可以把word1修改一个字符之后与word2相等,修改第i个字符
dp[i][j] = dp[i-1][j-1] + 1
dp[i][j]取以上三者最小值。
class Solution {
public int minDistance(String word1, String word2) {
int n = word1.length(), m = word2.length();
int[][] f = new int[n + 1][m + 1];
for (int i = 0; i <= n; i++) {
f[i][0] = i;
}
for (int j = 0; j <= m; j++) {
f[0][j] = j;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
f[i][j] = f[i - 1][j - 1];
} else {
f[i][j] = Math.min(f[i - 1][j], Math.min(f[i][j - 1], f[i - 1][j - 1])) + 1;
}
}
}
return f[n][m];
}
}
440. 字典序的第K小数字
类比成一个树,第一行从1到9,1的子树为11到19。
先计算1这棵树一共有多少个结点curNum,如果小于k,说明肯定不在1这棵树上。从2开始找第k-curNum小的数。如果大于等于k,那么在1这棵树上,从11开始找第k-1小的数。
class Solution {
public int findKthNumber(int n, int k) {
int res = 1;
while (k > 1) {
long curNum = 0;
long first = res;
long next = res + 1;
while (first <= n) {
curNum += Math.min(next, (long) (n + 1)) - first;
next *= 10;
first *= 10;
}
if (curNum < k) {
//不在当前子树
res++;
k -= curNum;
} else {
res *= 10;
k--;
}
}
return res;
}
}
32. 最长有效括号
dp[i]代表到i位置的最长有效括号。
边界:dp[0] = 0
转移方程:
如果s[i] = '(' ,dp[i] = 0
如果s[i] = ')',如果s[i-1] = '(',那么dp[i] = dp[i-2] + 2
如果s[i-1] = ')' ,那么第i-1位置的')'的最长有效括号长度是dp[i-1],index = i-dp[i-1]-1是与第i个位置的')'对应的括号,如果这个括号是')',那么d[[i] = 0。如果这个括号是'(',那么dp[i] = dp[i-1] + 2 + dp[index-1]
class Solution {
public int longestValidParentheses(String s) {
int n = s.length();
if (n == 0) {
return 0;
}
int[] dp = new int[n];
int res = 0;
for (int i = 1; i < n; i++) {
if (s.charAt(i) == '(') {
continue;
}
if (s.charAt(i - 1) == '(') {
dp[i] = (i - 2 >= 0 ? dp[i - 2] : 0) + 2;
res = Math.max(res, dp[i]);
continue;
}
int index = i - dp[i - 1] - 1;
if (index < 0) {
continue;
}
if (s.charAt(index) == '(') {
dp[i] = dp[i - 1] + (index - 1 >= 0 ? dp[index - 1] : 0) + 2;
}
res = Math.max(res, dp[i]);
}
return res;
}
}
精简:
class Solution {
public int longestValidParentheses(String s) {
int res = 0;
int[] dp = new int[s.length()];
for (int i = 1; i < s.length(); i++) {
if (s.charAt(i) == ')') {
if (s.charAt(i - 1) == '(') {
dp[i] = (i - 2 >= 0 ? dp[i - 2] : 0) + 2;
} else {
int index = i - dp[i - 1] - 1;
if (index >= 0 && s.charAt(index) == '(') {
dp[i] = dp[i - 1] + 2 + (index - 1 >= 0 ? dp[index - 1] : 0);
}
}
}
res = Math.max(res, dp[i]);
}
return res;
}
}
10. 正则表达式匹配
比较难理解的是p的第j个数是*的情况
如果s的第i个数等于p的第j-1个数 或者 p的第j-1个数是 .
dp[i][j] = dp[i-1][j] // a* 记为多个a
or dp[i][j] = dp[i-1][j-2] // a* 记为单个a
or dp[i][j] = dp[i][j-2] // a*记为空
否则
dp[i][j] = dp[i][j-2]
class Solution {
public boolean isMatch(String s, String p) {
int sLen = s.length();
int pLen = p.length();
boolean[][] dp = new boolean[sLen][pLen];
dp[0][0] = true;
for (int j = 2; j <= pLen; j++) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = dp[0][j - 2];
}
}
for (int i = 1; i <= sLen; i++) {
for (int j = 1; j <= pLen; j++) {
if (s.charAt(i - 1) == p.charAt(j - 1) || p.charAt(j - 1) == '.') {
dp[i][j] = dp[i - 1][j - 1];
} else if (p.charAt(j - 1) == '*') {
if (s.charAt(i - 1) == p.charAt(j - 2) || p.charAt(j - 2) == '.') {
dp[i][j] = dp[i][j - 2] || dp[i - 1][j - 2] || dp[i - 1][j];
} else {
dp[i][j] = dp[i][j - 2];
}
}
}
}
return dp[sLen][pLen];
}
}
135. 分发糖果
用一个数组就行,先初始都分配一个糖果,再从左往右遍历,只要右边的孩子比左边的孩子评分高,就把右边的孩子比左边孩子多分配一个糖果。
再从右往左遍历,只要左边的孩子评分比右边的孩子高,那么左边孩子分配的糖果数等于以下两者较大值:之前分配给左边孩子的糖果树与右边孩子的糖果树+1。
class Solution {
public int candy(int[] ratings) {
int[] res = new int[ratings.length];
Arrays.fill(res, 1);
for (int i = 1; i < ratings.length; i++) {
if (ratings[i] > ratings[i - 1]) {
res[i] = res[i - 1] + 1;
}
}
for (int i = ratings.length - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1]) {
res[i] = Math.max(res[i], res[i + 1] + 1);
}
}
int sum = 0;
for (int i : res) {
sum += i;
}
return sum;
}
}
41. 缺失的第一个正数
把i位置上的数�nums[i]放到nums[i]-1位置上去,然后遍历数组,找到第一个nums[i] != i+1的数,就是缺失的第一个正数。注意判断nums[i]-1位置是否在数组大小内。
class Solution {
public int firstMissingPositive(int[] nums) {
for (int i = 0; i < nums.length; i++) {
while (nums[i] - 1 >= 0 && nums[i] - 1 < nums.length && nums[i] - 1 != i && nums[i] != nums[nums[i] - 1]) {
swap(i, nums[i] - 1, nums);
}
}
for (int i = 0; i < nums.length; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
return nums.length + 1;
}
public void swap(int a, int b, int[] nums) {
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}
76. 最小覆盖子串
滑动窗口,用left,right表示滑动窗口的左边界和右边界,通过改变left,right来扩展和收缩滑动窗口。先不断增加right使滑动窗口增大,直到窗口包含了t的所有元素
,再不断增加left使滑动窗口缩小,直到减去一个必须包含的元素,记录此时滑动窗口没减去之前的长度,并保存最小值。减去之后滑动窗口肯定不满足条件了,那么继续移动右窗口,直到包含t所有元素,重复以上步骤。找到最小覆盖子串。
class Solution {
public String minWindow(String s, String t) {
int[] need = new int[128];
int[] window = new int[128];
for (char c : t.toCharArray()) {
need[c]++;
}
String res = "";
int minLen = Integer.MAX_VALUE;
int cnt = 0;
for (int r = 0, l = 0; r < s.length(); r++) {
char c = s.charAt(r);
window[c]++;
if (need[c] > 0 && window[c] <= need[c]) {
cnt++;
}
while (cnt == t.length()) {
if (r - l + 1 < minLen) {
minLen = r - l + 1;
res = s.substring(l, r + 1);
}
char ch = s.charAt(l);
l++;
window[ch]--;
if (need[ch] > 0 && window[ch] < need[ch]) {
cnt--;
}
}
}
return res;
}
}
84. 柱状图中最大的矩形
单调递增栈。对于出栈的元素cur,当前待入栈的元素i是右边第一个比它矮的柱子,栈顶元素peek是左边第一个比它矮的柱子,对于高为heights[cur]的最大矩形面积已可以计算,宽等于 i - stack.peek() - 1。
采用哨兵机制可以减少栈空的判断。
class Solution {
public int largestRectangleArea(int[] heights) {
int res = 0;
Deque<Integer> stack = new ArrayDeque<>();
int[] newHeights = new int[heights.length + 2];
newHeights[0] = newHeights[heights.length + 1] = 0;
System.arraycopy(heights, 0, newHeights, 1, heights.length);
heights = newHeights;
for (int i = 0; i < heights.length; i++) {
while (!stack.isEmpty() && heights[i] < heights[stack.peek()]) {
int cur = stack.pop();
int h = heights[cur];
int w = i - stack.peek() - 1;
res = Math.max(res, h * w);
}
stack.push(i);
}
return res;
}
}
128. 最长连续序列
哈希表,先将所有数加入到HashSet中,再遍历每个数,只要set中不存在num-1,那么就有可能是连续序列的起始位置。然后不断判断num往上加1之后,set中是否存在,直到找到最长的序列。
class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int i : nums) {
set.add(i);
}
int res = 0;
for (int i : nums) {
if (!set.contains(i - 1)) {
int count = 1;
while (set.contains(i + 1)) {
count++;
i++;
}
res = Math.max(res, count);
}
}
return res;
}
}
并查集:
class Solution {
int[] father;
int[] size;
int res = 1;
private void init() {
for (int i = 0; i < father.length; i++) {
father[i] = i;
size[i] = 1;
}
}
private int findFather(int x) {
while (father[x] != x) {
x = father[x];
}
return x;
}
private void union(int a, int b) {
int fA = findFather(a);
int fB = findFather(b);
if (fA != fB) {
father[fA] = fB;
size[fB] += size[fA];
res = Math.max(res, size[fB]);
}
}
public int longestConsecutive(int[] nums) {
if (nums.length == 0) {
return 0;
}
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], i);
}
father = new int[nums.length];
size = new int[nums.length];
init();
for (int i : nums) {
if (map.containsKey(i + 1)) {
union(map.get(i), map.get(i + 1));
}
}
return res;
}
}
45. 跳跃游戏 II
curPos不断右移,达到end位置时,需要进行下一跳,res+1。
注意当curPos走到最后一个位置时,不管是否等于end,都不需要跳了。所以遍历条件写while(curPos < nums.length - 1)即可。
class Solution {
public int jump(int[] nums) {
int curPos = 0;//当前位置
int maxPos = 0;//最远可到达的位置
int end = 0;//当前要到达的位置
int res = 0;
while (curPos < nums.length - 1) {
maxPos = Math.max(maxPos, curPos + nums[curPos]);
if (curPos == end) {
res++;
end = maxPos;
}
curPos++;
}
return res;
}
}
85. 最大矩形
转化成第84题。
把图形的每一排开始看做一个柱状图,就是求多个柱状图中的最大矩形。
class Solution {
private int cal(int[] heights) {
int[] newHeights = new int[heights.length + 2];
newHeights[0] = newHeights[heights.length + 1] = 0;
System.arraycopy(heights, 0, newHeights, 1, heights.length);
heights = newHeights;
Deque<Integer> stack = new ArrayDeque<>();
int res = 0;
for (int i = 0; i < heights.length; i++) {
while (!stack.isEmpty() && heights[i] < heights[stack.peek()]) {
int cur = stack.pop();
int h = heights[cur];
int w = i - stack.peek() - 1;
res = Math.max(res, h * w);
}
stack.push(i);
}
return res;
}
public int maximalRectangle(char[][] matrix) {
if (matrix.length == 0) {
return 0;
}
int[] heights = new int[matrix[0].length];
int res = 0;
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[0].length; j++) {
if (matrix[i][j] == '0') {
heights[j] = 0;
} else {
heights[j]++;
}
}
res = Math.max(res, cal(heights));
}
return res;
}
}
51. N 皇后
转化成全排列问题,对于所有的全排列情况,i,arr[i]代表皇后在第i行第arr[i]列。这样一定在不同行不同列,然后只需判断所有的皇后是否不在斜线上就行了。
class Solution {
List<List<String>> res = new ArrayList<>();
int[] arr;
boolean[] isVisited;
private boolean judge(int n) {
boolean flag = true;
label:
for (int i = 1; i <= n; i++) {//第i行的皇后和后面所有行的皇后判断是否在对角线上
for (int j = i + 1; j <= n; j++) {
if (Math.abs(i - j) == Math.abs(arr[i] - arr[j])) {
flag = false;
break label;
}
}
}
return flag;
}
private void dfs(int begin, int n) {
if (begin == n + 1) {
if (judge(n) == true) {
List<String> list = new ArrayList<>();
for (int i = 1; i <= n; i++) {
StringBuilder sb = new StringBuilder();
for (int j = 1; j <= n; j++) {
if (j == arr[i]) {
sb.append('Q');
} else {
sb.append('.');
}
}
list.add(sb.toString());
}
res.add(list);
}
return;
}
for (int i = 1; i <= n; i++) {
if (isVisited[i] == false) {
arr[begin] = i;
isVisited[i] = true;
dfs(begin + 1, n);
isVisited[i] = false;
}
}
}
public List<List<String>> solveNQueens(int n) {
arr = new int[n + 1];
isVisited = new boolean[n + 1];
dfs(1, n);
return res;
}
}
329. 矩阵中的最长递增路径
记忆化dfs:
由于同一个单元格对应的最长递增路径的长度是固定不变的,因此可以使用记忆化的方法进行优化。用矩阵memo作为缓存矩阵,已经计算过的单元格的结果存储到缓存矩阵中。
class Solution {
int[][] memo;
int[] dirX = {1, -1, 0, 0};
int[] dirY = {0, 0, 1, -1};
private int dfs(int i, int j, int[][] matrix) {
if (memo[i][j] != 0) {
return memo[i][j];
}
memo[i][j]++;
for (int k = 0; k < 4; k++) {
int newI = i + dirX[k];
int newJ = j + dirY[k];
if (newI >= 0 && newI < matrix.length && newJ >= 0 && newJ < matrix[0].length && matrix[newI][newJ] > matrix[i][j]) {
memo[i][j] = Math.max(memo[i][j], dfs(newI, newJ, matrix) + 1);
}
}
return memo[i][j];
}
public int longestIncreasingPath(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
memo = new int[m][n];
int res = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
res = Math.max(res, dfs(i, j, matrix));
}
}
return res;
}
}
297. 二叉树的序列化与反序列化
序列化的时候,如果根结点为null,直接返回"null"。如果根结点不为null,使用层次遍历,注意稍微有一点区别,就是某个结点的左右子树不管是否为空,都加入到队列中。出队的时候判断结点是否为空,如果为空,结果集中加入"null",然后进行下一次出队。如果不为空,那么结果集加入这个结点的值,然后把这个结点的左右子树都入队。
反序列化的时候,如果就等于"null",直接返回null。否则按照“ , ” 分割数组,先把根结点创建出来,值等于split[0]。 根结点入队。然后遍历分割后的数组,每一次出队的结点,去建立它的左右子树。最后返回根结点。
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if (root == null) {
return "null";
}
StringBuilder res = new StringBuilder();
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
int size = q.size();
for (int i = 0; i < size; i++) {
TreeNode front = q.poll();
if (front == null) {
res.append("null,");
continue;
} else {
res.append(front.val + ",");
q.offer(front.left);
q.offer(front.right);
}
}
}
res.deleteCharAt(res.length() - 1);
return res.toString();
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if ("null".equals(data)) {
return null;
}
String[] splits = data.split(",");
TreeNode root = new TreeNode(Integer.parseInt(splits[0]));
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
for (int i = 1; i < splits.length; i++) {
TreeNode front = q.poll();
if (!"null".equals(splits[i])) {
front.left = new TreeNode(Integer.parseInt(splits[i]));
q.offer(front.left);
}
i++;
if (!"null".equals(splits[i])) {
front.right = new TreeNode(Integer.parseInt(splits[i]));
q.offer(front.right);
}
}
return root;
}
}
354. 俄罗斯套娃信封问题
相当于二维的最长递增子序列问题。先把信封按第一维宽度从小到大排列。
然后对第二维求最长递增子序列,稍微有点区别的地方就是在求dp的时候,要保证第一个维度也要严格小于。
class Solution {
private int lis(int[][] envelopes) {
int[] dp = new int[envelopes.length];
dp[0] = 1;
int res = 1;
for (int i = 1; i < envelopes.length; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (envelopes[j][1] < envelopes[i][1] && envelopes[j][0] < envelopes[i][0]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
res = Math.max(res, dp[i]);
}
return res;
}
public int maxEnvelopes(int[][] envelopes) {
if (envelopes.length == 0) {
return 0;
}
Arrays.sort(envelopes, (o1, o2) -> o1[0] - o2[0]);
return lis(envelopes);
}
}
剑指 Offer 51. 数组中的逆序对
归并排序代码:
public void mergeSort(int[] arr, int lo, int hi) {
if (lo >= hi) {
return;
}
int mid = lo + (hi - lo) / 2;
mergeSort(arr, lo, mid);
mergeSort(arr, mid + 1, hi);
int[] temp = new int[hi - lo + 1];
int i = lo, j = mid + 1, pos = 0;
while (i <= mid && j <= hi) {
temp[pos++] = arr[i] < arr[j] ? arr[i++] : arr[j++];
}
while (i <= mid) {
temp[pos++] = arr[i++];
}
while (j <= hi) {
temp[pos++] = arr[j++];
}
System.arraycopy(temp, 0, arr, lo, pos);
}
在并的过程中计算逆序对:
方式一:在右半部分的指针j移动的时候计算逆序对,j要移动的时候,说明arr[j] < arr[i],此时左半部分从i到mid一共mid-i+1个数都比arr[j]大。只需要在 while (i <= mid && j <= hi) 的循环里面计算逆序对。因为当while (j <= hi) 的时候,arr[j]比左边所有数都大了,不可能产生逆序对。
方式二:在左半部分的指针i移动的时候计算逆序对,i要移动的时候,说明arr[i] <= arr[j],此时右半部分第一个数到j-1一共 (j - 1) - (mid +1) +1 = j - mid - 1个数都比arr[i]小。
class Solution {
public int reversePairs(int[] nums) {
return mergeSort(nums, 0, nums.length - 1);
}
public int mergeSort(int[] arr, int lo, int hi) {
if (lo >= hi) {
return 0;
}
int mid = lo + (hi - lo) / 2;
int res = mergeSort(arr, lo, mid) + mergeSort(arr, mid + 1, hi);
int[] temp = new int[hi - lo + 1];
int i = lo, j = mid + 1, pos = 0;
while (i <= mid && j <= hi) {
res += arr[i] <= arr[j] ? 0 : mid + 1 - i;//arr[j]小,从i到mid的所有数与j构成逆序对f
temp[pos++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
}
while (i <= mid) {
temp[pos++] = arr[i++];
}
while (j <= hi) {
temp[pos++] = arr[j++];//此时arr[j]比左边的所有数都大了,不可能构成逆序对
}
System.arraycopy(temp, 0, arr, lo, pos);
return res;
}
}
460. LFU 缓存
使用平衡二叉树来记录顺序关系。
使用map缓存某个key是否在树中。
class LFUCache {
Map<Integer, Node> cache;
TreeSet<Node> treeSet;
int timestamp;
int capacity;
class Node implements Comparable<Node> {
int key;
int value;
int time;
int count;
public Node(int key, int value, int time, int count) {
this.key = key;
this.value = value;
this.time = time;
this.count = 0;
}
@Override
public int compareTo(Node o) {
return count != o.count ? count - o.count : time - o.time;
}
}
public LFUCache(int capacity) {
this.capacity = capacity;
this.timestamp = 0;
this.cache = new HashMap<>();
this.treeSet = new TreeSet<>();
}
public int get(int key) {
if (capacity == 0) {
return -1;
}
Node node = cache.get(key);
if (node == null) {
return -1;
} else {
treeSet.remove(node);
node.count++;
node.time = ++timestamp;
treeSet.add(node);
return node.value;
}
}
public void put(int key, int value) {
if (capacity == 0) {
return;
}
Node node = cache.get(key);
if (node == null) {
node = new Node(key, value, ++timestamp, 1);
if (treeSet.size() == capacity) {
cache.remove(treeSet.first().key);
treeSet.remove(treeSet.first());
}
cache.put(key, node);
treeSet.add(node);
} else {
treeSet.remove(node);
node.value = value;
node.count++;
node.time = ++timestamp;
treeSet.add(node);
}
}
}
239. 滑动窗口最大值
单调队列,从队头到队尾单调递减,队头存储的永远是窗口中最大的值。
对于一个要进队的数来说,如果队尾的数比它小,那么这个队尾的数不可能是窗口的最大值,直接把队尾的数出队,再把这个数进队。
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
Deque<Integer> q = new LinkedList<>();
int[] res = new int[nums.length - k + 1];
for (int i = 0; i < nums.length; i++) {
while (!q.isEmpty() && nums[i] >= nums[q.peekLast()]) {
q.pollLast();
}
q.addLast(i);
if (q.peekFirst() <= i - k) {
q.pollFirst();
}
if (i - k + 1 >= 0) {
res[i - k + 1] = nums[q.peekFirst()];
}
}
return res;
}
}
726. 原子的数量
44. 通配符匹配
如果p.charAt(j - 1) == '*'
*匹配空字符串,dp[i][j] = dp[i][j-1]
*匹配多个字符,dp[i][j] = dp[i-1][j]
class Solution {
public boolean isMatch(String s, String p) {
int sLen = s.length();
int pLen = p.length();
boolean[][] dp = new boolean[sLen + 1][pLen + 1];
dp[0][0] = true;
for (int j = 1; j <= pLen; j++) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = true;
} else {
break;
}
}
for (int i = 1; i <= sLen; i++) {
for (int j = 1; j <= pLen; j++) {
if (s.charAt(i - 1) == p.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else if (p.charAt(j - 1) == '?') {
dp[i][j] = dp[i - 1][j - 1];
} else if (p.charAt(j - 1) == '*') {
dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
}
}
}
return dp[sLen][pLen];
}
}
123. 买卖股票的最佳时机 III
dp[i][j][k]代表第i天结束时,已经进行了j次交易,手中还有k份股票的最大收益。
class Solution {
public int maxProfit(int[] prices) {
if (prices.length == 0) {
return 0;
}
int[][][] dp = new int[prices.length][3][2];
for (int j = 1; j <= 2; j++) {
dp[0][j][1] = -prices[0];
}
for (int i = 1; i < prices.length; i++) {
for (int j = 1; j <= 2; j++) {
dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
}
}
return dp[prices.length - 1][2][0];
}
}
97. 交错字符串
dp[i][j]代表s1的前i个字符与s2的前j个字符是否可以交错组成s3的前i+j个字符
边界:
dp[0][0] = true ,若 s1的前i个字符等于s3的前i个字符dp[i][0] = true , 若s2的前j个字符等于s3的前j个字符dp[0][j] = true。
转移方程:
若s1的第i个字符等于s3的第i+j个字符,且dp[i-1][j] = true, 那么dp[i][j] = true
若s2的第j个字符等于s3的第i+j个字符,且dp[i][j-1] = true,那么dp[i][j] = true
class Solution {
public boolean isInterleave(String s1, String s2, String s3) {
int m = s1.length();
int n = s2.length();
if (m + n != s3.length()) {
return false;
}
boolean[][] dp = new boolean[m + 1][n + 1];
dp[0][0] = true;
for (int i = 1; i <= m; i++) {
if (s1.charAt(i - 1) == s3.charAt(i - 1)) {
dp[i][0] = true;
} else {
break;
}
}
for (int j = 1; j <= n; j++) {
if (s2.charAt(j - 1) == s3.charAt(j - 1)) {
dp[0][j] = true;
} else {
break;
}
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1.charAt(i - 1) == s3.charAt(i + j - 1) && dp[i - 1][j]) {
dp[i][j] = true;
} else if (s2.charAt(j - 1) == s3.charAt(i + j - 1) && dp[i][j - 1]) {
dp[i][j] = true;
}
}
}
return dp[m][n];
}
}
887. 鸡蛋掉落
public class Solution {
public int superEggDrop(int K, int N) {
// dp[i][j]:一共有 i 层楼梯的情况下,使用 j 个鸡蛋的最少实验的次数
// 注意:
// 1、i 表示的是楼层的大小,不是第几层的意思,例如楼层区间 [8, 9, 10] 的大小为 3,这一点是在状态转移的过程中调整的定义
// 2、j 表示可以使用的鸡蛋的个数,它是约束条件,我个人习惯放在后面的维度,表示消除后效性的意思
// 0 个楼层和 0 个鸡蛋的情况都需要算上去,虽然没有实际的意义,但是作为递推的起点,被其它状态值所参考
int[][] dp = new int[N + 1][K + 1];
// 由于求的是最小值,因此初始化的时候赋值为一个较大的数,9999 或者 i 都可以
for (int i = 0; i <= N; i++) {
Arrays.fill(dp[i], i);
}
// 初始化:填写下标为 0、1 的行和下标为 0、1 的列
// 第 0 行:楼层为 0 的时候,不管鸡蛋个数多少,都测试不出鸡蛋的 F 值,故全为 0
for (int j = 0; j <= K; j++) {
dp[0][j] = 0;
}
// 第 1 行:楼层为 1 的时候,0 个鸡蛋的时候,扔 0 次,1 个以及 1 个鸡蛋以上只需要扔 1 次
dp[1][0] = 0;
for (int j = 1; j <= K; j++) {
dp[1][j] = 1;
}
// 第 0 列:鸡蛋个数为 0 的时候,不管楼层为多少,也测试不出鸡蛋的 F 值,故全为 0
// 第 1 列:鸡蛋个数为 1 的时候,这是一种极端情况,要试出 F 值,最少次数就等于楼层高度(想想复杂度的定义)
for (int i = 0; i <= N; i++) {
dp[i][0] = 0;
dp[i][1] = i;
}
// 从第 2 行,第 2 列开始填表
for (int i = 2; i <= N; i++) {
for (int j = 2; j <= K; j++) {
for (int k = 1; k <= i; k++) {
// 碎了,就需要往低层继续扔:层数少 1 ,鸡蛋也少 1
// 不碎,就需要往高层继续扔:层数是当前层到最高层的距离差,鸡蛋数量不少
// 两种情况都做了一次尝试,所以加 1
dp[i][j] = Math.min(dp[i][j], Math.max(dp[k - 1][j - 1], dp[i - k][j]) + 1);
}
}
}
return dp[N][K];
}
}
public class Solution {
private int cal(int K, int T) {
if (T == 1 || K == 1) {
return T + 1;
}
return cal(K - 1, T - 1) + cal(K, T - 1);
}
public int superEggDrop(int K, int N) {
int T = 1;
while (cal(K, T) < N + 1) {
T++;
}
return T;
}
}
315. 计算右侧小于当前元素的个数
注意这道题只能在i增加的过程中计算逆序对的个数,因为i增加时,从mid+1到j-1个数都是右侧小于当前元素的个数。还要使用一个数组来保存每个数的初始位置。
class Solution {
int[] res;
int[] index;//当前位置的数对应原数组的第几个位置
public List<Integer> countSmaller(int[] nums) {
res = new int[nums.length];
index = new int[nums.length];
for (int i = 0; i < index.length; i++) {
index[i] = i;
}
mergeSort(nums, 0, nums.length - 1);
return Arrays.stream(res).boxed().collect(Collectors.toList());
}
private void mergeSort(int[] nums, int lo, int hi) {
if (lo >= hi) {
return;
}
int mid = lo + (hi - lo) / 2;
mergeSort(nums, lo, mid);
mergeSort(nums, mid + 1, hi);
int[] temp = new int[hi - lo + 1];
int[] tempindex = new int[hi - lo + 1];
int i = lo, j = mid + 1, pos = 0;
while (i <= mid && j <= hi) {
res[index[i]] += nums[i] <= nums[j] ? j - mid - 1 : 0;
tempindex[pos] = nums[i] <= nums[j] ? index[i] : index[j];
temp[pos++] = nums[i] <= nums[j] ? nums[i++] : nums[j++];
}
while (i <= mid) {
res[index[i]] += j - mid - 1;
tempindex[pos] = index[i];
temp[pos++] = nums[i++];
}
while (j <= hi) {
tempindex[pos] = index[j];
temp[pos++] = nums[j++];
}
System.arraycopy(tempindex, 0, index, lo, pos);
System.arraycopy(temp, 0, nums, lo, pos);
}
}
493. 翻转对
在归并排序的过程中计算翻转对,注意和之前的区别是要先单独统计一次,因为题目要统计的条件是nums[i] > 2 * nums[j],而合并的时候条件只是nums[i]与nums[j]比较。
class Solution {
public int reversePairs(int[] nums) {
return mergeSort(nums, 0, nums.length - 1);
}
private int mergeSort(int[] nums, int lo, int hi) {
if (lo >= hi) {
return 0;
}
int mid = lo + (hi - lo) / 2;
int res = 0;
res += mergeSort(nums, lo, mid) + mergeSort(nums, mid + 1, hi);
int[] temp = new int[hi - lo + 1];
int i = lo, j = mid + 1, pos = 0;
//先单独统计,因为合并的时候统计不了
while (i <= mid && j <= hi) {
if ((long) nums[i] > 2 * (long) nums[j]) {
res += mid - i + 1;
j++;
} else {
i++;
}
}
i = lo;
j = mid + 1;
while (i <= mid && j <= hi) {
temp[pos++] = nums[i] <= nums[j] ? nums[i++] : nums[j++];
}
while (i <= mid) {
temp[pos++] = nums[i++];
}
while (j <= hi) {
temp[pos++] = nums[j++];
}
System.arraycopy(temp, 0, nums, lo, pos);
return res;
}
}
407. 接雨水 II
对于每一个块,去找它四个方向最高的高度中的最小值作为上界,当前块作为下界。 我们从矩阵的最外围往里面遍历,像一个圈不断缩小的过程,为了防止重复遍历用visited记录,其次要用小根堆(以高度为判断基准)来存入所有快的四周(即圈是不断缩小的,小顶堆存的就是这个圈)
为什么要让高度较小的块先出队?(关键点)
- 一开始时候就讲了基础做法是:对于每一个块,去找它四个方向最高的高度中的最小值(二维下则是左右最高的高度取较小的那一个)作为上界,当前块作为下界
- 而我们现在反过来不是以中心块为处理单元,而是以四周作为处理单元
- 我们如果能确保当前出队的元素对于该中心块来说是它周围四个高度中的最小值那么就能确定接雨水的大小
- 为什么队头元素的高度比中心块要高它就一定是中心块周围四个高度中的最小值呢?
因为我们的前提就是小顶堆:高度小的块先出队,所以对于中心块来说,先出队的必然是中心块四周高度最小的那一个
- 步骤:
- 构建小顶堆,初始化为矩阵的最外围(边界所有元素)
- 不断出队,倘若队头元素的四周有中心块比队头矮,即代表能够接雨水:队头元素减去该中心块即当前中心块能接雨水的值
- 但是接完雨水之后中心块还要存进队列中,但这时要存入的中心块是接完雨水后的中心块
*/
class Solution {
public int trapRainWater(int[][] heightMap) {
if (heightMap.length == 0) {
return 0;
}
Queue<int[]> pq = new PriorityQueue<>((o1, o2) -> o1[2] - o2[2]);
int row = heightMap.length;
int col = heightMap[0].length;
boolean[][] isVisited = new boolean[row][col];
int res = 0;
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
if (i == 0 || i == row - 1 || j == 0 || j == col - 1) {
pq.offer(new int[]{i, j, heightMap[i][j]});
isVisited[i][j] = true;
}
}
}
int[] dirX = {1, -1, 0, 0};
int[] dirY = {0, 0, 1, -1};
while (!pq.isEmpty()) {
int[] front = pq.poll();
for (int i = 0; i < 4; i++) {
int nx = front[0] + dirX[i];
int ny = front[1] + dirY[i];
if (nx >= 0 && nx < row && ny >= 0 && ny < col && isVisited[nx][ny] == false) {
if (heightMap[nx][ny] < front[2]) {
res += front[2] - heightMap[nx][ny];
}
pq.offer(new int[]{nx, ny, Math.max(heightMap[nx][ny], front[2])});
isVisited[nx][ny] = true;
}
}
}
return res;
}
}
295. 数据流的中位数
设置一个最大堆和一个最小堆。
把数据流所有数按顺序分为两半,左边的数全部在最大堆中,堆顶就是左边最大的数。右边的数全部放在最小堆中,堆顶就是右边最小的数。保证最大堆永远比最小堆多一个数或相等。每次先入最大堆,入完之后把最大堆堆顶出到最小堆中。如果此时最小堆的数比最大堆还多一个,那就把最小堆的堆顶再出到最大堆中。
最后答案就是最大堆的堆顶或者两个堆顶的平均值。
class MedianFinder {
Queue<Integer> maxHeap;
Queue<Integer> minHeap;
int count;
/**
* initialize your data structure here.
*/
public MedianFinder() {
this.minHeap = new PriorityQueue<>();
this.maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
this.count = 0;
}
public void addNum(int num) {
maxHeap.add(num);
minHeap.add(maxHeap.poll());
if (count % 2 == 0) {
maxHeap.add(minHeap.poll());
}
count++;
}
public double findMedian() {
if (count % 2 == 0) {
return (maxHeap.peek() + minHeap.peek()) / 2.0;
} else {
return maxHeap.peek();
}
}
}
188. 买卖股票的最佳时机 IV
dp[i][j][k]代表第i天结束时,已经进行了j次买入交易,手中持有k份股票的最大收益。
class Solution {
public int maxProfit(int k, int[] prices) {
if (prices.length == 0) {
return 0;
}
int[][][] dp = new int[prices.length][k + 1][2];
for (int j = k; j >= 1; j--) {
dp[0][j][1] = -prices[0];
}
for (int i = 1; i < prices.length; i++) {
for (int j = k; j >= 1; j--) {
dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
}
}
return dp[prices.length - 1][k][0];
}
}
862. 和至少为 K 的最短子数组
先计算前缀和,preSum[i]代表前i个数的和。
目标找到preSum[y] - preSum[x] >= K且y - x最小。
暴力法超时:
class Solution {
public int shortestSubarray(int[] A, int K) {
int len = A.length;
long[] preSum = new long[len + 1];
for (int i = 0; i < len; i++) {
preSum[i + 1] = preSum[i] + (long) A[i];
}
int res = len + 1;
for (int i = len; i >= 1; i--) {
for (int j = i - 1; j >= 0; j--) {
if (preSum[i] - preSum[j] >= K) {
res = Math.min(res, i - j);
break;
}
}
}
return res < len + 1 ? res : -1;
}
}
单调队列,从队尾到队头preSum递减。遍历,待入队的preSum[i]如果小于等于队尾,队尾可以直接出队。因为如果从队尾开始的某段子数组和大于等于K,那么从i位置开始的某段子数组和也一定大于等于K。然后判断队头到i的子数组是否大于等于K,如果满足,不断把队头出队,直到不满足为止。
class Solution {
public int shortestSubarray(int[] A, int K) {
int len = A.length;
long[] preSum = new long[len + 1];
for (int i = 0; i < len; i++) {
preSum[i + 1] = preSum[i] + (long) A[i];
}
int res = len + 1;
Deque<Integer> q = new LinkedList<>();
for (int i = 0; i < len + 1; i++) {
while (!q.isEmpty() && preSum[i] <= preSum[q.getLast()]) {
q.removeLast();
}
while (!q.isEmpty() && preSum[i] >= preSum[q.getFirst()] + K) {
res = Math.min(res, i - q.removeFirst());
}
q.addLast(i);
}
return res < len + 1 ? res : -1;
}
}
312. 戳气球
dp[i][j] 表示开区间 (i,j) 内你能拿到的最多金币,注意是开区间,所以dp[i][i]和dp[i][i+1]初始都为0。
把数组左右多加一个数1。这样最后的答案就是dp[0][len-1]
k是最后戳破的气球。
class Solution {
public int maxCoins(int[] nums) {
int[] newNums = new int[nums.length + 2];
newNums[0] = newNums[nums.length + 1] = 1;//注意这里是1,不是0
System.arraycopy(nums, 0, newNums, 1, nums.length);
nums = newNums;
int[][] dp = new int[nums.length][nums.length];
for (int L = 3; L <= nums.length; L++) {
for (int i = 0; i + L - 1 < nums.length; i++) {
int j = i + L - 1;
for (int k = i + 1; k < j; k++) {
dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j]);
}
}
}
return dp[0][nums.length - 1];
}
}
224. 基本计算器
class Solution {
Map<String, Integer> inPriority, outPriority;
public Solution() {
inPriority = new HashMap<>();//栈内优先级
outPriority = new HashMap<>();//栈外优先级
inPriority.put("#", 0);
inPriority.put("(", 1);
inPriority.put("*", 5);
inPriority.put("/", 5);
inPriority.put("+", 3);
inPriority.put("-", 3);
inPriority.put(")", 6);
outPriority.put("#", 0);
outPriority.put("(", 6);
outPriority.put("*", 4);
outPriority.put("/", 4);
outPriority.put("+", 2);
outPriority.put("-", 2);
outPriority.put(")", 1);
}
public int calculate(String s) {
List<String> infix = new ArrayList<>();//中缀表达式
boolean flag = false;
int num = 0;
for (char c : s.toCharArray()) {
if (c == ' ') {
continue;
} else if (Character.isDigit(c)) {
flag = true;
num = num * 10 + (c - '0');
} else {
if (flag == true) {
infix.add(String.valueOf(num));
flag = false;
num = 0;
}
infix.add(String.valueOf(c));
}
}
if (flag == true) {
infix.add(String.valueOf(num));
}
List<String> rpn = getRpn(infix);
return (int) cal(rpn);
}
//中缀表达式转后缀
public List<String> getRpn(List<String> infix) {
Deque<String> stack = new ArrayDeque<>();
stack.push("#");
List<String> rpn = new ArrayList<>();
for (String str : infix) {
if ("(".equals(str) || ")".equals(str) || "+".equals(str) || "-".equals(str) ||
"*".equals(str) || "/".equals(str)) {
if (comparePriority(stack.peek(), str) == 1) {
stack.push(str);
} else if (comparePriority(stack.peek(), str) == 0) {
stack.pop();
} else {
while (comparePriority(stack.peek(), str) == -1) {
String z = stack.pop();
rpn.add(z);
}
if (")".equals(str)) {
stack.pop();
} else {
stack.push(str);
}
}
} else {
rpn.add(str);
}
}
while (!stack.isEmpty()) {
rpn.add(stack.pop());
}
rpn.remove(rpn.size() - 1);
return rpn;
}
private int comparePriority(String in, String out) {
if (outPriority.get(out) > inPriority.get(in)) {
return 1;
} else if (outPriority.get(out) == inPriority.get(in)) {
return 0;
} else {
return -1;
}
}
//后缀表达式求值
public double cal(List<String> s) {
Deque<Double> stack = new ArrayDeque<>();
for (int i = 0; i < s.size(); i++) {
double a, b;
if (s.get(i).equals("+")) {
b = stack.pop();
a = stack.pop();
stack.push(a + b);
continue;
} else if (s.get(i).equals("-")) {
b = stack.pop();
a = stack.pop();
stack.push(a - b);
continue;
} else if (s.get(i).equals("*")) {
b = stack.pop();
a = stack.pop();
stack.push(a * b);
continue;
} else if (s.get(i).equals("/")) {
b = stack.pop();
a = stack.pop();
stack.push(a / b);
continue;
} else {
stack.push(Double.valueOf(s.get(i)));
}
}
return stack.peek();
}
}
403. 青蛙过河
dp[i]代表可以以大小为step的一跳跳到第i个位置的集合。
边界:
dp[0]的集合只有一个元素0,只需要0跳就可以到达第一个位置。
转移方程:
遍历集合,那么下一跳可以跳的步数为step-1 ~ step+1,判断可以到达的位置是否有石块,如果有,就把那个石块对应的集合增加这一跳的步数。
最后判断最后一个石头的集合是否为空即可。如果不为空,说明可以跳到这里来。
class Solution {
public boolean canCross(int[] stones) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < stones.length; i++) {
map.put(stones[i], i);
}
Set<Integer>[] dp = new HashSet[stones.length];
for (int i = 0; i < dp.length; i++) {
dp[i] = new HashSet<>();
}
dp[0].add(0);
for (int i = 0; i < stones.length; i++) {
for (int j : dp[i]) {
for (int step = j - 1; step <= j + 1; step++) {
if (step > 0 && map.containsKey(stones[i] + step)) {
dp[map.get(stones[i] + step)].add(step);
}
}
}
}
return dp[stones.length - 1].size() > 0;
}
}
679. 24 点游戏
回溯,遍历所有情况,只要有一种情况满足24点,返回true。
注意作除法时,分母如果等于0,直接continue。
1e-6是浮点型误差,只要结果与24之差在这个范围内就可以看成等于24。
class Solution {
private boolean dfs(List<Double> list) {
if (list.size() == 1) {
return Math.abs(list.get(0) - 24) < 1e-6;
}
for (int i = 0; i < list.size(); i++) {
for (int j = 0; j < list.size(); j++) {
if (i == j) {
continue;
}
List<Double> newList = new ArrayList<>();
for (int k = 0; k < list.size(); k++) {
if (k != i && k != j) {
newList.add(list.get(k));
}
}
for (int k = 0; k < 4; k++) {
double a = list.get(i);
double b = list.get(j);
if (k == 0) {
newList.add(a + b);
} else if (k == 1) {
newList.add(a - b);
} else if (k == 2) {
newList.add(a * b);
} else if (k == 3) {
if (b == 0) {
continue;
} else {
newList.add(a / b);
}
}
if (dfs(newList)) {
return true;
} else {
newList.remove(newList.size() - 1);
}
}
}
}
return false;
}
public boolean judgePoint24(int[] nums) {
List<Double> list = new ArrayList<>();
for (int i : nums) {
list.add((double) i);
}
return dfs(list);
}
}
1044. 最长重复子串
class Solution {
long mod = (long)Math.pow(2, 37) - 1;
long a = 26;
int nums[];
int n;
public String longestDupSubstring(String S) {
nums = new int [S.length()];
for (int i = 0; i < S.length(); i ++) {
nums[i] = S.charAt(i) - 'a';
}
this.n = S.length();
int left = 1;
int right = n;
while (left != right) {
int mid = left + (right - left) / 2;
if (search(mid) != -1) left = mid + 1;
else right = mid;
}
int begin = search(left - 1);
if (begin != -1) return S.substring(begin, begin + left - 1);
return "";
}
public int search(int length) {
long cur = 0;
for (int i = 0; i < length; i ++) {
cur = (cur * a + nums[i]) % mod;
}
long aL = 1;
for (int i = 0; i < length; i++) {
aL = (aL * a ) % mod;
}
HashSet<Long> hs = new HashSet<>();
hs.add(cur);
for (int i = 1; i <= n - length; i ++) {
cur = (cur * a - aL * nums[i - 1] % mod + mod) % mod;
cur = (cur + nums[i + length - 1]) % mod;
if (hs.contains(cur)) return i;
hs.add(cur);
}
return -1;
}
}
410. 分割数组的最大值
dp[i][j]代表数组的前i个数分成m个子数组,和的最大值最小。
边界:
dp[0][0] = 0
转移方程:
class Solution {
public int splitArray(int[] nums, int m) {
int n = nums.length;
int[][] dp = new int[n + 1][m + 1];
for (int i = 0; i < dp.length; i++) {
Arrays.fill(dp[i], Integer.MAX_VALUE);
}
int[] preSum = new int[n + 1];
for (int i = 0; i < preSum.length - 1; i++) {
preSum[i + 1] = preSum[i] + nums[i];
}
dp[0][0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= Math.min(i, m); j++) {
for (int k = 0; k < i; k++) {
dp[i][j] = Math.min(dp[i][j], Math.max(dp[k][j - 1], preSum[i] - preSum[k]));
}
}
}
return dp[n][m];
}
}
60. 排列序列
class Solution {
boolean[] isVisited;
StringBuilder temp = new StringBuilder();
String res = null;
int count = 0;
private void dfs(int n, int k) {
if (temp.length() == n) {
count++;
if (count == k) {
res = temp.toString();
}
return;
}
for (int i = 1; i <= n; i++) {
if (isVisited[i] == false) {
isVisited[i] = true;
temp.append(i);
dfs(n, k);
if (res != null) {
return;
}
isVisited[i] = false;
temp.deleteCharAt(temp.length() - 1);
}
}
}
public String getPermutation(int n, int k) {
isVisited = new boolean[n + 1];
dfs(n, k);
return res;
}
}
37. 解数独
class Solution {
private boolean dfs(char[][] board) {
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (board[i][j] == '.') {
for (char c = '1'; c <= '9'; c++) {
if (check(i, j, c, board)) {
board[i][j] = c;
if (dfs(board)) {
return true;
} else {
board[i][j] = '.';
}
}
}
return false;
}
}
}
return true;
}
private boolean check(int row, int col, char c, char[][] board) {
int blockRow = 3 * (row / 3);
int blockCol = 3 * (col / 3);
for (int i = 0; i < board.length; i++) {
if (board[i][col] == c) {
return false;
}
if (board[row][i] == c) {
return false;
}
if (board[blockRow + i / 3][blockCol + i % 3] == c) {
return false;
}
}
return true;
}
public void solveSudoku(char[][] board) {
dfs(board);
}
}
30. 串联所有单词的子串
暴力法,先计算所有子串的长度,然后不断截取这个长度的字符串,判断是否和words匹配。
class Solution {
Map<String, Integer> map = new HashMap<>();
private boolean judge(String s, String[] words) {
Map<String, Integer> temp = new HashMap<>();
int wordLen = words[0].length();
for (int i = 0; i < words.length; i++) {
String sub = s.substring(i * wordLen, i * wordLen + wordLen);
temp.put(sub, temp.getOrDefault(sub, 0) + 1);
}
for (String key : temp.keySet()) {
if (!temp.get(key).equals(map.get(key))) {
return false;
}
}
return true;
}
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> res = new ArrayList<>();
if ("".equals(s)) {
return res;
}
for (String word : words) {
map.put(word, map.getOrDefault(word, 0) + 1);
}
int len = words.length * words[0].length();
for (int i = 0; i + len - 1 < s.length(); i++) {
if (judge(s.substring(i, i + len), words)) {
res.add(i);
}
}
return res;
}
}
132. 分割回文串 II
dp[i]代表以位置i结尾的字符串的最少分割次数。
边界:
dp[i] = i
转移方程:
遍历字符串,如果以位置i为结尾的字符串就是回文串,直接令dp[i] = 0,然后继续遍历。如果不是,那么遍历j从0到i-1,只要从j到i的子串是回文串,那么dp[i] = dp[j] +1,在遍历j的过程中取dp[i]的最小值。
class Solution {
private boolean isPar(String s) {
int lo = 0, hi = s.length() - 1;
while (lo < hi) {
if (s.charAt(lo++) != s.charAt(hi--)) {
return false;
}
}
return true;
}
public int minCut(String s) {
int[] dp = new int[s.length()];
for (int i = 0; i < s.length(); i++) {
dp[i] = i;
}
for (int i = 1; i < s.length(); i++) {
if (isPar(s.substring(0, i + 1))) {
dp[i] = 0;
continue;
}
for (int j = 0; j < i; j++) {
if (isPar(s.substring(j + 1, i + 1))) {
dp[i] = Math.min(dp[i], dp[j] + 1);
}
}
}
return dp[s.length() - 1];
}
}
57. 插入区间
判断两个区间是否相交的方法:
左端点的最大值小于等于右端点的最小值,则相交。
遍历原有区间,只要与新插入区间相交,则更新插入的区间,左端点更新为两个区间左端点的较小值,右端点更新为两个区间右端点的较大值,不加到结果集。
如果不相交,则可以加到结果集中。
最后将新区间加到结果集中,然后按照左端点从小到大排序。
class Solution {
//判断两个区间是否相交
private boolean check(int[] a, int[] b) {
return Math.max(a[0], b[0]) <= Math.min(a[1], b[1]);
}
public int[][] insert(int[][] intervals, int[] newInterval) {
List<int[]> res = new ArrayList<>();
for (int[] interval : intervals) {
if (check(interval, newInterval)) {
newInterval[0] = Math.min(interval[0], newInterval[0]);
newInterval[1] = Math.max(interval[1], newInterval[1]);
} else {
res.add(interval);
}
}
res.add(newInterval);
Collections.sort(res, (o1, o2) -> o1[0] - o2[0]);
return res.toArray(new int[res.size()][]);
}
}
301. 删除无效的括号
看到最少数量,优先想到使用bfs求解。
先判断整个字符串是否合法, 如果合法的话就将其加入到结果中。否则的话,进行下一步。
只删掉 1 个括号,考虑所有的删除情况,然后判断剩下的字符串是否合法,如果合法的话就将其加入到结果中。否则的话,进行下一步。
只删掉 2 个括号,考虑所有的删除情况,然后判断剩下的字符串是否合法,如果合法的话就将其加入到结果中。否则的话,进行下一步...
因为我们考虑删除最少的括号数,如果上边某一步出现了合法情况,后边的步骤就不用进行了。
要解决重复的问题,除了解法一在最后返回前用 set 去重。这里我们也可以在过程中使用一个 set ,在加入队列之前判断一下是否重复。
class Solution {
private boolean isValid(String s) {
int count = 0;
for (char c : s.toCharArray()) {
if (c == '(') {
count++;
} else if (c == ')') {
count--;
}
if (count < 0) {
return false;
}
}
return count == 0;
}
public List<String> removeInvalidParentheses(String s) {
List<String> res = new ArrayList<>();
Queue<String> q = new LinkedList<>();
Set<String> set = new HashSet<>();
q.offer(s);
while (true) {
int size = q.size();
for (int i = 0; i < size; i++) {
String front = q.poll();
if (isValid(front)) {
res.add(front);
} else if (res.size() == 0) {
for (int j = 0; j < front.length(); j++) {
if (s.charAt(j) == '(' || s.charAt(j) == ')') {
String sub = front.substring(0, j) + front.substring(j + 1);
if (!set.contains(sub)) {
q.offer(sub);
set.add(sub);
}
}
}
}
}
if (res.size() > 0) {
break;
}
}
return res;
}
}
1153. 字符串转化
当str1 == str2时可以转化
如果str2包含所有的26个字母,则没有了操作空间,因此不能转化
如果str1某两个下标i, j对应的字符相同,则必须要求str2中的相同下标也必须相同
如果判断以上情况后没有问题,则可以转化成功
class Solution {
private boolean check(String s) {
int count = 0;
Map<Character, Integer> map = new HashMap<>();
for (char c : s.toCharArray()) {
if (!map.containsKey(c)) {
count++;
map.put(c, 1);
}
}
return count < 26;
}
public boolean canConvert(String str1, String str2) {
if (str1.equals(str2)) {
return true;
}
if (!check(str2)) {
return false;
}
Map<Character, List<Integer>> map = new HashMap<>();
for (int i = 0; i < str1.length(); i++) {
if (!map.containsKey(str1.charAt(i))) {
map.put(str1.charAt(i), new ArrayList<>());
}
map.get(str1.charAt(i)).add(i);
}
for (List<Integer> list : map.values()) {
char c = str2.charAt(list.get(0));
for (int i = 1; i < list.size(); i++) {
if (c != str2.charAt(list.get(i))) {
return false;
}
}
}
return true;
}
}
212. 单词搜索 II
class Solution {
int[] X = {0, 0, 1, -1}, Y = {1, -1, 0, 0};
boolean[][] isVisit;
private boolean helper(char[][] board, int i, int j, int num, String word) {
if (num == word.length()) {
return true;
}
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length || isVisit[i][j] == true) {
return false;
}
if (board[i][j] == word.charAt(num)) {
isVisit[i][j] = true;
for (int k = 0; k < 4; k++) {
int newI = i + X[k], newJ = j + Y[k];
if (helper(board, newI, newJ, num + 1, word) == true) {
return true;
}
}
isVisit[i][j] = false;
return false;
} else {
return false;
}
}
public List<String> findWords(char[][] board, String[] words) {
isVisit = new boolean[board.length][board[0].length];
Set<String> set = new HashSet<>();
for (String word : words) {
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
for (int k = 0; k < board.length; k++) {
Arrays.fill(isVisit[k], false);
}
if (helper(board, i, j, 0, word)) {
set.add(word);
}
}
}
}
return new ArrayList<>(set);
}
}
1235. 规划兼职工作