1 组合问题

LeeCode 39:组合总和

题目描述

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的所有 不同组合,并以列表形式返回。

candidates 中的同一个数字可以 无限制重复被选取。如果至少一个数字的被选数量不同,则两种组合是不同的。

  • \(1 \le candidates.length \le 30\)
  • \(1 \le candiates[i] \le 200\)
  • \(1 \le target \le 500\)

建立模型

  1. 关键要求:数字可以重复选取
  2. 定义递归函数 combinationSumImpl(int[] candidates, int index, int sum, int target)
  3. 确定递归终止条件 sum == target
  4. 剪枝优化,当 sum > target 时,无需再往下一层递归,可以直接 return

代码实现

public class Solution {
/**
* 定义两个全局变量
* @param res: 存储所有符合条件的组合
* @param temp: 存储当前组合包含的数
*/
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>(); public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
combinationSumImpl(candidates, 0, 0, target); return res;
} public void List<Integer> combinationSumImpl(int[] candidates, int index, int sum, int target) {
/**
* 递归终止条件
*/
if (sum == target) {
res.add(new ArrayList<>(temp));
return;
} for (int i = index; i < candidates.length; i++) {
// 剪枝: 若加上当前元素已经超过目标值,则无需继续递归下一层
// 因为 candidates 是递增排序的,再添加后面的值肯定也会超过目标值
if (sum + candidates[i] > target) {
break;
} temp.add(candidates[i]); /**
* 允许重复选取元素
* 所以下一层递归从当前元素开始
*/
combinationSumImpl(candidates, i, sum + candidates[i], target); temp.remove(temp.size() - 1);
}
}
}

LeeCode 40:组合总和II

题目描述

给的一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中每个数字在每个组合中只能使用一次。解集不能包含重复的组合。

  • \(1 \le candidates.length \le 100\)
  • \(1 \le candidates[i] \le 50\)
  • \(1 \le target \le 30\)

建立模型

  1. 本题与 LeeCode 39:组合总和 最主要的区别就是:每个数字只能使用一次,但 candidates 中数字是可以重复的
  2. 定义递归函数 combinationSum2Impl(int[] candidates, int index, int sum, int target)
  3. 确定递归终止条件:sum == target
  4. 去除重复项,因为 candidates 中存在重复数字,导致回溯过程中会产生重复的组合
  5. 剪枝优化,当 sum > target 时,无需再往下一层递归,可以直接 return

代码实现

public class Solution {
/**
* 定义两个全局变量
* @param res: 存储所有符合条件的组合
* @param temp: 存储当前组合包含的数
*/
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>(); public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
} public void combinationSum2Impl(int[] candidates, int index, int sum, int target) {
if (sum == target) {
res.add(new ArrayList<>(temp));
return;
} for (int i = index; i < candidates.length; i++) {
/**
* 去除重复项目
* i > index: 说明没有选择前一个数字
* 若此时当前数字等于前一个数字,则递归当前数字的所有组合均在递归前一个数字时出现过
* 所以跳过当前数字的递归
*/
if (i > index && candidates[i] == candidates[i - 1]) {
continue;
} // 剪枝: 若加上当前元素已经超过目标值,则无需继续递归下一层
// 因为 candidates 是递增排序的,再添加后面的值肯定也会超过目标值
if (sum + candidates[i] > target) {
break;
} temp.add(candidates[i]); /**
* 每个数字只能选取一次,所以下一层递归从当前数字后面一个开始
*/
combinationSum2Impl(candidates, i + 1, sum + candidates[i], target); temp.remove(temp.size() - 1);
}
}
}

LeeCode 216:组合总和III

题目描述

找出所有相加之和为 nk 个数的组合,且满足下列条件:

  • 只能使用数字 1~9
  • 每个数字最多使用一次

返回所有可能的有效组合的列表。解集不能包含重复的组合。

