文章目录
组合问题
组合总数

- 已经选择的元素个数:path.size();
- 所需需要的元素个数为: k - path.size();
- 列表中剩余元素(n-i) >= 所需需要的元素个数(k - path.size())
- 在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历
class Solution {
List<List<Integer>>res=new ArrayList<>();
LinkedList<Integer>path=new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
backTracking(n,k,1);
return res;
}
public void backTracking(int n,int k,int startIndex){
if(path.size()==k){
res.add(new LinkedList<>(path));
return;
}
for(int i=startIndex;i<=(n-(k-path.size())+1);i++){
path.add(i);
//递归
backTracking(n,k,i+1);
//回溯
path.removeLast();
}
}
}
限制选取个数的组合总和

递归时相加记录当前和,回溯时相减还原当前和
class Solution {
List<List<Integer>>res=new ArrayList<>();
LinkedList<Integer>path=new LinkedList<>();
static int currentSum;
public List<List<Integer>> combinationSum3(int k, int targetSum) {
bacKTracking(targetSum,k,currentSum,1);
return res;
}
public void bacKTracking(int targetSum,int k,int currentSum,int startIndex){
//剪枝,当前和超过了,直接返回
if(currentSum>targetSum){
return;
}
if(path.size()==k){
if(currentSum==targetSum){
res.add(new LinkedList<>(path));
}
}
//遍历宽度剪枝
for(int i=startIndex;i<=9-(k-path.size())+1;i++){
currentSum+=i;
path.add(i);
bacKTracking(targetSum,k,currentSum,i+1);
currentSum-=i;
path.removeLast();
}
}
}
可重复选取的组合总和

1. 递归终点是当前和大于等于目标和
2. 因为没有数量限制,因此回溯时的开始起点仍然从i开始
3. 剪枝:先排序,当前和+下一个的总和>目标和就break
class Solution {
List<List<Integer>>res=new ArrayList<>();
LinkedList<Integer>path=new LinkedList<>();
private int currentSum;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if(candidates==null|candidates.length==0){
return res;
}
Arrays.sort(candidates);
backTracking(candidates,target,0,0);
return res;
}
public void backTracking(int[]candidates,int target,int currentSum,int startIndex){
if(currentSum>target){
return;
}
if(currentSum==target){
res.add(new LinkedList(path));
return;
}
for(int i=startIndex;i<candidates.length;i++){
if(currentSum+candidates[i]>target){
break;
}
currentSum+=candidates[i];
path.add(candidates[i]);
//因为数量无限制,因此回溯时仍然从i开始
backTracking(candidates,target,currentSum,i);
currentSum-=candidates[i];
path.removeLast();
}
}
}
需要去重的组合总和

去重:用一个used数组来记录已使用过的数,如果candidates[i]==candidates[i-1]&&used[i-1]==false,则跳过

class Solution {
List<List<Integer>>res=new ArrayList<>();
LinkedList<Integer>path=new LinkedList<>();
static boolean[] used;
static int currentSum;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
used=new boolean[candidates.length+1];
if(candidates==null||candidates.length==0){
return res;
}
Arrays.fill(used,false);
Arrays.sort(candidates);
backTracking(candidates,target,0,0,used);
return res;
}
public void backTracking(int[]candidates,int target,int currentSum,int startIndex,boolean[]used){
if(currentSum==target){
res.add(new ArrayList<>(path));
return;
}
for(int i=startIndex;i<candidates.length;i++){
if(currentSum+candidates[i]>target){
break;
}
if(i>0&&candidates[i]==candidates[i-1]&&used[i-1]==false){
continue;
}
currentSum+=candidates[i];
used[i]=true;
path.add(candidates[i]);
backTracking(candidates,target,currentSum,i+1,used);
currentSum-=candidates[i];
used[i]=false;
path.removeLast();
}
}
}
电话号码的字母组合

1. 首先通过一个map来表示数字与字母的映射关系
2. 用一个index表示当前遍历到了digits的第几位了
3. 将digits每一位取出并转化为数字,再得到其对应的字符集
4. 遍历字符集,递归回溯
5.
在涉及字符串的拼接操作时,StringBuilder的效率更高
6.
映射得到的字符集只是一个临时变量,不能声明为static!
class Solution {
List<String>res=new ArrayList<>(); //最终结果集
static StringBuilder temp=new StringBuilder(); //拼接单条结果
static String []letterMap={
"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"
// 0 1 2 3 4 5 6 7 8 9
};
public List<String> letterCombinations(String digits) {
if(digits.length()==0||digits==null){
return res;
}
backTracking(digits,letterMap,0);
return res;
}
/*
digits:电话号码
letterMap:数字与字母的映射关系
index:遍历到号码的第几位数字
*/
public void backTracking(String digits,String[] letterMap,int index){
//递归结束条件
if(index==digits.length()){
res.add(temp.toString());
return;
}
//将digits中的其中一个数取出,并得到该数对应的字符集
//不能声名为全局变量
String str=letterMap[digits.charAt(index)-'0'];
for(int i=0;i<str.length();i++){
temp.append(str.charAt(i));
backTracking(digits,letterMap,index+1);
temp.deleteCharAt(temp.length()-1);
}
}
}
分割问题
分割回文串


