一看就懂的回溯算法

如何理解回溯算法 

我们每个人的一生,都会遇到很多岔路口,在人生的岔路口中,我们如何选择,将会对我们今后的人生产生很大的影响。如果选对了,可能我们的生活将会达到一个新的高度,我也是农村出来的孩子,大学不能说玩了两年,不过也差不多,现在我面对的就是一个岔路口,我打算去考研。有的人也可能会选择错误的路口,今后可能会碌碌无为。如果我们的人生可以量化,那么我们怎么去选择一个正确的岔路口呢?

之前我们聊过贪心算法,在每次面对人生的岔路口的时候,我们都选择最优的那一个路口,期望这一个局部最优解选择可以使得我们人生的全局达到最优解。但是,之前我们也分析过了,并不一定可以达到最优,所以,还有没有什么办法可以得到最优呢?

不知道大家喜不喜欢看小说,幻想过自己穿越到小时候的某个时间段,然后再我们之前后悔做过的选择之前,重新做出选择。这里的思想就是回溯算法。

回溯算法一般用到与”搜索“有关的问题,这里说的搜索是在一组可能的解中搜索满足期望的解。

回溯的处理思想有的类似枚举。枚举所有的解,找出其中满足期望的解。为了有规律地枚举所有可能地解,避免遗漏和重复,我们把问题求解的过程分为多个阶段。每个阶段都会面临一个岔路口,我们先随意选一条路走,发现这条路走不通的时候,就返回到上一个岔路口,另选一种走法继续走。

经典的回溯算法问题有很多,数独(今天我刷到这一题的时候用的遍历哈哈哈),八皇后,0-1背包,图的着色等等。

八皇后问题

八皇后问题是回溯算法的一个经典例子,题目是这么说的:

有一个8*8的棋盘,我们往里面放8个棋子,要求每个棋子所在的行,列,对角线都不能有另外一个棋子。右边的图是错误的,不符合放法。

 现在我们就用回溯算法的思想去解决这个问题,想一想,什么是回溯算法呢?我们把求解的问题分成八个阶段:在第一行放置棋子,第二行放置棋子,,,第八行放置棋子。在放置的过程中,我们不停地检查当前地做法是否满足要求。如果满足,则跳到下一行继续放置棋子,如果不满足,就换一种方法继续尝试。这个时候我们可能回想到递归这个思路来解决问题,下面看看递归来实现代码。

int[] result = new int[8];//下标表示行,值表示皇后存储在哪一列
public void cal8queens(int row){ //调用方式,cal8queens(0)
   if(row == 8){
      //八个棋子都放置好了,输出结果
      printQueens(result);
      return;//8行棋子都放好了,已经没法再往下递归了,因此返回
   }
   for(int column = 0;column < 8;column++){//每一行都有八种放法
      if(isOk(row,column)){ //有些放法不满足需求
         result[row] = column; //第row行的棋子放到了column列
         cal8queens(row+1);//考察下一行
      }
   }
}

//判断row行,column列放置是否合适
private boolean isOk(int row,int column){
   //定义左上,右上变量
   int leftup = column - 1,rightup = column + 1;
   //逐行往上考察一行
   for(int i = row - 1;i >= 0;i--){
      if(result[i] == column) return false;//第i行,第column列有棋子
      if(leftup >= 0){
         //考察左上对角线:第i行,第leftup列有棋子吗
         if(result[i] == rightup) return false;
      }
      if(rightup < 8){
         //考察右上角对角线,第i行,第rightup列有棋子吗
         if(result[i] == rightup) return false;   
      }
      --leftup;
      ++rightup;
   }
   return true;
}

private void printQueens(int[] result){
   //输出一个二维矩阵
   for(int row = 0;row < 8;row++){
     for(int column = 0;column < 8;column++){
       if(result[row] == column) 
          System.out.print("Q");
       else
          System.out.print("*");
      }
     System.out.println();
   } 
   System.out.println();
}

0-1背包问题

