【算法题型总结】---1.回溯
〇、算法介绍
1、三要素:路径、选择列表、结束条件
递归前做选择,递归后撤销选择
2、常见题型
组合问题
排序问题(全排列)
子集问题
棋盘问题(n皇后、数独)
字符串切割问题
3、代码提取
void backtracking(){ if(终止条件) { 记录结果; return; } for(集合){ 做选择; 递归操作; 撤销选择; } }
4、目录
(一)排列
- 去重的全排列(字符串)
- 允许重复的字符串全排列
- 有重复项数字的所有排列
- 没有重复项数字的所有排列--按字典序排列
(二)组合
- 输出"{a,b}c{d,e}"的组合
- 生成n对括号组成的合法组合(类似生成合法的IP地址)
- 数组中加起来为目标值的组合
- 复原ip地址
(三)子集
- NC27 集合的所有子集
- 递增子序列(力扣491)
(四)分割
- 分割回文串-力扣131
- 数字字符串转为ip地址
(五)棋盘
- N皇后问题
- 数独
一、排列问题
1、去重的全排列(字符串)
package com.liujinhui; import java.util.*; import java.util.stream.Collectors; public class BackTrack { public static Set<String> res = new HashSet<>(); public static void main(String[] args) { Permutation("ab").forEach(System.out::println); } public static ArrayList<String> Permutation(String str) { char[] arr = str.toCharArray(); LinkedList<Character> track = new LinkedList<>(); backTrack(arr, track); return new ArrayList<>(res.stream().collect(Collectors.toList())); } //元素去重的全排列 public static void backTrack(char[] arr, LinkedList<Character> track) { //终止条件 if (track.size() == arr.length) { StringBuilder sb = new StringBuilder(); for (Character character : track) { sb.append(character); } res.add(sb.toString()); return; } for (int i = 0; i < arr.length; i++) { //做选择 if (track.contains(arr[i])){ continue; } track.add(arr[i]); //递归 backTrack(arr, track); //撤销选择 track.removeLast(); } } }
2、允许重复的字符串全排列
NC121 字符串的排列
import java.util.*; import java.util.stream.Collectors; public class Solution { public static Set<String> res = new HashSet<>(); public static LinkedList<Character> track = new LinkedList<>(); public static ArrayList<String> Permutation(String str) { char[] arr = str.toCharArray(); boolean[] flag = new boolean[arr.length]; backTrack(arr, flag); return new ArrayList<>(res.stream().collect(Collectors.toList())); } public static void backTrack(char[] arr, boolean[] flag) { // 结束条件 if (track.size() == arr.length) { StringBuilder sb = new StringBuilder(); track.stream().forEach(x -> sb.append(x)); res.add(sb.toString()); return; } //循环 for (int i = 0; i < arr.length; i++) { //做选择 if (i > 0 && arr[i] == arr[i-1] && flag[i-1] == false) { continue; } if (flag[i] == false) { flag[i] = true; track.add(arr[i]); //递归 backTrack(arr, flag); //撤销选择-回溯 flag[i] = false; track.removeLast(); } } } }
3、有重复项数字的所有排列
public ArrayList<ArrayList<Integer>> permuteUnique(int[] num)
public ArrayList<ArrayList<Integer>> permuteUnique(int[] num) {
ArrayList<Integer> path = new ArrayList<>();
HashSet<ArrayList<Integer>> resSet = new HashSet<>();
boolean[] visited = new boolean[num.length];
dfs(0,path,resSet,num,visited);
return new ArrayList<>(resSet);
}
private void dfs(int level, ArrayList<Integer> path, HashSet<ArrayList<Integer>> resSet, int[] num, boolean[] visited) {
if (level==num.length) {
resSet.add(new ArrayList<>(path));
return;
}
for (int i = 0; i <num.length ; i++) {
if (!visited[i]){
//给当前层上个锁 这个数不能再用了
visited[i] = true;
path.add(num[i]);//★
dfs(level+1,path,resSet,num,visited);//★ level是计数器
//*删除一定注意这里 删除易错 每次删最后那个 你要相信上一层一直到最底层已经帮你删好元素了 你只要控制之前插入的那个就好
path.remove(path.size()-1); visited[i] = false;
}
}
}
4、没有重复项数字的所有排列--按字典序排列
public ArrayList<ArrayList<Integer>> permute(int[] num)
import java.util.*;
public class Solution {
ArrayList<ArrayList<Integer>> res=new ArrayList<ArrayList<Integer>>();
public ArrayList<ArrayList<Integer>> permute(int[] nums) {
if (nums == null || nums.length < 1) return res;
Arrays.sort(nums);
ArrayList<Integer> list = new ArrayList<Integer>();
solve(list, nums);
return res;
}
private void solve(ArrayList<Integer> list, int[] nums) {
if (list.size() == nums.length) {
res.add(new ArrayList<Integer>(list));
return;
}
for (int i = 0; i < nums.length; i++) {
if (!list.contains(nums[i])) { //如果已经包含就不再加入---相当于剪纸啊?
list.add(nums[i]);
solve(list, nums);
list.remove(list.size() - 1);
}
}
}
}
二、组合
1、蔚来22秋招真题--输出"{a,b}c{d,e}"的组合
import java.util.*; public class Nio { public static int sum = 0; public static LinkedList<String> res = new LinkedList<>(); public static LinkedList<Character> track = new LinkedList<>(); public static void main(String[] args) { String[] split = split("{a,b}c{d,e}"); Arrays.stream(split).forEach(System.out::println); } /** * 第二题,输出"{a,b}c{d,e}"的组合 * @param str * @return */ public static String[] split (String str) { // "{a,b}c{d,e}" char[] arrs = str.toCharArray(); LinkedList<String> list = new LinkedList<>(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < arrs.length; i++) { if (arrs[i] == '}') { list.add(sb.toString()); sb = new StringBuilder(); } else if (arrs[i] == ',' || arrs[i] == '{') { continue; } else if (i > 1 && i + 1 < arrs.length && arrs[i-1] == '}' && arrs[i+1] == '{'){ sb.append(arrs[i]); list.add(sb.toString()); sb = new StringBuilder(); } else { sb.append(arrs[i]); } } //[[a,b],[c],[de]] backtracking(list, 0); String[] arrays = new String[res.size()]; for (int i = 0; i < res.size(); i++) { arrays[i] = res.get(i); } return arrays; } public static void backtracking(LinkedList<String> list, int num) { //结束条件 if (num == list.size()) { StringBuilder sb = new StringBuilder(); for (Character character : track) { sb.append(character); } res.add(sb.toString()); // track = new LinkedList<>(); track会随着remove陆续清空 return; } char[] array = list.get(num).toCharArray(); //循环 for (int i = 0; i < array.length; i++) { //选择 track.add(array[i]); //递归 backtracking(list, num + 1); track.removeLast(); //track会随着remove陆续清空 } } }
2、生成n对括号组成的合法组合(类似生成合法的IP地址)
public ArrayList<String> generateParenthesis (int n)
定义函数递归,字符长度=n,添加结果,left<n,加左括号,right小于left,加右括号
public List<String> generateParenthesis(int n) {
ArrayList<String> result = new ArrayList<>(10);
backtrack("", 0, 0, n, result);
return result;
}
private void backtrack(String string, int open, int close, int n, List<String> result) {
if (string.length() == n << 1) {
result.add(string);
return;
}
if (open < n) {
backtrack(string+"(", open+1, close, n, result);
}
if (close < open) {
backtrack(string+")", open, close+1, n, result);
}
}
3、数组中加起来为目标值的组合
public ArrayList<ArrayList<Integer>> combinationSum2(int[] num, int target)
需要先排序
import java.util.*; public class Solution { public ArrayList<ArrayList<Integer>> combinationSum2(int[] num, int target) { ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>(); ArrayList<Integer> arr = new ArrayList<Integer>(); if(num == null || num.length==0 || target<0)return res; Arrays.sort(num); dfs(num,target,res,arr,0); return res; } void dfs(int[] num,int target,ArrayList<ArrayList<Integer>> res,ArrayList<Integer> arr,int start){ if(target==0){ res.add(new ArrayList<Integer>(arr)); return; } if(start >= num.length)return; for(int i=start;i<num.length;i++){ if(i > start && num[i] == num[i-1])continue; if(num[i] <= target){ arr.add(num[i]); dfs(num,target-num[i],res,arr,i+1); arr.remove(arr.size()-1); } } return; } }
4、复原ip地址
给定一个只包含数字的字符串,复原它并返回所有可能的 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 地址。
示例 1: 输入:s = "25525511135" 输出:["255.255.11.135","255.255.111.35"]
示例 2: 输入:s = "0000" 输出:["0.0.0.0"]
示例 3: 输入:s = "1111" 输出:["1.1.1.1"]
示例 4: 输入:s = "010010" 输出:["0.10.0.10","0.100.1.0"]
示例 5: 输入:s = "101023" 输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
提示: 0 <= s.length <= 3000 s 仅由数字组成
步骤 :参数、结束条件、循环、做选择、递归、移除选择/回溯
class Solution { public List<String> restoreIpAddresses(String s) { } }
解法:回溯
class Solution { List<String> result = new ArrayList<>(); public List<String> restoreIpAddresses(String s) { if (s.length() > 12) return result; // 算是剪枝了 backTrack(s, 0, 0); return result; } // startIndex: 搜索的起始位置, pointNum:添加逗点的数量 private void backTrack(String s, int startIndex, int pointNum) { if (pointNum == 3) {// 逗点数量为3时,分隔结束 // 判断第四段⼦字符串是否合法,如果合法就放进result中 if (isValid(s,startIndex,s.length()-1)) { result.add(s); } return; } for (int i = startIndex; i < s.length(); i++) { if (isValid(s, startIndex, i)) { s = s.substring(0, i + 1) + "." + s.substring(i + 1); //在str的后⾯插⼊⼀个逗点 pointNum++; backTrack(s, i + 2, pointNum);// 插⼊逗点之后下⼀个⼦串的起始位置为i+2 pointNum--;// 回溯 s = s.substring(0, i + 1) + s.substring(i + 2);// 回溯删掉逗点 } else { break; } } } // 判断字符串s在左闭⼜闭区间[start, end]所组成的数字是否合法 private Boolean isValid(String s, int start, int end) { if (start > end) { return false; } if (s.charAt(start) == '0' && start != end) { // 0开头的数字不合法 return false; } int num = 0; for (int i = start; i <= end; i++) { if (s.charAt(i) > '9' || s.charAt(i) < '0') { // 遇到⾮数字字符不合法 return false; } num = num * 10 + (s.charAt(i) - '0'); if (num > 255) { // 如果⼤于255了不合法 return false; } } return true; } }
三、子集
import java.util.*; import java.util.stream.Collectors; public class Solution { public static ArrayList<ArrayList<Integer>> res = new ArrayList<>(); public static LinkedList<Integer> track = new LinkedList<>(); public static ArrayList<ArrayList<Integer>> subsets(int[] S) { if (S.length == 0) { res.add(new ArrayList<>()); return res; } backtracking(S, 0); return res; } public static void backtracking(int[] S, int startIndex) { //每次得到的结果都是子集,都加入res res.add(new ArrayList<>(track.stream().collect(Collectors.toList()))); //结束条件 if (startIndex == S.length) { return; } //循环 for (int i = startIndex; i < S.length; i++) { //满足条件做选择 track.add(S[i]); //递归 backtracking(S, i + 1); //撤销选择 track.removeLast(); } } }
2、递增子序列(力扣491)
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
import java.util.*; class Solution { public List<List<Integer>> res = new ArrayList<>(); public LinkedList<Integer> track = new LinkedList<>(); public List<List<Integer>> findSubsequences(int[] nums) { if (nums.length == 0) { return new ArrayList<>(); } backtracking(nums, 0); return res; } public void backtracking(int[] nums, int startIndex) { //最后执行完才结束,每次都要添加 if (track.size() > 1) { res.add(new ArrayList<>(track)); //res.add(new ArrayList<>(track));传进去的是空 } boolean[] used = new boolean[201]; //循环 for (int i = startIndex; i < nums.length; i++) { //不满足条件,继续 if ((!track.isEmpty() && nums[i] < track.get(track.size() - 1)) || used[nums[i] + 100]) { continue; } //做选择 track.add(nums[i]); used[nums[i] + 100] = true; //递归 backtracking(nums, i + 1); //回溯 track.remove(track.size() - 1); } } }
注意:方法和成员变量要声明成非静态,以便相互调用
四、分割
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
class Solution { List<List<String>> lists = new ArrayList<>(); Deque<String> deque = new LinkedList<>(); public List<List<String>> partition(String s) { backTracking(s, 0); return lists; } private void backTracking(String s, int startIndex) { //如果起始位置大于s的大小,说明找到了一组分割方案 if (startIndex >= s.length()) { lists.add(new ArrayList(deque)); return; } for (int i = startIndex; i < s.length(); i++) { //如果是回文子串,则记录 if (isPalindrome(s, startIndex, i)) { String str = s.substring(startIndex, i + 1); deque.addLast(str); } else { continue; } //起始位置后移,保证不重复 backTracking(s, i + 1); deque.removeLast(); } } //判断是否是回文串 private boolean isPalindrome(String s, int startIndex, int end) { for (int i = startIndex, j = end; i < j; i++, j--) { if (s.charAt(i) != s.charAt(j)) { return false; } } return true; } }
2、数字字符串转为ip地址
public ArrayList<String> restoreIpAddresses (String s)
回溯,split分割字符串,判断第一位不能为0并且数不能大于255
分别递归一位数、两位数、三位数时的情况backTrack(String s, int start, int cnt)
import java.util.*;
public class Solution {
/**
*
* @param s string字符串
* @return string字符串ArrayList
*/
ArrayList<String> res = new ArrayList<>();
public ArrayList<String> restoreIpAddresses (String s) {
// write code here
if(s.length() == 0)
return res;
//表示当前字符串s,可以从第0个位置开始插入'.' ,还有3个'.'可以插入
backTrack(s, 0, 3);
return res;
}
public void backTrack(String s, int start, int cnt){
if(cnt == 0){
String[] splits = s.split("\\.");
//没有插入4个合法的小数点
if(splits.length < 4)
return;
//判断每一位是否合法
for(String str:splits){
if(str.length() > 1 && str.charAt(0) == '0') return; //最前面的数字不能为0
if(Integer.valueOf(str) > 255) return; //每一位都不能大于255
}
res.add(s);
return;
}
if(start >= s.length()) return; //没有插完全部的点 就已经超出字符串的范围了
int len = s.length();
//每次将一个字符作为一位
backTrack(s.substring(0,start+1)+'.'+s.substring(start+1,len), start+2, cnt-1);
//每次将两位字符作为一位
if(start < len-2)
backTrack(s.substring(0,start+2)+'.'+s.substring(start+2,len), start+3, cnt-1);
//每次将三位字符作为一位
if(start < len-3)
backTrack(s.substring(0,start+3)+'.'+s.substring(start+3,len), start+4, cnt-1);
}
}
五、棋盘
1、N皇后问题
public int Nqueen (int n)
判断两个点是否冲突
public class Solution {
int answer;
public int Nqueen (int n) {
int to=n+1;
//ans[1]为第一个皇后所在位置, 依此类推
int[] ans=new int[to];
//加了ans[0]占位,第一个皇后就可以不用单独遍历
ans[0]=Integer.MIN_VALUE;
dfs(ans,1,to);
return answer;
}
public void dfs(int[] now,int n,int to){
if(n==to){
answer++;
return;
}
//第n个皇后依次判断能够放在0到to-1的位置i上
for(int i=0;i<to-1;i++){
boolean flag=true;
//依次判断第0到第n-1个皇后j的位置与n是否冲突
for(int j=0;j<n;j++){
int sub=n-j;
int indexJ=now[j];
//列差==行差则在对角线上
if(indexJ+sub==i||indexJ-sub==i||i==indexJ){
//有冲突则判断下一个位置i+1
flag=false;
break;
}
}
if(flag){
now[n]=i;
dfs(now,n+1,to);
}
}
}
}
其他:
class Solution { List<List<String>> res = new ArrayList<>(); public List<List<String>> solveNQueens(int n) { char[][] chessboard = new char[n][n]; for (char[] c : chessboard) { Arrays.fill(c, '.'); } backTrack(n, 0, chessboard); return res; } public void backTrack(int n, int row, char[][] chessboard) { if (row == n) { res.add(Array2List(chessboard)); return; } for (int col = 0;col < n; ++col) { if (isValid (row, col, n, chessboard)) { chessboard[row][col] = 'Q'; backTrack(n, row+1, chessboard); chessboard[row][col] = '.'; } } } public List Array2List(char[][] chessboard) { List<String> list = new ArrayList<>(); for (char[] c : chessboard) { list.add(String.copyValueOf(c)); } return list; } public boolean isValid(int row, int col, int n, char[][] chessboard) { // 检查列 for (int i=0; i<row; ++i) { // 相当于剪枝 if (chessboard[i][col] == 'Q') { return false; } } // 检查45度对角线 for (int i=row-1, j=col-1; i>=0 && j>=0; i--, j--) { if (chessboard[i][j] == 'Q') { return false; } } // 检查135度对角线 for (int i=row-1, j=col+1; i>=0 && j<=n-1; i--, j++) { if (chessboard[i][j] == 'Q') { return false; } } return true; } }
2、数独
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.' 表示。
class Solution { public void solveSudoku(char[][] board) { solveSudokuHelper(board); } private boolean solveSudokuHelper(char[][] board){ //「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列, // 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」 for (int i = 0; i < 9; i++){ // 遍历行 for (int j = 0; j < 9; j++){ // 遍历列 if (board[i][j] != '.'){ // 跳过原始数字 continue; } for (char k = '1'; k <= '9'; k++){ // (i, j) 这个位置放k是否合适 if (isValidSudoku(i, j, k, board)){ board[i][j] = k; if (solveSudokuHelper(board)){ // 如果找到合适一组立刻返回 return true; } board[i][j] = '.'; } } // 9个数都试完了,都不行,那么就返回false return false; // 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解! // 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」 } } // 遍历完没有返回false,说明找到了合适棋盘位置了 return true; } /** * 判断棋盘是否合法有如下三个维度: * 同行是否重复 * 同列是否重复 * 9宫格里是否重复 */ private boolean isValidSudoku(int row, int col, char val, char[][] board){ // 同行是否重复 for (int i = 0; i < 9; i++){ if (board[row][i] == val){ return false; } } // 同列是否重复 for (int j = 0; j < 9; j++){ if (board[j][col] == val){ return false; } } // 9宫格里是否重复 int startRow = (row / 3) * 3; int startCol = (col / 3) * 3; for (int i = startRow; i < startRow + 3; i++){ for (int j = startCol; j < startCol + 3; j++){ if (board[i][j] == val){ return false; } } } return true; } }
本文来自博客园,作者:哥们要飞,转载请注明原文链接:https://www.cnblogs.com/liujinhui/p/15118926.html