第一 排列、组合、子集问题
提示:这部分练习可以帮助我们熟悉「回溯算法」的一些概念和通用的解题思路。解题的步骤是:先画图,再编码。去思考可以剪枝的条件, 为什么有的时候用 used 数组,有的时候设置搜索起点 begin 变量,理解状态变量设计的想法。
- 46.全排列(中等)
- 47.全排列 II(中等):思考为什么造成了重复,如何在搜索之前就判断这一支会产生重复;
- 39.组合总和(中等)
- 40.组合总和 II(中等)
- 77.组合(中等)
- 78.子集(中等)
- 90.子集 II(中等):剪枝技巧同 47 题、39 题、40 题;
- 60.第 k 个排列(中等):利用了剪枝的思想,减去了大量枝叶,直接来到需要的叶子结点;
- 93.复原 IP 地址(中等)
46.全排列(中等)
class Solution {
List<List<Integer>> list = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
if(nums.length==0)return null;
List<Integer> temp = new ArrayList<>();
dfs(0,nums,temp);
return list;
}
void dfs(int k,int[] nums,List<Integer> temp){
if(temp.size()==nums.length){
list.add(new ArrayList<>(temp));
return;
}
// 注意这里的 起始点 i 要等于0 这里是从头算
for(int i=0;i<nums.length;i++){
if(temp.contains(nums[i]))continue;
temp.add(nums[i]);
dfs(i+1,nums,temp);
temp.remove(temp.size()-1);
}
}
}
47.
class Solution {
public List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
if(nums.length == 0){
return result;
}
Arrays.sort(nums);
findUnique(nums,new boolean[nums.length],new LinkedList<Integer>());
return result;
}
public void findUnique(int[] nums, boolean[] visited,List<Integer> temp){
//结束条件
if(temp.size() == nums.length){
result.add(new ArrayList<>(temp));
return ;
}
//选择列表
for(int i = 0; i<nums.length; i++){
//已经选择过的不需要再放进去了
if(visited[i]) continue;
//去重
if(i>0 && nums[i] == nums[i-1] && visited[i-1]) break;
temp.add(nums[i]);
visited[i] = true;
findUnique(nums,visited,temp);
temp.remove(temp.size()-1);
visited[i] = false;
}
}
}
39.
class Solution {
List<List<Integer>> list = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if(candidates.length == 0 || target < 0){
return list;
}
List<Integer> temp = new ArrayList<>();
backTrace(0,candidates,target,temp);
return list;
}
public void backTrace(int start, int[] candidates, int target, List<Integer> temp){
//递归的终止条件
if (target < 0) {
return;
}
if(target == 0){
list.add(new ArrayList<>(temp));
}
for(int i = start; i < candidates.length; i++){
temp.add(candidates[i]);
backTrace(i,candidates,target-candidates[i], temp);
temp.remove(temp.size()-1);
}
}
}
这个时候需要注意一点 就是首先 这个dfs是带着参数k进行往下遍历的 如果不带着k 就会出现下面的输出情况 和不带参数是一个样子
在这里面 就会出现重复排列 即223 和 232 是同一个
这与全排列的概念不一样
第二个需要注意的点就是 在for循环中下一次dfs的起始位置 是i还是i+1!!!
如果是i+1 就会返回如下内容
40.
class Solution {
List<List<Integer>> list = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
if(candidates.length==0 || target<0)return null;
Arrays.sort(candidates);
List<Integer> temp = new ArrayList<>();
dfs(0,candidates,temp,target);
return list;
}
void dfs(int k,int[] candidates,List<Integer> temp,int target){
if(target<0)return;
if(target==0){
list.add(new ArrayList<>(temp));
}
for(int i=k;i<candidates.length;i++){
if(i>k && candidates[i]==candidates[i-1])continue;
temp.add(candidates[i]);
dfs(i+1,candidates,temp,target-candidates[i]);
temp.remove(temp.size()-1);
}
}
}
本题首先注意!题目中含有重复的元素 所以先sort
其次是 同一个数字只能被使用一次
所以需要去重操作
if(i>k&&nums[i]==nums[i-1])continue;
这句话是关键 而且要注意 是i>k 不是>0!!!
77.
class Solution {
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
List<Integer> temp = new ArrayList<>();
dfs(1,n,k,temp);
return res;
}
void dfs(int j,int n,int k,List<Integer> temp){
if(temp.size()==k){
res.add(new ArrayList(temp));
return;
}
for(int i=j;i<=n;i++){
temp.add(i);
dfs(i+1,n,k,temp);
temp.remove(temp.size()-1);
}
}
}
78.
class Solution {
List<List<Integer>> list = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
if(nums.length == 0)return null;
List<Integer> temp = new ArrayList<>();
dfs(0,nums,temp);
return list;
}
void dfs(int k, int[] nums, List<Integer> temp){
list.add(new ArrayList<>(temp));
for(int j= k;j<nums.length;j++){
temp.add(nums[j]);
dfs(j+1,nums,temp);
temp.remove(temp.size()-1);
}
}
}
90.
class Solution {
// List<List<Integer>> list = new ArrayList<>();
// Set<List<Integer>> set = new HashSet<>();
// public List<List<Integer>> subsetsWithDup(int[] nums) {
// if(nums.length==0)return null;
// Arrays.sort(nums);
// List<Integer> temp = new ArrayList<>();
// dfs(0,nums,temp);
// return list;
// }
// void dfs(int k,int[] nums,List<Integer> temp){
// if(!set.contains(temp)){
// set.add(new ArrayList<>(temp));
// list.add(new ArrayList<>(temp));
// }
// for(int j=k;j<nums.length;j++){
// temp.add(nums[j]);
// dfs(j+1,nums,temp);
// temp.remove(temp.size()-1);
// }
// }
List<List<Integer>> list = new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
if(nums.length == 0){
return null;
}
Arrays.sort(nums);
List<Integer> temp = new ArrayList<>();
backTrace(0, nums, temp);
return list;
}
public void backTrace(int start, int[] nums, List<Integer> temp){
list.add(new ArrayList<>(temp));
for(int i = start; i < nums.length; i++){
//剪枝策略
if(i > start && nums[i] == nums[i-1]){
continue;
}
temp.add(nums[i]);
backTrace(i+1,nums,temp);
temp.remove(temp.size()-1);
}
}
}
60.排列序列
import java.util.Arrays;
public class Solution {
/**
* 记录数字是否使用过
*/
private boolean[] used;
/**
* 阶乘数组
*/
private int[] factorial;
private int n;
private int k;
public String getPermutation(int n, int k) {
this.n = n;
this.k = k;
calculateFactorial(n);
// 查找全排列需要的布尔数组
used = new boolean[n + 1];
Arrays.fill(used, false);
StringBuilder path = new StringBuilder();
dfs(0, path);
return path.toString();
}
/**
* @param index 在这一步之前已经选择了几个数字,其值恰好等于这一步需要确定的下标位置
* @param path
*/
private void dfs(int index, StringBuilder path) {
if (index == n) {
return;
}
// 计算还未确定的数字的全排列的个数,第 1 次进入的时候是 n - 1
int cnt = factorial[n - 1 - index];
for (int i = 1; i <= n; i++) {
if (used[i]) {
continue;
}
if (cnt < k) {
k -= cnt;
continue;
}
path.append(i);
used[i] = true;
dfs(index + 1, path);
// 注意 1:不可以回溯(重置变量),算法设计是「一下子来到叶子结点」,没有回头的过程
// 注意 2:这里要加 return,后面的数没有必要遍历去尝试了
return;
}
}
/**
* 计算阶乘数组
*
* @param n
*/
private void calculateFactorial(int n) {
factorial = new int[n + 1];
factorial[0] = 1;
for (int i = 1; i <= n; i++) {
factorial[i] = factorial[i - 1] * i;
}
}
}
93.复原IP地址
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Stack;
public class Solution {
public List<String> restoreIpAddresses(String s) {
int len = s.length();
List<String> res = new ArrayList<>();
// 如果长度不够,不搜索
if (len < 4 || len > 12) {
return res;
}
Deque<String> path = new ArrayDeque<>(4);
int splitTimes = 0;
dfs(s, len, splitTimes, 0, path, res);
return res;
}
/**
* 判断 s 的子区间 [left, right] 是否能够成为一个 ip 段
* 判断的同时顺便把类型转了
*
* @param s
* @param left
* @param right
* @return
*/
private int judgeIfIpSegment(String s, int left, int right) {
int len = right - left + 1;
// 大于 1 位的时候,不能以 0 开头
if (len > 1 && s.charAt(left) == '0') {
return -1;
}
// 转成 int 类型
int res = 0;
for (int i = left; i <= right; i++) {
res = res * 10 + s.charAt(i) - '0';
}
if (res > 255) {
return -1;
}
return res;
}
private void dfs(String s, int len, int split, int begin, Deque<String> path, List<String> res) {
if (begin == len) {
if (split == 4) {
res.add(String.join(".", path));
}
return;
}
// 看到剩下的不够了,就退出(剪枝),len - begin 表示剩余的还未分割的字符串的位数
if (len - begin < (4 - split) || len - begin > 3 * (4 - split)) {
return;
}
for (int i = 0; i < 3; i++) {
if (begin + i >= len) {
break;
}
int ipSegment = judgeIfIpSegment(s, begin, begin + i);
if (ipSegment != -1) {
// 在判断是 ip 段的情况下,才去做截取
path.addLast(ipSegment + "");
dfs(s, len, split + 1, begin + i + 1, path, res);
path.removeLast();
}
}
}
}
题型2、泛洪填充
733. 图像渲染
class Solution {
int[][] image;
public int[][] floodFill(int[][] image, int sr, int sc, int color) {
this.image = image;
int m = image.length;
int n = image[0].length;
boolean[][] vis = new boolean[m][n];
int flag = image[sr][sc];
dfs(image,sr,sc,vis,color,flag);
return image;
}
void dfs(int[][] image,int sr,int sc,boolean[][] vis,int color,int flag){
if(sr<0||sr>=image.length||sc<0||sc>=image[0].length||vis[sr][sc]||image[sr][sc]!=flag)return;
image[sr][sc] = color;
vis[sr][sc]=true;
dfs(image,sr+1,sc,vis,color,flag);
dfs(image,sr-1,sc,vis,color,flag);
dfs(image,sr,sc+1,vis,color,flag);
dfs(image,sr,sc-1,vis,color,flag);
}
}
200.岛屿数量
class Solution {
public int numIslands(char[][] grid) {
int count =0;
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++){
if(grid[i][j]=='1'){
dfs(grid,i,j);
count++;
}
}
}
return count;
}
void dfs(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';
dfs(grid,i+1,j);
dfs(grid,i-1,j);
dfs(grid,i,j+1);
dfs(grid,i,j-1);
}
}
130. 被围绕的区域
class Solution {
public void solve(char[][] board) {
if (board == null || board.length == 0) return;
int m = board.length;
int n = board[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 从边缘o开始搜索
boolean isEdge = i == 0 || j == 0 || i == m - 1 || j == n - 1;
if (isEdge && board[i][j] == 'O') {
dfs(board, i, j);
}
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'O') {
board[i][j] = 'X';
}
if (board[i][j] == '#') {
board[i][j] = 'O';
}
}
}
}
public void dfs(char[][] board, int i, int j) {
if (i < 0 || j < 0 || i >= board.length || j >= board[0].length || board[i][j] == 'X' || board[i][j] == '#') {
// board[i][j] == '#' 说明已经搜索过了.
return;
}
board[i][j] = '#';
dfs(board, i - 1, j); // 上
dfs(board, i + 1, j); // 下
dfs(board, i, j - 1); // 左
dfs(board, i, j + 1); // 右
}
}
79. 单词搜索
class Solution {
public boolean exist(char[][] board, String word) {
char[] words = word.toCharArray();
for(int i=0;i<board.length;i++){
for(int j=0;j<board[0].length;j++){
if(dfs(board,i,j,0,words))return true;
}
}
return false;
}
boolean dfs(char[][] board,int i,int j,int k,char[] word){
if(i>=board.length || j>=board[0].length || i<0 || j<0 || board[i][j]!=word[k])return false;
if(k == word.length-1)return true;
board[i][j]='\0';
boolean res = dfs(board,i+1,j,k+1,word) || dfs(board,i,j+1,k+1,word) ||
dfs(board,i-1,j,k+1,word) || dfs(board,i,j-1,k+1,word);
board[i][j] = word[k];
return res;
}
}
题型三、字符串中的回溯问题
17.电话号码字母组合
class Solution {
public List<String> letterCombinations(String digits) {
List<String> res = new ArrayList<>();
if(digits == "")return res;
Map<Character,String> map = new HashMap<>();
map.put('2', "abc");
map.put('3', "def");
map.put('4', "ghi");
map.put('5', "jkl");
map.put('6', "mno");
map.put('7', "pqrs");
map.put('8', "tuv");
map.put('9', "wxyz");
backtrack(res,map,digits,0,new StringBuilder());
return res;
}
void backtrack(List<String> res,Map<Character,String> map,String digits,int i,StringBuilder temp){
if(i==digits.length()){
res.add(temp.toString());
}else{
char d = digits.charAt(i);
String letter = map.get(d);
for(int j=0;j<letter.length();j++){
temp.append(letter.charAt(j));
backtrack(res,map,digits,i+1,temp);
temp.deleteCharAt(i);
}
}
}
}
784. 字母大小写全排列
class Solution {
private List<String> res = new ArrayList<>();
public List<String> letterCasePermutation(String s) {
char[] ch = s.toCharArray();
int n = ch.length;
backTracking(ch, 0, n);
return res;
}
private void backTracking(char[] ch, int index, int n) {
res.add(new String(ch));
for (int i = index; i < n; i++) {
if (isDigit(ch[i])) {
continue;
}
ch[i] ^= 32;
backTracking(ch, i + 1, n);
ch[i] ^= 32;
}
}
private boolean isDigit(char c) {
if (c >= '0' && c <= '9') {
return true;
}
return false;
}
}
21.括号生成
class Solution {
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
if(n==0)return res;
dfs("",n,n,res);
return res;
}
void dfs(String curStr,int left,int right,List<String> res){
if(left==0&&right==0){
res.add(curStr);
return;
}
if(left>right)return;
if(left>0){
dfs(curStr+"(",left-1,right,res);
}
if(right>0)dfs(curStr+")",left,right-1,res);
}
}
51. n皇后问题
class Solution {
// 符合条件的单个结果(路径)
private List<String> path = new LinkedList<>();
// 符合条件的结果集合
private List<List<String>> ans = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
// 新建字符棋盘,相比操作字符串,更方便
char[][] board = new char[n][n];
// 初始化棋盘
for (int i=0; i<board.length; ++i) {
Arrays.fill(board[i], '.');
}
// 从 row=0 第一行开始遍历
backtracking(n, 0, board);
return ans;
}
private void backtracking(int n, int row, char[][] board) {
// 结束条件:所有行都遍历完成,注意是n不是n-1
if (row == n) {
path = array2List(board);
ans.add(path);
return;
}
// 遍历选择列表,这里的一个选择就是 row+col
for (int col=0; col<n; ++col) {
if (!isValid(n, row, col, board)) {
// 排除不合法的选择(皇后存在冲突)
continue;
}
// 做选择
board[row][col] = 'Q';
// 递归调用,进入下一行的决策
backtracking(n, row+1, board);
// 撤销选择
board[row][col] = '.';
}
}
/**
* 将符合条件的某个棋盘数组转换成字符串列表
*/
private List<String> array2List(char[][] board) {
List<String> ans = new ArrayList<>();
for (int i=0; i<board.length; ++i) {
ans.add(String.valueOf(board[i]));
}
return ans;
}
/**
* 判断是否可以在当前棋盘 board[row][col] 这个位置放置皇后Q
* n是棋盘的大小,避免重复计算,所以作为参数传入
*/
private boolean isValid(int n, int row, int col, char[][] board) {
// 检查board[row][col]这个位置所在这一列正上方中,看是否已经存在 Q,存在说明列存在冲突,不能再放置皇后Q
for (int i=0; i<row; ++i) {
if (board[i][col] == 'Q') {
return false;
}
}
// 检查board[row][col]这个位置左上方对角线是否已经存在皇后,左下方不用检查,因为backtracking函数是一行一行遍历的,下方的还没遍历到呢
for (int i=row-1,j=col-1; i>=0 && j>=0; --i,--j) {
if (board[i][j] == 'Q') {
return false;
}
}
// 检查board[row][col]这个位置右上方对角线是否已经存在皇后,右下方不用检查,因为 backtracking函数是一行一行遍历的,下方的还没遍历到呢
for (int i=row-1,j=col+1; i>=0 && j<n; --i,++j) {
if (board[i][j] == 'Q') {
return false;
}
}
return true;
}
}