Java编程题:回溯(汇总)

回溯

——任何算法的核心都是穷举,回溯算法就是一个暴力穷举算法。

回溯算法实际上是一个类似枚举的搜索尝试过程,主要在搜索过程中寻找问题的解,当发现不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

回溯过程需要考虑三个问题:

  • 路径:已经做出的选择;
  • 选择列表:当前还可以做的选择;
  • 结束条件:无法再做选择的条件;

回溯的大体框架:

def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return

    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

回溯的核心就是 for 循环里面的递归,在递归调用之前做选择,在递归调用之后撤销选择。
注意:
如果在回溯过程中消除了部分重叠子问题的计算,这就相当于对回溯算法进行了剪枝,提升了算法在某些情况下的效率,但算不上质的飞跃。

1 全排列问题

参考东哥的公众号labuladong,看看是否能够解决我对回溯的困扰。

求三个数的全排列[1,2,3],有3!种排法。其回溯树如下:
在这里插入图片描述
从根遍历这棵树,记录路径上的数字,就是所有的全排列。我们将这棵树称为回溯算法的决策树。之所以叫决策树,是因为每个节点上都在做决策,可以向哪边走。
在这里插入图片描述

针对红色节点所在路径,2就是已经做出的选择即路径,1,3就是待选择列表,结束条件就是选择列表为空的时候。
路径和选择列表可以作为每个节点的属性。比如蓝色节点的路径为3,1,待选择列表为2

如何遍历一棵树?各种搜索问题其实都是树的遍历问题,而多叉树的遍历框架就是这样:

void traverse(TreeNode root) {
    for (TreeNode child : root.childern)
        // 前序遍历需要的操作
        traverse(child);
        // 后序遍历需要的操作
}

而所谓的前序遍历和后序遍历,他们只是两个很有用的时间点:前序遍历的代码在进入某一个节点之前的那个时间点执行(即进入它的子树之前操作),后序遍历代码在离开某个节点之后的那个时间点执行(即它的子树遍历完之后)。
在这里插入图片描述
「路径」和「选择」是每个节点的属性,函数在树上游走要正确维护节点的属性,那么就要在这两个特殊时间点搞点动作:
在这里插入图片描述
再看看回溯算法的这段核心框架

for 选择 in 选择列表:
    # 做选择
    将该选择从待选择列表中移除
    路径.add(选择)
    backtrack(路径, 选择列表)
    # 撤销选择
    路径.remove(选择)
    将该选择再加入待选择列表

我们只要在递归之前做出选择,在递归之后撤销刚才的选择,就能正确得到每个节点的选择列表和路径。
直接看全排列代码:

力扣:全排列

List<List<Integer>> res = new LinkedList<>();// 记录结果

/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List<List<Integer>> permute(int[] nums) {
    // 记录「路径」
    LinkedList<Integer> track = new LinkedList<>();
    backtrack(nums, track);
    return res;
}

// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList<Integer> track) {
    // 触发结束条件
    if (track.size() == nums.length) {
        res.add(new LinkedList(track));
        return;
    }

    for (int i = 0; i < nums.length; i++) {
        // 排除不合法的选择,也可以用数组记录已经选择的节点
        if (track.contains(nums[i]))
            continue;
        // 做选择
        track.add(nums[i]);
        // 进入下一层决策树
        backtrack(nums, track);
        // 取消选择
        track.removeLast();
    }
}

在这里插入图片描述
注意:
链表使用contains方法需要 O(N) 的时间复杂度。

2 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

在这里插入图片描述
示例:

输入:“23”
输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].

说明: 尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。

来源:力扣(LeetCode)

DFS+回溯

class Solution {
    public static String[] mapString = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
    public List<String> letterCombinations(String digits) {
        List<String> ret = new ArrayList<>();
        StringBuilder curStr = new StringBuilder();
        dfs(ret,curStr,digits,0);
        return ret;
    }

    public void dfs(List<String> ret, StringBuilder curStr,String digits,int index){
    //边界,找到一种组合,放入数组中,结束此路径,向上回溯
        if(index==digits.length()){
            if(curStr.length()!=0){
                ret.add(curStr.toString());
            }
            return;
        }
 		//找到当前字符在String映射表中位置
        int mapIndex = digits.charAt(index)-48;
        String str = mapString[mapIndex];

//遍历每一种可能的组合
        for(int i=0;i<str.length();i++){
            curStr.append(str.charAt(i));
            dfs(ret,curStr,digits,index+1);
            curStr.deleteCharAt(curStr.length()-1);
        }
    }
}
3 组合总和

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。

示例 1:

输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]

示例 2:

输入: candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]

来源:力扣(LeetCode)

此题相加的元素可以重复,所以取下一个元素时可以从当前位置开始。

  1. 从第一个元素开始相加
  2. 让局部和继续累加候选的剩余值
  3. 局部和等于目标值,保存组合,向上回退,寻找其它组合。