建立模型

  1. 本题与 LeeCode 40 组合总和II 主要的区别是:目标不仅有值约束(n)还有个数约束(k),但 candidates 是固定的 [1, 2, 3, 4, 5, 6, 7, 8, 9]
  2. 定义递归函数 combinationSum3Impl(int k, int n, int sum, int index)
  3. 确定递归终止条件 temp.size() == k || (temp.size() < k && sum >= n)
  4. 剪枝优化,当 temp.size() == k && sum >= n 时,无需再往下一层递归,可直接返回
  5. 剪枝优化,当 index > 9 - (k - temp.size()) + 1 时,即把后面所有项都加上也无法满足 k 项

代码实现

public class Solution {
/**
* 定义两个全局变量
* @param res: 存储所有符合条件的组合
* @param temp: 存储当前组合包含的数
*/
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>(); public List<List<Integer>> combinationSum3(int k, int n) {
combinationSum3Impl(k, n, 0, 0);
} public void combinationSum3Impl(int k, int n, int sum, int index) {
// 递归终止条件1:temp.size() == k
if (temp.size() == k) {
if (sum == n) {
res.add(new ArrayList<>(temp));
}
return;
} /**
* 递归终止条件2:temp.size() < k && sum >= n
* 剪枝优化
* 当前和已经大于等于目标值,但个数还不满足,又只能添加正数,所以继续添加不可能满足要求
*/
if (sum >= n) {
return;
} /**
* 剪枝优化
* 若 i > 9 - (k - temp.size()) + 1
* 则把当前项后面所有项都加上,无法满足 k 项,所有不可能满足要求
*/
for (int i = index; i <= 9 - (k - temp.size()) + 1; i++) {
temp.add(i);
combinationSum3Impl(k, n, sum + i, i + 1);
temp.remove(temp.size() - 1);
}
}
}

2 子集问题

LeeCode 78:子集

题目描述

给你一个整数数组 nums ,数组中的元素互不相同。返回该数组所有可能的子集。

解集不能包含重复的子集。可以按任意顺序返回子集。

建立模型

  1. 通过递归来实现子集的枚举
  2. 关键信息:数组中的元素互不相同,所有枚举过程中不会出现重复的子集
  3. 定义递归函数 subsetsImpl(int[] nums, int index)
  4. 确定递归终止条件 index == nums.length

代码实现

public class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>(); public List<List<Integer>> subsets(int[] nums) {
subsetsImpl(nums, 0);
return res;
} public void subsetsImpl(int[] nums, int index) {
if (index == nums.length) {
res.add(new ArrayList<>(temp));
return;
} // 添加当前元素递归
temp.add(nums[index]);
subsetsImpl(nums, index + 1); // 不添加当前元素递归
temp.remove(temp.size() - 1);
subsetsImpl(nums, index + 1);
}
}

LeeCode 90:子集II

题目描述

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集。

解集不能包含重复的子集。返回的解集中,子集可以按任意顺序排列。

建立模型

  1. 本题与 LeeCode 78 子集 最主要的区别就是:nums 中存在重复元素,枚举子集的过程中会产生重复的子集
  2. 关键信息:nums 中存在重复元素,枚举子集过程中如何去重
  3. 定义递归函数 subsetsWithDupImpl(int[] nums, int index, boolean flag)
  4. 确定递归终止条件 index == nums.length
  5. 去除重复子集条件 !flag && index > 0 && nums[index] == nums[index - 1]

代码实现

public class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>(); public List<List<Integer>> subsetsWithDup(int[] nums) {
subsetsWithDupImpl(nums, 0, false);
return res;
} /**
* @param flag: 表示是否 temp 中是否包含当前元素的前一个元素
*/
public void subsetsWithDupImpl(int[] nums, int index, boolean flag) {
if (index == nums.length) {
res.add(new ArrayList<>(temp));
return;
} subsetsWithDupImpl(nums, index + 1, false); // 不添加当前元素 /**
* flag = false: 未添加前一个元素
* index > 0: 不是首元素
* nums[index] == nums[index - 1]: 当前元素 等于 前一个元素
*
* 此时递归当前元素产生的子集均在递归前一个元素时出现过
* 所以跳过当前数字的递归
*/
if (!flag && index > 0 && nums[index] == nums[index - 1]) {
return;
} temp.add(nums[index]);
subsetsWithDupImpl(nums, index + 1, true); // 添加当前元素
temp.remove(temp.size() - 1);
}
}

