Java编程题:回溯(汇总)

目录

    • 回溯
        • (1)电话号码的字母组合
        • (2)组合总和
        • (3)活字印刷
        • (4)N皇后

回溯

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

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

回溯框架:

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

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

注意:
如果在回溯过程中消除了部分重叠子问题的计算,这就相当于对回溯算法进行了「剪枝」,提升了算法在某些情况下的效率,但算不上质的飞跃。

(1)电话号码的字母组合

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

Java编程题:回溯(汇总)_第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);
        }
    }
}

(2)组合总和

给定一个无重复元素的数组 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;
            }
        }
    }
}

(3)活字印刷

你有一套活字字模 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);
        }
    }
}

(4)N皇后

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
Java编程题:回溯(汇总)_第2张图片

上图为 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<>();

        for(List<pair> solution:solutions){
//n*n char:每行有n个元素,把皇后的位置修改为Q
            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);
            }

            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;
    }
}

你可能感兴趣的:(java编程题)