回溯
// 主函数里面先对集合排序
/*
回溯:
void dfs(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素) { // 横向处理
// 对于有重复元素的情况要加上以下判断
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue;
处理节点;
dfs(路径,选择列表); // 纵向递归
回溯,撤销处理结果
}
}
*/
组合
✅77. 组合
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
dfs(1, n, k);
return res;
}
private void dfs(int begin, int n, int k) {
if (path.size() == k) {
res.add(new ArrayList<>(path));
return;
}
// 如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
// 已经选择的元素个数:path.size();
// 还需要的元素个数为: k - path.size();
// n-i+1 >= k - path.size()
// i <= n+1-(k - path.size())
// 在集合n中至多要从该起始位置 : n + 1 - (k - path.size()),开始遍历
for (int i = begin; i <= n + 1 - (k - path.size()); i++) {
path.add(i);
dfs(i + 1, n, k);
path.remove(path.size() - 1);
}
}
}
✅17. 电话号码的字母组合
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
class Solution {
List<String> res = new ArrayList<>();
String[] map = {"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
StringBuilder sb = new StringBuilder();
public List<String> letterCombinations(String digits) {
if (digits == null || digits.length() == 0) return res;
dfs(digits, 0);
return res;
}
private void dfs(String digits, int index) {
if (sb.length() == digits.length()) { // 递归出口
res.add(sb.toString());
return;
}
char[] choices = map[digits.charAt(index) - '2'].toCharArray(); // 处理当前层
for (char ch : now.toCharArray()) { // 选择本层集合中的元素ch
sb.append(ch); // 处理节点
dfs(digits, index + 1); // 递归
sb.deleteCharAt(sb.length() - 1); // 回溯,撤销上面处理的节点
}
}
}
✅39. 组合总和
给定一个无重复元素的正整数数组 candidates
和一个正整数 target
,找出 candidates
中所有可以使数字和为目标数 target
的唯一组合。
candidates
中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是唯一的。
对于给定的输入,保证和为 target
的唯一组合数少于 150
个。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
dfs(candidates, 0, target);
return res;
}
private void dfs(int[] candidates, int begin, int target) {
if (target == 0) {
res.add(new ArrayList<>(path));
return;
}
if (target < 0) return; // 纵向剪枝
for (int i = begin; i < candidates.length; i++) {
if (target - candidates[i] < 0) break; // beat 10% -> 96%,横向剪枝
path.add(candidates[i]);
dfs(candidates, i, target - candidates[i]); // 下一次i位置的元素还可以选
path.remove(path.size() - 1);
}
}
}
⭕️40. 组合总和 II
给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
**注意:**解集不能包含重复的组合。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
boolean[] used = new boolean[candidates.length];
dfs(candidates, 0, target, used);
return res;
}
private void dfs(int[] candidates, int begin, int target, boolean[] used) {
if (target < 0) return;
if (target == 0) {
res.add(new ArrayList<>(path));
return;
}
for (int i = begin; i < candidates.length; i++) {
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) continue;
if (target - candidates[i] < 0) break; // beat 14% -> 98%
path.add(candidates[i]);
used[i] = true;
dfs(candidates, i + 1, target - candidates[i], used);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
✅216. 组合总和 III
找出所有相加之和为 n
的 k
个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
dfs(k, n, 1);
return res;
}
private void dfs(int k, int n, int begin) {
if (k == 0 && n == 0) {
res.add(new ArrayList<>(path));
return;
}
for (int i = begin; i <= 9; i++) {
path.add(i);
dfs(k - 1, n - i, i + 1);
path.remove(path.size() - 1);
}
}
}
排列
✅46. 全排列
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
boolean[] used = new boolean[nums.length];
dfs(nums, used);
return res;
}
private void dfs(int[] nums, boolean[] used) {
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i] == false) {
path.add(nums[i]);
used[i] = true;
dfs(nums, used);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
}
⭕️47. 全排列 II
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
dfs(nums, used);
return res;
}
private void dfs(int[] nums, boolean[] used) {
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
// 在上一题的基础上加了这句判断
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue;
if (used[i] == false) {
path.add(nums[i]);
used[i] = true;
dfs(nums, used);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
}
⭕️分割
⭐131. 分割回文串
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是 回文串 。返回 s
所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
class Solution {
List<List<String>> res = new ArrayList<>();
List<String> path = new ArrayList<>();
public List<List<String>> partition(String s) {
dfs(s, 0);
return res;
}
private void dfs(String s, int begin) {
if (begin == s.length()) { // 分割到字符串最后结束
res.add(new ArrayList<>(path));
return;
}
for (int i = begin; i < s.length(); i++) {
if (isReverse(s.substring(begin, i + 1))) {
path.add(s.substring(begin, i + 1));
dfs(s, i + 1);
path.remove(path.size() - 1);
}
}
}
private boolean isReverse(String s) {
int len = s.length();
int left = 0, right = len - 1;
while (left < right) {
if (s.charAt(left) != s.charAt(right)) return false;
left++;
right--;
}
return true;
}
}
⭐⭐93. 复原 IP 地址
有效 IP 地址 正好由四个整数(每个整数位于 0
到 255
之间组成,且不能含有前导 0
),整数之间用 '.'
分隔。
- 例如:
"0.1.2.201"
和"192.168.1.1"
是 有效 IP 地址,但是"0.011.255.245"
、"192.168.1.312"
和"192.168@1.1"
是 无效 IP 地址。
给定一个只包含数字的字符串 s
,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s
中插入 '.'
来形成。你 不能 重新排序或删除 s
中的任何数字。你可以按 任何 顺序返回答案。
class Solution {
List<String> res = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
if (s.length() > 12) return res;
dfs(s, 0, 0);
return res;
}
// 相较于上题,这道题限制了组合的次数,也就是规定只能割3次
private void dfs(String s, int begin, int pointCnt) {
if (pointCnt == 3) { // 分割了3次结束
if (isValid(s.substring(begin, s.length()))) {
res.add(s);
return;
}
}
for (int i = begin; i < s.length(); i++) {
if (isValid(s.substring(begin, i + 1))) {
s = s.substring(0, i + 1) + '.' + s.substring(i + 1);
pointCnt++;
dfs(s, i + 2, pointCnt);
pointCnt--;
s = s.substring(0, i + 1) + s.substring(i + 2);
}
}
}
// 判断是否合法
private boolean isValid(String s) {
if (s == null || s.length() == 0) return false;
if (s.length() > 1 && s.charAt(0) == '0') return false;
if (s.length() > 4) return false;
int val = 0;
for (char c : s.toCharArray()) {
if (!(c >= '0' && c <= '9')) return false;
val = val * 10 + (c - '0');
}
return val >= 0 && val <= 255;
}
}
子集
子集问题类似于组合问题,都是使用begin。排列问题使用used数组。
78. 子集
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
if (nums == null || nums.length == 0) return res;
dfs(nums, 0);
return res;
}
private void dfs(int[] nums, int begin) {
res.add(new ArrayList<>(path));
for (int i = begin; i < nums.length; i++) {
path.add(nums[i]);
dfs(nums, i + 1);
path.remove(path.size() - 1);
}
}
}
90. 子集 II
给你一个整数数组 nums
,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
// 在上一题基础上使用used数据处理去重
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
if (nums == null || nums.length == 0) return res;
boolean[] used = new boolean[nums.length];
Arrays.sort(nums);
dfs(nums, used, 0);
return res;
}
private void dfs(int[] nums, boolean[] used, int begin) {
res.add(new ArrayList<>(path));
for (int i = begin; i < nums.length; i++) {
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
path.add(nums[i]);
used[i] = true;
dfs(nums, used, i + 1);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
⭕️491. 递增子序列
给你一个整数数组 nums
,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
if (nums == null || nums.length == 0) return res;
dfs(nums, 0);
return res;
}
private void dfs(int[] nums, int begin) {
if (path.size() >= 2) {
res.add(new ArrayList<>(path));
// 注意这里不要加return,要取树上的节点
}
Set<Integer> set = new HashSet<>(); // 使用set对本层元素进行去重
for (int i = begin; i < nums.length; i++) {
if (set.contains(nums[i]) || (!path.isEmpty() && nums[i] < path.get(path.size() - 1))) continue;
set.add(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了,对于本层的其他分支,自然也就不用撤回这个选择
path.add(nums[i]);
dfs(nums, i + 1);
path.remove(path.size() - 1);
}
}
}
⭕️棋盘
51. N 皇后
n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] board = new char[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
board[i][j] = '.';
}
}
dfs(0, n, board);
return res;
}
private void dfs(int row, int n, char[][] board) {
if (row == n) {
res.add(new ArrayList<>(transfer(board)));
return;
}
for (int col = 0; col < n; col++) {
if (isValid(row, col, n, board)) {
board[row][col] = 'Q';
dfs(row + 1, n, board);
board[row][col] = '.';
}
}
}
private List<String> transfer(char[][] board) {
List<String> res = new ArrayList<>();
for (int i = 0; i < board.length; i++) {
res.add(String.valueOf(board[i]));
}
return res;
}
private boolean isValid(int row, int col, int n, char[][] board) {
// 竖线
for (int i = 0; i < row; i++) {
if (board[i][col] == 'Q') return false;
}
// 45°
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board[i][j] == 'Q') return false;
}
// 135°
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] == 'Q') return false;
}
return true;
}
}
37. 解数独
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
- 数字
1-9
在每一行只能出现一次。 - 数字
1-9
在每一列只能出现一次。 - 数字
1-9
在每一个以粗实线分隔的3x3
宫内只能出现一次。
数独部分空格内已填入了数字,空白格用 '.'
表示。
class Solution {
public void solveSudoku(char[][] board) {
dfs(board);
}
private boolean dfs(char[][] board) {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
// 对于每一个空的地方尝试插入元素
if (board[i][j] != '.') continue;
for (char value = '1'; value <= '9'; value++) {
if (isValid(board, i, j, value)) {
board[i][j] = value;
if (dfs(board)) {
return true;
}
board[i][j] = '.';
}
}
return false;
}
}
return true;
}
// 判断是否可以插入元素value
private boolean isValid(char[][] board, int row, int col, char value) {
for (int j = 0; j < 9; j++) {
if (board[row][j] == value) return false;
}
for (int i = 0; i < 9; i++) {
if (board[i][col] == value) return false;
}
int beginRow = (row / 3) * 3, beginCol = (col / 3) * 3;
for (int i = beginRow; i < beginRow + 3; i++) {
for (int j = beginCol; j < beginCol + 3; j++) {
if (board[i][j] == value) {
return false;
}
}
}
return true;
}
}
36. 有效的数独
请你判断一个 9 x 9
的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。
- 数字
1-9
在每一行只能出现一次。 - 数字
1-9
在每一列只能出现一次。 - 数字
1-9
在每一个以粗实线分隔的3x3
宫内只能出现一次。
class Solution {
public boolean isValidSudoku(char[][] board) {
// 记录某行,某位数字是否已经被摆放
boolean[][] row = new boolean[9][9];
// 记录某列,某位数字是否已经被摆放
boolean[][] col = new boolean[9][9];
// 记录某 3x3 宫格内,某位数字是否已经被摆放
boolean[][] block = new boolean[9][9];
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] != '.') {
int num = board[i][j] - '1';
int blockIndex = i / 3 * 3 + j / 3;
if (row[i][num] || col[j][num] || block[blockIndex][num]) {
return false;
} else {
row[i][num] = true;
col[j][num] = true;
block[blockIndex][num] = true;
}
}
}
}
return true;
}
}
岛屿问题
⭐200. 岛屿数量
给你一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
class Solution {
public int numIslands(char[][] grid) {
int res = 0;
int m = grid.length, n = grid[0].length;
boolean[][] visited = new boolean[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1' && !visited[i][j]) {
dfs(grid, visited, i, j);
res++;
}
}
}
return res;
}
private void dfs(char[][] grid, boolean[][] visited, int i, int j) {
if (!check(grid, i, j) || visited[i][j] || grid[i][j] == '0') return;
visited[i][j] = true;
dfs(grid, visited, i + 1, j);
dfs(grid, visited, i - 1, j);
dfs(grid, visited, i, j + 1);
dfs(grid, visited, i, j - 1);
}
private boolean check(char[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
return i >= 0 && i < m && j >= 0 && j < n;
}
}
不用额外的空间
class Solution {
// 不用额外的visited,每次遍历完1后将其置为2表示已访问
public int numIslands(char[][] grid) {
int row = grid.length, col = grid[0].length;
int res = 0;
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
if (grid[i][j] == '1') {
dfs(grid, i, j);
res++;
}
}
}
return res;
}
private void dfs(char[][] grid, int i, int j) {
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] != '1') return;
grid[i][j] = '2';
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
}
}
⭐463. 岛屿的周长
给定一个 row x col
的二维网格地图 grid
,其中:grid[i][j] = 1
表示陆地, grid[i][j] = 0
表示水域。
网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。
/**
* 岛屿的周围就是,一个岛屿格子的边与地图边界或者海洋的部分相交的话,那么这条边就是岛屿的周长的一部分。
dfs即从i,j这个岛屿开始涂色,涂色的时候发现当前是边界或者是海洋,那么当前边就算入周长中,否则的话如果当前已经涂过色,直接return,否则继续递归。
*/
class Solution {
int res = 0;
public int islandPerimeter(int[][] grid) {
if (grid == null || grid.length == 0) return 0;
int m = grid.length, n = grid[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
dfs(grid, i, j);
}
}
}
return res;
}
private void dfs(int[][] grid, int i, int j) {
if ((i < 0 || i >= grid.length)
|| (j < 0 || j >= grid[0].length)
|| grid[i][j] == 0) {
res++;
return;
}
if (grid[i][j] != 1) return;
grid[i][j] = -1;
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
}
}
方法2:
// 迭代判断每个岛屿的四周是否是边界或者海洋
class Solution {
public int islandPerimeter(int[][] grid) {
if (grid == null || grid.length == 0) return 0;
int m = grid.length, n = grid[0].length;
int res = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
res += getOuter(grid, i, j);
}
}
return res;
}
private int getOuter(int[][] grid, int i, int j) {
int outer = 0;
if (grid[i][j] == 1) {
if (i - 1 < 0 || grid[i - 1][j] == 0) outer++;
if (i + 1 >= grid.length || grid[i + 1][j] == 0) outer++;
if (j + 1 >= grid[0].length || grid[i][j + 1] == 0) outer++;
if (j - 1 < 0 || grid[i][j - 1] == 0) outer++;
}
return outer;
}
}
⭐695. 岛屿的最大面积
给你一个大小为 m x n
的二进制矩阵 grid
。
岛屿 是由一些相邻的 1
(代表土地) 构成的组合,这里的「相邻」要求两个 1
必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid
的四个边缘都被 0
(代表水)包围着。
岛屿的面积是岛上值为 1
的单元格的数目。
计算并返回 grid
中最大的岛屿面积。如果没有岛屿,则返回面积为 0
。
/**
每次dfs进行涂色,涂色前初始化当前涂色区域的面积
如果dfs涂完了,说明这个区域遍历完了,更新最大面积。
*/
class Solution {
int maxArea = 0;
int area;
public int maxAreaOfIsland(int[][] grid) {
if (grid == null || grid.length == 0) return 0;
int m = grid.length, n = grid[0].length;
int res = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
area = 0;
dfs(grid, i, j);
maxArea = Math.max(maxArea, area);
}
}
}
return maxArea;
}
private void dfs(int[][] grid, int i, int j) {
if ((i < 0 || i >= grid.length) || (j < 0 || j >= grid[0].length)||
grid[i][j] != 1) return;
area++;
grid[i][j] = -1;
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
}
}