0-1 背包问题有很多变体,我们看一个比较基础的。我们有一个背包,背包总的承载重 量是 Wkg。现在我们有 n 个物品,每个物品的重量不等,并且不可分割。我们现在期望选 择几件物品,装载到背包中。在不超过背包所能装载重量的前提下,如何让背包中物品的总 重量最大?
实际上,背包问题我们在贪心算法那一节,已经讲过一个了(豆子还记得嘛),不过那里讲的物品是可以分割 的,我可以装某个物品的一部分到背包里面。今天讲的这个背包问题,物品是不可割的, 要么装要么不装,所以叫 0-1 背包问题。
显然,这个问题已经无法通过贪心算法来解决 了。我们现在来看看,用回溯算法如何来解决。对于每个物品来说,都有两种选择,装进背包或者不装进背包。对于 n 个物品来说,总的 装法就有 2^n 种,去掉总重量超过 Wkg 的,从剩下的装法中选择总重量最接近 Wkg 的。不过,我们如何才能不重复地穷举出这 2^n 种装法呢? 这里就可以用回溯的方法。我们可以把物品依次排列,整个问题就分解为了 n 个阶段,每 个阶段对应一个物品怎么选择。先对第一个物品进行处理,选择装进去或者不装进去,然后 再递归地处理剩下的物品。描述起来很费劲,我们直接看代码,反而会更加清晰一些。 这里还稍微用到了一点搜索剪枝的技巧,就是当发现已经选择的物品的重量超过 Wkg 之 后,我们就停止继续探测剩下的物品。
public int maxW = Integer.MIN_VALUE;//存储背包中物品总重量的最大值
//cw表示已经装进去的物品的重量和;i表示考察到哪个物品了
//w表示背包可以承载的最大重量;item表示每个物品的重量;n表示物品个数
//假设背包可承受重量为100,物品个数为10,物品重量存储再数组a中
//那么可以调用函数:f(0,0,a,10,100)
public void f(int i,int cw,int[] items,int n,int w){
   //cw == w表示装满了;i == n表示已经考察完所有的物品
   if(cw == w || i == n){
      if(cw > maxW) maxW = cw;
      return;
   }
   f(i+1,cw,itwms,n,w);
   if(cw + items[i] <= w){
     //没有超过背包可以承载最大重量
     f(i+1,cw + items[i],items,n,w);
   }
}

正则表达式匹配问题

正则表达式中,最重要的就是通配符,通配符结合在一起,可以表达非常丰富的语义。为了 方便理解,我假设正表达式中只包含“*”和“?”这两种通配符,并且对这两个通配符的语 义稍微做些改变,其中,“*”匹配任意多个(大于等于 0 个)任意字符,“?”匹配零个或者一个任意字符。基于以上背景假设,我们看下,如何用回溯算法,判断一个给定的文 本,能否跟给定的正则表达式匹配?
我们依次考察正则表达式中的每个字符,当是非通配符时,我们就直接跟文本的字符进行匹 配,如果相同,则继续往下处理;如果不同,则回溯。 如果遇到特殊字符的时候,我们就有多种处理方式了,也就是所谓的岔路口,比如“*”有 多种匹配方案,可以匹配任意个文本串中的字符,我们就先随意的选择一种匹配方案,然后 继续考察剩下的字符。如果中途发现无法继续匹配下去了,我们就回到这个岔路口,重新选 择一种匹配方案,然后再继续匹配剩下的字符。 有了前面的基础,是不是这个问题就好懂多了呢?我把这个过程翻译成了代码,你可以结合 着一块看下,应该有助于你理解。
class Pattern{
    private boolean matched = false;
    private char[] pattern;//正则表达式
    private int plen;//正则表达式长度

    public Pattern(char[] pattern,int plen){
        this.pattern = pattern;
        this.plen = plen;
    }

    public boolean match(char[] text,int tlen){
        //文本串和长度
        matched = false;
        rmatch(0,0,text,tlen);
        return matched;
    }

    private void rmatch(int ti,int pj,char[] text,int tlen){
        if(matched) return;//如果已经匹配,就不用继续递归了
        if(pj == plen){
            //正则表达式到结尾了
            if(ti == plen) matched = true;//文本串也到结尾了
            return;
        }
        if(pattern[pj] == '*'){
            // *匹配任意个字符
            for(int k = 0;k <= tlen-ti;k++){
                rmatch(ti+k,pj+1,text,tlen);
            }
        } else if(pattern[pj] == '?'){
          rmatch(ti,pj+1,text,tlen);
          rmatch(ti+1,pj+1,text,tlen);
        } else if(ti < tlen && pattern[pj] == text[ti]){//纯字符匹配才行
            rmatch(ti+1,pj+1,text,tlen);
        }
    }
}

其实说到这里,回溯算法的思想很简单,而且更多是搭配递归来使用,难得是如何解决问题,大家可以多刷刷算法题。

参考《数据结构与算法之美》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值