7.1 递归、回溯、深度搜索
7.1.1 小白上楼梯问题
以三步问题为例,解题答案:
// 方式1:递归实现,虽然简洁,但是在会出现超时!
public int waysToStep(int n) {
if(n == 1)
return 1;
if(n == 2)
return 2;
if(n == 3)
return 4;
return waysToStep(n - 1) % 1000000007 + waysToStep(n - 2) % 1000000007 + waysToStep(n - 3) % 1000000007;
}
// 方式2:
public int waysToStep(int n) {
if(n == 1)
return 1;
if(n == 2)
return 2;
if(n == 3)
return 4;
int[] dp = new int[n+1];
dp[1] = 1;
dp[2] = 2;
dp[3] = 4;
for(int i = 4;i < dp.length;i++){
//取模,对两个较大的数之和取模再对整体取模,防止越界(这里也是有讲究的)
//假如对三个dp[i-n]都 % 1000000007,那么也是会出现越界情况(导致溢出变为负数的问题)
//因为如果本来三个dp[i-n]都接近 1000000007 那么取模后仍然不变,但三个相加则溢出
//但对两个较大的dp[i-n]:dp[i-2],dp[i-3]之和mod 1000000007,
//那么这两个较大的数相加大于 1000000007但又不溢出
//取模后变成一个很小的数,与dp[i-1]相加也不溢出
//所以取模操作也需要仔细分析
dp[i] = (dp[i-1] + (dp[i-2] + dp[i-3]) % 1000000007) % 1000000007;
}
return dp[n];
}
7.1.2 机器人走方格问题
有一个X * Y的网格,一个机器人只能走格点且只能向右或向下走,要从左上角走到右下角。请设计一个算法,计算机器人有多少种走法。
给定两个正整数
int x,int y,请返回机器人的走法数目。保证x+y小于等于12。举例子:
(2,2)
返回:2即2 * 2的网格中,机器人从左上角走到右上角的走法有2种。
牛客网例题:机器人走方格
解题思路:
代码如下:
// 方案1:递归实现
public int countWays(int x, int y) {
if(x == 1 || y == 1)
return 1;
return countWays(x-1,y) + countWays(x,y-1);
}
如果不适用递归的方式,可以使用如下方式:其中机器人从左上角到右下角的总路线数就可以以右下角的值表示
| x=y=1 | 1 |
|---|---|
| x=1 y=2 | 1 |
| x=2 y=1 | 1 |
| x=2 y=2 | 1+1=2 分解为向右走一步(x=2-1, y=2)+向下走一步(x=2 y=2-1) —>2 |
| x=3 y=2 | 1+2=3 分解为向右走一步(x=3-1 y=2)+向下走一步(x=3 y=2-1) —>3 |
| x=2 y=3 | 1+2=3 分解为向右走一步(x=2-1 y=3)+向下走一步(x=2 y=3-1) —>3 |
| x=3 y=3 | 3+3=6 分解为向右走一步(x=3-1 y=3)+向下走一步(x=3 y=3-1) —>6 |
| 1 | 1 | 1 |
|---|---|---|
| 1 | 2 | 3 |
| 1 | 3 | 6 |
代码如下:
// 方案2: 递推
public int countWays(int x, int y) {
int[][] a=new int[x+1][y+1];
// 二维数组初始化所有值为1
for (int i = 1; i <=x; i++) {
a[i][1]=1;
}
for (int j = 1; j <=y; j++) {
a[1][j]=1;
}
// 递推计算
for (int i = 2; i <=x ; i++) {
for (int j = 2; j <=y ; j++) {
a[i][j]=a[i-1][j]+a[i][j-1];
}
}
// 返回结果
return a[x][y];
}
7.1.3 硬币表示问题
力扣题目链接:面试题 08.11. 硬币
解题思路:
递归方式:
动态规划的方式:
代码如下:
public class Test31 {
public static void main(String[] args) {
System.out.println(waysToChange(50));
System.out.println(countWays2(50));
}
static int waysToChange(int n) {
if (n <= 0)
return 0;
int[] coins = { 1, 5, 10, 25 };// 钱币面值数组
return countWays(n, coins, 3);// 从下标3对应的最大面值开始递归
}
/**
* 统计n分有几种表示法(递归方式)
*
* @param money
* @param coins
* @param index
* @return
*/
static int countWays(int money, int[] coins, int index) {
if (index == 0)
return 1;
int result = 0;
for (int i = 0; coins[index] * i <= money; i++) {
int shengyu = money - coins[index] * i;// 剩余金额
result = result + countWays(shengyu, coins, index - 1);// 表示法数量
}
return result;
}
/**
* 统计n分有几种表示法(动态规划方式)
*
* @param n 总金额
* @return
*/
static int countWays2(int n) {
int mod = 1000000007;
int[] dp = new int[n + 1];// dp[n] 就表示:n分表示法总数
int[] coins = new int[]{1,5,10,25};// 钱币面值数组
dp[0] = 1;// 不使用任何硬币,表示0元有一种方法
for (int coin : coins) {// 每一次循环,使用新面值coin
for (int i = coin; i <= n; i++) {// 增加新面值后,方法总数受影响的是从coin开始的
dp[i] = (dp[i] + dp[i - coin]) % mod;
// 方法总数 = 使用新的面值方法总数((j-面值)的方法总数) + 不使用新面值的方法总数(为原来值)
}
}
return dp[n];
}
}
7.1.4 "逐步生成结果”类问题之非数值型:括号
题目链接:面试题 08.09. 括号
代码如下:
public class Test32 {
public static void main(String[] args) {
List<String> list = new Test32().generateParenthesis(3);
System.out.println(list);
}
public List<String> generateParenthesis(int n) {
List<String> resultList = new ArrayList<>();
if (n == 0)
return resultList;
// 做加法: left 和 right 从 0 开始,也可以做减法,二者从n开始
dfs("", resultList, 0, 0, n);
return resultList;
}
/**
* @param str 当前递归得到的字符串结果
* @param resultList 结果集
* @param left 左括号已经用了几个
* @param right 右括号已经用了几个
* @param n 左括号、右括号一共得用几个
*/
private void dfs(String str, List resultList, int left, int right, int n) {
// 达到目标括号对数n,将结果添加到resultList
if (left == n && right == n) {
resultList.add(str);
return;
}
// 剪枝:当左括号数量小于有括号数量时不满足条件
/*
* 题目要求的括号成立条件需要满足如下2点:
* 1.任意前缀中"("的数量都需要>= ")"的数量。
* 2.左右括号数量达到目标对数n,且二者相等。
*/
if (left < right) {
return;
}
if (left < n) {
// 拼接左括号
dfs(str + "(", resultList, left + 1, right, n);
}
if (right < n) {
// 拼接右括号
dfs(str + ")", resultList, left, right + 1, n);
}
}
}
7.1.5 幂集
题目链接:面试题 08.04. 幂集
解题分析(非递归方式):
代码如下:
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>(1 << nums.length);
//先添加一个空的集合
res.add(new ArrayList<>());
for (int num : nums) {
//每遍历一个元素就在之前子集中的每个集合追加这个元素,让他变成新的子集
for (int i = 0, j = res.size(); i < j; i++) {
//遍历之前的子集,重新封装成一个新的子集
List<Integer> list = new ArrayList<>(res.get(i));
//然后在新的子集后面追加这个元素
list.add(num);
//把这个新的子集添加到集合中
res.add(list);
}
}
return res;
}
7.1.6 全排列
题目链接:46. 全排列
解题思路(回溯 + 深度优先遍历):
代码如下:
public class Test34 {
public static void main(String[] args) {
List<List<Integer>> resultList = permute(new int[] { 1, 2, 3 });
System.out.println(resultList);
}
/**
* 全排列
*
* @param nums 源数组
* @return
*/
public static List<List<Integer>> permute(int[] nums) {
int len = nums.length;// 数组长度
List<List<Integer>> res = new ArrayList<>();// 结果集
if (len == 0) {
return res;
}
// 栈:已经被选择的数存入栈中
Stack<Integer> path = new Stack<Integer>();
// 布尔数组:表示那些数是被选择过了
boolean[] used = new boolean[len];
// 回溯 + 深度优先
dfs(nums, len, 0, path, used, res);
return res;
}
/**
* 回溯 + 深度优先
*
* @param nums 源数组
* @param len 源数组长度
* @param depth 深度:递归到了第几层
* @param path 栈:已经被选择的数存入栈中
* @param used 布尔数组:表示那些数是被选择过了
* @param res 结果集
*/
private static void dfs(int[] nums, int len, int depth, Stack<Integer> path, boolean[] used,
List<List<Integer>> res) {
// 达到最大深度
if (depth == len) {
res.add(new ArrayList<Integer>(path));// 添加栈中已选择的数到结果集
return;
}
for (int i = 0; i < len; i++) {
if (used[i]) {// 如果该数已经选择过了,则跳过本次循环,继续执行下次循环
continue;
}
// 将nums[i]存入path栈中
path.push(nums[i]);
// 将下标i对应的数设置为已经被选择
used[i] = true;
// 回溯:深度+1
dfs(nums, len, depth + 1, path, used, res);
// 状态重置
path.pop();
used[i] = false;
}
}
}
7.1.7 和为k的子数组
力扣题目链接:560. 和为K的子数组
代码如下:
public class Test35 {
public static void main(String[] args) {
System.out.println(subarraySum(new int[] { 1, -1, 0 }, 0));
}
/**
* 暴力解法
*
* @param nums
* @param k
* @return
*/
public static int subarraySum(int[] nums, int k) {
int count = 0;
for (int i = 0; i < nums.length; i++) {
int result = nums[i];
if (result == k) {
count++;
}
for (int j = i + 1; j < nums.length; j++) {
result = result + nums[j];
if (result == k) {
count++;
}
}
}
return count;
}
/**
* 枚举解法
*
* @param nums
* @param k
* @return
*/
public static int subarraySum2(int[] nums, int k) {
int count = 0;
for (int start = 0; start < nums.length; ++start) {
int sum = 0;
for (int end = start; end >= 0; --end) {
sum += nums[end];
if (sum == k) {
count++;
}
}
}
return count;
}
}
7.1.8 N皇后问题
参考文章:Java解决八皇后、N皇后问题(包含例题、视频讲解链接)
回溯
- 递归调用代表开启一个分支,如果希望这个分支返回后某些数据恢复到分支开启前的状态以便重新开始,就要使用回溯技巧
- 全排列的交换法,数独,部分和,用到了回溯
剪枝
- 深搜时,如已明确从当前状态无论如何转移都不会存在(更优)解,就应该中断往下的继续搜索,这种方法称为剪枝
- 数独里面有剪枝
- 部分和里面有剪枝