3 排列问题

LeeCode 46:全排列

题目描述

给定一个不含重复数字的数组 nums ,返回其所有可能的全排列。你可以按任意顺序返回答案。

  • \(1 \le nums.length \le 6\)
  • \(-10 \le nums[i] \le 10\)
  • nums 所有数互不相同

建立模型

  1. 相较于组合问题,排列问题中的数字有顺序
  2. 关键信息:nums 不包含重复元素,所有递归过程中不会产生重复的排列
  3. 定义 used 数组,标识元素是否已经使用过
  4. 定义递归函数 permuteImpl(int[] nums, int index, boolean[] used)
  5. 确定递归终止条件 index == nums.length

代码实现

// 实现方式一
public class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>(); public List<List<Integer>> permute(int[] nums) {
boolean[] used = new boolean[nums.length];
permuteImpl(nums, 0, used); return res;
} public void permuteImpl(int[] nums, int index, boolean[] used) {
if (index == nums.length) {
res.add(new ArrayList<>(temp));
return;
} // 对于第 index 位,尝试填入 nums 中的每一个数字
for (int i = 0; i < nums.length; i++) {
if (!used[i]) {
used[i] = true;
temp.add(nums[i]);
permuteImpl(nums, index + 1, used);
temp.remove(temp.size() - 1);
used[i] = false;
}
}
}
} // 实现方式二, 不使用标记数组
public class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>(); public List<List<Integer>> permute(int[] nums) {
for (int num : nums) {
temp.add(num);
} permuteImpl(nums, 0);
return res;
} public void permuteImpl(int[] nums, int index) {
if (index == nums.length) {
res.add(new ArrayList<>(temp));
return;
} /**
* 0 ~ index - 1: 已填充位置
* index ~ nums.length - 1: 待填充位置
*
* 使用 [index, nums.length - 1]里的数去填充第 index 个数
* 即 Collections.swap(temp, index, i)
*/
for (int i = index; i < nums.length; i++) {
Collections.swap(temp, index, i);
permuteImpl(nums, index + 1);
Collections.swap(temp, index, i);
}
}
}

LeeCode 47 全排列II

题目描述

给定一个包含重复数字的序列 nums ,按任意顺序返回所有不重复的全排列。

  • \(1 \le nums.length \le 8\)
  • \(-10 \le nums[i] \le 10\)

建立模型

  1. 本题与 LeeCode 46 全排列 最大的区别就是:nums 中存在重复元素,递归过程中会产生重复的排列
  2. 定义递归函数 permuteUniqueImpl(int[] nums, int index, boolean[] used)
  3. 确定递归终止条件 index == nums.length
  4. 去除重复排列条件 (i > 0 && nums[i] == nums[i - 1] && !used[i - 1])

代码实现

public class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>(); public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums); // 排序使得相同的数字出现在一起
boolean[] used = new boolean[nums.length];
} public void permuteUniqueImpl(int[] nums, int index, boolean[] used) {
if (index == nums.length) {
res.add(new ArrayList<>(temp));
return;
} for (int i = 0; i < nums.length; i++) {
/**
* 去重重复排列
* i > 0: 当前元素不是首元素
* nums[i] == nums[i - 1]: 当前元素等于前一个元素
* !used[i - 1]: 未添加前一个元素
*
* 即在当前 index 位置添加当前元素产生的排列,均在 当前index位置添加前一个元素中 出现过
* 所以跳过此元素
*/
if (used[i] || (i > 0 && nums[i] == nums[i - 1] && !used[i - 1])) {
continue;
} temp.add(nums[i]);
used[i] = true; permuteUniqueImpl(nums, index + 1, used); used[i] = false;
temp.remove(temp.size() - 1);
}
}
}

4 其它回溯问题

LeeCode 491:递增子序列

题目描述

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中至少有两个元素。你可以按任意顺序返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

