递归算法切记不要使用debug的方式层层深入进去,这样会把自己绕进去,而且问题也得不到解决。还是要理解递归的思想,从思想的角度去理解代码。
回溯算法
回溯算法类似于枚举搜索,枚举所有的解,找出满足期望的解.它把问题分为多个阶段,每个阶段都会面对一个岔路口,我们先随意一条路走,当发现这条路走不通的时候,就回退到上一个岔路口,另选一种走法继续走.
举个例子,最近看了一部电视剧叫做《老酒馆》,主演陈宝国,扮演的主人公叫陈怀海,他为了给儿子报仇,去关东山一个叫做干饭盆的地方去找仇家,可惜到了树林迷路了,不管怎么走都回到同一个地方,于是他想了一个办法,在这个地方绑了一根绳子作为标记,然后随便找个方向寻找出口,如果方向不对,他一定会再回到这地方,然后换一条路走,这样子就找到了出口。
回溯算法应用有很多,比如数独问题、八皇后问题、0-1背包问题、图的着色、旅行商问题、全排列等等。
有的时候语言解释比较费劲,想搞清楚只有通过代码理解逻辑,反而语言更能说明清楚,这个时候就得研究下代码了
回溯算法非常适合用递归来实现,在实现的过程中,剪枝操作是提高.回溯效率的一种技巧。利用剪枝,我们并不需要穷举搜索所有的情况,从而提高搜索效率。
八皇后问题
八皇后问题是一个8×8的棋盘,往里边放入8颗棋子(皇后),每个棋子所在行、列、对角线都不能出现另一个皇后。8皇后问题就是为了找出所有满足这种要求的棋子放置方式。
做法就是依次将8个棋子放到第一行、第二行、第三行…第八航。放置过程中,不停检查当前棋子的放置方法是否满足要求。如果满足则继续下一行放置下一个棋子,如果不满足就换一种方法继续尝试。
package com.study.algorithm.backtracking;
/**
* 8皇后问题的解法
* @author jeffSheng
* @date 20191004
*
*/
public class EightQueue {
int[] result = new int[8];// 全局或成员变量, 下标表示行, 值表示 queen 存储在哪一列
public void cal8queens(int row) { // 调用方式:cal8queens(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();
}
public static void main(String[] args) {
EightQueue eightQueue = new EightQueue();
eightQueue.cal8queens(0);
}
}
0-1背包问题
0-1背包问题是非常经典的算法题,很多场景都可以抽象成这个问题模型。这个问题的经典解法是动态规划,不过还有一种方法但是没那么高效,就是回溯算法。0-1背包问题变体很多,这里是比较基础的一种,有一个背包,背包总承重X kg。我们有n个物品,每个物品重量不等,且整块不可分割。现在期望选择几件物品,装载到背包中,在不超过背包称重的前提下,如何让背包中的物品总重量最大?
做法是,对于每种物品来说,都有两种选择,装进背包或者不装进背包。对于n个物品来说,总的装法有2^n次方种,出过去超过承重XKg的,从剩下的装法中选择总重量最接近Xkg的。这里用回溯的方法,可以把物品依次排列,整个问题分为n个阶段,每个阶段对应一个物品怎么选择。先对第一个物品进行处理,选择装进去或者不装进去,然后再递归处理剩下的物品。
package com.study.algorithm.backtracking;
/**
* 回溯算法的应用 :0-1背包问题
* @author jeffSmile
* @date 20191004
*
*/
public class Knapsack {
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);
}
}
}
优化后的0-1背包问题:
package com.study.algorithm.backtracking;
/**
* @Auther: JeffSheng
* @Date: 2019/10/16 14:45
* @Description: 回溯算法实现0-1背包问题
* 在满足背包总重量w=9的前提下,背包中可以容纳的最大值是多少
*/
public class Knapsack {
/**
* 回溯算法实现。注意:我把输入的变量都定义成了成员变量。
* 结果放到 maxW 中
*/
private int maxW = Integer.MIN_VALUE;
/**
* 物品重量
*/
private int[] weight = {2,2,4,6,3};
/**
* 物品个数
*/
private int n = 5;
/**
* 背包承受的最大重量
*/
private int w = 9;
/**
* 调用 f(0, 0)
* @param i
* @param cw
*/
public void f(int i,int cw){
//cw==w 表示装满了,i==n 表示物品都考察完了
if(cw == w || i == n){
if(cw > maxW){
maxW = cw;
}
return;
}
// 选择不装第 i 个物品
f(i+1,cw);
// 选择装第 i 个物品
if(cw + weight[i] <= w){
f(i+1,cw + weight[i]);
}
}
/**
* 备忘录,默认值 false
*/
private boolean[][] mem = new boolean[5][10];
/**
* 优化后的0-1背包问题,使用备忘录存储已经计算过的值,再次遇到直接取出来用
* 调用 f(0, 0)
* @return
*/
public void f1(int i, int cw) {
// cw==w 表示装满了,i==n 表示物品都考察完了
if (cw == w || i == n) {
if (cw > maxW){
maxW = cw;
}
return;
}
// 重复状态
if (mem[i][cw]){
return;
}
// 记录 (i, cw) 这个状态
mem[i][cw] = true;
// 选择不装第 i 个物品
f1(i+1, cw);
// 选择装第 i 个物品
if (cw + weight[i] <= w) {
f1(i+1,cw + weight[i]);
}
}
/**
* 结果放到 maxV 中
*/
private int maxV = Integer.MIN_VALUE;
/**
* 物品的价值
*/
private int[] value = {3,4,8,9,6};
/**
*当把物品价值考虑在内时,使用回溯算法计算袋子承重限制条件下的物品最大总价值
* 调用 f(0, 0, 0)
* @param i 表示处理的是第几个物品
* @param cw 表示袋子中物品的重量
* @param cv 表示袋子中物品总价值
*/
public void ff(int i, int cw, int cv) {
// cw==w 表示装满了,i==n 表示物品都考察完了
if (cw == w || i == n) {
if (cv > maxV){
maxV = cv;
}
return;
}
// 选择不装第 i 个物品
ff(i+1, cw, cv);
if (cw + weight[i] <= w) {
// 选择装第 i 个物品
ff(i+1,cw + weight[i], cv + value[i]);
}
}
public static void main(String[] args) {
Knapsack k = new Knapsack();
k.f(0,0);
System.out.println(k.maxW);
}
}
正则表达式
正则表达式中主要就是通配符。假设正则表达式中只有*和?两种通配符,并且这俩个通配符语义这样定义:*表示匹配任意多个任意字符,?匹配不大于1个任意字符,来判断给定一个文本,能否跟给定的正则表达式匹配?
package com.study.algorithm.backtracking;
/**
* 自定义正则表达式的实现
*
* @author jeffSmile
* @date 20191004
*
*/
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);
}
}
}