首先对数组进行排序,这样有利于剪枝。nums数组用来记录每个下标的元素用了多少个,用来记录数字组合。
剪枝操作:sum + candidates[i] > target的时候表示sum + candidates[n](n > i)的元素都会大于target,所以break,然后往上回溯。

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> ret = new ArrayList<>();
        if(candidates.length==0 || candidates==null){
            return ret;
        }
        int sum =0;
        int[] nums = new int[candidates.length];//记录每个下标的元素用了多少个,用来记录组合元素

        Arrays.sort(candidates);//利于剪枝操作
        dfs(ret,candidates,nums,sum,target,0);
        return ret;
    }

    public void dfs(List<List<Integer>> ret, int[] candidates, int[] nums, int sum, int target, int index){
        if(sum==target){
            List<Integer> list = new ArrayList<>();
            for(int i=0;i<nums.length;i++){
                if(nums[i]>0){
                    int tmp = nums[i];//不能破坏nums,所以用tmp代替
                    while(tmp-->0){
                        list.add(candidates[i]);
                    }
                   
                }
            }
            ret.add(list);
            return;
        }

        for(int i=index;i<candidates.length;i++){
            if(sum+candidates[i]<=target){
                nums[i]++;
                dfs(ret,candidates,nums,sum+candidates[i],target,i);
                nums[i]--;
            }else{
                break;
            }
        }
    }
}
4 求和

输入两个整数 n 和 m,从数列1,2,3…n 中随意取几个数,使其和等于 m ,要求将其中所有的可能组合列出来

输入描述:
每个测试输入包含2个整数,n和m

输出描述:
按每个组合的字典序排列输出,每行输出一种组合

输入
5 5
输出
1 4 2 3 5

链接:求和

import java.util.*;

public class Main{
    
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int m = sc.nextInt();
        List<List<Integer>> ret = new ArrayList<>();
        List<Integer> list = new ArrayList<>();
        dfs(n,m,1,ret,list);
        
        for(List<Integer> li:ret){
            int size = li.size();
            for(int i=0;i<size-1;i++){
                System.out.print(li.get(i)+" ");
            }
            System.out.println(li.get(size-1));
        }
    }
    
    public static void dfs(int n, int target,int index,List<List<Integer>> ret,List<Integer> list){
        if(target==0){
            ret.add(new ArrayList<>(list));
            return;
        }
        for(int i=index;i<=target&&i<=n;i++){
            list.add(i);
            // 求1...n 中取若干个数字和为m, 把问题拆解为
			//求2...n 中取若干给数字和为m - 1
            dfs(n,target-i,i+1,ret,list);
            list.remove(list.size()-1);
        }
    }
}
5 活字印刷

你有一套活字字模 tiles,其中每个字模上都刻有一个字母 tiles[i]。返回你可以印出的非空字母序列的数目。

注意:本题中,每个活字字模只能使用一次。

示例 1:

输入:“AAB”
输出:8
解释:可能的序列为 “A”, “B”, “AA”, “AB”, “BA”, “AAB”, “ABA”, “BAA”。

示例 2:

输入:“AAABBC”
输出:188

提示:
1 <= tiles.length <= 7
tiles 由大写英文字母组成

来源:力扣(LeetCode)

解析:
此题组合的长度不唯一,最小组合长度为1,最大组合长度为tiles的长度。
按照题意tiles中每一个位置的字符在组合中只能出现一次,所以可以用一个标记辅助。

当去组合新的组合时,可以与tiles中的每一个位置组合,但是如果当前位置已经在当前组合中出现过,则跳过。

虽然此题中每一个位置的字符在组合中只能出现一次,但是tiles中可能有相同的字符,所以需要考虑重复的组合,而Set可以天然去重,可以用其去重。

DFS+回溯

  1. 当前组合不为空,则插入set中
  2. 继续恰当组合拼接新的组合,尝试拼接tiles每一个位置的字符
  3. 如果当前位置已在组合中出现过,则返回到2,否则标记当前位置,继续拼接更长的组合
  4. 回溯,尝试组合其它位置,返回2
    当所有位置都已经使用过时,当前递归就结束了,继续向上层DFS回退,最终返回set的大小即为组合数目。
class Solution {
    public int numTilePossibilities(String tiles) {
        if(tiles.length()==0){
            return 0;
        }
//保存所有的组合
        HashSet<String> set = new HashSet<>();
//拼接当前组合
        StringBuilder curStr = new StringBuilder();
//标记全部初始化为未使用,记录每个字母的使用情况
        int[] nums = new int[tiles.length()];

        dfs(set, curStr, nums,tiles);
        return set.size();
    }

public void dfs(HashSet<String> set, StringBuilder curStr, int[] nums, String tiles){
//添加新的组合
        if(curStr.length()!=0){
            set.add(curStr.toString());
        }
//标记保证所有位都用完之后,就结束了
        for(int i=0;i<tiles.length();i++){
 		//当前位置的字符已经用过,直接跳过
            if(nums[i]==1){
                continue;
            }
//标记当前字母已经用过
            nums[i]=1;
            dfs(set,curStr.append(tiles.charAt(i)),nums,tiles);
//回退,尝试其它字符
            nums[i]=0;
            curStr.deleteCharAt(curStr.length()-1);
        }
    }
}
6 N皇后

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
在这里插入图片描述

