BFS
广度优先搜索一层一层地进行遍历,每层遍历都以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。
第一层:
- 0 -> {6,2,1,5}
第二层:
- 6 -> {4}
- 2 -> {}
- 1 -> {}
- 5 -> {3}
第三层:
- 4 -> {}
- 3 -> {}
每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <= dj。利用这个结论,可以求解最短路径等 最优解 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径,无权图是指从一个节点到另一个节点的代价都记为 1。
在程序实现 BFS 时需要考虑以下问题:
- 队列:用来存储每一轮遍历得到的节点;
- 标记:对于遍历过的节点,应该将它标记,防止重复遍历。
1. 计算在网格中从原点到特定点的最短路径长度
Leetcode-1091. 二进制矩阵中的最短路径
在一个 N × N 的方形网格中,每个单元格有两种状态:空(0)或者阻塞(1)。
一条从左上角到右下角、长度为 k 的畅通路径,由满足下述条件的单元格 C_1, C_2, …, C_k 组成:
- 相邻单元格 C_i 和 C_{i+1} 在八个方向之一上连通(此时,C_i 和 C_{i+1} 不同且共享边或角)
- C_1 位于 (0, 0)(即,值为 grid[0][0])
- C_k 位于 (N-1, N-1)(即,值为 grid[N-1][N-1])
- 如果 C_i 位于 (r, c),则 grid[r][c] 为空(即,grid[r][c] == 0)
返回这条从左上角到右下角的最短畅通路径的长度。如果不存在这样的路径,返回 -1 。
总结:0 表示可以经过某个位置,求解从左上角到右下角的最短路径长度。
提示:
1 <= grid.length == grid[0].length <= 100
grid[i][j] 为 0 或 1
解法:
- Java
用于求最短路径,非常重要
class Solution {
private int[][] directions = {
{
-1, 0}, {
-1, 1}, {
-1, -1}, {
0, -1}, {
0, 1}, {
1, 0}, {
1, 1}, {
1, -1}};
public int shortestPathBinaryMatrix(int[][] grid) {
int m = grid.length, n = grid[0].length;
if (grid[0][0]==1 || grid[m-1][n-1]==1) return -1;
Queue<int[]> q = new LinkedList<>();
q.add(new int[]{
0,0});
grid[0][0] = 1;
int res = 1;
while (!q.isEmpty()) {
int size = q.size();
for (int i=0;i<size;i++) {
int[] pos = q.remove();
if (pos[0]==m-1 && pos[1]==n-1) return res;
for (int[] d: directions) {
int nextX = pos[0]+d[0], nextY = pos[1]+d[1];
if (nextX>=0 && nextX<m && nextY>=0 && nextY<n && grid[nextX][nextY]==0) {
q.add(new int[]{
nextX, nextY});
grid[nextX][nextY] = 1;
}
}
}
res++;
}
return -1;
}
}
2. 组成整数的最小平方数数量
Leetcode-279. 完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
示例 1:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.
示例 2:
输入: n = 13
输出: 2
解释: 13 = 4 + 9.
解法:
动态规划
- Java
class Solution {
public int numSquares(int n) {
if (n<=1) return n;
List<Integer> lst = generateSquareList(n);
int[] dp = new int[n+1];
dp[1] = 1;
for (int i=2;i<=n;i++) {
int min = Integer.MAX_VALUE;
for (int square:lst) {
if (square <= i) min = Math.min(min, 1+dp[i-square]);
}
dp[i] = min;
}
return dp[n];
}
private List<Integer> generateSquareList(int n) {
List<Integer> lst = new ArrayList<>();
int diff = 3;
int square = 1;
while (square<=n) {
lst.add(square);
square += diff;
diff += 2;
}
return lst;
}
}
广度优先搜索
- Java
可以将每个整数看成图中的一个节点,如果两个整数之差为一个平方数,那么这两个整数所在的节点就有一条边。
要求解最小的平方数数量,就是求解从节点 n 到节点 0 的最短路径。
marked可以省下很多的时间,如果不使用marked同样可以ac,但是效率慢很多。
class Solution {
public int numSquares(int n) {
List<Integer> lst = generateSquareList(n);
boolean[] marked = new boolean[n+1];
marked[n] = true;
int res = 0;
Queue<Integer> q = new LinkedList<>();
q.add(n);
while (!q.isEmpty()) {
int size = q.size();
res++;
while (size-- > 0) {
int cur = q.remove();
for (int square: lst) {
int next = cur - square;
if (next<0) break;
if (next==0) return res;
if (marked[next]) continue;
q.add(next);
marked[next] = true;
}
}
}
return n;
}
private List<Integer> generateSquareList(int n) {
List<Integer> lst = new ArrayList<>();
int diff = 3;
int square = 1;
while (square<=n) {
lst.add(square);
square += diff;
diff += 2;
}
return lst;
}
}
3. 最短单词路径
Leetcode-127. 单词接龙
给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:
每次转换只能改变一个字母。
转换过程中的中间单词必须是字典中的单词。
说明:
- 如果不存在这样的转换序列,返回 0。
- 所有单词具有相同的长度。
- 所有单词只由小写字母组成。
- 字典中不存在重复的单词。
- 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
示例 1:
输入:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]
输出: 5
解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog",
返回它的长度 5。
示例 2:
输入:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]
输出: 0
解释: endWord "cog" 不在字典中,所以无法进行转换。
解法:
- Java
https://leetcode-cn.com/problems/word-ladder/solution/dan-ci-jie-long-by-leetcode/
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
wordList.add(beginWord);
int n = wordList.size();
int start = n-1, end = 0;
while (end<n && !wordList.get(end).equals(endWord)) end++;
if (end==n) return 0;
List<Integer>[] simListAry = getSimListAry(wordList);
return getShortestPath(start, end, simListAry);
}
private List<Integer>[] getSimListAry(List<String> wordList) {
int n = wordList.size();
List<Integer>[] simListAry = new ArrayList[n];
for (int i=0;i<n;i++) {
simListAry[i] = new ArrayList<>();
for (int j=0;j<n;j++) {
if (isCorrect(wordList.get(i), wordList.get(j)) && j!=i) simListAry[i].add(j);
}
}
return simListAry;
}
private boolean isCorrect(String str1, String str2) {
int count = 0;
for (int i=0;i<str1.length();i++) {
if (str1.charAt(i)!=str2.charAt(i)) count++;
}
return count==1;
}
private int getShortestPath(int start, int end, List<Integer>[] simListAry) {
int res = 1;
Queue<Integer> q = new LinkedList<>();
boolean[] marked = new boolean[simListAry.length];
q.add(start);
marked[start] = true;
while (!q.isEmpty()) {
int size = q.size();
res++;
while (size-- > 0) {
int idx = q.remove();
for (int next: simListAry[idx]) {
if (next==end) return res;
if (marked[next]) continue;
marked[next] = true;
q.add(next);
}
}
}
return 0;
}
}
DFS
广度优先搜索一层一层遍历,每一层得到的所有新节点,要用队列存储起来以备下一层遍历的时候再遍历。
而深度优先搜索在得到一个新节点时立即对新节点进行遍历:从节点 0 出发开始遍历,得到到新节点 6 时,立马对新节点 6 进行遍历,得到新节点 4;如此反复以这种方式遍历新节点,直到没有新节点了,此时返回。返回到根节点 0 的情况是,继续对根节点 0 进行遍历,得到新节点 2,然后继续以上步骤。
从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 可达性 问题。
在程序实现 DFS 时需要考虑以下问题:
- 栈:用栈来保存当前节点信息,当遍历新节点返回时能够继续遍历当前节点。可以使用递归栈。
- 标记:和 BFS 一样同样需要对已经遍历过的节点进行标记。
1. 查找最大的连通面积
Leetcode-695. 岛屿的最大面积
给定一个包含了一些 0 和 1的非空二维数组 grid , 一个 岛屿 是由四个方向 (水平或垂直) 的 1 (代表土地) 构成的组合。你可以假设二维矩阵的四个边缘都被水包围着。
找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为0。)
示例 1:
[[0,0,1,0,0,0,0,1,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,1,1,0,1,0,0,0,0,0,0,0,0],
[0,1,0,0,1,1,0,0,1,0,1,0,0],
[0,1,0,0,1,1,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,0,0,0,0,0,0,1,1,0,0,0,0]]
对于上面这个给定矩阵应返回 6。注意答案不应该是11,因为岛屿只能包含水平或垂直的四个方向的‘1’。
示例 2:
[[0,0,0,0,0,0,0,0]]
对于上面这个给定的矩阵, 返回 0。
注意: 给定的矩阵grid 的长度和宽度都不超过 50。
解法:
- Java
对于矩阵类型,一定要遍历每个进行backtrack的位置,找到开始backtrack的点
dfs返回int类型案例
class Solution {
private int m, n;
private int[][] directions = {
{
-1, 0}, {
1, 0}, {
0, -1}, {
0, 1}};
public int maxAreaOfIsland(int[][] grid) {
if (grid==null || grid.length==0) return 0;
m = grid.length;
n = grid[0].length;
int maxArea = 0;
for (int i=0;i<m;i++) {
for (int j=0;j<n;j++) {
maxArea = Math.max(maxArea, dfs(grid, i, j));
}
}
return maxArea;
}
private int dfs(int[][] grid, int i, int j) {
if (i<0 || i>=m || j<0 || j>=n || grid[i][j]==0) return 0;
grid[i][j] = 0;
int area = 1;
for (int[] d: directions) {
area += dfs(grid, i+d[0], j+d[1]);
}
return area;
}
}
2. 矩阵中的连通分量数目
Leetcode-200. 岛屿数量
给定一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
示例 1:
输入:
11110
11010
11000
00000
输出: 1
示例 2:
输入:
11000
11000
00100
00011
输出: 3
解法:
- Java
dfs返回int类型案例
class Solution {
private int m, n;
private int[][] directions = {
{
-1, 0}, {
1, 0}, {
0, -1}, {
0, 1}};
public int numIslands(char[][] grid) {
if (grid==null || grid.length==0) return 0;
m = grid.length;
n = grid[0].length;
int num = 0;
for (int i=0;i<m;i++) {
for (int j=0;j<n;j++) {
num += dfs(grid, i, j);
}
}
return num;
}
private int dfs(char[][] grid, int i, int j) {
if (i<0 || i>=m || j<0 || j>=n || grid[i][j]=='0') return 0;
grid[i][j] = '0';
for (int[] d: directions) {
dfs(grid, i+d[0], j+d[1]);
}
return 1;
}
}
class Solution {
private int m, n;
private int[][] directions = {
{
-1, 0}, {
1, 0}, {
0, -1}, {
0, 1}};
public int numIslands(char[][] grid) {
if (grid==null || grid.length==0) return 0;
m = grid.length;
n = grid[0].length;
int num = 0;
for (int i=0;i<m;i++) {
for (int j=0;j<n;j++) {
if (grid[i][j]=='1') {
dfs(grid, i, j);
num++;
}
}
}
return num;
}
private void dfs(char[][] grid, int i, int j) {
if (i<0 || i>=m || j<0 || j>=n || grid[i][j]=='0') return;
grid[i][j] = '0';
for (int[] d: directions) {
dfs(grid, i+d[0], j+d[1]);
}
}
}
3. 好友关系的连通分量数目
547. 朋友圈
班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。
给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
示例 1:
输入:
[[1,1,0],
[1,1,0],
[0,0,1]]
输出: 2
说明:已知学生0和学生1互为朋友,他们在一个朋友圈。
第2个学生自己在一个朋友圈。所以返回2。
示例 2:
输入:
[[1,1,0],
[1,1,1],
[0,1,1]]
输出: 1
说明:已知学生0和学生1互为朋友,学生1和学生2互为朋友,所以学生0和学生2也是朋友,所以他们三个在一个朋友圈,返回1。
注意:
- N 在[1,200]的范围内。
- 对于所有学生,有M[i][i] = 1。
- 如果有M[i][j] = 1,则有M[j][i] = 1。
解法:
- Java
dfs一般都是返回空值
class Solution {
private int n;
public int findCircleNum(int[][] M) {
if (M==null || M.length==0) return 0;
n = M.length;
int num = 0;
boolean[] marked = new boolean[n];
for (int i=0;i<n;i++) {
if (!marked[i]) {
dfs(M, i, marked);
num++;
}
}
return num;
}
private void dfs(int[][] M, int i, boolean[] marked) {
marked[i] = true;
for (int j=0;j<n;j++) {
if (M[i][j]==1 && !marked[j]) dfs(M, j, marked);
}
}
}
4. 填充封闭区域
Leetcode-130. 被围绕的区域
给定一个二维的矩阵,包含 ‘X’ 和 ‘O’(字母 O)。
找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。
示例:
X X X X
X O O X
X X O X
X O X X
运行你的函数后,矩阵变为:
X X X X
X X X X
X X X X
X O X X
解释:
被围绕的区间不会存在于边界上,换句话说,任何边界上的 ‘O’ 都不会被填充为 ‘X’。 任何不在边界上,或不与边界上的 ‘O’ 相连的 ‘O’ 最终都会被填充为 ‘X’。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
解法:
- Java
dfs返回空值
由于边界上的O是不会被填充的,所以只要和边界上连接的O就都不会被填充,从边界上向内部进行搜索,找到的O都改成*,在进行整体遍历,所有*
都是O,剩余的O就是没有与边界连接的,变成X
class Solution {
private int m, n;
private int[][] directions = {
{
-1, 0}, {
1, 0}, {
0, -1}, {
0, 1}};
public void solve(char[][] board) {
if (board==null || board.length==0) return;
m = board.length;
n = board[0].length;
for (int i=0;i<m;i++) {
dfs(board, i, 0);
dfs(board, i, n-1);
}
for (int j=0;j<n;j++) {
dfs(board, 0, j);
dfs(board, m-1, j);
}
for (int i=0;i<m;i++) {
for (int j=0;j<n;j++) {
if (board[i][j] == '*') board[i][j] = 'O';
else if (board[i][j] == 'O') board[i][j] = 'X';
}
}
return;
}
private void dfs(char[][]board, int i, int j) {
if (i<0 || i>=m || j<0 || j>=n || board[i][j]!='O') return;
board[i][j] = '*';
for (int[] d: directions) {
dfs(board, i+d[0], j+d[1]);
}
}
}
5. 能到达的太平洋和大西洋的区域
Leetcode-417. 太平洋大西洋水流问题
给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。
规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。
请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。
提示:
- 输出坐标的顺序不重要
- m 和 n 都小于150
示例:
给定下面的 5x5 矩阵:
太平洋 ~ ~ ~ ~ ~
~ 1 2 2 3 (5) *
~ 3 2 3 (4) (4) *
~ 2 4 (5) 3 1 *
~ (6) (7) 1 4 5 *
~ (5) 1 1 2 4 *
* * * * * 大西洋
返回:
[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (上图中带括号的单元).
解法:
- Java
跟上题类似,dfs的作用就是让能调用到dfs的位置标记为true,用两个boolean数组标记能访问到P和A的位置,都为true则添加到list中
class Solution {
private int m, n;
private int[][] directions = {
{
-1, 0}, {
1, 0}, {
0, -1}, {
0, 1}};
public List<List<Integer>> pacificAtlantic(int[][] matrix) {
List<List<Integer>> res = new ArrayList<>();
if (matrix==null || matrix.length==0) return res;
m = matrix.length;
n = matrix[0].length;
boolean[][] canReachP = new boolean[m][n];
boolean[][] canReachA = new boolean[m][n];
for (int i=0;i<m;i++) {
dfs(matrix, i, 0, canReachP);
dfs(matrix, i, n-1, canReachA);
}
for (int j=0;j<n;j++) {
dfs(matrix, 0, j, canReachP);
dfs(matrix, m-1, j, canReachA);
}
for (int i=0;i<m;i++) {
for (int j=0;j<n;j++){
if (canReachP[i][j] && canReachA[i][j]) res.add(Arrays.asList(i, j));
}
}
return res;
}
private void dfs(int[][] matrix, int i, int j, boolean[][] canReach) {
if (canReach[i][j] || i<0 || i>=m || j<0 || j>=n) return;
canReach[i][j] = true;
int r = 0, c = 0;
for (int[] d: directions) {
r = i+d[0];
c = j+d[1];
if ( r<0 || r>=m || c<0 || c>=n ||matrix[i][j] > matrix[r][c]) continue;
dfs(matrix, r, c, canReach);
}
}
}
Backtracking
Backtracking(回溯)属于 DFS。
- 普通 DFS 主要用在 可达性问题 ,这种问题只需要执行到特点的位置然后返回即可。
- 而 Backtracking 主要用于求解 排列组合 问题,例如有 { ‘a’,‘b’,‘c’ } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。
因为 Backtracking 不是立即返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题:
- 在访问一个新元素进入新的递归调用时,需要将新元素标记为已经访问,这样才能在继续递归调用时不用重复访问该元素;
- 但是在递归返回时,需要将元素标记为未访问,因为只需要保证在一个递归链中不同时访问一个元素,可以访问已经访问过但是不在当前递归链中的元素。
1. 数字键盘组合
Leetcode-17. 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
说明:
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
解法:
- Java
class Solution {
private static final String[] KEYS = {
"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public List<String> letterCombinations(String digits) {
List<String> res = new ArrayList<>();
if (digits==null || digits.length()==0) return res;
backtracking(new StringBuilder(), digits, res);
return res;
}
private void backtracking(StringBuilder sb, String digits, List<String> res) {
if (sb.length()==digits.length()) {
res.add(sb.toString());
return;
}
int curDigit = digits.charAt(sb.length())-'0';
String letter = KEYS[curDigit];
for (char c: letter.toCharArray()) {
sb.append(c);
backtracking(sb, digits, res);
sb.deleteCharAt(sb.length()-1);
}
}
}
2. IP 地址划分
Leetcode-93. 复原IP地址
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
示例:
输入: "25525511135"
输出: ["255.255.11.135", "255.255.111.35"]
解法:
- Java
class Solution {
public List<String> restoreIpAddresses(String s) {
List<String> res = new ArrayList<>();
backtracking(0, new StringBuilder(), res, s);
return res;
}
private void backtracking(int k, StringBuilder sb, List<String> res, String s) {
// ip总共四部分
if (k==4 || s.length()==0) {
if (k==4 && s.length()==0) res.add(sb.toString());
return;
}
// 每次长度可以是1-3个字符
for (int i=0;i<s.length() && i<=2;i++) {
if (s.charAt(0)=='0' && i!=0) break;
String part = s.substring(0, i+1);
if (Integer.valueOf(part)<=255) {
if (k!=0) part = "."+part;
sb.append(part);
backtracking(k+1, sb, res, s.substring(i+1));
sb.delete(sb.length()-part.length(), sb.length());
}
}
}
}
3. 在矩阵中寻找字符串
Leetcode-79. 单词搜索
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
board =
[
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
]
给定 word = "ABCCED", 返回 true.
给定 word = "SEE", 返回 true.
给定 word = "ABCB", 返回 false.
解法:
- Java
很重要的案例,backtrack返回boolean类型
class Solution {
private int[][] directions = {
{
-1, 0}, {
1, 0}, {
0, -1}, {
0, 1}};
private int m, n;
public boolean exist(char[][] board, String word) {
// if (word==null || word.length()==0) return true;
if (board==null || board.length==0 || board[0].length==0) return false;
m = board.length;
n = board[0].length;
boolean[][] marked = new boolean[m][n];
for (int i=0;i<m;i++) {
for (int j=0;j<n;j++) {
if (backtracking(0, i, j, marked, board, word)) return true;
}
}
return false;
}
private boolean backtracking(int len, int r, int c, boolean[][] marked, final char[][] board, final String word) {
if (len==word.length()) return true;
if (r<0 || r>=m || c<0 || c>=n || marked[r][c] || board[r][c]!=word.charAt(len)) return false;
marked[r][c] = true;
for (int[] d: directions) {
if (backtracking(len+1, r+d[0], c+d[1], marked, board, word)) return true;
}
marked[r][c] = false;
return false;
}
}
4. 输出二叉树中所有从根到叶子的路径
Leetcode-257. 二叉树的所有路径
给定一个二叉树,返回所有从根节点到叶子节点的路径。
说明: 叶子节点是指没有子节点的节点。
示例:
输入:
1
/ \
2 3
\
5
输出: ["1->2->5", "1->3"]
解释: 所有根节点到叶子节点的路径为: 1->2->5, 1->3
解法:
- Java
树种进行backtrack,很重要的案例
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
List<String> res = new ArrayList<>();
if (root==null) return res;
List<Integer> path = new ArrayList<>();
backtracking(root, path, res);
return res;
}
private void backtracking(TreeNode node, List<Integer> path, List<String> res) {
if (node==null) return;
path.add(node.val);
if (node.left==null && node.right==null) res.add(buildPath(path));
else {
backtracking(node.left, path, res);
backtracking(node.right, path, res);
}
path.remove(path.size()-1);
}
private String buildPath(List<Integer> path) {
StringBuilder sb = new StringBuilder();
for (int i=0;i<path.size();i++) {
sb.append(path.get(i));
if (i!=path.size()-1) sb.append("->");
}
return sb.toString();
}
}
5. 排列
Leetcode-46. 全排列
给定一个没有重复数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
解法:
- Java
没有重复值的全排列,需要marked进行判断标记,backtrack内部进行未标记的取值
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> premuteLst = new ArrayList<>();
boolean[] marked = new boolean[nums.length];
backtracking(res, premuteLst, marked, nums);
return res;
}
private void backtracking(List<List<Integer>> res, List<Integer> premuteLst, boolean[] marked, int[] nums) {
if (premuteLst.size() == nums.length) {
res.add(new ArrayList<>(premuteLst)); // 必须重新构造premuteLst,否则添加的都是同一个premuteLst
return;
}
for (int i=0;i<nums.length;i++) {
if (marked[i]) continue;
marked[i] = true;
premuteLst.add(nums[i]);
backtracking(res, premuteLst, marked, nums);
premuteLst.remove(premuteLst.size()-1);
marked[i] = false;
}
}
}
6. 含有相同元素求排列
Leetcode-47. 全排列 II
给定一个可包含重复数字的序列,返回所有不重复的全排列。
示例:
输入: [1,1,2]
输出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
解法:
- Java
升级版,使用判前的方式,一定要先进行sort
在实现上,和 Permutations 不同的是要先排序,然后在添加一个元素时,判断这个元素是否等于前一个元素,如果等于,并且前一个元素还未访问,那么就跳过这个元素。(相当于前一个元素已经经历过marked[i] = true和marked[i] = false的过程或者已经被跳过,如果前一个元素marked为true,说明正在前一个元素的遍历过程中,应该继续进行)
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> premuteLst = new ArrayList<>();
boolean[] marked = new boolean[nums.length];
Arrays.sort(nums);
backtracking(res, premuteLst, marked, nums);
return res;
}
private void backtracking(List<List<Integer>> res, List<Integer> premuteLst, boolean[] marked, final int[] nums) {
if (premuteLst.size() == nums.length) {
res.add(new ArrayList<>(premuteLst));
return;
}
for (int i=0;i<nums.length;i++) {
if (i!=0 && nums[i]==nums[i-1] && !marked[i-1]) continue;
if (marked[i]) continue;
marked[i] = true;
premuteLst.add(nums[i]);
backtracking(res, premuteLst, marked, nums);
premuteLst.remove(premuteLst.size()-1);
marked[i] = false;
}
}
}
7. 组合
Leetcode-77. 组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
解法:
- Java
这道题跟上面的不同之处在于[1, 2]和[2, 1]是相同的,需要排除掉,如果还按照上题这么写会有大量重复的问题
// 错误答案
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> combineLst = new ArrayList<>();
boolean[] marked = new boolean[n+1];
backtracking(res, combineLst, marked, n, k);
return res;
}
private void backtracking(List<List<Integer>> res, List<Integer> combineLst, boolean[] marked, final int n, final int k) {
if (combineLst.size() == k) {
res.add(new ArrayList<>(combineLst));
return;
}
for (int i=1;i<=n;i++) {
if (marked[i]) continue;
marked[i] = true;
combineLst.add(i);
backtracking(res, combineLst, marked, n, k);
combineLst.remove(combineLst.size()-1);
marked[i] = false;
}
}
}
所以我们需要更改每次遍历时开始的位置,利用start防止回头,start仍为i表示i位置可以多次使用,为i+1表示只能使用一次。有了start就可以不用marked了,因为不会访问前面的元素了
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> lst = new ArrayList<>();
backtracking(res, lst, n, k, 1);
return res;
}
private void backtracking(List<List<Integer>> res, List<Integer> lst, int n, int k, int start) {
if (lst.size() == k) {
res.add(new ArrayList<>(lst));
return;
}
for (int i=start;i<=n;i++) {
lst.add(i);
backtracking(res, lst, n, k, i+1);
lst.remove(lst.size()-1);
}
}
}
8. 组合求和
Leetcode-39. 组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
- 所有数字(包括 target)都是正整数。
- 解集不能包含重复的组合。
示例 1:
输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:
输入: candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
解法:
- Java
依然还是使用start进行防止回头,否则会有[2, 2, 3]和[2, 3, 2]这种重复例子
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> comLst = new ArrayList<>();
backtracking(candidates, target, 0, res, comLst);
return res;
}
private void backtracking(final int[] candidates, int target, int start, List<List<Integer>> res, List<Integer> comLst) {
if (target==0) {
res.add(new ArrayList<>(comLst));
return;
}
for (int i=start;i<candidates.length;i++) {
if (candidates[i]>target) continue;
comLst.add(candidates[i]);
backtracking(candidates, target-candidates[i], i, res, comLst);
comLst.remove(comLst.size()-1);
}
}
}
9. 含有相同元素的组合求和
Leetcode-40. 组合总和 II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
- 所有数字(包括目标数)都是正整数。
- 解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
解法:
- Java
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> comLst = new ArrayList<>();
Arrays.sort(candidates);
boolean[] marked = new boolean[candidates.length];
backtracking(candidates, target, marked, 0, res, comLst);
return res;
}
private void backtracking(final int[] candidates, int target, boolean[] marked, int start, List<List<Integer>> res, List<Integer> comLst) {
if (target==0) {
res.add(new ArrayList<>(comLst));
return;
}
for (int i=start;i<candidates.length;i++) {
if (i!=0 && candidates[i]==candidates[i-1] && !marked[i-1]) continue;
if (candidates[i]>target) continue;
marked[i] = true;
comLst.add(candidates[i]);
backtracking(candidates, target-candidates[i], marked, i+1, res, comLst);
comLst.remove(comLst.size()-1);
marked[i] = false;
}
}
}
10. 1-9 数字的组合求和
Leetcode-216. 组合总和 III
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
- 所有数字都是正整数。
- 解集不能包含重复的组合。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解法:
- Java
class Solution {
public List<List<Integer>> combinationSum3(int k, int n) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> lst = new ArrayList<>();
backtracking(res, lst, n, k, 1);
return res;
}
private void backtracking(List<List<Integer>> res, List<Integer> lst, int n, int k, int start) {
if (lst.size()==k) {
if (n==0) res.add(new ArrayList<>(lst));
return;
}
for (int i=start;i<=9;i++) {
if (i>n) continue;
lst.add(i);
backtracking(res, lst, n-i, k, i+1);
lst.remove(lst.size()-1);
}
}
}
11. 子集
Leetcode-78. 子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
解法:
- Java
子集不能重复,需要start防止回头
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> lst = new ArrayList<>();
if (nums==null || nums.length==0) return res;
backtracking(res, lst, nums, 0);
return res;
}
private void backtracking(List<List<Integer>> res, List<Integer> lst, int[] nums, int start) {
res.add(new ArrayList<>(lst));
for (int i=start; i<nums.length; i++) {
lst.add(nums[i]);
backtracking(res, lst, nums, i+1);
lst.remove(lst.size()-1);
}
}
}
12. 含有相同元素求子集
Leetcode-90. 子集 II
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
解法:
- Java
与前面的题类似
结果与子集内元素顺序有关(需要去重),元素只能使用一次,使用start,backtrack的时候令start=i+1
元素可以使用无限次,使用start,backtrack的时候令start=i即可
含有重复,并且只能用一次,结果与子集内元素顺序有关(需要去重),使用start+boolean[],并且backtrack的时候令start=i+1
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> lst = new ArrayList<>();
res.add(new ArrayList<>());
boolean[] marked = new boolean[nums.length];
Arrays.sort(nums);
backtracking(res, lst, nums, marked, 0);
return res;
}
private void backtracking(List<List<Integer>> res, List<Integer> lst, int[] nums, boolean[] marked, int start) {
if (start==nums.length || marked[start]) return;
for (int i=start; i<nums.length; i++) {
if (i!=0 && nums[i]==nums[i-1] && !marked[i-1]) continue;
if (marked[i]) continue;
lst.add(nums[i]);
marked[i] = true;
res.add(new ArrayList<>(lst));
backtracking(res, lst, nums, marked, i+1);
marked[i] = false;
lst.remove(lst.size()-1);
}
}
}
13. 分割字符串使得每个部分都是回文数
Leetcode-131. 分割回文串
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例:
输入: "aab"
输出:
[
["aa","b"],
["a","a","b"]
]
解法:
- Java
class Solution {
public List<List<String>> partition(String s) {
List<List<String>> res = new ArrayList<>();
List<String> partitionLst = new ArrayList<>();
backtracking(s, res, partitionLst);
return res;
}
private void backtracking(String s, List<List<String>> res, List<String> partitionLst) {
if (s.length()==0) {
res.add(new ArrayList<>(partitionLst));
return;
}
for (int i=0;i<s.length();i++) {
if (!isPalindrome(s, 0, i)) continue;
partitionLst.add(s.substring(0,i+1));
backtracking(s.substring(i+1), res, partitionLst);
partitionLst.remove(partitionLst.size()-1);
}
}
private boolean isPalindrome(String s, int begin, int end) {
if (s==null) return false;
while (begin < end) {
if (s.charAt(begin++) != s.charAt(end--)) return false;
}
return true;
}
}
14. 数独
Leetcode-37. 解数独
编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法需遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
空白格用 ‘.’ 表示。
一个数独。
答案被标成红色。
Note:
- 给定的数独序列只包含数字 1-9 和字符 ‘.’ 。
- 你可以假设给定的数独只有唯一解。
- 给定数独永远是 9x9 形式的。
解法:
- Java
如果是for i for j两层循环中遍历各个正方形的位置,使用这样的公式作为board[m][n]中的m、n(对应36题):
int m = i/3*3 + j/3;
int n = i%3*3 + j%3;
如果是已知位置的i、j左边,想遍历board[i][j]所在的小cube中的所有元素,应该使用这样的m、n(对应37题):
int m = row/3*3 + i/3;
int n = col/3*3 + i%3;
class Solution {
public void solveSudoku(char[][] board) {
if (board==null || board.length==0 || board[0].length==0) return;
backtracking(board);
}
private boolean backtracking(char[][] board) {
for (int i=0;i<board.length;i++) {
for (int j=0;j<board[0].length;j++) {
if (board[i][j] != '.') continue;
for (char c='1';c<='9';c++) {
if (!isValid(board, i, j, c)) continue;
board[i][j] = c;
if (backtracking(board)) return true;
board[i][j] = '.';
}
return false; // 没有位置可以填写数字,说明之前填的有错误
}
}
return true; // 所有位置都被填完
}
private boolean isValid(char[][] board, int row, int col, char c) {
for (int i=0;i<board.length;i++) {
int m = row/3*3 + i/3;
int n = col/3*3 + i%3;
if (board[row][i]==c || board[i][col]==c || board[m][n]==c) return false; // 行、列、cube中都满足
}
return true;
}
}
36题类似,判断有效的数独。
class Solution {
public boolean isValidSudoku(char[][] board) {
return backtracking(board);
}
private boolean backtracking(char[][] board) {
for (int i=0; i<board.length; i++) {
for (int j=0; j<board[0].length; j++) {
if (board[i][j] == '.') continue;
char c = board[i][j];
board[i][j] = '.';
if (!isValid(board, i, j, c)) return false;
board[i][j] = c;
}
}
return true;
}
private boolean isValid(char[][] board, int row, int col, char c) {
for (int i=0; i<board.length; i++) {
int m = row/3*3 + i/3;
int n = col/3*3 + i%3;
if (board[row][i]==c || board[i][col]==c || board[m][n]==c) return false;
}
return true;
}
}
15. N 皇后
Leetcode-51. N皇后
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
上图为 8 皇后问题的一种解法。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
示例:
输入: 4
输出: [
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解释: 4 皇后问题存在两个不同的解法。
解法:
- Java
左对角线上元素横坐标加纵坐标和相同(注意纵坐标向下增加,左上角为原点)
右对角线上元素横坐标减纵坐标差相同
class Solution {
private int n;
public List<List<String>> solveNQueens(int n) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> lst = new ArrayList<>();
Set<Integer> cols = new HashSet<>();
Set<Integer> leftDiags = new HashSet<>();
Set<Integer> rightDiags = new HashSet<>();
this.n = n;
backtracking(res, lst, cols, leftDiags, rightDiags, 0);
return formatting(res);
}
private void backtracking(List<List<Integer>> res, List<Integer> lst, Set<Integer> cols, Set<Integer> leftDiags, Set<Integer> rightDiags, int level) {
if (lst.size() == n) {
res.add(new ArrayList<>(lst));
return;
}
for (int i=0; i<n; i++) {
if (cols.contains(i) || leftDiags.contains(i+level) || rightDiags.contains(i-level)) continue;
lst.add(i);
cols.add(i);
leftDiags.add(i+level);
rightDiags.add(i-level);
backtracking(res, lst, cols, leftDiags, rightDiags, level+1);
rightDiags.remove(i-level);
leftDiags.remove(i+level);
cols.remove(i);
lst.remove(lst.size()-1);
}
}
private List<List<String>> formatting(List<List<Integer>> lst) {
List<List<String>> res = new ArrayList<>();
for (List<Integer> l: lst) {
List<String> strlst = new ArrayList<>();
for (int idx: l) {
String str = "";
for (int i=0;i<n;i++) {
if (i==idx) str += 'Q';
else str += '.';
}
strlst.add(str);
}
res.add(strlst);
}
return res;
}
}