1. 判断片段是否是回文子串,如果是则截取
2. 否则就跳过,回溯下一层
class Solution {
List<List<String>>res=new ArrayList<>();
LinkedList<String>path=new LinkedList<>();
//双指针判断回文字符串
public boolean isParlindrom(String s,int start,int end){
for(int i=start,j=end;i<j;i++,j--){
if(s.charAt(i)!=s.charAt(j)){
return false;
}
}
return true;
}
public void backTracking(String s,int startIndex){
if(startIndex>=s.length()){
res.add(new ArrayList<>(path));
return;
}
//注意循环开始位置是startIndex
for(int i=startIndex;i<s.length();i++){
//如果[startIndex,i]是回文字符串,则截取[startIndex,i]
if(isParlindrom(s,startIndex,i)){
String str=s.substring(startIndex,i+1);
path.add(str);
}else{
continue;
}
backTracking(s,i+1);
path.removeLast();
}
}
public List<List<String>> partition(String s) {
backTracking(s,0);
return res;
}
}
复原IP地址

1.写一个判断一个区间是否有效的方法
2.如果有效,则通过拼接插入’.',并记录其数量
3.递归终点:如果分隔符数量为3,且该地址有效,则加入答案
class Solution {
List<String>res=new ArrayList<>();
public void backTracking(String s,int startIndex,int pointNum){
//如果分隔符数量为3,且地址有效,则加入答案
if(pointNum==3){
if(isValid(s,startIndex,s.length()-1)){
res.add(s);
return;
}
}
for(int i=startIndex;i<s.length();i++){
//如果[startIndex,i]是有效地址,则添加分隔符并回溯
if(isValid(s,startIndex,i)){
//在s后面插入'.' 使用拼接:[0,i].[i,end]
s=s.substring(0,i+1)+'.'+s.substring(i+1);
//分隔符数量+1
pointNum++;
//递归下一层,由于添加了.,则下一层从i+2开始而不是i+1
backTracking(s,i+2,pointNum);
//回溯
pointNum--;
s=s.substring(0,i+1)+s.substring(i+2);
}
}
}
/*
有效判断分为四个方面
1.区间异常
2.数字0开头
3.非数字
4.大于255
*/
public boolean isValid(String s,int start,int end){
//区间异常
if(start>end){
return false;
}
//数字0开头
if(s.charAt(start)=='0'&&start!=end){
return false;
}
int num=0;
for(int i=start;i<=end;i++){
//非数字
if(s.charAt(i)>'9'||s.charAt(i)<'0'){
return false;
}
//大于255
num=num*10+(s.charAt(i)-'0');
if(num>255){
return false;
}
}
return true;
}
public List<String> restoreIpAddresses(String s) {
//剪枝
if(s.length()>12){
return res;
}
backTracking(s,0,0);
return res;
}
}
子集问题
无重复集合的子集

遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合。
class Solution {
List<List<Integer>>res=new ArrayList<>();
LinkedList<Integer>path=new LinkedList<>();
public void backTracking(int[]nums,int startIndex){
res.add(new ArrayList<>(path)); //防止遗漏本身
if(startIndex>=nums.length){
return;
}
for(int i=startIndex;i<nums.length;i++){
path.add(nums[i]);
backTracking(nums,i+1);
path.removeLast();
}
}
public List<List<Integer>> subsets(int[] nums) {
backTracking(nums,0);
return res;
}
}
包含重复集合的子集

去重:同一树层的去重,nums[i]=nums[i-1]&&used[i-1]=false,则跳过
class Solution {
List<List<Integer>>res=new ArrayList<>();
LinkedList<Integer>path=new LinkedList<>();
static boolean[]used;
public void backTracking(int []nums,int startIndex,boolean[] used){
res.add(new ArrayList<>(path)); //防止遗漏本身
if(startIndex>=nums.length){
return;
}
for(int i=startIndex;i<nums.length;i++){
if(i>=1&&nums[i]==nums[i-1]&&used[i-1]==false){
continue;
}
path.add(nums[i]);
used[i]=true;
backTracking(nums,i+1,used);
used[i]=false;
path.removeLast();
}
}
public List<List<Integer>> subsetsWithDup(int[] nums) {
used=new boolean[nums.length+1]; //used数组需要声明在这里!
Arrays.sort(nums);
backTracking(nums,0,used);
return res;
}
}
递增子序列

