问题总览
准备知识
❗️ 回溯需要注意的三个元素:
- 走过的路径
- 可选列表
- 结束条件
❓️ 什么是组合:给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合
一、排列组合子集问题
问题列表
| 类型 | 问题 | 完成 |
|---|---|---|
| 无重复数字不可复选 | 46. 全排列 | ✅ |
| 51. N 皇后 | ✅ | |
| 78. 子集 | ✅ | |
| 77.组合 | ✅ | |
| 216. 组合总和 III | ✅ | |
| 包含重复数字 | 47. 全排列 II | ✅ |
| 90.子集 II | ✅ | |
| 40. 组合总和 II | ✅ | |
| 无重复元素可重复选 | 39. 组合总和 | ✅ |
相同问题:
剑指 Offer II 079. 所有子集
剑指 Offer II 080. 含有 k 个元素的组合
剑指 Offer II 081. 允许重复选择元素的组合
剑指 Offer II 082. 含有重复元素集合的组合
剑指 Offer II 083. 没有重复元素集合的全排列
剑指 Offer II 084. 含有重复元素集合的全排列
1.1 分析
🏳️🌈有四种情况:
-
无重复数字、不可重复选择
-
有重复数字、不可重复选择
我们通过保证元素之间的相对顺序不变来防止出现重复的子集。
-
无重复数字,可重复选择
-
有重复数字、可重复选择(其实和3没什么区别,因为可重复选择其实不就是有重复数字的意思么)
🏳️🌈有三种输出:
-
排列:在回溯完成后输出;
for (int col = 0; col < len; col++)用
boolean[] used = new boolean[]来判断这个元素是否已经遍历过 -
组合:组合问题其实就是子集问题,组合是固定长度的子集;
for (int i = start; i < n; i++) -
子集:在回溯的过程中记录元素并输出。
// 无重复元素的子集 void backtrack(int[] nums, int start) { for (int i = start; i < n; i++) { backtrack(nums, i+1); } }
1.2 不包含重复数字
特点是:输出的元素个数和输入相同,结束条件一般是:
track.size() == nums.length
class Solution {
boolean[] visited;
List<List<Integer>> ans;
public List<List<Integer>> permute(int[] nums) {
visited = new boolean[nums.length];
ans = new ArrayList<>();
backtrace(nums, new LinkedList<Integer>());
return ans;
}
public void backtrace(int[] nums, LinkedList<Integer> path) {
if (path.size() == nums.length) {
ans.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (visited[i]) {
continue;
}
path.add(nums[i]);
visited[i] = true;
backtrace(nums, path);
path.removeLast();
visited[i] = false;
}
}
}
// 时间复杂度:O(n^2)
// 空间复杂度:O(n^2)
基本套路和全排列是一样的,只是判断是否可以放置皇后的判断复杂一些
不过N皇后没有使用used数组,而是使用了row来计数
import java.util.LinkedList;
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
List<List<String>> ans;
int N = 0;
public List<List<String>> solveNQueens(int n) {
ans = new ArrayList<>();
N = n;
char[][] paths = new char[n][n];
for (char[] path : paths) {
Arrays.fill(path, '.');
}
backtrace(0, paths);
return ans;
}
public List<String> convert(char[][] paths) {
List<String> ans = new ArrayList<>();
for (int i = 0; i < paths.length; i++) {
StringBuilder sb = new StringBuilder();
for (char c : paths[i]) {
sb.append(c);
}
ans.add(sb.toString());
}
return ans;
}
public void backtrace(int row, char[][] paths) {
if (row == N) {
ans.add(convert(paths));
return;
}
for (int col = 0; col < N; col++) {
if (!canPut(paths, row, col)) {
continue;
}
paths[row][col] = 'Q';
backtrace(row + 1, paths);
paths[row][col] = '.';
}
}
public boolean canPut(char[][] path, int row, int col) {
for (int i = row - 1; i >= 0; i--) {
// 检查同一列,如果已经有Q了,那么这个位置不能放
if (path[i][col] == 'Q') {
return false;
}
}
// 检查左上
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (path[i][j] == 'Q') {
return false;
}
}
// 检查右上
for (int i = row - 1, j = col + 1; i >= 0 && j < N; i--, j++) {
if (path[i][j] == 'Q') {
return false;
}
}
return true;
}
}
// 时间复杂度:O(n!)
// 空间复杂度:O(n^2)
class Solution {
char[][] chars;
int N = 0;
int count = 0;
public int totalNQueens(int n) {
N = n;
chars = new char[n][n];
for(char[] c: chars){
Arrays.fill(c, '.');
}
// 开始尝试从第一行,第一个元素开始放置皇后
put(chars, 0);
return count;
}
private void put(char[][] chars, int row){
if(row == N){
count++;
return;
}
for(int col = 0 ;col < N; col++){
// 检查该位置是否可以放置皇后
if(!canPut(chars, row, col)){
continue;
}
chars[row][col] = 'Q';
put(chars, row + 1);
chars[row][col] = '.';
}
}
private boolean canPut(char[][] chars, int row,int col){
// 同一列
for(int i= 0;i<row;i++){
if(chars[i][col] == 'Q'){
return false;
}
}
// 左斜上
for(int i=row-1,j=col-1;i>=0 && j>=0;i--,j--){
if(chars[i][j] == 'Q'){
return false;
}
}
// 右斜上,最多走到右边角
for(int i=row-1,j=col+1;i>=0 && j<chars[0].length;i--,j++){
if(chars[i][j] == 'Q'){
return false;
}
}
return true;
}
}
class Solution {
List<List<Integer>> ans;
public List<List<Integer>> subsets(int[] nums) {
ans = new ArrayList<>();
backtrace(nums, 0, new LinkedList<Integer>());
return ans;
}
public void backtrace(int[] nums, int idx, LinkedList<Integer> path) {
ans.add(new ArrayList<>(path));
for (int i = idx; i < nums.length; i++) {
path.add(nums[i]);
backtrace(nums, i + 1, path);
path.removeLast();
}
}
}
// 时间复杂度:O(n^2)
// 空间复杂度:O(n^2)
class Solution {
private LinkedList<Integer> track = new LinkedList<>();
private List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backtrace(n, 0, k);
return res;
}
public void backtrace(int n, int start, int k) {
if (track.size() == k) {
res.add(new LinkedList<>(track));
return;
}
for (int i = start; i < n; i++) {
track.add(i + 1);
backtrace(n, i + 1, k);
track.removeLast();
}
}
}
import java.util.LinkedList;
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
List<List<Integer>> ans;
int n;
int k;
public List<List<Integer>> combinationSum3(int k, int n) {
// 元素只能使用1-9,并且不重复使用
ans = new ArrayList<>();
this.n = n;
this.k = k;
backtrace(1, new LinkedList<Integer>(), 0);
return ans;
}
public void backtrace(int idx, LinkedList<Integer> path, int sum) {
if (sum == n && path.size() == k) {
ans.add(new LinkedList<>(path));
return;
}
if (sum > n) {
return;
}
for (int i = idx; i < 10; i++) {
path.add(i);
sum += i;
backtrace(i + 1, path, sum);
path.removeLast();
sum -= i;
}
}
}
//leetcode submit region end(Prohibit modification and deletion)
1.3 元素重复,不可复选
算法精髓:
- 先排序
- 额外加一步剪枝
import java.util.ArrayList;
import java.util.List;
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
private List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
// 先排序
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
backtrace(nums, new LinkedList<>(), used);
return res;
}
public void backtrace(int[] nums, LinkedList<Integer> trace, boolean[] used) {
if (nums.length == trace.size()) {
res.add(new LinkedList<>(trace));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) {
continue;
}
// 剪枝的条件是和前一个元素相等,但是前一个元素
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}
trace.add(nums[i]);
used[i] = true;
backtrace(nums, trace, used);
trace.removeLast();
used[i] = false;
}
}
}
//leetcode submit region end(Prohibit modification and deletion)
class Solution {
private LinkedList<Integer> track = new LinkedList<>();
private List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
backtrace(nums, 0);
return res;
}
public void backtrace(int[] nums, int start) {
res.add(new LinkedList<>(track));
for (int i = start; i < nums.length; i++) {
if (i > start && nums[i] == nums[i - 1]) {
continue;
}
track.add(nums[i]);
backtrace(nums, i + 1);
track.removeLast();
}
}
}
class Solution {
private List<List<Integer>> res = new ArrayList<>();
private LinkedList<Integer> track = new LinkedList<>();
private int sum = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
// 可重不可复选,等于子集问题
// 先排序
Arrays.sort(candidates);
boolean[] used = new boolean[candidates.length];
backtrack(candidates, target, 0);
return res;
}
public void backtrack(int[] nums, int target, int start){
if(target == sum){
res.add(new LinkedList<>(track));
return;
}
if(sum > target){
return;
}
for(int i=start; i<nums.length;i++){
if(i>start && nums[i] == nums[i-1]){
continue;
}
track.add(nums[i]);
sum += nums[i];
backtrack(nums, target, i+1);
track.removeLast();
sum -= nums[i];
}
}
}
1.4 元素无重可以复选
class Solution {
List<List<Integer>> ans;
int target;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
//无重就不需要排序
ans = new ArrayList<>();
this.target = target;
backtrace(candidates, 0, new LinkedList<>(), 0);
return ans;
}
public void backtrace(int[] nums, int idx, LinkedList<Integer> path, int sum) {
if (sum == target) {
ans.add(new LinkedList<>(path));
return;
}
if (sum > target) {
return;
}
for (int i = idx; i < nums.length; i++) {
path.add(nums[i]);
sum += nums[i];
backtrace(nums, i, path, sum);
path.removeLast();
sum -= nums[i];
}
}
}
二、岛屿问题
| 类型 | 问题 | 完成 |
|---|---|---|
| 1020. 飞地的数量 | ✅ | |
| 1254. 统计封闭岛屿的数目 | ✅ | |
| 1905. 统计子岛屿 | ✅ | |
| 200. 岛屿数量 | ✅ | |
| 305. 岛屿数量 II | ||
| 694. 不同岛屿的数量 | ||
| 695. 岛屿的最大面积 | ✅ |
相同问题:
剑指 Offer II 105. 岛屿的最大面积
class Solution {
int[][] directions = new int[][]{{0,1},{1,0},{0,-1},{-1,0}};
public int numEnclaves(int[][] grid) {
// 到达边界也可以离开,所以要寻找的是不与边界连接的陆地网格
int m = grid.length;
int n = grid[0].length;
for(int i=0;i<m;i++){
flooded(grid, i,0);
flooded(grid, i,n-1);
}
for(int j=0;j<n;j++){
flooded(grid, 0,j);
flooded(grid, m-1,j);
}
int count = 0;
for(int i=1;i<m-1;i++){
for(int j=1;j<n-1;j++){
if(grid[i][j] == 1){
count++;
}
}
}
return count;
}
// 寻找当前陆地能够找到的所有陆地,将其淹没
public void flooded(int[][] grid , int i , int j){
if(i<0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] == 0){
return;
}
grid[i][j] = 0;
for(int[] dir: directions){
int x = i + dir[0];
int y = j + dir[1];
flooded(grid, x, y);
}
}
}
class Solution {
int[][] directions = new int[][]{{0,1},{1,0},{0,-1},{-1,0}};
public int closedIsland(int[][] grid) {
// 到达边界也可以离开,所以要寻找的是不与边界连接的陆地网格
int m = grid.length;
int n = grid[0].length;
for(int i=0;i<m;i++){
flooded(grid, i,0);
flooded(grid, i,n-1);
}
for(int j=0;j<n;j++){
flooded(grid, 0,j);
flooded(grid, m-1,j);
}
int count = 0;
for(int i=1;i<m-1;i++){
for(int j=1;j<n-1;j++){
if(grid[i][j] == 0){
count++;
// 一个海岛只保留一块陆地
flooded(grid, i,j);
}
}
}
return count;
}
public void flooded(int[][] grid , int i , int j){
if(i<0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] == 1){
return;
}
//置为海洋
grid[i][j] = 1;
for(int[] dir: directions){
int x = i + dir[0];
int y = j + dir[1];
flooded(grid, x, y);
}
}
}
class Solution {
int[][] directions = new int[][]{{0,1},{1,0},{0,-1},{-1,0}};
boolean isSub = true;
public int countSubIslands(int[][] grid1, int[][] grid2) {
int m = grid1.length;
int n = grid1[0].length;
int count = 0;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(grid2[i][j] == 1){
isSub = true;
flooded(grid1, grid2, i, j);
if(isSub){
count++;
}
}
}
}
return count;
}
private void flooded(int[][] grid1,int[][] grid2, int i , int j){
if(i<0 || j < 0 || i >= grid2.length || j >= grid2[0].length || grid2[i][j] == 0){
return;
}
// 先判断是否为子岛屿
if(grid1[i][j] == 0){
isSub = false;
}
//置为海洋
grid2[i][j] = 0;
for(int[] dir: directions){
int x = i + dir[0];
int y = j + dir[1];
flooded(grid1, grid2, x, y);
}
}
}
class Solution {
public int numIslands(char[][] grid) {
int m=grid.length;
int n=grid[0].length;
int count = 0;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(grid[i][j] == '1'){
count++;
flooded(grid, i ,j);
}
}
}
return count;
}
public void flooded(char[][] grid , int i , int j){
if(i<0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] == '0'){
return;
}
//置为水
grid[i][j] = '0';
flooded(grid, i + 1, j);
flooded(grid, i - 1, j);
flooded(grid, i, j+1);
flooded(grid, i, j-1);
}
}
class Solution {
int maxArea = 0;
public int maxAreaOfIsland(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
// 找到岛屿了,开始计算最大面积
if(grid[i][j] == 1){
maxArea = Math.max(flooded(grid, i, j),maxArea);
}
}
}
return maxArea;
}
public int flooded(int[][] grid , int i , int j){
if(i<0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] == 0){
return 0;
}
// 置为水
grid[i][j] = 0;
// 为什么要加1?
// 可以理解为面积为自身和四周所有面积之和,自身面积为1
return flooded(grid, i + 1, j) +
flooded(grid, i - 1, j)+
flooded(grid, i, j+1)+
flooded(grid, i, j-1)+1;
}
}
三、其他
| 类型 | 问题 | 完成 |
|---|---|---|
| 131. 分割回文串 | ✅ | |
| 93. 复原 IP 地址 | ✅ | |
| 698. 划分为k个相等的子集 | ||
| 37. 解数独 | ||
| 752. 打开转盘锁 | ||
| 剑指 Offer II 109. 开密码锁 | ||
| 773. 滑动谜题 |
class Solution {
LinkedList<String> path = new LinkedList<>();
List<List<String>> ans = new ArrayList<>();
public List<List<String>> partition(String s) {
backtrace(s, 0);
return ans;
}
public void backtrace(String s, int start) {
if (start == s.length()) {
ans.add(new ArrayList<>(path));
return;
}
for (int i = start; i < s.length(); i++) {
if (!isPalindrome(s, start, i)) {
continue;
}
path.add(s.substring(start, i + 1));
backtrace(s, i + 1);
path.removeLast();
}
}
public boolean isPalindrome(String s, int lo, int hi) {
while (lo < hi) {
if (s.charAt(lo) == s.charAt(hi)) {
lo++;
hi--;
} else {
return false;
}
}
return true;
}
}
class Solution {
LinkedList<String> path = new LinkedList<>();
List<String> ans = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
// 最少是4位,最多是12位
backtrace(s, 0);
return ans;
}
public void backtrace(String s, int start) {
if (start == s.length() && path.size() == 4) {
StringBuilder sb = new StringBuilder();
for (String p : path) {
sb.append(p).append(".");
}
ans.add(sb.deleteCharAt(sb.length() - 1).toString());
return;
}
if (start < s.length() && path.size() == 4) {
return;
}
for (int i = start; i < s.length(); i++) {
if (!isSubIp(s, start, i)) {
continue;
}
path.add(s.substring(start, i + 1));
backtrace(s, i + 1);
path.removeLast();
}
}
public boolean isSubIp(String s, int start, int end) {
// 如果只有一个字符,肯定是满足的,因为这个字符在0-9范围内
if (start == end) {
return true;
}
if (s.charAt(start) == '0') {
return false;
}
if (end - start < 2) {
return true;
} else if (end - start > 2) {
return false;
}
if (Integer.parseInt(s.substring(start, end + 1)) > 255) {
return false;
}
return true;
}
}