上图为 8 皇后问题的一种解法。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

示例:
输入: 4
输出: [
[".Q…", // 解法 1
“…Q”,
“Q…”,
“…Q.”],
["…Q.", // 解法 2
“Q…”,
“…Q”,
“.Q…”]
]
解释: 4 皇后问题存在两个不同的解法。

提示:
皇后,是国际象棋中的棋子,意味着国王的妻子。皇后只做一件事,那就是“吃子”。当她遇见可以吃的棋子时,就迅速冲上去吃掉棋子。当然,她横、竖、斜都可走一到七步,可进可退。(引用自 百度百科 - 皇后 )

来源:力扣(LeetCode)

N皇后问题:把N个皇后放置N*N的二维矩阵中,保证他们相互不能攻击:即不在同一行,同一列,同一个斜线上。

思想:DFS+回溯
从第一行开始放置皇后,每确定一个位置,判断是否会冲突:是否在同一列(不可能在同一行,所以行不用考虑)。

  • 同一列:纵坐标相同
  • 反对角线(红色)对应的位置,横坐标加上纵坐标的值是相同的。
  • 正对角线(黑色)对应的位置,横坐标减去纵坐标的值也是相同的。
    在这里插入图片描述
    当前位置确定之后,继续确定下一行的位置。回退,尝试当前行的其它位置。
class pair{
    public int x;
    public int y;
    public pair(int x,int y){
        this.x = x;
        this.y = y;
    }
}

class Solution {

	public List<List<String>> solveNQueens(int n) {
		//按坐标位置存放所有解决方案
        List<List<pair>> solutions = new ArrayList<>();
		//存放一种解决方案中的所有皇后的位置
        List<pair> solution = new ArrayList<>();
        dfs(solutions,solution,0,n);
		//把坐标位置转成string
        return tranString(solutions,n);
    }
    
    public void dfs(List<List<pair>> solutions,List<pair> solution,int row, int n){
        if(row==n){
            List<pair> list = new ArrayList<>();
            for(pair x:solution){
                list.add(x);
            }
            solutions.add(list);
        }
		 //尝试当前行的每一个位置是否可以放置一个皇后
        for(int col=0;col<n;col++){
            if(isValid(row,col,solution)){
		 //如果可以,在保存当前位置,继续确定下一行的皇后位置
		//直接调用构造函数,内部构造pair
                solution.add(new pair(row,col));
                dfs(solutions,solution,row+1,n);
		//回溯,删除当前位置,尝试当前行的其它位置
                solution.remove(solution.size()-1);
            }
        }
    }
    
//solution:一个解决方案,从第一行开始到当前行的上一行每一行已经放置皇后的点
public boolean isValid(int row, int col, List<pair> solution){
//判断当前行尝试的皇后位置是否和前面几行的皇后位置有冲突
    //i.secongd == col:第i个皇后位置是否和前面几行的皇后位置有冲突
    //i.first + i.second == row +col:第i个皇后与当前点在撇上,横坐标+纵坐标值相同
    //i.first - i.second == row - col:第i个皇后与当前点在捺上,横坐标-纵坐标值相同
        for(pair i:solution){
            if(i.y==col || i.x+i.y == col+row || i.x-i.y == row-col){
                return false;
            }
        }
        return true;
    }

public List<List<String>> tranString(List<List<pair>> solutions, int n){
//把每一种解决方案都转换为string形式,最终结果
        List<List<String>> ret = new ArrayList<>();
        
		//n*n char:每行有n个元素,把皇后的位置修改为Q
        for(List<pair> solution:solutions){
            List<StringBuilder> strs = new ArrayList<>();
            
			//先把.都填完
            for(int i=0;i<n;i++){
                StringBuilder str = new StringBuilder();
                for(int j=0;j<n;j++){
                    str.append('.');
                }
                strs.add(str);
            }
			//把每一行皇后的位置修改为Q
            for(pair i:solution){
                strs.get(i.x).setCharAt(i.y,'Q');
            }
            List<String> cur = new ArrayList<>();
            for(StringBuilder i:strs){
                cur.add(i.toString());
            }
            ret.add(cur);
           
        }
         return ret;
    }
}

注意:

  if(row==n){
            List<pair> list = new ArrayList<>();
            for(pair x:solution){
                list.add(x);
            }
            solutions.add(list);
        }

这地方一定要new ArrayList<>(). 不可以直接solutions.add(solution),如果这样做相当于是添加的引用,如果后期solution发生改变,那么solutions中添加的元素也都跟着solution改变。

 public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        List<List<Integer>> ret = new ArrayList<>();
        List<Integer> list = new ArrayList<>();
        for(int i=0;i<5;i++){
            list.add(i);
        }
        ret.add(list);
        System.out.println(ret);

        list.remove(list.get(list.size()-1));
        System.out.println(ret);
    }
    
结果:
[[0, 1, 2, 3, 4]]
[[0, 1, 2, 3]]  //后期list发生修改后,ret中的元素也跟着变化。
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值