建立模型

  • 关键信息:nums 中存在重复元素,所以可能会产生重复的子序列

  • 定义递归函数 findSubsequenceImpl(int[] nums, int index, int lastValue)

  • 确定递归终止条件 index == nums.length

  • 去除重复子序列条件 nums[index] != lastValue ,如果有两个连续元素相同,会产生四种情况:

    1. 前者被添加,后者也被添加
    2. 前者不被添加,后者也不被添加
    3. 前者被添加,后者不被添加
    4. 前者不被添加,后者被添加

    情况3和情况4是等价的,即会产生重复子序列,所以通过 nums[index] != lastValue 条件去除情况3,保留情况4。

代码实现

public class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> temp = new ArrayList<>(); public List<List<Integer>> findSubsequence(int[] nums) {
findSubsequenceImpl(nums, 0, Integer.MIN_VALUE);
return res;
} /**
* @param lastValue: temp最近添加的元素值
*/
public void findSubsequenceImpl(int[] nums, int index, int lastValue) {
if (index == nums.length) {
if (temp.size() >= 2) {
res.add(new ArrayList<>(temp));
} return;
} // 添加当前元素递归
if (nums[index] >= lastValue) {
temp.add(nums[index]);
findSubsequenceImpl(nums, index + 1, nums[index]);
temp.remove(temp.size() - 1);
} // 若当前值不等于最近添加的值,则不添加当前元素递归
if (nums[index] != lastValue) {
findSubsequenceImpl(nums, index + 1, lastValue);
} // 若当前值等于最近添加的值,则不进行递归返回
return;
}
}

LeeCode 131:分割回文串

题目描述

给你一个字符串 s ,请你将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。

建立模型

  1. 本题本质上是一个组合问题,且不会产生重复的解集
  2. 关键问题:判断回文串
  3. 构建记忆化搜索函数辅助判断回文串 isPalindrome(String s, int i, int j, int[][] dp)
  4. 定义递归函数 partitionImpl(String s, int index, int[][] dp)
  5. 确定递归终止条件 index == s.length()

代码实现

public class Solution {
List<List<String>> res = new ArrayList<>();
List<String> temp = new ArrayList<>(); public List<List<String>> partition(String s) {
int[][] dp = new int[s.length()][s.length()];
partitionImpl(s, 0, dp); return res;
} public void partitionImpl(String s, int index, int[][] dp) {
if (index == s.length()) {
res.add(new ArrayList<>(temp));
return;
} /**
* 尝试 [index, s.length() - 1] 中所有的回文串,并递归
*/
for (int i = index; i < s.length(); i++) {
if (isPalindrome(s, index, i, dp) == 1) {
temp.add(s.substring(index, i + 1));
partitionImpl(s, i + 1, dp);
temp.remove(temp.size() - 1);
}
}
} /**
* @param i: 起始位置
* @param j: 结束位置
* @return 1: 是回文串
* @return -1: 不是回文串
*/
public int isPalindrome(String s, int i, int j, int[][] dp) {
if (dp[i][j] != 0) {
return dp[i][j];
} if (i >= j) {
dp[i][j] = 1;
}
else if (s.charAt(i) == s.charAt(j)) {
dp[i][j] = dp[i + 1][j - 1];
}
else {
dp[i][j] = -1;
} return dp[i][j];
}
}

