回溯算法:从电影<蝴蝶效应>中学习回溯算法的核心思想
数独、八皇后、0-1背包、图的着色、旅行商问题、全排列问题都能用到
理解“回溯算法”
回溯的思想,类似枚举搜索,枚举所有的解,找到满足期望的解,为了有规律枚举所有可能的解,把问题求解的过程分为多个阶段,每个阶段,都会面对一个岔路口,先随意选一条路,当发现这条路不通的时候(不满足期望)就回退到上一个岔路口,另选一种。
八皇后问题:
有一个8x8棋盘,往里面放8个棋子(皇后),每个棋子所在行、列、对角线都不能有另外一个棋子
把这个问题划分为8个阶段,依次将8个棋子放到第一行、第二行、……、第八行,在放置的过程中,不停的检查是否满足规则,如果满足就跳到下一行继续放置棋子,不满足就换一种放法尝试
int[] result = new int[8];//全局或成员变量,下标表示行,值表示queen存储在哪一列
public void cal8queens(int row){ //调用方式:cal8queen(0);
if(row == 8){ //8个棋子都放置好了打印结果
printQueens(result);
return; //8个棋子都放好了,已经没法再往下递归了,所以就return
}
for(int column = 0 ; column < 8 ; ++column){ //每一行都有8种放法
if(isOk(row,column)){ //有些放法不满足要求
result[row] = column; //第row行的棋子放到了column列
cal8queens(row+1); //考察下一行
}
}
}
private boolean isOk(int row , int column){ //判断row行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] == leftup) 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();
}
两个回溯算法的经典应用
1. 0-1背包
有一个背包,背包总的承载重量是Wkg,我们有n个物品,每个物品的重量不等,且不可分割,期望选择几件物品装载到背包中,在不超过背包所能装载重量的前提下,如何让背包物品的总重量最大?
对于n个物品来说,总的装法有2^n
种,去掉总重量最接近Wkg,从剩下的装法中选择总重量最接近Wkg的,如何才能不重复穷举这2^n
种装法呢?
把物品依次排列,整个问题就分解成了n个阶段,每个阶段对应一个物品怎么选择,先对第一个物品进行处理,选择装进去还是不装,再递归处理剩下的物品
如果发现已经选择的物品重量超过了Wkg,停止继续探测剩下的物品
public int maxW = Integer.MIN_VALUE;//存储背包中物品总重量的最大值
//cw表示当前已经装进去的物品的重量和; i表示考察到哪个物品了;
//w背包重量 ; items表示每个物品的重量;n表示物品个数
//假设背包可承受重量100,物品个数10,物品重量存储在数组a中,那可以这样调用函数:
//f(0,0,a,10,100)
public void f(int i , int cw, int[] items , int n , int w){
if(cw == w || i == n ){ // cw == w表示装满了;i== n表示已经考察完所有的物品
if(cw > maxW) maxW = cw;
return;
}
f(i+1 , cw , items , n ,w);
if(cw + items[i] <= w){ //已经超过可以背包承受的重量的时候,就不要再装了
f(i+1 , cw + items[i] , items ,n , w);
}
}
2. 正则表达式
正则表达式中,最重要的是通配符,假设正则表达式中只包含 *
和?
两个通配符,其中,*
匹配任意多个(大于等于0个)任意字符,?
匹配零个或者一个任意字符,如何通过回溯算法,判断一个给定的文本,能否跟给定的正则表达式匹配?
依次考察正则表达式中的每个字符,当时非通配符时,直接跟文本的字符进行匹配,如果相同,继续往下处理,如果不同,则回溯
如果遇到特殊字符的时候,比如*
有多种匹配方案,可以匹配任意个文本串中的字符,先随意选择一种匹配方案,继续考察剩下的字符
public 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 ==tlen) 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] == "?"){ //?匹配0个或者1个字符
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);
}
}
}