使用map来对同一树层上的结点去重
class Solution {
List<List<Integer>>res=new ArrayList<>();
LinkedList<Integer>path=new LinkedList<>();
public void backTracking(int []nums,int startIndex){
if(path.size()>1){
res.add(new ArrayList<>(path));
//不加return,因为要取树结点?
}
//使用map来去重
HashMap<Integer,Integer>hashMap=new HashMap<>();
for(int i=startIndex;i<nums.length;i++){
//保持递增
if(!path.isEmpty()&&nums[i]<path.getLast()){
continue;
}
//同一树层已经使用过的就跳过
if(hashMap.getOrDefault(nums[i],0) >=1){
continue;
}
hashMap.put(nums[i],hashMap.getOrDefault(nums[i],0)+1);
path.add(nums[i]);
backTracking(nums,i+1);
path.removeLast();
}
}
public List<List<Integer>> findSubsequences(int[] nums) {
backTracking(nums,0);
return res;
}
}
排列问题
全排列

因为是排列问题,所以不用startIndex,用used数组记录已经使用过的
class Solution {
List<List<Integer>>res=new ArrayList<>();
LinkedList<Integer>path=new LinkedList<>();
boolean used[];
public void backTracing(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]){
continue;
}
path.add(nums[i]);
used[i]=true;
backTracing(nums,used);
path.removeLast();
used[i]=false;
}
}
public List<List<Integer>> permute(int[] nums) {
used=new boolean[nums.length];
backTracing(nums,used);
return res;
}
}
包含重复数字的全排列

- 去重前要对数组排序,这样才好判断哪些元素重复使用了
- 组合问题和排列问题是在树形结构的叶子节点上收集结果,因此需要return
- 而子集问题就是取树上所有节点的结果,不需要return。

class Solution {
List<List<Integer>>res=new ArrayList<>();
LinkedList<Integer>path=new LinkedList<>();
boolean used[];
public void backTracing(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){
used[i]=true;
path.add(nums[i]);
backTracing(nums,used);
used[i]=false;
path.removeLast();
}
}
}
public List<List<Integer>> permuteUnique(int[] nums) {
used=new boolean[nums.length];
//为了判断重复使用的数,先对数组排序
Arrays.sort(nums);
backTracing(nums,used);
return res;
}
}
棋盘问题
N皇后

class Solution {
List<List<String>>res=new ArrayList<>();
char[][]chessboard;
/*
一共有n行,当前遍历到第row行
*/
public void backTracking(int n,int row,char[][]chessboard){
if(n==row){ //结束
res.add(Array2List(chessboard));
return;
}
for(int col=0;col<n;col++){
if(isValid(row,col,n,chessboard)){
chessboard[row][col]='Q';
backTracking(n,row+1,chessboard);
chessboard[row][col]='.';
}
}
}
/*
同一行,同一列,同一斜线不能有皇后
*/
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;
}
}
//副对角线
for(int i=row-1,j=col-1;i>=0&&j>=0;i--,j--){
if(chessboard[i][j]=='Q'){
return false;
}
}
//主对角线
for(int i=row-1,j=col+1;i>=0&&j<n;i--,j++){
if(chessboard[i][j]=='Q'){
return false;
}
}
return true;
}
/*
将字符数组转化为<List>String
*/
public List<String>Array2List(char [][]chessboard){
List<String>list=new ArrayList<>();
for(char[]c:chessboard){
list.add(String.copyValueOf(c));
}
return list;
}
public List<List<String>> solveNQueens(int n) {
chessboard=new char[n][n];
//初始化棋盘
for(char[]c:chessboard){
Arrays.fill(c,'.');
}
backTracking(n,0,chessboard);
return res;
}
}
解数独

- 因为每行都不只填一个数,因此要二维遍历进行递归
- 回溯的返回类型是bool型,因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。
- 本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。 递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了),所以不需要终止条件!
class Solution {
//二维递归
private boolean backTracking(char[][]board){
for(int i=0;i<board.length;i++){
for(int j=0;j<board[0].length;j++){
if(board[i][j]=='.'){ //如果还没有填
for(char k='1';k<='9';k++){ //试填1~9
if(isValid(i,j,k,board)){ //如果合法就填入k
board[i][j]=k;
if(backTracking(board))return true; //如果递归成功返回保留值
board[i][j]='.'; //回溯
}
}
return false;
}
}
}
return true;
}
private boolean isValid(int row,int col,char val,char[][]board){
//同一列不重复
for(int i=0;i<9;i++){
if(board[i][col]==val){ //同一列重复,返回false
return false;
}
}
//同一行不重复
for(int j=0;j<9;j++){
if(board[row][j]==val){
return false;
}
}
//九宫格内不重复
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;
}
public void solveSudoku(char[][] board) {
backTracking(board);
}
}
本文详细介绍了使用递归和回溯算法解决组合问题,包括组合总数、有限制的组合总和、可重复选取的组合总和以及去重的组合总和。同时,还涵盖了分割问题,如分割回文串和复原IP地址。此外,讨论了子集问题,包括无重复和有重复元素的子集,以及递增子序列。最后,文章提到了排列问题,包括全排列和包含重复数字的全排列。所有问题的解决方案都基于递归和回溯策略,通过构建树形结构并遍历所有可能的路径来找到答案。
1834

被折叠的 条评论
为什么被折叠?



