回溯
-
思想
-
什么时候需要回溯
当问题需要 “回头”,以此来查找出所有的解的时候,使用回溯算法。即满足结束条件或者发现不是正确路径的时候(走不通),要撤销选择,回退到上一个状态,继续尝试,直到找出所有解为止 -
步骤
①画出递归树,找到状态变量(回溯函数的参数),这一步非常重要※
②根据题意,确立结束条件
③找准选择列表(与函数参数相关),与第一步紧密关联※
④判断是否需要剪枝
⑤作出选择,递归调用,进入下一层
⑥撤销选择 -
例题
leetcode 46 全排列
leetcode 31 下一个排列
leetcode 22 括号生成
leetcode 78 子集生成
leetcode 90 子集II
DFS
1. 网格类DFS
2. 步骤
void dfs(int[][] grid, int r, int c) {
// 判断 base case
if (!inArea(grid, r, c)) {
return;
}
// 如果这个格子不是岛屿,直接返回
if (grid[r][c] != 1) {
return;
}
grid[r][c] = 2; // 将格子标记为「已遍历过」
// 访问上、下、左、右四个相邻结点
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
return 0 <= r && r < grid.length
&& 0 <= c && c < grid[0].length;
}
3. 例题
leetcode 200 岛屿数量
动态规划
子序列问题
- 分类
- 第一种思路模板是一个一维的 dp 数组:
在子数组array[0…i]中,以array[i]结尾的目标子序列(最长递增子序列)的长度是dp[i]。 - 第二种思路模板是一个二维的 dp 数组:
2.1 涉及两个字符串/数组时(比如最长公共子序列),dp 数组的含义如下:
子序列:在子数组arr1[0…i]和子数组arr2[0…j]中,我们要求的子序列(最长公共子序列)长度为dp[i][j]。
子数组:在子数组arr1[0…i]和子数组arr2[0…j]中,以arr1[i],arr2[j]结尾的子数组长度为dp[i][j]。
2.2 只涉及一个字符串/数组时(比如本文要讲的最长回文子序列),dp 数组的含义如下:
子序列:在子数组array[i…j]中,我们要求的子序列(最长回文子序列)的长度为dp[i][j]。注意对角线遍历。
子串:子数组array[i…j]中,数组i-j满足条件当dp[i+1][j-1]且array[i]=array[j]
- 例题
leetcode 53 最大子数组和
leetcode 300 最长递增子序列
leetcode 72 编辑距离
leetcode 1143 最长公共子序列
leetcode 718 最长重复子数组
leetcode 53 最长回文子序列
其他问题
背包问题
- 思想
给定一个背包容量target,再给定一个数组nums(物品),能否按一定方式选取nums中的元素得到target
注意:
1、背包容量target和物品nums的类型可能是数,也可能是字符串
2、target可能题目已经给出(显式),也可能是需要我们从题目的信息中挖掘出来(非显式)(常见的非显式target比如sum/2等)
3、选取方式有常见的一下几种:每个元素选一次/每个元素选多次/选元素进行排列组合 - 背包分类
常见的背包类型主要有以下几种:
1、0/1背包问题:每个元素最多选取一次
2、完全背包问题:每个元素可以重复选择
3、组合背包问题:背包中的物品要考虑顺序
4、分组背包问题:不止一个背包,需要遍历每个背包
而每个背包问题要求的也是不同的,按照所求问题分类,又可以分为以下几种:
1、最值问题:要求最大值/最小值
2、存在问题:是否存在…………,满足…………
3、组合问题:求所有满足……的排列组合
-
解题模板
首先是背包分类的模板:
1、0/1背包:外循环nums,内循环target,target倒序且target>=nums[i];
2、完全背包:外循环nums,内循环target,target正序且target>=nums[i];
3、组合背包(考虑顺序):外循环target,内循环nums,target正序且target>=nums[i];
4、分组背包:这个比较特殊,需要三重循环:外循环背包bags,内部两层循环根据题目的要求转化为1,2,3三种背包类型的模板
然后是问题分类的模板:
1、最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums);
2、存在问题(bool):dp[i]=dp[i]||dp[i-num];
3、组合问题:dp[i]+=dp[i-num]; -
解题步骤
-
确定dp数组以及下标的含义
-
确定递推公式
-
确定遍历顺序
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
-
-
模板
int dp[N+1][amount+1] dp[0][..] = 0 dp[..][0] = 1 for i in [1..N]: for j in [1..amount]: 把物品 i 装进背包, 不把物品 i 装进背包 return dp[N][amount]
贪心算法
-
思想
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。 -
时机
刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。 -
步骤
将问题分解为若干个子问题
找出适合的贪心策略
求解每一个子问题的最优解
将局部最优解堆叠成全局最优解 -
例题
leetcode 122. 买卖股票的最佳时机 II
局部最优:收集每天的正利润-> 整体最优 :求得最大利润
leetcode 135 分发糖果
局部最优:对于右评分大于左评分 糖果数加一 对于左评分大于右评分 糖果数取现有糖果数和右糖果数加一的最大值 -> 整体最优
leetcode 55 跳跃游戏
局部最优: 每次跳跃取最大的覆盖范围 -> 整体最优: 整体跳跃的最大覆盖范围
leetcode 435 无重叠子区间
右边界排序之后,局部最优:优先选右边界小的区间,所以从左向右遍历,留给下一个区间的空间大一些,从而尽量避免交叉。全局最优:选取最多的非交叉区间。
leetcode 56 合并区间
按照左边界排序,排序之后局部最优:每次合并都取最大的右边界,这样就可以合并更多的区间了,整体最优:合并所有重叠的区间。
BFS
1. 思想
把一些问题抽象成图,从一个点开始,向四周开始扩散。一般来说,我们写 BFS 算法都是用「队列」这种数据结构,每次将一个节点周围的所有节点加入队列。
2.场景
问题的本质就是让你在一幅「图」中找到从起点start到终点target的最近距离
字符串相关
滑动窗口
- 思想
- 场景
- 步骤
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
字符串相加
二叉树
-
思想
二叉树题目的递归解法可以分两类思路,第一类是遍历一遍二叉树得出答案,第二类是通过分解问题计算出答案,这两类思路分别对应着 回溯算法核心框架 和 动态规划核心框架。
是否可以通过遍历一遍二叉树得到答案?如果不能的话,是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案? -
思考
关于回溯遍历问题:
// 二叉树遍历函数
void traverse(TreeNode root) {
if (root == null) {
return;
}
// 前序遍历位置,记录节点值
path.append(root.val);
if (root.left == null && root.right == null) {
// 到达叶子节点,累加路径和
res += Integer.parseInt(path.toString());
}
// 二叉树递归框架,遍历左右子树
traverse(root.left);
traverse(root.right);
// 后续遍历位置,撤销节点值
path.deleteCharAt(path.length() - 1);
}
关于后序遍历:
前序位置的代码只能从函数参数中获取父节点传递来的数据,而后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。
一旦你发现题目和子树有关,那大概率要给函数设置合理的定义和返回值,在后序位置写代码了。
关于层序遍历
// 输入一棵二叉树的根节点,层序遍历这棵二叉树
void levelTraverse(TreeNode root) {
if (root == null) return;
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
// 从上到下遍历二叉树的每一层
while (!q.isEmpty()) {
int sz = q.size();
// 从左到右遍历每一层的每个节点
for (int i = 0; i < sz; i++) {
TreeNode cur = q.poll();
// 将下一层节点放入队列
if (cur.left != null) {
q.offer(cur.left);
}
if (cur.right != null) {
q.offer(cur.right);
}
}
}
}
- 框架
void traverse(TreeNode root) {
if (root == null) {
return;
}
// 前序位置
traverse(root.left);
// 中序位置
traverse(root.right);
// 后序位置
}
/* 迭代遍历数组 */
void traverse(int[] arr) {
for (int i = 0; i < arr.length; i++) {
}
}
/* 递归遍历数组 */
void traverse(int[] arr, int i) {
if (i == arr.length) {
return;
}
// 前序位置
traverse(arr, i + 1);
// 后序位置
}
/* 迭代遍历单链表 */
void traverse(ListNode head) {
for (ListNode p = head; p != null; p = p.next) {
}
}
/* 递归遍历单链表 */
void traverse(ListNode head) {
if (head == null) {
return;
}
// 前序位置
traverse(head.next);
// 后序位置
}
- 例题
分解问题
leetcode 104. 二叉树的最大深度
leetcode 110. 平衡二叉树
leetcode 543. 二叉树的直径
leetcode 101.对称二叉树
leetcode 226.翻转二叉树
leetcode 124.二叉树的最大路径和
回溯遍历
leetcode 129. 求根节点到叶节点数字之和
位运算
-
思想
a ^ a = 0;
a ^ b = b ^ a;
a ^ b ^ c = a ^ (b ^ c) = (a ^ b) ^ c
a ^ 0 = a
1 ^ 0 = 1 -
步骤
考虑其中是否需要一下几个-
大小写互换
('d' ^ ' ') = 'D' ('D' ^ ' ') = 'd'
-
不用临时变量交换两个数
int a = 1, b = 2; a ^= b; b ^= a; a ^= b;
-
消除数字n的二进制表示中最后一个1
n & (n-1)
-
-
例题
leetcode 67. 二进制求和
leetcode 136.只出现一次的数字
leetcode 191.位1的个数
leetcode 231. 2的幂
剑指 Offer 53 - II. 0~n-1中缺失的数字
剑指 Offer 65. 不用加减乘除做加法
剑指 Offer II 003. 前 n 个数字二进制中 1 的个数