【数据结构与算法】回溯算法

https://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247484523&idx=1&sn=8c403eeb9bcc01db1b1207fa74dadbd1&source=41#wechat_redirect

// 二叉树遍历框架
def traverse(root):
    if root is None: return
    # 前序遍历代码写在这
    traverse(root.left)
    # 中序遍历代码写在这 
    traverse(root.right)
    # 后序遍历代码写在这

// N 叉树遍历框架
def traverse(root):
    if root is None: return
    for child in root.children:
        # 前序遍历代码写在这
        traverse(child)
        # 后序遍历代码写在这

回溯算法框架

"""
choiceList:当前可以进行的选择列表
track:可以理解为决策路径,即已经做出一系列选择
answer:用来储存我们的符合条件决策路径
"""

def backtrack(choiceList, track, answer):
    if track is OK:
        answer.add(track)
    else:
        for choice in choiceList:
            # choose:选择一个 choice 加入 track
            backtrack(choices, track, answer)
            # unchoose:从 track 中撤销上面的选择

可以理解为,回溯算法相当于一个决策过程,递归地遍历一棵决策树,穷举所有的决策,同时把符合条件的决策挑出来。

unchoose过程是为了能遍历完choiceList中的所有选择

全排序

https://leetcode-cn.com/problems/permutations/
在这里插入图片描述

public List<List<Integer>> permute(int[] nums) {
    List<List<Integer>> ans = new ArrayList<>();
    List<Integer> track = new ArrayList<>();
    backtrack(nums,track,ans);
    return ans;
}

private void backtrack(int[] nums, List<Integer> track, List<List<Integer>> ans){
    // 把符合条件的决策路径挑出来
    if(track.size()==nums.length){
        ans.add(new ArrayList<>(track));
    } else{
        for(int i=0; i<nums.length; i++){
            // choose过程
            // 决策路径中已经存在的元素不能再选择
            if(track.contains(nums[i])) continue;
            track.add(nums[i]); // 加入决策路径
            // 进入下一步决策
            backtrack(nums, track, ans);
            // unchoose过程
            track.remove(track.size()-1);
        }
    }
}
要是不进行撤回操作,输出结果是 [[1,2,3],[1,3,2],[3,1,2],[3,2,1],[1,2,3],[1,3,2]]

复杂度分析
递归树的复杂度都是这样分析:总时间 = 递归树的节点总数 × 每个递归节点需要的时间

全排列问题,节点总数等于 n + n*(n-1) + n*(n-1) (n-2) … * 1!,总之不超过 O(n*n!)。

对于 Java 代码的那个解法,处理每个节点需要 O(n) 的时间,因为 track.contains(nums[i]) 这个操作要扫描数组。

所以全排列问题总时间不超过 O(n^2 * n!)。

可见,回溯算法的复杂度是极其高的,甚至比指数级还高,因为树形结构注定了复杂度爆炸的结局。

N皇后问题

https://leetcode-cn.com/problems/n-queens/
在这里插入图片描述
在这里插入图片描述

class Solution {
    public List<List<String>> solveNQueens(int n) {
        char[][] board = new char[n][n];
        //初始化数组
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                board[i][j] = '.';
        List<List<String>> ans = new ArrayList<>();
        backtrack(0,board,ans,n);
        return ans;
    }
    private void backtrack(
        int row, char[][] board, List<List<String>> ans, int n
    ){
        if(row == n){
            ans.add(construct(board));
        } else{
            // 每一行可以放置一个皇后
            for(int col=0; col<n; col++){
                // 如果该位置的皇后会被攻击,则跳过
                if(!isValid(board,row,col,n)) continue;
                // choose
                board[row][col]='Q';
                // 进入下一步决策,即下一行的选择
                backtrack(row+1,board,ans,n);
                // unchoose
                board[row][col]='.';
            }

        }
    }

    // 判断 board[row][col] 是否可以放置Q
    private boolean isValid(char[][] board, int row,int col,int n){
        // 检查正上方
        for(int i=0;i<n;i++){
            if (board[i][col]=='Q') return false;
        }
        // 检查右斜上方
        for(int i=row-1,j=col+1; i>=0 && j<n; i--,j++){
            if (board[i][j]=='Q') return false;
        }
        // 检查左斜上方
        for(int i=row-1,j=col-1; i>=0 && j>=0; i--,j--){
            if (board[i][j]=='Q') return false;
        }
        // 下方没有放置皇后,不用检查
        return true;
    }

