回溯算法总结

目录

回溯算法简介

回溯算法中的三个概念

回溯算法的步骤

具体流程

代码实现框架

经典例子

选取数字集合

皇后问题

最大 k 乘积问题


回溯算法简介

回溯法是一种类似枚举的搜索尝试过程,既然是枚举,那么就会遍历解空间树中的所有解(或者是“路径”),搜索的过程按照DFS原则,而尝试就意味着,在遍历的过程中,有可能到达某一个结点后,发现不能够满足约束条件,在这次尝试中,这条“路”是不优的,将走不通,即无法找到所求的解,那么就会回退到上一步的状态,重新作出选择。如果即满足约束条件,但是依然没有获得有效的解,那么我们就需要在此基础上做下一步选择,即将当前结点当做一个新的根结点。所以经常会使用递归的方法。如果一步步下来的选择结果正好满足我们所求的问题,那么就是一个有效的解。

回溯法思想:在包含问题所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度搜索解空间树。当搜索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被检索一遍才结束。而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。

若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。

若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。

回溯算法中的三个概念

(1)约束函数:约束函数是根据题意定出的。通过描述合法解的一般特征用于去除不合法的解(即使这个解并不是完整的解),从而避免继续搜索出这个不合法解的剩余部分,起到了剪值函数的作用。因此,约束函数是对于解空间树上的任何节点都有效、等价的。

(2)状态空间树:状态空间树是一个对所有解的图形描述

(3)扩展节点:当前正求出它的子节点的节点,在DFS中,只允许有一个扩展节点。

        活节点:通过与约束函数对照,节点本身和其父节点均满足约束函数要求的节点。

        死节点: 与活节点正好相反,死节点是在与约束函数对照中,不满足约束函数的节点。那么就不需要求该节点的子节点情况了。

回溯算法的步骤

在回溯法执行时,应当:保存当前步骤,如果是一个解就输出;维护状态,使搜索路径(含子路径)尽量不重复。必要时,应该对不可能为解的部分进行剪枝(pruning)。

(1)定义一个解空间,它包含问题的解。明确问题的解空间树内容,比如,在求组合问题时,最后有效解中元素是否可重复的,元素个数是否是固定的等。且保证问题的解空间中应至少包含问题的一个解。

(2)利用适于搜索的方法组织解空间。确定结点的扩展搜索规则,在回溯法中,就是深度优先遍历规则。扩展节点就是当前正在求出它的子节点的节点,在DFS中,只允许有一个扩展节点。

(3)利用深度优先法搜索解空间。

(4)利用限界函数避免移动到不可能产生解的子空间。 构造约束条件。作为剪枝函数,避免无效搜索。

 问题的解空间通常是在搜索问题的解的过程中动态产生的,这是回溯算法的一个重要特性。

具体流程

(1)设置初始化的方案(给变量赋初值,读入已知数据等);

(2)选择所到深度层(k)中其中的一个节点进行试探,如果k层的节点已经全部都试探完毕,则进入(7)

(3)如果试探不成功(不满足约束条件),则转入(2)

(4)如果试探成功,则进入下一个深度层进行试探

(5)如果正确解还没找到,则进入(2)

(6)如果找到了正解,如果问题只求其中的一个解,那么可以退出程序了,如果是求一组解,那么将该种解存储(一般用一个类中的变量);

(7)退回上一步的状态(深度为k-1时,变量的状态),如果没有退到头,则进入(2)

(8)已退到头则结束或打印无解

在使用回溯法解决实际问题时,往往会和递归算法结合,更容易解决问题,但是也可以使用非递归的方法实现,这就需要具体情况进行分析了。
 

代码实现框架

bool finished = FALSE; /* 是否获得全部解? */
backtrack(int a[], int k, data input)
{
    int c[MAXCANDIDATES]; /*这次搜索的候选 */
    int ncandidates; /* 候选数目 */
    int i; /* counter */
    if (is_a_solution(a,k,input))
    process_solution(a,k,input);
    else {
        k = k+1;
        construct_candidates(a,k,input,c,&ncandidates);
        for (i=0; i<ncandidates; i++) {
            a[k] = c[i];
            make_move(a,k,input);
            backtrack(a,k,input);
            unmake_move(a,k,input);
            if (finished) return; /* 如果符合终止条件就提前退出 */
        }
    }
}

对于其中的函数和变量,解释如下:

a[]表示当前获得的部分解;

k表示搜索深度;

input表示用于传递的更多的参数;

is_a_solution(a,k,input)判断当前的部分解向量a[1...k]是否是一个符合条件的解

construct_candidates(a,k,input,c,ncandidates)根据目前状态,构造这一步可能的选择,存入c[]数组,其长度存ncandidates

process_solution(a,k,input)对于符合条件的解进行处理,通常是输出、计数等

make_move(a,k,input)unmake_move(a,k,input)前者将采取的选择更新到原始数据结构上,后者把这一行为撤销。

 

经典例子

选取数字集合

做一个白话版的描述,给你两个整数 n和k,从1-n中选择k个数字的组合。比如n=4,k=2,那么从1,2,3,4中选取两个数字的组合

可以看到上面的每个变量都出现了,用一个类变量保存结果

