回溯算法

1.基本概念

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

2.基本思想

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

3.解空间的树结构

使用回溯法的解空间一般有两种解空间:子集树和排列树

3.1子集树

当所给的问题从n个元素的集合S中找出满足某种性质的子集时,相应的解空间树称为子集树。常用于0-1问题,如0-1背包问题。

如下图:

image.png

3.2排列树

当所给的问题是确定n个元素满足某种性质的排列时,相应的解空间树称之为排列树。排序树通常有n!叶结点。

如下图:

image.png

4.用回溯法解题的一般步骤

  1. 针对所给问题,确定问题的解空间:
    首先明确定义问题的解空间,问题的解空间应该至少包含问题的一个解
  2. 确定结点扩展搜索规则
  3. 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索

5.算法框架

1.问题框架

  设问题的解是一个n维向量(a1,a2,………,an),约束条件是ai(i=1,2,3,…..,n)之间满足某种条件,记为f(ai)。

2.递归回溯框架

回溯法是对解空间的深度优先搜索,在一般情况下使用递归函数来实现回溯法比较简单,其中i为搜索的深度,框架如下:

void backtrack (int t) //t表示递归深度
{
       if (t>n) output(x); //n表示深度界限
       else
         for (int i=f(n,t);i<=g(n,t);i++) // f(n,t),g(n,t)分别表示当前扩展结点未搜索过的子树的起始编号和终止编号
           {
           x[t]=h(i); 
           if (constraint(t)&&bound(t)) //满足约束函数和限界函数
              backtrack(t+1);
           }
}

3.非递归的算法框架

void iterativeBacktrack ()
{
  int t=1;
  while (t>0) {
    if (f(n,t)<=g(n,t)) 
      for (int i=f(n,t);i<=g(n,t);i++) {
        x[t]=h(i);
        if (constraint(t)&&bound(t)) {
          if (solution(t)) output(x);
          else t++;}
        }
    else t--;
    }
}

6.举个例子

现在举个例子,给定一组数组nums,再给定一个目标值target,找出集合nums的所有子集使其之和刚好为目标值,nums中的数只能使用一次。

例如:给定数组nums = {1,2,3,4,5},给定目标值target = 9,那么所能得到的结果有:

[0, 1, 1, 1, 0]
[0, 0, 0, 1, 1]

也即是:2+3+4和4+5

现在我们来考虑一下如何求解这个题,要找出所有子集,这显然有点“穷举搜索”的感觉,回溯就是这样一种“搜索”算法。

那么按照回溯步骤:

6.1 确定问题的解空间

用0/1表示是否是用集合中某个元素,那么对于整个集合,我们可以定义一个数组x,x[i]=0,则表示不使用第i个元素;x[i]=1,表示是用该元素。这样就将问题转化问了经典的0-1问题,而二叉树天然具有这样的性质(左子树对应1,右子树对应0,当然也可以反过来定义)。构造出的解空间树如上文中的子集树。这里就不赘述了。

6.2 确定结点扩展搜索规则

这个没什么好说的,大多数回溯问题均使用深度遍历。

6.3 进行搜索,必要时加入剪枝函数

什么是剪枝函数?其实这是一个很形象的名词,在上文中我们构造了一个解空间树,那么这颗树就有很多分支。在搜索过程中,我们可以将一些明显不可能产生解的分支给去掉,降低搜索次数。如果我们不使用剪枝函数,那么就会全部进行遍历,这往往不是我们想要的。

首先贴上不加剪枝函数的代码:

import java.util.Arrays;

public class Main {

    /**
     * 回溯法
     *
     */
    static int[] nums = {1,2,3,4,5};
    static int target = 9;
    static int n = nums.length;
    static int[] currentX = new int[n];
    public static void main(String[] args) {
        backTrack(0);
    }

    static int iter = 0;
    static void backTrack(int i ) {
        if(i == n) {
            //结束
            return;
        }
        //求和
        int currentSum = sum(currentX);
        if(currentSum+nums[i] < target) {
            //满足约束条件
            currentX[i] = 1;
            backTrack(i+1);
        }else if(currentSum+nums[i]  == target) {
            //满足约束条件
            currentX[i] = 1;
            System.out.println(Arrays.toString(currentX));
            return;
        }
        //准备进入右子树,不一定能进入
        currentX[i] = 0;
        //不满足约束条件,加入剪枝函数,减低遍历次数
        //设计规则:当前和只加上右子树的最大值都无法达到target,则不用进入右子树
       // if(currentSum + bound(i+1) >= target) {
            System.out.println(++iter);
            backTrack(i+1);
        //}

    }
    static int bound(int i) {
        int sum = 0;
        for(;i<n;i++) {
            sum+=nums[i];
        }
        return sum;
    }
    static int sum(int[] x) {
        int sum = 0;
        for (int i = 0; i < x.length; i++) {
            if(x[i] == 1) {
                sum+=nums[i];
            }
        }
        return sum;
    }

}

这里我将代码中的剪枝函数注释掉了,下面是运行结果:

1
2
3
4
5
6
7
8
9
[1, 0, 1, 0, 1]
10
11
12
13
[0, 1, 1, 1, 0]
14
15
16
17
18
19
20
21
[0, 0, 0, 1, 1]
22
23

进入右子树23次,一共有三个解。

现在我们将注释删掉,运行结果如下:

1
2
3
4
[1, 0, 1, 0, 1]
5
6
7
[0, 1, 1, 1, 0]
8
9
10
11
[0, 0, 0, 1, 1]

可以看到,进入右子树的次数只有11次了,也就是加入剪枝函数后,我们的搜索次数减少了12次。显然这是很可观。

7.最后

回溯法的介绍大概就这么多了,回溯法能够找到所有满足约束条件的解,还有一种常用求解优化问题的算法-分支限界法,用来求解满足约束条件的一个解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,639评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,277评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,221评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,474评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,570评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,816评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,957评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,718评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,176评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,511评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,646评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,322评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,934评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,755评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,987评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,358评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,514评论 2 348