做题最重要的就是找准方向,鉴宝里有一个术语叫开门,如何做题能一眼开门呢?我总结了一下力扣热门题目的开门思路。
题目 | 思路 | 描述 |
---|---|---|
739.每日温度 | 双端队列 | 从右向左构造单调递增的 |
647. 回文子串 | 动态规划 | 构建n*n的回文dp矩阵 |
738. 单调递增的数字 | 保留递增的高位序列,低位补9 | |
621. 任务调度器 | 贪心算法 | 按次数最高任务计算最小冷却时间,注意讨论和次数最高任务次数相同的任务 |
617. 合并二叉树 | 递归 | |
581. 最短无序连续子数组 | 排序 | 排序后与原数组不同的区域就是无序子数组 |
560. 和为K的子数组 | 滑动窗口 | 连续的子数组 |
543. 二叉树的直径 | dfs | 不能只求深度,注意边求深度边计算最大值,因为有子树直径为最大值 |
538. 把二叉搜索树转换为累加树 | 后序遍历求右子树和,深度优先根据父节点和右子树构建新树 | |
494. 目标和 | 动态规划(背包问题) | 由于初始和不超过1000可以使用背包和和方案数构建dp数组 |
461. 汉明距离 | 异或 ^ |
|
448. 找到所有数组中消失的数字 | 数组下标映射 | |
438. 找到字符串中所有字母异位词 | 滑动窗口 | |
437. 路径总和 III | dfs+前缀和 | 通过map记忆<前缀和,路径数>,注意要通过dfs回溯 |
416. 分割等和子集 | 动态规划 | 类似背包问题,存前缀和 |
406. 根据身高重建队列 | 身高升序 人数降序 | 排序后ki+前面已经入列的人数就是在新数组的位置, |
399. 除法求值 | bfs | 注意字符串相同不代表是同一个节点,所以需要将字符串映射成整数 |
394. 字符串解码 | 递归/栈 | |
347. 前 K 个高频元素 | Map + 堆 | |
338. 比特位计数 | 动态规划 | |
337. 打家劫舍 III | dfs | |
322. 零钱兑换 | 记忆化搜索 | 保存中间值 |
312. 戳气球 | 记忆化递归 | 将戳破改成填充更容易操作 |
309. 最佳买卖股票时机含冷冻期 | 动态规划 | |
301. 删除无效的括号 | dfs | |
300. 最长递增子序列 | 动态规划 | |
297. 二叉树的序列化与反序列化 | bfs | |
287. 寻找重复数 | 快慢指针 | 数组中下标和值的映射可以看作链表的指针,因此可以用快慢指针法找到重复的值 |
283. 移动零 | 滑动窗口 | 维护全是0的滑动窗口 |
279. 完全平方数 | 动态规划 | 想不到的地方是要求出n的所有完全平方数列表 |
240. 搜索二维矩阵Ⅱ | 缩减矩阵空间 | |
239. 滑动窗口最大值 | 双向队列 | |
238. 除自身以外数组的乘积 | 两次遍历保存左右乘积 | |
236. 二叉树的最近公共祖先 | 递归 | |
234. 回文链表 | 快慢指针+翻转链表 | |
226. 翻转二叉树 | 递归 | |
221. 最大正方形 | 动态规划 | dp(i, j)的值是由上、左、斜上方的最小值决定的 |
215. 数组中的第K个最大元素 | 大顶堆 | 删除K次 |
208. 实现 Trie (前缀树) | trie | |
207. 课程表 | bfs | 只有入度为0的节点才可以加入队列 |
206. 反转链表 | 指针 | |
200. 岛屿数量 | dfs | |
198. 打家劫舍 | 动态规划 | |
169. 多数元素 | 计数法 | |
160. 相交链表 | 交叉遍历 | |
155. 最小栈 | 额外的栈用来存储最小数 | |
152. 乘积最大子数组 | 动态规划 | 分别构建乘积最小dp数组和乘积最大数组 |
146. LRU 缓存机制 | hashmap+双向队列 | |
142. 环形链表 II | 快慢指针 | 相遇表示有环,从head到相遇点再相遇则是成环点 |
1. 两数之和 | hashmap | hashset的问题是无法判断重复数字 |
2. 两数相加 | 链表 | 由于是逆序存储 需要使用头尾指针 |
3. 无重复字符的最长子串 | hashset+滑动窗口 | |
4. 寻找两个正序数组的中位数 | 二分 | 每次比较可以排除k/2个元素,直到k=1或遍历到数组尾部 |
5. 最长回文子串 | 动态规划 | |
10. 正则表达式匹配 | 动态规划 | 重点在于状态转移方程 |
11. 盛最多水的容器 | 双指针 | |
15. 三数之和 | 三指针 | 左右指针用于收缩范围,中间指针用于遍历 |
17. 电话号码的字母组合 | 递归回溯 | |
19. 删除链表的倒数第 N 个结点 | 前后指针 | |
20. 有效的括号 | 栈 | |
22. 括号生成 | 递归回溯 | |
23. 合并K个升序链表 | 堆 | |
31. 下一个排列 | 从左向右找最小数,从右向左找较大数,交换位置后对低位排序 | |
32. 最长有效括号 | 动态规划 | 每两个元素检查一次 |
33. 搜索旋转排序数组 | 二分法 | |
34. 在排序数组中查找元素的第一个和最后一个位置 | 二分法 | 分别定义leftsearch和rightsearch |
39. 组合总和 | hashset+递归回溯 | |
42. 接雨水 | 双指针 | 难点在于想清楚左右边界哪个是可信的,通过优先选择较小的一侧遍历,使得答案满足短板效应 |
46. 全排列 | 递归回溯 | |
48. 旋转图像 | 利用旋转后的对称性 | |
49. 字母异位词分组 | hashmap | |
53. 最大子序和 | 动态规划 | |
55. 跳跃游戏 | 贪心+遍历 | |
56. 合并区间 | 排序 | |
62. 不同路径 | 动态规划 | |
64. 最小路径和 | 动态规划 | 初始首行和首列 |
70. 爬楼梯 | 动态规划 | |
72. 编辑距离 | 动态规划 | dp[i][j]保存编辑距离 |
76. 最小覆盖子串 | hashmap+滑动窗口 | |
78. 子集 | 递归回溯 | |
79. 单词搜索 | dfs | |
84. 柱状图中最大的矩形 | 单调栈 | 2个单调栈分别存储向左和向右能够勾勒的最远位置 |
85. 最大矩形 | 单调栈 | 按列转换为柱状图然后按照84题来做 |
96. 不同的二叉搜索树 | 动态规划+笛卡尔积 | |
98. 验证二叉搜索树 | 递归 | |
101. 对称二叉树 | 递归 | |
102. 二叉树的层序遍历 | bfs | |
104. 二叉树的最大深度 | dfs | |
105. 从前序与中序遍历序列构造二叉树 | 递归 | 利用中序和前序遍历头节点的性质 |
114. 二叉树展开为链表 | 前序遍历 | |
121. 买卖股票的最佳时机 | 记录最小值 | |
124. 二叉树中的最大路径和 | 递归 | |
128. 最长连续序列 | hashset | |
136. 只出现一次的数字 | 异或 | |
139. 单词拆分 | hashset+动态规划 |
2.1. [位运算]给定范围按位与
给定范围 [m, n],其中 0 <= m <= n <= 2147483647,返回此范围内所有数字的按位与(包含 m, n 两端点)
因为范围中只要有一次变化则该位就必为0,所以这题就是求高位无变化数,找到递增过程中一直没有变化过的高位1,如 :
110 和 100 结果为 100
11101110 和 10101110 则结果为 10000000
public static int rangeBitwiseAnd(int m, int n) {
int i = 0;
while (m != n){
m >>= 1;
n >>= 1;
i++;
}
return n<<i;
}
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
进阶:你可以实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案吗?
由于这道题时间和空间复杂度要求很高,因此必须考虑使用原地Hash。通过数组下标来标记相应整数是否在数组中存在。但是这种标记必须是在不干扰原有数据的情况下进行,因此可以将标记的位置数据置为-num[i],如果原有数据即为负数则置为N+1,避免干扰。
public int firstMissingPositive(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; ++i) {
if (nums[i] <= 0) {
nums[i] = n + 1;
}
}
for (int i = 0; i < n; ++i) {
int num = Math.abs(nums[i]);
if (num <= n) {
nums[num - 1] = -Math.abs(nums[num - 1]);
}
}
for (int i = 0; i < n; ++i) {
if (nums[i] > 0) {
return i + 1;
}
}
return n + 1;
}
给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
这道题的优化思路是:实际上我们不需要计算出以i为结尾所有可能子数组的和,而只需要得到位置i之前,前缀和为k-pre[i](pre为前缀和数组)的子数组个数,就可以得到以i为结尾所有和为k的连续子数组个数。
public int subarraySum(int[] nums, int k) {
int count = 0, pre = 0;
HashMap < Integer, Integer > mp = new HashMap < > ();
mp.put(0, 1);
for (int i = 0; i < nums.length; i++) {
pre += nums[i];
if (mp.containsKey(pre - k)) {
count += mp.get(pre - k);
}
mp.put(pre, mp.getOrDefault(pre, 0) + 1);
}
return count;
}
题目描述
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100",“5e2”,"-123",“3.1416"和”-1E-16"都表示数值。 但是"12e",“1a3.14”,“1.2.3”,"±5"和"12e+4.3"都不是。
import java.util.regex.Pattern;
public class Solution {
public static boolean isNumeric(char[] str) {
String pattern = "^[-+]?\\d*(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?$";
String s = new String(str);
return Pattern.matches(pattern,s);
}
}
^
和 $
框定正则表达式,它指引这个正则表达式对文本中的所有字符都进行匹配。如果省略这些标识,那么只要一个字符串中包含一个数字这个正则表达式就会进行匹配。如果仅包含 ^ ,它将匹配以一个数字开头的字符串。如果仅包含$ ,则匹配以一个数字结尾的字符串。例如如果不加这两个符号的话就会匹配120
时却匹配到11201
。
[-+]?
正负号后面的 ? 后缀表示这个负号是可选的,表示有0到1个负号或者正号
\\d*
\d
的含义和[0-9]一样。它匹配一个数字。后缀 * 指引它可匹配零个或者多个数字。多一个\
是为了字符串合法。
(?:\\.\\d*)?
(?: …)?
表示一个可选的非捕获型分组。* 指引这个分组会匹配后面跟随的0个或者多个数字的小数点。
(?:[eE][+\\-]?\d+)?
这是另外一个可选的非捕获型分组。它会匹配一个e(或E)、一个可选的正负号以及一个或多个数
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
时间复杂度:O(nlogn)最坏 O(n2)
桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
时间复杂度:最好O(nlogn) 最坏O(n2)
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
时间复杂度:O(n2)
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
时间复杂度:O(nlogn)
LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
int get(int key) - 如果键存在于缓存中,则获取键的值,否则返回 -1。
void put(int key, int value) - 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最久未使用 的键。
注意「项的使用次数」就是自插入该项以来对其调用 get 和 put 函数的次数之和。使用次数会在对应项被移除后置为 0 。
链接:https://leetcode-cn.com/problems/lfu-cache
要点:使用一个链表来保证相同使用频率的键值,最久未使用一定在链表头部。
Node removeNode() {
LinkedHashSet<Node> set = freqMap.get(min);
Node deadNode = set.iterator().next();
set.remove(deadNode);
return deadNode;
}
使用快慢指针,当p1和p2相遇后,让p1回到原点,p2在相遇点,且两者每次都前进一个节点,当两者再次相遇时,就是环的起点。
int FindBeginLoop(ListNode head){
ListNode p1 = head, p2 = head;
boolean loopExists = false;
if(head == null)
return false;
//判断环是否存在
while(p2.getNext()!=null&&p2.getNext().getNext()!=null){
p1 = p1.getNext();
p2 = p2.getNext().getNext();
if(p1==p2)
loopExists = true;
break;
}
//如果环存在,寻找环的起点
if(loopExists){
p1 = head;
while(p1!=p2){
p1 = p1.getNext();
p2 = p2.getNext();
}
return p1;
}
return null; //不存在
}
如何求环的长度?
让快慢指针从第一次相遇点继续运动,下一次相遇必然还是在上一次相遇点相遇,所以下一次相遇所用步数就是环的长度。
MyCalendar 有一个 book(int start, int end)方法。它意味着在start到end时间内增加一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数 x 的范围为, start <= x < end。
当 K 个日程安排有一些时间上的交叉时(例如K个日程安排都在同一时间内),就会产生 K 次预订。
每次调用 MyCalendar.book方法时,返回一个整数 K ,表示最大的 K 次预订。
本题有两个要点,一个是在每个时间段的头部记录进入次数,然后在尾部-1,这样就可以获得最大的重叠次数。
第二是treemap使用红黑树作为存储结构,可以保证key在遍历中的顺序(升序)。
static class MyCalendarThree {
TreeMap<Integer, Integer> delta;
public MyCalendarThree() {
delta = new TreeMap();
}
public int book(int start, int end) {
delta.put(start, delta.getOrDefault(start, 0) + 1);
delta.put(end, delta.getOrDefault(end, 0) - 1);
int active = 0, ans = 0;
for (int d: delta.values()) {
active += d;
if (active > ans) ans = active;
}
return ans;
}
}
线段树是一种非常灵活的数据结构,它可以用于解决多种范围查询问题,比如在对数时间内从数组中找到最小值、最大值、总和、最大公约数、最小公倍数等。
下面是一段构建总和线段树及使用的完整代码:
class Tree {
int[] tree;
int n;
public Tree(int[] nums) {
if (nums.length > 0) {
n = nums.length;
tree = new int[n * 2];
buildTree(nums);
}
}
/**
* 我们从叶节点开始,用输入数组的元素 a[0,1,...,n-1]初始化它们。
* 然后我们逐步向上移动到更高一层来计算父节点的和,直到最后到达线段树的根节点。
* @param nums data
*/
private void buildTree(int[] nums) {
for (int i = n, j = 0; i < 2 * n; i++, j++)
tree[i] = nums[j];
for (int i = n - 1; i > 0; --i)
tree[i] = tree[i * 2] + tree[i * 2 + 1];
}
void update(int pos, int val) {
pos += n; // 定位到叶子节点
tree[pos] = val;
while (pos > 0) {
int left = pos;
int right = pos;
if (pos % 2 == 0) {
right = pos + 1;
} else {
left = pos - 1;
}
// parent is updated after child is updated
tree[pos / 2] = tree[left] + tree[right];
pos /= 2;
}
}
public int sumRange(int l, int r) {
// get leaf with value 'l'
l += n;
// get leaf with value 'r'
r += n;
int sum = 0;
while (l <= r) {
if ((l % 2) == 1) {
// 当前左指针为其父节点的右子节点 因此无法通过其父节点计算总和。 直接相加,然后寻找右侧新的父节点遍历
sum += tree[l];
l++;
}
if ((r % 2) == 0) {
// 当前右指针为其父节点的左子节点 因此无法通过其父节点计算总和。 直接相加,然后寻找左侧新的父节点遍历
sum += tree[r];
r--;
}
l /= 2;
r /= 2;
}
return sum;
}
}
线段树利用了二叉树的性质,left = right+1 = parent *2
给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。
链接:https://leetcode-cn.com/problems/count-of-smaller-numbers-after-self
这道题目乍一看很简单,但是暴力求解的话,时间复杂度会达到O(n^2)。接下来容易想到用树来解决这个问题,那么如何构造这个树呢?
class TreeNode {
int val;
int count; // 保存子树中小于该节点的数量
TreeNode left; // <= 存放在左子树
TreeNode right; // > 存放在右子树
TreeNode(int val) {
this.val = val;
left = null;
right = null;
count = 0;
}
}
下面是递归构造树的函数。其中ret是存放counts的值,由于是求右侧元素个数所以初始状态应该从右向左遍历。
public TreeNode insert(TreeNode root, TreeNode node, Integer[] ret, int i) {
if (root == null) {
// 最后新节点会插入到叶子节点
root = node;
return root;
}
if (root.val >= node.val) {
// 当前节点大于等于待插入节点 比当前节点小的节点数+1
// 由于这个节点是插入后才开始计数 所以记录的是其左侧的情况 所以在后面累加的时候不用考虑重复计数的问题
root.count++;
root.left = insert(root.left, node, ret, i);
} else {
// 当前节点小于待插入节点
// 由于是从右向左遍历 说明当前节点及已遍历节点均为待插入节点右侧元素 所以ret+=count+1
// 然后继续查找当前节点的右子树
ret[i] += root.count + 1;
root.right = insert(root.right, node, ret, i);
}
return root;
}
另外这道题通常的解法是将右侧转换为有序数组,然后利用二分查找来构造新的有序数组,类似归并排序的思想,这里就不赘述了。
并查集是用来将属于同一组的元素所在的集合合并的数据结构,通过parent[]
来标记对应元素属于哪个集合。
class UnionSet{
private int[] parent;
private int size; // 总集合数
public UnionSet(int n){
size = n;
parent = new int[n];
for(int i = 0; i < n; i++){
parent[i] = i;
}
}
public int find(int i){
int target = parent[i];
if(target != i) parent[i] = find(target); // 减少深度
return parent[i];
}
public void union (int a, int b){
int targetA = find(a);
int targetB = find(b);
if(targetA != targetB) size--; // 每次合并减少一个集合
parent[targetB] = targetA;
}
public int count(){
return size;
}
}
在由 1 x 1 方格组成的 N x N 网格 grid 中,每个 1 x 1 方块由 /、\ 或空格构成。这些字符会将方块划分为一些共边的区域。(请注意,反斜杠字符是转义的,因此 \ 用 “\” 表示。)。返回区域的数目。
链接:https://leetcode-cn.com/problems/regions-cut-by-slashes
public int regionsBySlashes(String[] grid) {
if(grid == null || grid.length == 0 || grid[0].equals("")) return 0;
int width = grid[0].length();
int n = grid.length * width * 4;
/* 每一个1*1小格有可能分成四块 因此假设共有 4*n*n 个区域
\1/2
0/3\ 每个小格又编为四块区域
*/
UnionSet u = new UnionSet(n);
for(int i = 0; i < grid.length; i++){
for(int j = 0; j < width; j++){
char c = grid[i].charAt(j);
int indexZero = i*width*4 + j*4;
// 单元内合并
if(c == '/'){
u.union(indexZero, indexZero+1);
u.union(indexZero+2, indexZero+3);
}else if(c == '\\'){
u.union(indexZero, indexZero+3);
u.union(indexZero+1, indexZero+2);
}else{
u.union(indexZero, indexZero+1);
u.union(indexZero+1, indexZero+2);
u.union(indexZero+2, indexZero+3);
}
// 单元间合并 从左往右 从上到下
if(j+1 < width){
u.union(indexZero+2, indexZero+4);
}
if(i+1 < grid.length){
u.union(indexZero+3, indexZero+width*4+1);
}
}
}
return u.count();
}
实现原理:堆是一种完全二叉树,还要求子节点大于父节点(最小堆)。完全二叉树可以用数组存储,因此为了维护二叉树的性质,所有操作都在数组尾部进行,插入时先将元素插入数组尾部,然后递归向上比较交换(上浮);弹出时讲堆顶与数组尾部交换,然后弹出尾部,新的堆顶递归和子节点比较交换(下沉)。
常用代码为:
PriorityQueue<int[]> heap =
new PriorityQueue<int[]>((n1, n2) -> n1[1] - n2[1]);
给你一个 m x n 的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。
示例:给出如下 3x6 的高度图:
[
[1,4,3,1,3,2],
[3,2,1,3,2,4],
[2,3,3,2,3,1]
]
返回 4 。
链接:https://leetcode-cn.com/problems/trapping-rain-water-ii
这道题其实就是利用短板效应由外向内遍历寻找边界,首先构建最外层边界,放入最小堆中(便于寻找短板),然后搜索前后左右(去除已遍历),构筑新的边界,如果比原来的短板还要短就注水后放入堆中否则直接加入堆中。
public static int trapRainWater(int[][] heightMap) {
int width = heightMap[0].length;
int height = heightMap.length;
if (width<3 || height<3){return 0;}
// 标记
boolean[][] flags = new boolean[width][height];
// 最小堆
PriorityQueue<int[]> queue = new PriorityQueue<>( (o1,o2) -> o1[2]-o2[2] );
// 存放的三元组 (x,y,height) 注意 x指横坐标 y指纵坐标
// 初始化边界
for (int i = 0; i < width; i++){
queue.offer(new int[]{i, 0, heightMap[0][i]});
queue.offer(new int[]{i, height-1, heightMap[height-1][i]});
}
for (int i = 1; i < height-1; i++){
queue.offer(new int[]{0, i, heightMap[i][0]});
queue.offer(new int[]{width-1, i, heightMap[i][width-1]});
}
int sum = 0;
// 方向数组
int[] dirs = new int[]{-1, 0, 1, 0, -1};
while (!queue.isEmpty()){
int[] poll = queue.poll();
// 查找四个方向
for (int k = 0; k < 4; k++){
// 目标点坐标
int x = poll[0] + dirs[k];
int y = poll[1] + dirs[k+1];
// 判断合法性且应未被标记过 且不应是最外围节点
if (x>0 && x<width-1 && y>0 && y<height-1 && !flags[x][y]){
// 如果目标点低于外边界中的最低点则说明可以蓄水
if (poll[2] > heightMap[y][x]){
sum += poll[2] - heightMap[y][x];
// 注满水以后添加进去
queue.offer(new int[]{x, y, poll[2]});
}else {
queue.offer(new int[]{x, y, heightMap[y][x]});
}
flags[x][y] = true;
}
}
}
return sum;
}
给你一个按升序排序的整数数组 num(可能包含重复数字),请你将它们分割成一个或多个长度至少为 3 的子序列,其中每个子序列都由连续整数组成。
如果可以完成上述分割,则返回 true ;否则,返回 false 。
链接:https://leetcode-cn.com/problems/split-array-into-consecutive-subsequences
public boolean isPossible(int[] nums) {
// 因为原数组已经从小到大排列 所以可以依次遍历数组作为子序列的尾
// 使用最小堆存储子序列长度保证每次取出的都是最短的子序列长度
Map<Integer, PriorityQueue<Integer>> map = new HashMap<>();
for(int i : nums){
if(map.get(i) == null){
map.put(i, new PriorityQueue<Integer>());
}
if(map.get(i-1)==null || map.get(i-1).isEmpty()){
map.get(i).offer(1);
}else{
int l = map.get(i-1).poll();
map.get(i).offer(l+1);
}
}
for(Map.Entry<Integer, PriorityQueue<Integer>> entry : map.entrySet()){
if(!entry.getValue().isEmpty()){
if(entry.getValue().peek() < 3) return false;
}
}
return true;
}
深度优先搜索
给你一个 m * n 的网格,其中每个单元格不是 0(空)就是 1(障碍物)。每一步,您都可以在空白单元格中上、下、左、右移动。
如果您 最多 可以消除 k 个障碍物,请找出从左上角 (0, 0) 到右下角 (m-1, n-1) 的最短路径,并返回通过该路径所需的步数。如果找不到这样的路径,则返回 -1。
链接:https://leetcode-cn.com/problems/shortest-path-in-a-grid-with-obstacles-elimination
使用dfs搜索,同时使用visited矩阵存储访问状态防止重复访问,与一般的visited不同的是,本题的visited矩阵需要记录使用k的次数
class Solution {
private int min_step;
private int m,n;
private boolean[][][]vis;
public int shortestPath(int[][] grid, int k) {
m=grid.length;
n=grid[0].length;
min_step=Integer.MAX_VALUE;
if (k >= m + n - 3) return m + n - 2;
vis=new boolean[m][n][k+1];
dfs(grid,0,0,k,0);
return min_step==Integer.MAX_VALUE?-1:min_step;
}
private void dfs(int[][]grid,int x,int y,int k,int step){
if(step>=min_step) return ;
if(x==m-1&&y==n-1){
min_step=Math.min(min_step,step);
return ;
}
int dx[]={1,0,-1,0};
int dy[]={0,1,0,-1};
vis[x][y][k]=true;
for(int i=0;i<4;i++){
int a=x+dy[i],b=y+dx[i];
if(a>=0&&a<m&&b>=0&&b<n&&!vis[a][b][k]){
if(grid[a][b]==0) dfs(grid,a,b,k,step+1);
else if(k>0) dfs(grid,a,b,k-1,step+1);
}
}
}
}
这里有一个非负整数数组 arr,你最开始位于该数组的起始下标 start 处。当你位于下标 i 处时,你可以跳到 i + arr[i] 或者 i - arr[i]。
请你判断自己是否能够跳到对应元素值为 0 的 任一 下标处。
链接:https://leetcode-cn.com/problems/jump-game-iii
我做这道题时思考了visited数组的使用合法性,即无论dfs还是bfs如果该节点已经被遍历过,则之后无论何时遍历只会返回与先前遍历相同的结果。所以这些情况使用visited可以终结递归,否则则无法使用visited数组。
public boolean canReach1st(int[] arr, int start) {
boolean[] visited = new boolean[arr.length];
return dfs(arr, start, visited);
}
private boolean dfs(int[] arr, int curPos, boolean[] visited) {
if (curPos < 0 || curPos >= arr.length || visited[curPos]) return false;
if (arr[curPos] == 0) return true;
visited[curPos] = true;
return dfs(arr, curPos - arr[curPos], visited) || dfs(arr, curPos + arr[curPos], visited);
}
宽度优先搜索很好理解,最简单的代码结构就是下面这样:
Queue<T> q = new LinkedList<>();
q.offer(head);
while(!q.isEmpty()){
T tmp = q.poll;
q.offer(tmp.next);
}
使用bfs可以有效遍历同一层的所有树节点。
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if (root == null){
return "null";
}
StringBuilder sb = new StringBuilder();
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
sb.append(root.val+",");
while (!q.isEmpty()){
TreeNode tmp = q.poll();
if (tmp.right != null){ q.offer(tmp.right); sb.append(tmp.right.val+","); }
else { sb.append("null,"); }
if (tmp.left != null) { q.offer(tmp.left); sb.append(tmp.left.val+","); }
else { sb.append("null,"); }
}
return sb.toString();
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if (data.equals("null")) return null;
String[] arrs = data.split(",");
int i = 0;
Queue<TreeNode> q = new LinkedList<>();
TreeNode head = new TreeNode(Integer.valueOf(arrs[i++]));
q.offer(head);
for (; i < arrs.length && !q.isEmpty(); i+=2){
TreeNode tmp = q.poll();
if (!arrs[i].equals("null")){
TreeNode right = new TreeNode(Integer.valueOf(arrs[i]));
tmp.right = right;
q.offer(right);
}
if (!arrs[i+1].equals("null")){
TreeNode left = new TreeNode(Integer.valueOf(arrs[i+1]));
tmp.left = left;
q.offer(left);
}
}
return head;
}
链接:https://leetcode-cn.com/problems/shortest-path-visiting-all-nodes
这道题的特色是将访问过的节点以二进制的形式保存到搜索队列中。
static class Solution {
public int shortestPathLength(int[][] graph) {
int len=graph.length;
if(graph==null || graph.length==0){
return 0;
}
boolean[][] visited=new boolean[len][1<<len]; // 标记是否访问过,用于避免重复访问
int finishState=(1<<len)-1; // 用于检查是否访问完所有的节点,每个位代表一个节点的状态,形如1111
Queue<int[]> queue=new LinkedList<>(); // 队列里的数组,第一个记录的是标号,第二个是状态
for(int i=0; i<len; i++){
queue.offer(new int[]{i,1<<i});
}
int step=0;
while(!queue.isEmpty()){
// 逐层遍历
for(int i=queue.size(); i>0; i--){
int[] node=queue.poll();
if(finishState==node[1]){ // 如果标记的节点访问状态是结束,那么返回步长
return step;
}
for(int next:graph[node[0]]){
int nextState=node[1]|(1<<next); // 2个节点相或,标记着访问了这条边的2个点
if(visited[next][nextState]){
continue;
}
visited[next][nextState]=true;
queue.offer(new int[]{next,nextState}); // 将该节点和边的信息加入bfs对列
}
}
step++;
}
return step;
}
}
给出一些不同颜色的盒子,盒子的颜色由数字表示,即不同的数字表示不同的颜色。
你将经过若干轮操作去去掉盒子,直到所有的盒子都去掉为止。每一轮你可以移除具有相同颜色的连续 k 个盒子(k >= 1),这样一轮之后你将得到 k*k 个积分。
当你将所有盒子都去掉之后,求你能获得的最大积分和。
https://leetcode-cn.com/problems/text-justification/
动态规划需要列出dp数组的目的是为了减少计算重复子问题,因此本题的dp数组设计为 :
以i为首,长度为j的子序列的最大积分和——dp[i][j] ,相比较传统的以i和j为首尾的定义方式,这种定义更方便初始化和遍历。
状态转移时我们考虑两种情况,一是新增元素与原序列无关联,直接原序列积分和+1; 二是递归查找,去除与新增元素不相同颜色后的积分和(这里也利用dp数组来减少移除子序列时积分和的重复计算,在递归过程中如果去除之后新的积分和小于不去的则直接返回不去除的情况积分和)。然后选取两者中大的一个。
class Solution_2 {
public int removeBoxes(int[] boxes) {
if (boxes == null || boxes.length == 0) {
return 0;
}
int n = boxes.length;
int[][] dp = new int[n + 1][n + 1];
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= n - i; j++) {
if (i == 1) {
dp[i][j] = 1; // 1个字符得分为1
} else {
// 深度搜索
dp[i][j] = Math.max(dp[i - 1][j] + 1, deepSearch(boxes, j, i + j - 1, boxes[i + j - 1], 1, dp));
}
}
}
return dp[n][0];
}
private int deepSearch(int[] box, int start, int end, int cur, int count, int[][] dp) {
int max = dp[end - start][start] + count * count;
int preEnd = end;
for (int i = end - 1; i >= start; i--) {
// 连续的情况直接加
if (box[i] == cur && box[i + 1] == cur) {
count++;
int ret = dp[i - start][start] + count * count;
max = Math.max(max, ret);
preEnd = i;
} else if (box[i] == cur) { // 非连续的情况 需要加中间的得分
// 获得中间的得分
int ret = dp[preEnd - i - 1][i + 1];
// 继续深度搜索
ret += deepSearch(box, start, i, cur, count + 1, dp);
// 取最大值
max = Math.max(max, ret);
}
}
// 返回与任意位置结合后连续得分的最大值
return max;
}
}
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。
直接求n个骰子的概率不好求,但是1个骰子的概率易得为[1/6, 1/6, …, 1/6],而k+1个骰子的概率则是 curr[z] = sum(last[i] + new[j]) (i+j=z)
为了优化空间 我们可以假设骰子点数为0-5,这样方便直接使用数组求解。如果想要直观一点也可以用骰子(1-6)和为dp数组下标,然后取n及以后的元素为返回值。
public static double[] dicesProbability(int n) {
double[] dp = new double[6*n+1];
double each = 1.0d/6.0d;
for(int i = 1; i <= 6; i++){
dp[i] = 1.0d/6.0d;
}
for(int i = 2; i <= n; i++){
double[] tmp = new double[6*n+1]; // 防止同一轮次的结果相互影响
for(int j = 1; j <= 6; j++){
for(int z = 0; z < dp.length; z++){
if(dp[z] != 0){
tmp[z+j] += dp[z] * each;
}
}
}
dp = tmp;
}
double[] result = new double[6*n+1-n];
for(int i = 0; i < result.length; i++){
result[i] = dp[n+i];
}
return result;
}
给定 N,想象一个凸 N 边多边形,其顶点按顺时针顺序依次标记为 A[0], A[i], …, A[N-1]。
假设您将多边形剖分为 N-2 个三角形。对于每个三角形,该三角形的值是顶点标记的乘积,三角剖分的分数是进行三角剖分后所有 N-2 个三角形的值之和。
返回多边形进行三角剖分后可以得到的最低分。
链接:https://leetcode-cn.com/problems/minimum-score-triangulation-of-polygon
这道题如果直接去思考n边形的情况是很复杂的,不妨先思考三边形到四边形的变化过程,四边形实际上就是由两个三边形的值相加,推导到n边形,就是一个三边形+一个i边形+一个n-i边形(i>=3),其中三边形的顶点一定是由左端点+右端点+随机顶点组成的(因为我们假设新的多边形是由三个多边形组成的),通过遍历随机顶点i取最小值就可以得到多边形的最小值。
另外就是边界的处理,虽然几何上需要分类讨论n-i<3的情况(例如四边形只能分割出两个三边形,因为n=4时,i只能取2或者3),但是因为dp[i][i-1]、dp[i][i]恒为0,所以在代码中就忽略了这种情况。
1 —— 2
|~~~~ |
4 —— 3
public int minScoreTriangulation(int[] A) {
if(A==null || A.length<=0) return 0;
int N=A.length;
// dp[left][right] 代表left~right区间形成的环的最小得分值
int[][] dp=new int[N][N];
for (int len=3;len<=N;len++) { // 枚举长度,从3开始
for (int left=0;left<=N-len;left++) { //枚举左端点
int right=left+len-1;
//init
dp[left][right]=Integer.MAX_VALUE;
for (int i=left+1;i<right;i++) { // 枚举区间内的所有的点(不包括端点)),将环分割成左右两部分
dp[left][right]=Math.min(dp[left][right],dp[left][i]+dp[i][right]+A[i]*A[left]*A[right]);
}
}
}
return dp[0][N-1];
}
public static int maxProfit(int k, int[] prices) {
int n = prices.length;
if (n == 0 || k < 2){
return 0;
}
if (k >= n/2){
return getProfit_kn(prices); // k 足够大可以视为不限制次数
}else {
// 天数、第几次交易、是否持有股票(0不持股、1持股) 的现金
int[][][] dp = new int[n][k][2];
for(int i = 0; i < n; i++){
for (int j = 0; j < k; j++){
dp[i][j][1] = Integer.MIN_VALUE;
}
}
for (int i = 0; i < n; i++){
for (int j = 0; j < k; j++){
if (i == 0){
dp[i][j][0] = 0;
dp[i][j][1] = -prices[0];
}else {
if (j == 0){
dp[i][j][1] = Math.max(dp[i-1][j][1], - prices[i]);
}else {
// 状态转移 继续持有 或者 买入 (如当前交易日买入股票成本大于前一交易日买入股票怎买入)
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[n-1][k-1][0];
}
}
不限制次数
private static int getProfit_kn(int[] prices){
int n = prices.length;
int profit = 0;
for (int i = 1; i < n; i++){
if (prices[i] > prices[i-1]){
profit += prices[i] - prices[i-1];
}
}
return profit;
}