public class Solution {
   List<List<Integer>> result=new ArrayList<List<Integer>>();
   public List<List<Integer>> combine(int n, int k) {
       List<Integer> list=new ArrayList<Integer>();
       backtracking(n,k,1,list);
       return result;
    }
   public void backtracking(int n,int k,int start,List<Integer>list){
       if(k<0) return ;
       else if(k==0){
           result.add(new ArrayList(list));
       }else{
           for(int i=start;i<=n;i++){
                list.add(i);
                backtracking(n,k-1,i+1,list);
                list.remove(list.size()-1);
            }
       }
    }
}

皇后问题

N皇后问题是指在N*N的棋盘上放置N个皇后,使这N个皇后无法吃掉对方(也就是说两两不在一行,不在一列,也不在对角线上)。经典的是8皇后问题,这里我们为了简单,以4皇后为例。

    首先利用回溯算法,先给第一个皇后安排位置,如下图所示,安排在(1,1)然后给第二个皇后安排位置,可知(2,1),(2,2)都会产生冲突,因此可以安排在(2,3),然后安排第三个皇后,在第三行没有合适的位置,因此回溯到第二个皇后,重新安排第二个皇后的位置,安排到(2,4),然后安排第三个皇后到(3,2),安排第四个皇后有冲突,因此要回溯到第三个皇后,可知第三个皇后也就仅此一个位置,无处可改,故继续向上回溯到第二个皇后,也没有位置可更改,因此回溯到第一个皇后,更改第一个皇后的位置,继续上面的做法,直至找到所有皇后的位置,如下图所示。

    这里为什么我们用4皇后做例子呢?因为3皇后是无解的。同时我们也可以看到回溯算法虽然也是Brute-Force,但是它可以避免去搜索很多的不可能的情况,因此算法是优于Brute-Force的。

这里是每次确定第col行的棋子的位置

public class NQueensII {  
    int[] x;//当前解    
    int N;//皇后个数  
     int sum = 0;//当前已找到的可行方案数  
    public int totalNQueens(int n) {  
        N = n;  
        x = new int[N+1];  
        backTrace(1);  
        return sum;  
    }  
    /** 
     * col行这个点,x[col]列这个点,与已经存在的几个皇后,是否符合要求,放到这个位置上, 
     * @param col 
     * @return 
     */  
    private boolean place(int col){  
        for(int i = 1; i < col; i++){  
            if(Math.abs(col - i)==Math.abs(x[col]-x[i])||x[col]==x[i]){  
                return false;  
            }  
        }  
        return true;  
    }  
    private void backTrace(int t) {  
        if(t>N){  
            sum++;  
        }else {  
            //第t行,遍历所有的节点  
            for(int j = 1; j <= N; j++) {  
                 x[t] = j ;  
                 //如果第j个节点可以放下皇后  
                if(place(t)){  
                    //接着放下一个  
                    backTrace(t+1);  
                }  
            }  
        }  
          
    }  
    public static void main(String[] args) {  
        NQueensII n = new NQueensII();  
        System.out.println(n.totalNQueens(8));  
    }  
}

最大 k 乘积问题

设I是一个 n 位十进制整数。如果将 I 划分为 k 段,则可得到    k 个整数。这 k 个整数的乘积称为 I 的一个 k 乘积。试设计一个算法,对于给定的 I 和 k ,求出 I 的最大 k 乘积。

import java.util.Scanner;
 
 
public class 最大k乘积 {
    private static long ans;
    private static Scanner cin;
    static{
        cin = new Scanner(System.in);
    }
    static void work(int cur, int i, int k, long v){
        //System.out.println("i = " + i + " cur = " + cur + " k = " + k);
        if(i == k){
            ans = Math.max(ans, v);
            return;
        }
        if(cur == 0){
            return;
        }
        int MOD = 1;
        while(cur / MOD != 0){
            work(cur % MOD, i + 1, k, v * (cur / MOD));
            MOD *= 10;
        }
    }
    public static void main(String[] args) {
        int num, k;
        System.out.println("请输入数字num和要分成的段数k: ");
        while(cin.hasNext()){
            num = cin.nextInt();
            k = cin.nextInt();
            ans = Long.MIN_VALUE;
            work(num, 0, k, 1L);
            if(ans == Long.MIN_VALUE){
                System.out.println("整数" + num + "不能被分成" + k + "段");
            }else{
                System.out.println(num + "的最大" + k + "乘积是: " + ans);
            }
            System.out.println("请输入数字num和要分成的段数k: ");
        }
    }
}

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回溯算法是一种通过穷举所有可能的解来求解问题的算法。它通常用于解决组合、排列、子集和搜索等问题。在回溯算法中,我们通过递归的方式尝试所有可能的选择,并在每一步进行剪枝,以避免无效的搜索。 在给出的引用中,有三个例子展示了使用回溯算法解决不同的问题。第一个例子是找出给定数组中的所有递增子序列。通过递归和剪枝的方式,我们可以找到所有满足条件的子序列。 第二个例子是生成给定字符串中的所有字母大小写组合。通过递归和剪枝的方式,我们可以生成所有可能的组合。 第三个例子是生成有效的括号组合。通过递归和剪枝的方式,我们可以生成所有满足括号匹配规则的组合。 总的来说,回溯算法是一种非常灵活和强大的算法,可以用于解决各种组合和搜索问题。在实际应用中,我们可以根据具体问题的特点来设计回溯算法的实现。 #### 引用[.reference_title] - *1* [python 回溯算法总结](https://blog.csdn.net/weixin_45548695/article/details/124146238)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [python数据结构与算法--回溯算法](https://blog.csdn.net/Melo0705/article/details/99728116)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值