Hanoi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void process(int index, String from, String to, String other) {
if (index == 1) { // base case 如果仅剩一个盘子,则直接放到to位置
System.out.println("将" + index + "从" + from + "移动到" + to);
} else {
// i - 1, from -> other
process(index - 1, from, other, to);
// i, fom -> to
System.out.println("将" + index + "从" + from + "移动到" + to);
//i - 1, other -> to
process(index - 1, other, to, from);
}
}
public void hanoi(int n) {
if (n > 0) {
process(n, "左", "右", "中");
}
}

打印一个字符串的全部子序列,包括空串

算法思想

对于来到的第i位置,我们有两种选择

  • 选择1:我们走该路径
  • 选择2:我们不走该路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void process(char[] chs, int index) {

if (index == chs.length) { // 递归终止条件
System.out.println(String.valueOf(chs));
return;
}

// 选择1 我们要这个字符,并进入递归树的下一层
process(chs, index + 1);

// 将选择的字符保存起来
char temp = chs[index];

// 选择2 我们不要这个字符,并进入递归树的下一层
chs[index] = 0; // 咱不要它
process(chs, index + 1);
// 恢复到丢弃之前的状态
chs[index] = temp;

}

public void printAllSubSquences(String str) {
char[] chs = str.toCharArray();
process(chs, 0);
}

使用递归函数逆序栈(不能申请额外空间)

只能利用栈操作和递归逆序栈,栈一次仅能对一个元素操作,所以只能从弹出栈底出发,然后压栈操作

递归函数1:将栈stack的栈底元素返回并移除

递归函数2:逆序一个栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 获取栈底元素并移除它
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;
}
}

// 逆序栈
public void reverse(Stack<Integer> stack) {
if (stack.isEmpty()) {
return;
} else {
//获取栈底元素
int i = getAndRemoveLastElement(stack);
reverse(stack);
stack.push(i);
}
}

数字字符串转字母组合的种数

规定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开头 只有一种答案

综上相加即为返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* process表示来到 i 位置时返回的组合总数
*
* @param str
* @param i str[0..i-1]表示已经转换完成,str[i..]表示还没转换的情况下
* @return 最终返回答案
*/
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开头,其一是以str[i]构成个位数的答案
int res = process(str, i + 1);
// 当前i是以1开头,其二是以str[i]和str[i+1]构成两位数的答案
if (i + 1 < str.length) {
res += process(str, i + 2);
}
return res;
}

// 以'2'开头后续的答案
if (str[i] == '2') {
// 当前i是以2开头,其一是以str[i]构成个位数的答案
int res = process(str, i + 1);
// 当前i是以2开头,其二是在以str[i]和str[i+1]构成两位数不超过26情况下构成另一种答案
if (i + 1 < str.length && (str[i + 1] >= '0' && str[i + 1] <= '6')) {
res += process(str, i + 2);
}
return res;
}

// 剩余的情况是以3-9开头只有一种答案
return process(str, i + 1);
}

背包问题

给定两个长度都为 $N$ 的数组weightsvaluesweights[i]values[i] 分别代表 i 号物品的 重量价值 。给定一个正数bag,表示一个 载重 bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少?

算法思想

从左到右的尝试模型,来到i位置做出选择,或者不做出选择,最终合并答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 背包问题
*
* @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;
}


/**
* 我们来到 i 位置,提供两条路径
*
* 路径1:选择当前 i 位置的货物
* 路径2:不选择当前 i 位置的货物
*
* 最后返回两种路径中最大的货物价值s
*/
return Math.max(
// 路径1 选择当前货物,价值则加上选择的,同时背包重量相应增加,并进入下一层递归,继续选择
values[i] + process(weights, values, i + 1, alreadyweight + weights[i], bag),
// 路径2 不选择当前货物,价值和背包重量不变,并进入下一层递归,继续选择
process(weights, values, i + 1, alreadyweight, bag));
}

纸牌博弈

给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走 最左或最右 的纸牌,玩家A和玩家B都 绝顶聪明。请返回最后获胜者的分数。

arr = [1, 2, 100, 4]

  • 开始时,玩家A只能拿走14。如果开始时玩家A拿走 1,则排列变为 [2,100,4] ,接下来玩家B可以拿走 24,然后继续轮到玩家A

  • 如果开始时玩家A拿走 4,则排列变为 [1,2,100],接下来玩家B可以拿走 1100,然后继续轮到玩家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

先手玩家

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 先手拿牌的玩家返回的分数,先手必然拿的最好的牌
*
* @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)
);
}

后手玩家

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 后手拿牌的玩家返回的分数,后手必然拿的差的牌
*
* @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)
);
}

最终赢家

1
2
3
4
5
6
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));
}

N 皇后

N皇后问题是指在 N×N 的棋盘上要摆 N个皇后,要求任何两个皇后 不同行、不同列,也不在同一条斜线上 。给定一个整数n,返回皇后的摆法有多少种。

测试用例

  • n=1,返回1
  • n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0
  • n=8,返回92

暴力递归

算法思想

从第 i 行开始摆放皇后,在 $(i,j)$ 位置放置了一个皇后,接下来

  • 整个第 i 行位置不能放皇后

  • 整个第 j 列位置不能放皇后

如果位置 $(a,b)$ 满足 $∣a−i∣==∣b−j∣$ ,说明两点共斜线,不能放置皇后

判断摆放位置是否合法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 判断摆放位置是否合法
*
* 目前在 i 行放皇后,record[0:i-1] 需要注意,record[i...] 不需要注意
*
* @return 返回 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;
}

暴力递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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 += process(i + 1, record, n);
}
}
return res;
}

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int NQueens(int n) {

if (n < 1) {
return 0;
}

// record[i]表示第i行的皇后放在第几列
int[] record = new int[n];

/**
* 0 从第0行放皇后
*
* record 存放列的信息
*
* n 不能超过 n 行
*/
return process(0, record, n);
}

位运算(局限性)

局限性

当前由于int位限制,仅支持到32皇后以内的摆法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* process
*
* @param limit 限制数,由 n 决定
* @param colLim 列的限制,1的位置不能放皇后,0的位置可以
* @param leftDiaLim 左斜线的限制,1的位置不能放皇后,0的位置可以
* @param rightDiaLim 右斜线的限制,1的位置不能放皇后,0的位置可以
* @return 摆放个数
*/
public int process(int limit, int colLim, int leftDiaLim,
int rightDiaLim) {
// n行的n皇后放完了
if (colLim == limit) { // 列限制放完了,没地方放了
return 1;
}

// pos代表目前能放皇后的位置
int pos = 0;
// mostRightOne代表在pos中,最右边的1在什么位置
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 += process(limit, colLim | mostRightOne, // 列限制或当前的决定
(leftDiaLim | mostRightOne) << 1, // 左限制或当前决定整体左移
(rightDiaLim | mostRightOne) >>> 1);// 右限制或当前决定整体右移
}
return res;
}
1
2
3
4
5
6
7
8
9
10
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 process(limit, 0, 0, 0);
}