提示:本篇主要讲述的是基于二叉树的暴力递归模型
1. 汉诺塔问题
public void hanoi(int n) {
if(n > 0) {
func(n, "左", "右", "中");
}
}
public void process(int i, String start, String end, String other) {
if (i == 1) { // 终止条件
// 仅剩一枚盘子 直接放过去
System.out.println("Move 1 from " + start + "to" + end);
} else {
//i - 1 from -> other
func(i - 1, start, other, end);
//i from -> to
System.out.println("Move " + i + "from " + start + "to" + end);
//i - 1 other -> to
func(i - 1, other, end, start);
}
}
2. 递归函数逆序栈
[问题] 使用递归函数逆序栈,不允许申请额外空间
递归函数1:将栈stack的栈底元素返回并移除
public int getAndRemoveLastElement(Stack<Integer> stack) {
int result = stack.pop();
if (stack.isEmpty()) { // 递归结束条件
return result;
} else {
int last = getAndRemoveLastElement(stack);
stack.push(result);
return last;
}
}
递归函数2:逆序一个栈
public void reverse(Stack<Integer> stack) {
if (stack.isEmpty()) {
return;
} else {
// 获取栈底元素
int i =getAndRemoveLastElement(stack);
reverse(stack);
stack.push(i);
}
}
分析
只能利用栈操作和递归逆序栈,栈一次仅能对一个元素操作,所以只能从弹出栈底出发,然后压栈操作
3. 数字字符串转字母组合的方法数
[问题] 规定1和A
对应、2和B
对应、3和C
对应… 那么一个数字字符串比如"111",就可以转化为AAA
、KA
和AK
。 给定一个只有数字字符组成的字符串str,返回有多少种转化结果。
算法思路
定义递归函数process(i)
,其含义是str[0...i-1]
是已经做好的决定,str[i..N]
是没有转换完毕的,最后返回合法答案。
- 情况1
p(n)
表示当前来到尾部,答案已经转换完毕,只有一种结果 - 情况2 不满足情况1且
str[i]='0'
,说明是以’0’开头的违规情况,返回0种结果 - 情况3 当前i是以1开头,两种情况,其一是以
str[i]
构成个位数的答案,其二是以str[i]
和str[i+1]
构成两位数的答案 - 情况4 当前i是以2开头,两种情况,其一是以
str[i]
构成个位数的答案,其二是在以str[i]
和str[i+1]
构成两位数不超过26情况下构成另一种答案 - 情况5 剩余的情况是以3-9开头只有一种答案
- 综上相加即为返回结果
// 暴力尝试 来到i位置返回的种数
// i str[0..i-1]表示已经转换完毕,str[i..]表示还没转换的情况下
public int process(char[] str, int i) {
if (i == str.length) { // 递归终止条件
// 表示str[0..n-1]转换完毕,没有后续字符了,答案一种
return 1;
}
if (str[i] == '0') {
// 之前转换成,但遇到了0,进入无效状态
return 0;
}
// 以'1'开头后续的答案
if (str[i] == '1') {
// i自己作为单独部分,后续的答案 1位情况下的答案
int res = process(str, i + 1);
// i和i+1作为单独部分,后续的的答案 两位的情况下的答案
if (i + 1 < str.length) {
res +=process(str, i + 2);
}
return res;
}
// 以'2'开头后续的答案
if (str[i] == '2') {
// i自己作为单独部分,后续的答案 1位情况下的答案
int res = process(str, i + 1);
// (i和i+1)作为单独的部分且没有超26,后续多少种写法
if (i + 1 < str.length && (str[i + 1] >= '0' && str[i + 1] <= '6')) {
res += process(str, i + 2);
}
return res;
}
// 当前i指向的字符是3-9,只有一种情况
return process(str, i + 1);
}
4. 背包问题
[问题] 给定两个长度都为N的数组weights
和values
,weights[i]
和values[i]
分别代表i
号物品的重量和价值。给定一个正数bag
,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少?
提示:典型的从左向右尝试模型
/**
* 暴力递归
* @param weights 货物重量
* @param values 货物价值
* @param i i位置
* @param alreadyweight 之前选择货物所达到的重量
* @param bag 载重
* @return [i..]的货物自由选则,形成最大价值返回
*/
public int process(int[] weights, int[] values, int i, int alreadyweight, int bag) {
if (alreadyweight > bag) { // 违规条件
return 0;
}
if (i == weights.length) { // 终止条件
// 到了末尾没有东西可拿了,价值为0
return 0;
}
// 两条路径
// 路径1 选择当前i位置的货物
// 路径2 不选择当前i位置的货物
// 返回两种路径中最大的货物价值
return Math.max(
// 路径1 选择当前货物
value[i] + process(weights, values, i + 1, alreadyweight + weight[i], bag),
// 路径2 不选择当前货物
process(weights, values, i + 1, alreadyweight, bag));
}
5. 纸牌博弈问题
[问题] 给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数。
举例 arr = [1, 2, 100, 4]
开始时,玩家A只能拿走1或4。如果开始时玩家A拿走1,则排列变为[2,100,4],接下来玩家B可以拿走2或4,然后继续轮到玩家A…
如果开始时玩家A拿走4,则排列变为[1,2,100],接下来玩家B可以拿走1或100,然后继续轮到玩家A…
玩家A作为绝顶聪明的人不会先拿4,因为拿4之后,玩家B将拿走100。所以玩家A会先拿1,让排列变为[2,100,4],接下来玩家B不管怎么选,100都会被玩家A拿走。玩家A会获胜,分数为101。所以返回101。
举例 arr=[1,100,2]
开始时,玩家A不管拿1还是2,玩家B作为绝顶聪明的人,都会把100拿走。玩家B会获胜,分数为100。所以返回100。
先手玩家
/**
* 先手拿牌的宝贝返回的分数,先手必然拿的最好的牌
* @param arr 牌组
* @param i 左边界
* @param j 右边界
* @return 返回分数
*/
public int first(int[] arr, int i, int j) {
if (i == j) { // 递归终止条件
//当牌只有一张,先手必然拿到
return arr[i];
}
/**
* 1.先手拿走了arr[i],剩下的必然后手
* 2.先手拿走了arr[j],剩下的必然后手
* 3.返回较大的分数
*/
return Math.max(
// 先手拿走了arr[i],剩下的必然后手
arr[i] + second(arr, i + 1, j),
// 先手拿走了arr[j],剩下的必然后手
arr[j] + second(arr, i, j - 1)
);
}
后手玩家
/**
* 后手拿牌的宝贝返回的分数,后手必然拿的差的牌
* @param arr 牌
* @param i 左边界
* @param j 右边界
* @return
*/
public int second(int[] arr, int i, int j) {
if (i == j) { // 递归终止条件
// 只剩一张牌时,作为后手必然拿不到
return 0;
}
/**
* 1.对手先拿牌,对手拿走了arr[i],后手从arr[i+1..j]拿
* 2.对手先拿牌,对手拿走了arr[j],后手从arr[i..j-1]拿
* 3.返回最差的分数
*/
return Math.min(
// 对手先拿牌,对手拿走了arr[i],后手从arr[i+1..j]拿的分数
first(arr, i + 1, j),
// 对手先拿牌,对手拿走了arr[j],后手从arr[i..j-1]拿的分数
first(arr, i, j - 1)
);
}
赢家
public int win(int[] arr) {
if(arr == null || arr.length == 0) {
return 0;
}
return Math.max(first(arr, 0, arr.length - 1), second(arr, 0, arr.length - 1));
}
6. N皇后
[问题] N皇后问题是指在N×N的棋盘上要摆N个皇后,要求任何两个皇后不同行、不同列,也不在同一条斜线上。给定一个整数n,返回n皇后的摆法有多少种。
n=1,返回1。
n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0。
n=8,返回92。
6.1 暴力破解
算法思想
从第i行开始摆放皇后,在 ( i , j ) (i,j) (i,j)位置放置了一个皇后,接下来
- 整个第
i
行位置不能放皇后 - 整个第
j
列位置不能放皇后 - 如果位置 ( a , b ) (a,b) (a,b)满足 ∣ a − i ∣ = = ∣ b − j ∣ |a-i|==|b-j| ∣a−i∣==∣b−j∣,说明两点共斜线,不能放置皇后
判断摆放位置是否合法
// 目前在i行放皇后,record[0:i-1] 需要注意,record[i...] 不需要注意
// 返回i行皇后,放在j列,是否合法
public boolean isValid(int[] record, int i, int j) {
for(int k = 0; k < i; k++) { // 之前的某个k行的皇后
// 共列 共斜线
if(j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
return false;
}
}
return true;
}
暴力递归
// record[i]表示第i行皇后所在的列数
public int process(int i, int[] record, int n) {
if (i == n) { // 终止条件
// 之前做的选择来到终止位置,有一种结果
return 1;
}
int res = 0;
// 在当前i行 0...n-1列尝试
for (int j = 0; j < n; j++) {
// 当前i行的皇后放在j列,会不会和之前的(0..i-1)行的皇后,共行共列或共斜线
// 若是,认为无效;若不是,认为有效
if(isValid(record, i, j)) {
record[i] = j;
// 尝试下一行
res += process1(i + 1, record, n);
}
}
return res;
}
最终调用
public int NQueens(int n) {
if (n < 1) {
return 0;
}
// record[i]表示第i行的皇后放在第几列
int[] record = new int[n];
/**
* 0 从第0行放皇后
* record 存放列的信息
* n 不能超过n行
*/
return process1(0, record, n);
}
6.2 位运算加速(仅限32皇后内)
[算法图示]
process
// colLim 列的限制,1的位置不能放皇后,0的位置可以
// leftDiaLim 左斜线的限制,1的位置不能放皇后,0的位置可以
// rightDiaLim 右斜线的限制,1的位置不能放皇后,0的位置可以
public int process(int limit, int colLim, int leftDiaLim,
int rightDiaLim) {
// n行的n皇后放完了
if (colLim == limit) {// 列限制放完了,没地方放了
return 1;
}
int pos = 0;
int mostRightOne = 0;
// colLim | leftDiaLim | rightDiaLim 总限制
// pos代表目前能放皇后的位置
pos = limit & (~(colLim | leftDiaLim | rightDiaLim));
int res = 0;
// pos上几个1循环几次
while (pos != 0) {
// mostRightOne代表在pos中,最右边的1在什么位置,
// 然后从右到左依次筛选出pos中可选择的位置进行尝试
mostRightOne = pos & (~pos + 1);
pos = pos - mostRightOne;
// 下一行怎么尝试
res += process2(limit, colLim | mostRightOne, // 列限制或当前的决定
(leftDiaLim | mostRightOne) << 1, // 左限制或当前决定整体左移
(rightDiaLim | mostRightOne) >>> 1);// 右限制或当前决定整体右移
}
return res;
}
最终调用
public int NQueens(int n) {
// 违规条件
if (n < 1 || n > 32) {
return 0;
}
// 生成二进制数,譬如,9皇后 该二进制数表示低9位是1,高位全是0
int limit = n == 32 ? -1 : (1 << n) - 1;
return process2(limit, 0, 0, 0);
}