    //把数组转为list
    private List<String> construct(char[][] chess) {
        List<String> path = new ArrayList<>();
        for (int i = 0; i < chess.length; i++) {
            path.add(new String(chess[i]));
        }
        return path;
    }
}

注意: 棋盘board的构造方式,并转换成List <String>

复杂度分析
N 皇后问题,节点总数为 n + n^2 + n^3 + … + n^n,不超过 O(n^(n+1))。

处理每个节点需要向上扫描棋盘以免皇后互相攻击,需要 O(n) 时间。

所以 N 皇后问题总时间不超过 O(n^(n+2))。

剑指 Offer 12. 矩阵中的路径

https://leetcode-cn.com/problems/ju-zhen-zhong-de-lu-jing-lcof/
在这里插入图片描述
注意:路径可以从任意一格开始。可以上 下 左 右 移动,但是不能进入重复的格子。

class Solution {
    public boolean exist(char[][] board, String word) {
        // string转数组
        char[] words = word.toCharArray();
        // 回溯法
        // 从board的[0][0]开始
        for(int i=0;i<board.length;i++){
            for(int j=0;j<board[0].length;j++){
                if(backtrack(board,words,i,j,0)) return true;
            }
        }
        return false;
    }
    // word用来判断决策路径是否满足条件
    // board,i,j 共同决定当前选择决策路径 (= choiceList)
    boolean backtrack(char[][] board, char[] word, int i, int j, int k) {
        if(i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != word[k]) return false;
        if(k == word.length - 1) return true;
        board[i][j] = '\0';                   //choose:选择一个 choice 加入 track
        boolean res = backtrack(board, word, i + 1, j, k + 1) || 
        		      backtrack(board, word, i - 1, j, k + 1) ||  
        			  backtrack(board, word, i, j + 1, k + 1) || 
        			  backtrack(board, word, i , j - 1, k + 1);
        board[i][j] = word[k];                //unchoose:撤销上面选择
        return res;
    }
}

剑指 Offer 13. 机器人的运动范围

https://leetcode-cn.com/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/

在这里插入图片描述
思路:使用前序遍历。
由于能够到达的格子,只由当前位置决定,与之前到达的格子无关。所以算不上回溯,即不用“撤销”,当前位置能够到达继续递归即可

该题与 剑指offer12 类似,但注意不同的是,只能从 [0,0] 开始移动,而且询问能够到达多少个格子。说明:1、之前已经到达过的格子没必要重复判断。2、机器人只需要在每步向右,向下移动即可
剑指 Offer 13. 机器人的运动范围( 回溯算法,DFS / BFS ,清晰图解)

方法一:DFS(前序)

1、使用类遍历 outcome 记录返回值
2、前序遍历:
1)当前位值 (i,j) 不能到达,直接 return:
超出边界,或已经遍历过,或当前位置不满足条件

if( i>m-1 || j>n-1 || visited[i][j] ||(bit(i)+bit(j))>k) return; // 这里又叫可行性剪枝

2)若当前位置能够访问,outcome+1,标记当前位置

        outcome++;
        visited[i][j]=true;

3)向右、或向下遍历

        backtrack(visited,i+1,j,m,n,k);
        backtrack(visited,i,j+1,m,n,k);

这里有个隐藏的优化:只进行向右和向下,没有对向上和向左进行搜索。原因是,可以推出:机器人可 仅通过向右和向下移动,访问所有可达解。

class Solution {
    int outcome=0;
    public int movingCount(int m, int n, int k) {
        //临时变量visited记录格子是否被访问过
        boolean[][] visited = new boolean[m][n];
        backtrack(visited,0,0,m,n,k);
        return outcome;
    }

    public void backtrack(boolean[][] visited, int i, int j, int m,int n, int k){
        if( i>m-1 || j>n-1 || visited[i][j] ||(bit(i)+bit(j))>k) return;
        outcome++;
        visited[i][j]=true;
        backtrack(visited,i+1,j,m,n,k);
        backtrack(visited,i,j+1,m,n,k);
    }
    public int bit(int x) {
        int y=0;
        while(x>0){
            y += x%10;
            x=x/10;
        }
        return y;
    }
}