LeeCode 回溯问题的更多相关文章

  1. 算法题思路总结和leecode继续历程

    2018-05-03 刷了牛客网的题目:总结思路(总的思路跟数学一样就是化简和转化) 具体启发点: 1.对数据进行预处理排序的思想:比如8皇后问题 2.对一个数组元素进行比较的操作,如果复杂,可以试试 ...

  2. leecode系列--Two Sum

    学习这件事在任何时间都不能停下.准备坚持刷leecode来提高自己,也会把自己的解答过程记录下来,希望能进步. Two Sum Given an array of integers, return i ...

  3. N皇后问题—初级回溯

    N皇后问题,最基础的回溯问题之一,题意简单N*N的正方形格子上放置N个皇后,任意两个皇后不能出现在同一条直线或者斜线上,求不同N对应的解. 提要:N>13时,数量庞大,初级回溯只能保证在N< ...

  4. jQuery 2.0.3 源码分析 回溯魔法 end()和pushStack()

    了解了jQuery对DOM进行遍历背后的工作机制,可以在编写代码时有意识地避免一些不必要的重复操作,从而提升代码的性能 从这章开始慢慢插入jQuery内部一系列工具方法的实现 关于jQuery对象的包 ...

  5. linux中oops信息的调试及栈回溯【转】

    本文转载自:http://blog.csdn.net/kangear/article/details/8217329 ========================================= ...

  6. Java数据结构之回溯算法的递归应用迷宫的路径问题

    一.简介 回溯法的基本思想是:对一个包括有很多结点,每个结点有若干个搜索分支的问题,把原问题分解为对若干个子问题求解的算法.当搜索到某个结点.发现无法再继续搜索下去时,就让搜索过程回溯(即退回)到该结 ...

  7. 回溯 DFS 深度优先搜索[待更新]

      首先申明,本文根据微博博友 @JC向北 微博日志 整理得到,本文在这转载已经受作者授权!   1.概念   回溯算法 就是 如果这个节点不满足条件 (比如说已经被访问过了),就回到上一个节点尝试别 ...

  8. 46. Permutations 回溯算法

    https://leetcode.com/problems/permutations/ 求数列的所有排列组合.思路很清晰,将后面每一个元素依次同第一个元素交换,然后递归求接下来的(n-1)个元素的全排 ...

  9. Js杂谈-正则的测试与回溯次数

    例子来源于<精通正则表达式(第三版)>这本书,我贴出来: 这里的NFA是正则的一种引擎,书中介绍了一共三种引擎:NFA,DFA和POSIX NFA.像一般我们常用的.NET,java.ut ...

  10. SDUT 1400 马的走法(回溯法)

    题目链接: 传送门 马的走法 Time Limit: 1000MS     Memory Limit: 65536K 题目描述 在一个4*5的棋盘上,马的初始位置坐标(纵 横)位置由键盘输入,求马能返 ...

随机推荐

  1. AES加密 php7版本 openssl_encrypt 遇到的坑

    与前端对接api ,解密不了前端加密的数据. 问题描述: 1.前端用 cryptojs  加密的 密钥是24位 , 2.后端用的php7的 openssl_encrypt  同密钥来进行解密,发现解密 ...

  2. egret tween 屏幕震动动画 ts

    let orig = { x: this.x, y: this.y }; var dir = 1; var tox = 0; var toy = 0; var count = 40; // if (n ...

  3. 深入理解Java内存(图解)

    这篇文章是转自http://blog.csdn.net/shimiso/article/details/8595564博文. 本文分析基于jdk1.8 进入正题前首先要知道的是Java程序运行在JVM ...

  4. 树莓派 IIC功能安装及测试

    参考连接: https://blog.csdn.net/panwen1111/article/details/81044428 https://blog.csdn.net/xukai871105/ar ...

  5. EurekaServer高可用搭建

    生产环境中需要搭建集群达到高可用.eurekaServer每个实例可以注册到其他一个或多个eurekaServer实例中达到高可用.配置比较简单 比如: application-master.prop ...

  6. java 线程池 带返回值

    import java.util.concurrent.ExecutionException;import java.util.concurrent.ExecutorService;import ja ...

  7. Chrome(谷歌)浏览器永久关闭恢复页面提示框(记录)

    使用脚本调用Chrome浏览器启动指定页面,然后代码里的命令关闭,会被浏览器识别为非正常关闭. 再次执行脚本的时候会停留在空白页面,无法进入指定页面,设置为主页也无法进入. 排查可能是浏览器自动恢复页 ...

  8. 转载C#加密方法

    方法一:    //须添加对System.Web的引用     using System.Web.Security;          ...          /// <summary> ...

  9. ideal中热部署JRebal的设置

    1.ideal中安装插件: 2.打开网址:https://www.guidgen.com/   打开链接获取新的GUID码 3.网址和UUID码拼接:http://127.0.0.1:8888/ca3 ...

  10. STM32 系统初始化

    #include "system.h" void system_init(void){ //系统中断设置,抢占优先级0~15,无子优先级 NVIC_PriorityGroupCon ...