如何理解回溯算法
我们每个人的一生,都会遇到很多岔路口,在人生的岔路口中,我们如何选择,将会对我们今后的人生产生很大的影响。如果选对了,可能我们的生活将会达到一个新的高度,我也是农村出来的孩子,大学不能说玩了两年,不过也差不多,现在我面对的就是一个岔路口,我打算去考研。有的人也可能会选择错误的路口,今后可能会碌碌无为。如果我们的人生可以量化,那么我们怎么去选择一个正确的岔路口呢?
之前我们聊过贪心算法,在每次面对人生的岔路口的时候,我们都选择最优的那一个路口,期望这一个局部最优解选择可以使得我们人生的全局达到最优解。但是,之前我们也分析过了,并不一定可以达到最优,所以,还有没有什么办法可以得到最优呢?
不知道大家喜不喜欢看小说,幻想过自己穿越到小时候的某个时间段,然后再我们之前后悔做过的选择之前,重新做出选择。这里的思想就是回溯算法。
回溯算法一般用到与”搜索“有关的问题,这里说的搜索是在一组可能的解中搜索满足期望的解。
回溯的处理思想有的类似枚举。枚举所有的解,找出其中满足期望的解。为了有规律地枚举所有可能地解,避免遗漏和重复,我们把问题求解的过程分为多个阶段。每个阶段都会面临一个岔路口,我们先随意选一条路走,发现这条路走不通的时候,就返回到上一个岔路口,另选一种走法继续走。
经典的回溯算法问题有很多,数独(今天我刷到这一题的时候用的遍历哈哈哈),八皇后,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背包问题
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);
}
}
正则表达式匹配问题
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);
}
}
}
其实说到这里,回溯算法的思想很简单,而且更多是搭配递归来使用,难得是如何解决问题,大家可以多刷刷算法题。
参考《数据结构与算法之美》