方法二:使用层级遍历

二叉树中的回溯

模板

def traverse(root, track, answer):
    if root is None: return

    #1、先进行选择,将当前节点加入track,并进行相关操作
    if track is OK: #2、判断做出选择后,当前路径是否是解,是的话加入 ans
        answer.add(track)
    traverse(root.left, track, answer)   # 对 左右子节点进行处理,对 track 没有影响(最后一定会撤销),但是 ans 里保留了正确的路径
    traverse(root.right, track, answer)
    # 3. 进行到此,不论 当前选择不是正确的选择,从 track 中撤销 root

剑指34. 二叉树中和为某一值的路径

在这里插入图片描述

思路:先序遍历+回溯
先序遍历: 按照 “根、左、右” 的顺序,遍历树的所有节点。
路径记录: 在先序遍历中,记录从根节点到当前节点的路径。当路径为 ① 根节点到叶节点形成的路径 且 ② 各节点值的和等于目标值 sum 时,将此路径加入结果列表。

中止条件:root==null,返回

先序遍历+回溯

1、使用 LinkedList

class Solution {
  LinkedList<List<Integer>> res = new LinkedList<>();
  LinkedList<Integer> path = new LinkedList<>(); 

    public List<List<Integer>> pathSum(TreeNode root, int sum) {

        backtrack(root,path,res,sum);
        return res;
    }
    
    public void backtrack(TreeNode root, LinkedList<Integer> path,LinkedList<List<Integer>> res, int tar){
        if(root==null) return;
        path.add(root.val);
        tar -= root.val;  // 做出选择

// 这里是做出了选择,再判断是否符合
        if(tar == 0 && root.left == null && root.right == null)
            res.add(new LinkedList(path));
        backtrack(root.left,path,res, tar);
        backtrack(root.right, path,res,tar);
        path.removeLast();
    }
}

2、使用 ArrayList

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    List<List<Integer>> res ;
    List<Integer> path ; 
    public List<List<Integer>> pathSum(TreeNode root, int sum) {
        res = new ArrayList<>();
        path = new ArrayList<>(); 
        backtrack(root,sum);
        return res;
    }
    
    public void backtrack(TreeNode root,  int tar){
        if(root==null) return;
        path.add(root.val);
        tar -= root.val; // 做出选择
        // 这里是做出了选择,再判断是否符合
        if(tar == 0 && root.left == null && root.right == null)
            res.add(new ArrayList(path));
        backtrack(root.left, tar);
        backtrack(root.right, tar);
        path.remove(path.size()-1); //回溯
        tar += root.val;
    }
}

字符串中的回溯/其实也是排列

剑指38. 字符串的排列

在这里插入图片描述

方法一:回溯(递归字符串位置)

思路:递归字符串的位置 x,则可选择的字符串为 str[i],i=x ~ n

做出选择后,把 str[x]与 str[i] 交换,表示做出选择。(这样,x+1 之后的字符又是递归 x+1 位置的可用字符)

剪枝:由于排列顺序不能重复,保证 “每种字符只在某位置 x 固定一次” 即可

返回条件:由于对位置进行递归,所以 x=len时 return
附·:使用 for 循环,所以其实循环的次数是一定的

图解过程:
在这里插入图片描述
在这里插入图片描述

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
    char[] charArr;
    List<String> res; // 储存结果
    public String[] permutation(String s) {
        if (s.length() == 0) {
            return new String[0];
        }
        // 转换成字符数组是常见的做法
        charArr = s.toCharArray();
        //使用动态数组
        res = new ArrayList<>();
        dfs(0); // 0 表示处理第0位,即递归处理 第0、1、2...len-1位
        // 记得转成字符串数组
        return res.toArray(new String[0]);
    }
    //递归处理 第0、1、2...len-1位
    private void dfs(int x) {
        if (x == charArr.length) {  //表示处理到最后一位已经处理完,x=len,加入结果列表res,并返回
            // res.add(new String(charArr)); // 生成新的字符串
            res.add(String.valueOf(charArr)); // 生成新的字符串
            return;
        }
        // 处理第x位时,使用 set 集合记录该位置选择过的字符
        // 每个位置只能选一种字符。剪枝,题目呀求不能重复
        HashSet<Character> set=new HashSet<>();
        for (int i = x; i < charArr.length; i++) {
            if(set.contains(charArr[i])) continue; //charArr[i]字符在该位置已经选过,跳出本次循环
            // 否则,进行选择
            set.add(charArr[i]);
            swap(x,i); // 交换charArr[x]与 charArr[i]
            dfs(x+1); // 处理下一位置
            swap(i,x); // 撤销
            // 撤销选择不用对set进行操作。set本来就是用于剪枝的
            
        }
    }
    private void swap(int x,int y){
        char tmp=charArr[x];
        charArr[x]=charArr[y];
        charArr[y]=tmp;
    }
}

