算法思想 - 回溯算法,0/1背包,全排列等问题

回溯算法

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

一、基本思想

回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。

二、解题步骤

用回溯算法解决问题的一般步骤:

  1. 针对所给问题,定义问题的 解空间,它至少包含问题的一个(最优)解。
  2. 确定易于搜索的解空间结构,通常为树形结构,使得能用回溯法方便地搜索整个解空间 。
  3. 深度优先 的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。

确定了解空间的组织结构后,回溯法就从开始结点(根结点)出发,以深度优先的方式搜索整个解空间。这个开始结点就成为一个活结点,同时也成为当前的扩展结点。在当前的扩展结点处,搜索向纵深方向移至一个新结点。这个新结点就成为一个新的活结点,并成为当前扩展结点。如果在当前的扩展结点处不能再向纵深方向移动,则当前扩展结点就成为死结点。此时,应往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展结点。回溯法即以这种工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已没有活结点时为止。

剪枝:
  不符合条件的不向下搜索了。

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

算法结构:
以迷宫问题为例,来说明算法结构。
例如,要从蓝色点到红色点:
在这里插入图片描述首先、从蓝色的开始节点遍历到节点1,然后从节点1遍历到节点2,再从节点2遍历到节点3,节点3继续往后遍历发现这条路走不通,便回退到节点2,此时发现节点2没有可以继续往后遍历的方案便回退到节点1,节点1按照这个规则选择其他遍历方案继续往后遍历,直到遍历到红色点处。

以下便是回溯法解决问题的算法结构:

  • x:遍历某一个节点,比如上图的节点1
  • 方案数:对于走迷宫问题,某一个节点遍历方案可以为:向上,向下,向左,向右四个方案,所以方案数为4。
  • 剪枝:if(方案可行),因为对于实际问题,可能某一个节点的遍历方案没有四种,比如节点1只有向上和向右。通过判断方案是否可行来进行剪枝处理,减少程序遍历的次数。

在这里插入图片描述

三、经典问题:

1. 全排列问题

  1. 问题描述:
    给定以一个正整数 n,求出从 1~n 的所有排列组合。

    如:输入为 n = 3
      输出:1 2 3, 1 3 2, 2 1 3, 2 3 1, 3 1 2, 3 2 1

  2. 问题分析:
    我们从第一位开始处理,每一位数都可以从1~n 中任选一个数 i,所以每一个节点(每一位)向下遍历的方案数为n。
    某一个数不能出现两次,这个通过剪枝来完善即可。
    在这里插入图片描述

  3. 完整算法:

    import java.util.ArrayList;
    import java.util.List;
    import java.util.Stack;
    
    /**
     * @projName: algorithm
     * @packgeName: indi.pentiumcm.thought
     * @className: BtPerm
     * @author: pentiumCM
     * @email: 842679178@qq.com
     * @date: 2020/4/4 23:10
     * @describe: 回溯法 - 全排列问题
     */
    public class BtPerm {
    
        // 记录数字是否已经使用过
        int[] used;
        // 方案数组
        int[] items;
    
    
        public List<List<Integer>> permute(int[] nums) {
            List<List<Integer>> res = new ArrayList<>();
            Stack<Integer> path = new Stack<>();
    
            items = nums;
            used = new int[nums.length];
            dfs(0, nums.length, path, res);
            return res;
        }
    
    
        /**
         * @param idx  第 idx 位进行搜索遍历
         * @param n    一共多少位
         * @param path 对应解空间的一个解
         * @param res  解空间
         */
        void dfs(int idx, int n, Stack<Integer> path, List<List<Integer>> res) {
            // 解空间:到达叶子节点,对应一个解
            if (idx == n) {
                // 输出解
                List<Integer> tmp = new ArrayList<>();
                for (int i : path) {
                    tmp.add(i);
                }
                res.add(tmp);
                return;
            }
    
            // 方案数
            for (int i = 0; i < items.length; i++) {
                if (used[i] == 0) {
                    // 第 idx 位选择元素 i
                    used[i] = 1;
                    path.push(items[i]);
                    dfs(idx + 1, n, path, res);
    
                    // 回溯
                    path.pop();
                    used[i] = 0;
                }
            }
        }
    
    
        public static void main(String[] args) {
            BtPerm btPerm = new BtPerm();
    
            int[] arr = new int[3];
            for (int i = 0; i < arr.length; i++) {
                arr[i] = i + 1;
            }
    
            List<List<Integer>> ans = btPerm.permute(arr);
        }
    }
    
    

