文章目录
刷题大纲
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等
核心模板
定义两个变量 一个记录路径path 一个为最终结果result
1. 终止条件是什么
(1)一般是path.size()
(2)取所有结果的不需要终止条件
2. for循环 是从0开始 还是从startIndex开始(取过的不能再取了,跟顺序无关[1,2] [2,1]算一种情况)
3. backTracking函数返回值是什么 结果唯一,返回Boolean
4. 调用递归函数backTracking函数的参数
5. 是否需要去重
6. 涉及到需要k个数的 可以考虑剪枝优化,对i的范围进一步限制
整体套路
画树形结构,for表示横向遍历,for的内容表示深度,分析上面问题------------------创建path result变量---------------------------------套模板
一、子集问题
78 子集(简单回溯)
求所有可能且无限制不需要终止条件
跟顺序无关,1,2和2,1表示同一个,因此每次index+1开始遍历
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backTracking(nums,0);
return result;
}
public void backTracking(int[] nums,int startIndex){
result.add(new ArrayList<>(path));
for(int i = startIndex;i<nums.length;i++){
path.add(nums[i]);
backTracking(nums,i+1);
path.removeLast();
}
}
90 子集2 (需要去重)
先排序,后去重: 当前元素与上一个元素相同
去重是在横向方向for循环上去重
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
backTracking(nums,0);
return result;
}
public void backTracking(int[] nums, int startIndex){
result.add(new ArrayList<>(path));
for(int i = startIndex; i <nums.length;i++){
if (i> startIndex && nums[i]==nums[i-1]){
continue;
}
path.add(nums[i]);
backTracking(nums,i+1);
path.removeLast();
}
}
}
二、组合问题
77 组合(常规回溯+剪枝优化)
限制i的范围进行剪枝优化
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
// List<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
backTrack(n,k,1);
return result;
}
public void backTrack(int n,int k, int startIndex){
if (path.size() == k){//终止条件:path的大小与所给一致
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i<=n-(k-path.size())+1; i++){ //剪枝优化:必须有足够多的数满足
path.add(i);
backTrack(n,k,i+1); //递归函数要+1
path.removeLast();
}
}
}
17 电话号码的字母组合(回溯+建立数字和字母映射关系)
画出树形结构
for循环的是每个数字对应的字母
遍历深度的是 所给字符串
把数字和字母对应起来
class Solution {
List<String> result = new ArrayList<>();
StringBuilder temp = new StringBuilder();
public List<String> letterCombinations(String digits) {
if (digits.length() == 0 || digits == null){
return result;
}
String[] numStr = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"}; //需要把数字映射到字符串上
backTracking(digits, numStr, 0);
return result;
}
public void backTracking(String digits,String[] numStr, int index){
if (index == digits.length()){
result.add(temp.toString());// 把列表变为字符串
return;
}
String str = numStr[digits.charAt(index) - '0'];
for (int i = 0; i < str.length(); i++){
temp.append(str.charAt(i));
backTracking(digits,numStr,index+1);
temp.deleteCharAt(temp.length()-1);
}
}
}
39 组合总和( 所给数组无重复元素 + 同一数字无限制重复选取)
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backTracking(candidates,target,0,0);
return result;
}
public void backTracking(int[] candidates, int target, int sum, int index){
if (sum > target){
return;
}
if (sum == target){
result.add(new ArrayList<>(path));
return;
}
for (int i = index; i < candidates.length;i++){ //搜索过的不需要再搜索
sum += candidates[i];
path.add(candidates[i]);
backTracking(candidates,target,sum,i); //遍历是从i开始
sum -= candidates[i];
path.removeLast();
}
}
}
backTracking(candidates,target,sum,i)注意此处不需要i+1,且for循环从index开始
40 组合总和 Ⅱ (给定集合存在重复数字 + 每个数字只能使用一次, 需要去重)
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList path = new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backTracking(candidates,target,0,0);
return result;
}
public void backTracking(int[] candidates, int target,int sum,int index){
if (sum > target){
return;//深度上剪枝
}
if (sum == target){
result.add(new ArrayList<>(path));
}
for (int i = index; i<candidates.length;i++){
if (i>index && candidates[i] == candidates[i-1]){
continue;//宽度上剪枝
}
sum += candidates[i];
path.add(candidates[i]);
backTracking(candidates,target,sum,i+1);
sum -= candidates[i];
path.removeLast();
}
}
}
216 组合总和 Ⅲ (回溯 + 剪枝)
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backTrack(k,n,1,0);
return result;
}
public void backTrack(int k,int n,int startIndex,int sum){
if (sum > n){ //深度剪枝
return;
}
if (path.size()==k){
if (sum == n){
result.add(new ArrayList<>(path));
return;
}
}
for (int i=startIndex; i <= 9-(k-path.size())+1; i++){ //宽度剪枝:数目不够
sum += i;
path.add(i);
backTrack(k,n,i+1,sum);
sum -= i;
path.removeLast();
}
}
}
三、排列问题
46.全排列
引入一个used数组记录已经使用过的元素即可
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
boolean[] used;
public List<List<Integer>> permute(int[] nums) {
used = new boolean[nums.length];
backTracking(nums);
return result;
}
public void backTracking(int[] nums){
if (path.size() == nums.length){
result.add(new ArrayList<>(path));
return;
}
for(int i =0;i<nums.length;i++){
if (used[i] == true){
continue;
}
used[i] = true;
path.add(nums[i]);
backTracking(nums);
path.removeLast();
used[i] = false;
}
}
}
47.全排列Ⅱ(宽度上进行去重)
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
boolean[] used;
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);
used = new boolean[nums.length];
backTracking(nums);
return result;
}
public void backTracking(int[] nums){
if (path.size() == nums.length){
result.add(new ArrayList<>(path));
return;
}
for(int i =0;i<nums.length;i++){
if (used[i] == true){
continue;
}
if (i>0 && nums[i-1] == nums[i] && used[i-1]==true){
continue;
}//在宽度上进行去重 而深度上可以出现重复的元素
used[i] = true;
path.add(nums[i]);
backTracking(nums);
path.removeLast();
used[i] = false;
}
}
}
四、分割问题
131.分割回文串
类似组合问题,先求出分割的可能性,再判断是否为回文串
(1) if 终止条件: 切割线大于字符串长度
(2) 取过的元素不能再取了 for开始遍历的坐标为startIndex
(3) 子串的位置为 startIndex,i+1
class Solution {
List<List<String>> result = new ArrayList<>();
LinkedList<String> path = new LinkedList<>();
public List<List<String>> partition(String s) {
backTracking(s,0);
return result;
}
public void backTracking(String s, int startIndex){
if (startIndex >= s.length()){
result.add(new ArrayList<>(path));
return;
}
//for为横向遍历
//递归为纵向遍历
for (int i = startIndex;i<s.length();i++){
if ( isPalindrom(s,startIndex,i)){//横向遍历
String str = s.substring(startIndex,i+1);//取子串
path.add(str);
}
else{continue;}
backTracking(s, i + 1);
path.removeLast();
}
}
//判断回文
public boolean isPalindrom(String s,int startIndex,int endIndex){
for (int i = startIndex, j = endIndex;i<j;i++,j--){
if(s.charAt(i) != s.charAt(j)){
return false;
}
}
return true;
}
}
93.复原ip地址
(1) if终止条件: 点数 数量为3
(2) for循环 起始位置 startIndex
(3) string.substring 取子串函数
s.charAt(i) 按索引取元素
class Solution {
List<String> result = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
if (s.length() > 12) {
return result;
} //长度最大为12
backTracking(s,0,0);
return result;
}
public void backTracking(String s, int startIndex, int pointNum){
if (pointNum == 3){
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);
pointNum++;
backTracking(s,i+2,pointNum);//加了分隔号 i+2
pointNum--;
s = s.substring(0,i+1) + s.substring(i+2);
}
else{break;}
}
}
//判断是否符合要求
public Boolean isValid(String s, int start, int end){
if (start>end){
return false;
}
if (s.charAt(start) == '0' && start != end){
return false; //前导不能为0
}
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){
return false;
}
}
return true;
}
}
五.棋盘问题
51.N皇后
棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度
for循环从0开始
游戏规则
不能同行
不能同列
不能同斜线 (45度和135度角)
class Solution {
List<List<String>> result = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
//初始化一个二维数组 模拟棋盘
char[][] chessboard = new char[n][n];
for (char[] c:chessboard){//记住写法
Arrays.fill(c,'.');
}
//调用回溯函数
backTracking(n,0,chessboard);
return result;
}
public void backTracking(int n, int row, char[][] chessboard){
if (row == n){
List<String> path = new ArrayList<>();
path = Array2List(chessboard); //把二维数组变成yi维数组
result.add(new ArrayList<>(path)); //添加结果
return;
}
for (int col = 0; col < n; col++){//列遍历 即宽度
if (isValid(n,row,col,chessboard)){//判断是否合法
chessboard[row][col] = 'Q';
backTracking(n, row+1, chessboard);
chessboard[row][col] = '.';
}
}
}
//把二维棋盘变为一维数组
public List<String> Array2List(char[][] chessboard){
List<String> path = new ArrayList<>();
for (char[] c:chessboard){//注意此格式
path.add(String.copyValueOf(c));
}
return path;
}
//判断是否合法 已知棋子的行和列
public boolean isValid(int n,int row,int col,char[][] chessboard){
//列:每一列不能出现相同的 如果该列已经存在Q了 那么将不能再放Q 即固定列 从第0行开始遍历到row行 不能出现Q
for(int i = 0; i < row; i++){
if(chessboard[i][col] == 'Q'){
return false;
}
}
//检查45对角线:检查 row行 col列左上角 不能存在Q
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;
}
37.解数独
一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!
回溯函数返回值必须为boolean
class Solution {
public void solveSudoku(char[][] board) {
backTracking(board);
}
public boolean backTracking(char[][] board) {
for(int i =0;i<9;i++){//行
for(int j=0;j<9;j++){//列遍历
if(board[i][j] != '.'){continue;}
for (char c ='1';c<='9';c++){
if (isVailid(i,j,c,board)){
board[i][j] = c;
if(backTracking(board)) {return true;}
board[i][j] = '.';
}
}
return false;
}
}
return true;
}
public boolean isVailid(int i,int j,char c,char[][] board){
//行:重复
for(int col=0;col<9;col++){
if (board[i][col] == c){
return false;
}
}
//列:重复
for (int row=0;row<9;row++){
if (board[row][j] == c){
return false;
}
}
//九宫格
int startRow = (i / 3) * 3;
int startCol = (j / 3) * 3;
for (int p = startRow; p <= startRow+2; p++){
for (int q = startCol; q<=startCol+2;q++){
if(board[p][q] == c){
return false;
}
}
}
return true;
}
}
六、其他问题
491. 递增子序列
(1)if终止条件:path大于2
(2)for循环从startIndex开始
(3)去重 用hashmap (难点在于 同一层如何去重)
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracking(nums,0);
return result;
}
public void backTracking(int[] nums,int startIndex){
if (path.size() >= 2){
result.add(new ArrayList<>(path));
}
HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
for (int i = startIndex;i<nums.length;i++){
if ( !path.isEmpty() && nums[i] < path.getLast()){
continue; // nums[i] 必须比path现有最后一个元素大
}
if (map.containsValue(nums[i]) ){
continue;
}
path.add(nums[i]);
map.put(i,nums[i]);
backTracking(nums,i+1);
path.removeLast();
}
}
}
332.重新安排行程
问题:
- 一个行程中,如果航班处理不好容易变成一个圈,成为死循环
用used[] 记录已经使用过的机票 - 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ?
Collections.sort(tickets, (a,b) -> a.get(1).compareTo(b.get(1))) 只要对机票的目的地排序即可 - 使用回溯法(也可以说深搜) 的话,那么终止条件是什么呢?
path.size = 机票数+1 - 搜索的过程中,如何遍历一个机场所对应的所有机场。
机票的第一个元素 == path的最后一个元素 - 回溯函数返回值为boolean,路径唯一
class Solution {
LinkedList<String> path = new LinkedList<>();
LinkedList<String> result;
public List<String> findItinerary(List<List<String>> tickets) {
Collections.sort(tickets, (a,b) -> a.get(1).compareTo(b.get(1)));//排序 按字母排序 写法复杂
path.add("JFK"); //起点必然为JFK
boolean[] used = new boolean[tickets.size()]; //记录已经用过的机票
backTracking(tickets,used);
return result;
}
public boolean backTracking(List<List<String>> tickets,boolean[] used){
if (path.size() == tickets.size()+1){
result = new LinkedList(path);
return true;
}
for(int i = 0; i < tickets.size();i++){
if (!used[i] && tickets.get(i).get(0).equals(path.getLast())){
path.add(tickets.get(i).get(1));//
used[i] = true;
if (backTracking(tickets,used)){
return true;
}
used[i] = false;
path.removeLast();
}
}
return false;
}
}