在这里插入图片描述
可以重点看看下部分代码

        HashSet<Character> set=new HashSet<>();
        for (int i = x; i < charArr.length; i++) {
            if(set.contains(charArr[i])) continue; //charArr[i]字符在该位置已经选过,跳出本次循环
            // 否则,进行选择
            set.add(charArr[i]);
            swap(x,i); // 交换charArr[x]与 charArr[i]
            dfs(x+1); // 处理下一位置
            swap(i,x); // 撤销
            // 撤销选择不用对set进行操作。set本来就是用于剪枝的

方法二:官方有个不需要回溯的方法

字符串的排列

17. 电话号码的字母组合

在这里插入图片描述
在这里插入图片描述
思路:与剑指offer 38 比较相似,只不过不需要进行剪枝
递归结果字符串的位置 x,则可选择的字符串为 数字映射的字符串

做出选择后:及时把映射中的某个字符加入path
剪枝:无
返回条件:由于对位置进行递归,所以 x=len时 return (一定要return)
附·:使用 for 循环,所以其实循环的次数是一定的

方法一:回溯

首先使用哈希表存储每个数字对应的所有可能的字母,然后进行回溯操作。


class Solution {
    Map<Character, String> phoneMap = new HashMap<Character, String>() {{
        put('2', "abc"); // 使用双括号法初始化
        put('3', "def");
        put('4', "ghi");
        put('5', "jkl");
        put('6', "mno");
        put('7', "pqrs");
        put('8', "tuv");
        put('9', "wxyz");
    }};
    List<String> ans;
    StringBuffer path;
    public List<String> letterCombinations(String digits) {
        if(digits.length()==0) return new ArrayList<>();
        ans=new ArrayList<>();
        path=new StringBuffer();
        //写一个回溯函数
        // 按照结果位置进行递归
        traverse(digits,0);
        return ans;
    }
    // 表示处理 第x位
    public void traverse(String digits,int x ){
        if(x==digits.length()) {
            ans.add(path.toString());
            return;
        }
        char ch=digits.charAt(x);
        String str =phoneMap.get(ch);
        for(int i=0; i<str.length(); i++){
            path.append(str.charAt(i)); //加入路径
            traverse(digits,x+1); //进行下一步选择
            path.deleteCharAt(path.length()-1); //撤回操作
        }
    }
}

在这里插入图片描述

组合中的回溯

39. 组合总和

在这里插入图片描述

方法一:回溯

思路:这是个组合问题。balabala 下次再总结

class Solution {
    List<List<Integer>> ans;
    List<Integer> tmp;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        //回溯算法
        //数字可以无限制重复被选取
        ans=new ArrayList<>();
        tmp=new ArrayList<>();
        dfs(candidates,target,0);
        return ans;
    }
    public void dfs(int[] candidates, int target, int start){ 
        if(target==0){
            ans.add(new ArrayList(tmp));
        }else if(target<0) return; //
        for(int i=start;i<candidates.length;i++){ //注意i从start开使,这样才能保证组合不重复,即按顺序 做选择
            //选择一个加入路径
            tmp.add(candidates[i]);
            target -= candidates[i];
            //元素可以重复,只需要i不加1就行
            dfs(candidates, target, i);
            tmp.remove(tmp.size()-1);
            target += candidates[i];
        }
    }
}

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我! 毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip毕设新项目-基于Java开发的智慧养老院信息管理系统源码+数据库(含vue前端源码).zip
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值