[算法入门笔记] 8. 暴力递归

提示:本篇主要讲述的是基于二叉树的暴力递归模型

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",就可以转化为AAAKAAK。 给定一个只有数字字符组成的字符串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的数组weightsvaluesweights[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| ai==bj,说明两点共斜线,不能放置皇后

判断摆放位置是否合法

// 目前在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);
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cyan Chau

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值