2. 0/1 背包问题

  1. 问题描述:
    0-1 背包问题: 给定 n 种物品和一个容量为 C 的背包,物品 i 的重量是 wi,其价值为 vi
    问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?

  2. 分析过程:
    我们从第0个物品开始遍历(0对于数组的第一个物体),每一件物品放入背包都有两种方案,放或者不放,用0代表不放该物品,用1代表放该物品。
    在遍历第 i 个物体时,通过判断背包剩余容量是否能够放下物体 i,来进行剪枝处理。在下图以红色的叉号即叶子节点的无来表示剪枝。
    在这里插入图片描述

  3. 算法实现:

    package indi.pentiumcm.thought;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @projName: algorithm
     * @packgeName: indi.pentiumcm.thought
     * @className: BtBack
     * @author: pentiumCM
     * @email: 842679178@qq.com
     * @date: 2020/4/4 17:47
     * @describe: 回溯法 - 0/1背包问题
     */
    public class BtBack {
    
        // 背包容量
        public int backCap = 8;
        // 物品个数
        public int goodNums = 4;
        // 物品的重量
        int[] weights = {2, 3, 4, 5};
        // 物品的价值
        int[] values = {3, 4, 5, 6};
    
        // 表示第 i 个物品是否放入背包
        int[] places = new int[goodNums];
    
        // 当前背包放入物体的重量
        public int curWeight = 0;
        // 当前价值
        public int curVals = 0;
        // 最大价值
        public int maxVals = 0;
    
        /**
         * 处理第 i 个物体
         *
         * @param i 第 i 个物体
         */
        public int backtrack(int i, List<Integer[]> res) {
    //      递归的终止条件
    //      遍历完所有的物品,即到叶子节点
            if (i == goodNums) {
                Integer[] resArr = {0, 0, 0, 0, 0};
                for (int j = 0; j < goodNums; j++) {
                    resArr[j] = places[j];
                }
                resArr[goodNums] = curVals;
                res.add(resArr);
                if (curVals > maxVals) {
                    maxVals = curVals;
                }
                return maxVals;
            }
    
    //      第 i 个物品的重量
            int weight = weights[i];
    
    //      方案数为 2,方案1用0表示 - 不装第i个物体,方案2用1表示 - 装第i个物体
            for (int j = 0; j < 2; j++) {
    //          不装第 i 个物品
                if (j == 0) {
                    places[i] = 0;
    //              遍历下一个物品
                    backtrack(i + 1, res);
                }
    //          装第 i 个物品
                else {
    //              剪枝处理,判断第 i 个物品是否可以装进背包
                    curWeight = arrmultiply(places, weights);
                    if (curWeight + weight <= backCap) {
                        places[i] = 1;
                        curVals = arrmultiply(places, values);
                        backtrack(i + 1, res);
                        places[i] = 0;
                    }
                }
            }
            return maxVals;
        }
    
        public int arrmultiply(int[] arr1, int[] arr2) {
            int result = 0;
            if (arr1.length == arr2.length) {
                for (int i = 0; i < arr1.length; i++) {
                    result += arr1[i] * arr2[i];
                }
            } else {
                System.out.print("数组长度不一致!");
            }
            return result;
        }
    
        public static void main(String[] args) {
            BtBack btBack = new BtBack();
    //      存放解空间的解
            List<Integer[]> res = new ArrayList<>();
            int max = btBack.backtrack(0, res);
    
            System.out.println("最大价值为:" + max);
    
            System.out.println("背包中的物品组合及价值(最后一位为价值):");
            for (int i = 0; i < res.size(); i++) {
                Integer[] arr = res.get(i);
                for (int j = 0; j < arr.length; j++) {
                    System.out.print(arr[j] + "  ");
                }
                System.out.print("\n");
            }
        }
    }
    

    运行结果:
    在这里插入图片描述


3. 字母大小写全排列

LeetCode:https://leetcode-cn.com/problems/letter-case-permutation/
在这里插入图片描述
分析过程及算法实现:

在这里插入图片描述

package indi.pentiumcm.leetcode;

import java.util.ArrayList;
import java.util.List;

/**
 * @projName: algorithm
 * @packgeName: indi.pentiumcm.leetcode
 * @className: Q19
 * @author: pentiumCM
 * @email: 842679178@qq.com
 * @date: 2020/4/4 11:17
 * @describe: LeetCode-784. 字母大小写全排列
 */
public class Q19 {

    public void dfs(char[] temp, String S, int start, List<String> res) {
        int len = S.length();
//      递归的终止条件
//      遍历到字符串的结尾,直接退出
        if (start == len) {
            res.add(new String(temp));
            return;
        }
        temp[start] = S.charAt(start);
        dfs(temp, S, start + 1, res);

//      当遍历到的位置为字符,遍历另一个分支
        if (Character.isLetter(S.charAt(start))) {
            char ant = (char) (S.charAt(start) ^ 32);
            temp[start] = ant;

            dfs(temp, S, start + 1, res);
        }
    }

    public List<String> letterCasePermutation(String S) {
        List<String> res = new ArrayList<>();

        int len = S.length();
        if (len == 0) {
            return res;
        }
        char[] temp = new char[S.length()];
        dfs(temp, S, 0, res);
        return res;
    }


    public static void main(String[] args) {
        List<String> res = new Q19().letterCasePermutation("a1b2");
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值