回溯法
当遇到组合问题的时候,要想到回溯法。
17. 电话号码的字母组合
class Solution {
List<String> res = new ArrayList<>();
// 储存隐射关系:这里要用Character
Map<Character,String> map = new HashMap<>();
public List<String> letterCombinations(String digits) {
/**
分析:
组合问题要想到回溯算法
第一次思考:如何储存隐射关系??处理隐射关系肯定是想到哈希表,但是哈希表只能多对一隐射,现在,一个数字对应多个字母,按道理是不能隐射的。那么只能一个数字隐射一个字符串来处理了。
上面思考结束后,根据下标去遍历字符串中的字母,如果使用stringbulider,那么就要回朔。如果是使用string,那么直接dfs
*/
if(digits.equals("")){
return res;
}
map.put('2',"abc");
map.put('3',"def");
map.put('4',"ghi");
map.put('5',"jkl");
map.put('6',"mno");
map.put('7',"pqrs");
map.put('8',"tuv");
map.put('9',"wxyz");
findCombinstion(digits,0,new StringBuilder());
return res;
}
public void findCombinstion(String digits,int index,StringBuilder sb){
if(index == digits.length()){
// 下标到达终点,添加字符串
res.add(sb.toString());
return ;
}
// 取字符
char c = digits.charAt(index);
// 取字符对应的字符串
char[] letter = map.get(c).toCharArray();
for(char x : letter){
// 遍历字符串 添加字符
sb.append(x);
// 递归
findCombinstion(digits,index+1,sb);
// 回朔
sb.deleteCharAt(sb.length() - 1);
}
}
}
若使用string写法,String拼接的时候String每次都新建一个StringBuilder对象,再转为String,相当于递归方法里面每次都新建一个新的String对象吧,方法外面的String一直没变。StringBuilder一直都是一个对象所以要先append,再还原(delete)
class Solution {
Map<Character,String> map = new HashMap<>();
ArrayList<String> res = new ArrayList<>();
public List<String> letterCombinations(String digits) {
if(digits.equals(""))
return res;
map.put('2',"abc");
map.put('3',"def");
map.put('4',"ghi");
map.put('5',"jkl");
map.put('6',"mno");
map.put('7',"pqrs");
map.put('8',"tuv");
map.put('9',"wxyz");
// 使用string
findCombination(digits, 0, "");
return res;
}
private void findCombination(String digits, int index, String s){
if(index == digits.length()){
res.add(s);
return;
}
Character c = digits.charAt(index);
String letters = map.get(c);
// String拼接的时候String每次都新建一个StringBuilder对象,再转为String,相当于递归方法里面每次都新建一个新的String对象吧,方法外面的String一直没变。StringBuilder一直都是一个对象所以要先append,再还原(delete)
for(int i = 0 ; i < letters.length() ; i ++){
findCombination(digits, index+1, s + letters.charAt(i));
}
return;
}
}
22. 括号生成
class Solution {
// https://leetcode-cn.com/problems/generate-parentheses/solution/shou-hua-tu-jie-gua-hao-sheng-cheng-hui-su-suan-fa/
List<String> res = new ArrayList<>();
public List<String> generateParenthesis(int n) {
/**
分析:
按道理来说组合问题是需要进行回溯操作的,但是本题看不到回溯的影子,为什么呢?因为java中string的特殊性!可以看下第17题的String解法题解。
总体思路是:左括号n个,右括号n个,那么如果有左括号,就优先拼接左括号,如果剩余右括号多余剩余左括号,那么可以拼接右括号。
在使用String的时候,内部是使用StringBuilder类,实际上已经帮我们做了回溯哦了!
*/
dfs(n, n, "");
return res;
}
private void dfs(int left, int right, String curStr) {
if (left == 0 && right == 0) { // 左右括号都不剩余了,递归终止
res.add(curStr);
return;
}
if (left > 0) { // 如果左括号还剩余的话,可以拼接左括号
dfs(left - 1, right, curStr + "(");
}
if (right > left) { // 如果右括号剩余多于左括号剩余的话,可以拼接右括号
dfs(left, right - 1, curStr + ")");
}
}
}
39. 组合总和
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
/**
分析:
组合问题一般都是需要回溯的,像这种target目标值问题,一般都是使用减法回溯模式
*/
if(candidates == null || candidates.length == 0){
return res;
}
dfs(candidates,target,0);
return res;
}
public void dfs(int[] candidates, int target,int index){
if(target == 0){
// 这里一定要使用new 因为path是一个全局变量,一直在变化
res.add(new ArrayList<>(path));
return;
}
if(target < 0){
return;
}
// 不断的遍历index,同时在遍历中去去递归探寻能不能找到target == 0 的数字组合出来
for(int i = index; i < candidates.length; i++ ){
path.add(candidates[i]);
dfs(candidates,target - candidates[i],i);
// 在探寻结束后,要回朔,删除最后一个节点
path.remove(path.size() - 1);
}
}
}
40. 组合总和 II
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
boolean[] used;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
/**
套用上一题代码后,将index 改成了 i+1,那么如何去重呢?先给数组排序,然后在遍历中就去continue去重
针对重复数字的组合数问题,个人觉得还是使用used数组去重比较好
*/
if(candidates == null || candidates.length == 0){
return res;
}
Arrays.sort(candidates);
this.used = new boolean[candidates.length];
dfs(candidates,target,0);
return res;
}
public void dfs(int[] candidates,int target,int index){
if(target == 0){
res.add(new ArrayList<>(path));
return;
}
if(target < 0){
return;
}
for(int i = index; i < candidates.length; i++){
// 去重操作
// 首先 i 必须比index大,其次判断前后两个值是否一致
// 实际上这里的 i > index等价于 i > 0 && !used[i-1] ( 同一树层找到了重复数字)
// if( i > index && candidates[i] == candidates[i - 1]){
// continue;
// }
// 同一树支下 是深度递归
// 同一树层中 会和同一树支下深度递归重复了,所以剪枝
if( i > 0 && candidates[i] == candidates[i - 1] && !used[i-1]){
continue;
}
path.add(candidates[i]);
// 标记访问
used[i] = true;
dfs(candidates,target - candidates[i],i+1);
path.remove(path.size() - 1);
// 回溯后取消标记
used[i] = false;
}
}
}
46. 全排列
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
// 很简单的回溯问题,心得体会:涉及重复问题要想办法进行剪枝去重
if(nums == null || nums.length == 0){
return res;
}
// 这里只传了nums参数
dfs(nums);
return res;
}
public void dfs(int[] nums){
// 根据题意,添加到res应该是全排列
if(path.size() == nums.length){
res.add(new ArrayList<>(path));
return;
}
// 遍历从0开始,然后想办法去重
for(int i = 0; i< nums.length; i++){
// 进行剪枝去重
if(path.contains(nums[i])){
continue;
}
path.add(nums[i]);
dfs(nums);
path.remove(path.size() - 1);
}
}
}
47. 全排列 II
本题有点难理解,理解剪枝操作是重点,像这种一般先排序,后剪枝。难点就是上一层数字used回溯后对下一层数字的影响!
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
// 定义used数组,判定该数字是否被使用过了
boolean[]used;
public List<List<Integer>> permuteUnique(int[] nums) {
if(nums == null || nums.length == 0){
return res;
}
this.used = new boolean[nums.length];
// 排序
Arrays.sort(nums);
dfs(nums);
return res;
}
public void dfs(int[]nums){
if(path.size() == nums.length){
res.add(new ArrayList<>(path));
return;
}
for(int i = 0; i < nums.length; i++){
// 如果该数字被使用过 跳过循环
if(used[i]){
continue;
}
// !used[i- 1]表示 上一轮的数字被撤销了
// 上面那句话比较难理解,为什么就表示上一轮的数字被撤销了?按道理说used[i-1]应该是true,被访问过了,如果是fasle,那么必然说明这个数字已经被撤销了
// 那么为什么 上一轮数字被撤销了,就跳过循环?举个例子:1” 1 2 和 1 1” 2,如果1”是上一轮被撤销的,那么就代表该数后面可以继续使用,但是实际上,上一层已经有该全排列,如:1” 1 2
if( i > 0 && nums[i] == nums[ i - 1] && !used[i- 1]){
continue;
}
path.add(nums[i]);
// 标记该数字已经使用
used[i] = true;
dfs(nums);
path.remove(path.size() - 1);
// 回朔后 该数字又可以重新被使用
used[i] = false;
}
}
}
77. 组合
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
dfs(n,k,1);
return res;
}
public void dfs(int n,int k,int index){
if(path.size() == k){
res.add(new ArrayList<>(path));
return;
}
for(int i = index; i <= n; i++){
// 这样会有重复数字出现
// 使用dfs应该使用一个index参数,每次进行叠加,从而不会用重复参数
// if(path.contains(i)) continue;
path.add(i);
// 这里使用的参数是 i + 1,每次都取后面的值(保证了不会重复取值)
dfs(n,k,i+1);
path.remove(path.size() - 1);
}
}
}
78. 子集
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
dfs(nums,0);
return res;
}
public void dfs(int[] nums,int index){
// 本题最奇葩的是没有递归出口 直接就是add操作
res.add(new ArrayList<>(path));
for(int i = index; i < nums.length; i++){
path.add(nums[i]);
// 参数是 i + 1,每次都向后添加一个
dfs(nums,i+1);
path.remove(path.size() - 1);
}
}
}
90. 子集 II
总结:组合问题的重复元素去重思路是去使用used数组去标记
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
boolean[]used;
public List<List<Integer>> subsetsWithDup(int[] nums) {
// 排序是为了去重
Arrays.sort(nums);
this.used = new boolean[nums.length];
dfs(nums,0);
return res;
}
public void dfs(int[] nums,int index){
// 添加路径
res.add(new ArrayList<>(path));
// 遍历树的宽度,重点理解 树的循环遍历和递归遍历
for(int i = index; i < nums.length; i++){
// 对于同一树层 后面的数字重复了,那么就可以直接剪枝
// 对于同一树支 后面的数字重复了 并不影响
// 这里的 i > index 等价于 i > 0 && !used[i-1]
// if( i > index && nums[i] == nums[i - 1]) continue;
if( i > 0 && nums[i] == nums[i - 1] && !used[i-1]) continue;
path.add(nums[i]);
used[i] = true;
// 移动下标
dfs(nums,i+1);
// 回溯
path.remove(path.size() - 1);
used[i] = false;
}
}
}
93. 复原 IP 地址
这题好难,我现在还没有理解到位
class Solution {
List<String> res = new ArrayList<>();
List<String> path = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
//这里就是对字符串的预处理,但是对于测试用例来说我觉得用处不大,毕竟不会蠢到用13位数字让你分割
if(s.length()<4 || s.length()>12){
return res;
}
//这里就是套用最经典的回溯模板了,相比于分割字符串只加入分割线一个参数以外,这里还需要添加额外的层数参数level
//因为合法的IP地址只有四段,我们不能无限对其进行分割
backtracking(s,0,0);
return res;
}
// 判断分割出来的每一段字符串是否是合法的IP地址
boolean isValidIp(String s){
//判断其是否含有前导0
if(s.charAt(0)=='0' && s.length()>1){
return false;
}
//长度为4就直接舍弃,加上这一步是为了后面parseInt做准备,防止超过了Integer可以表示的整数范围
if(s.length()>3){
return false;
}
//将字符转为int判断是否大于255,因为题目明确说了只由数字组成,所以这里没有对非数字的字符进行判断
if(Integer.parseInt(s)>255){
return false;
}
return true;
}
void backtracking(String s,int splitIndex,int level){
//递归终止条件,分割的四个字符串都是合法的IP地址
if(level==4){
//在代码的最后再利用join函数加上“.”,构造IP地址的表示形式
res.add(String.join(".",path));
return;
}
for(int i=splitIndex;i<s.length();i++){
//每一次分割之后,对剩余字符长度是否合理进行判断,剪枝操作,优化运行速度
if((s.length()-i-1) > 3*(3-level)){
continue;
}
//如果分割的字符串不是合理的IP地址,跳过
if(! isValidIp(s.substring(splitIndex,i+1))){
continue;
}
//把合法的IP地址段加入path存储
path.add(s.substring(splitIndex,i+1));
//每次把分割线往后移一位,且段数level+1
backtracking(s,i+1,level+1);
//进行回溯操作
path.remove(path.size()-1);
}
}
}
79. 单词搜索
参考视频:
https://www.bilibili.com/video/BV1zA411J7hY?from=search&seid=3466956450785063587&spm_id_from=333.337.0.0
class Solution {
boolean[][] used;
char[][]board;
String word;
int m;
int n;
public boolean exist(char[][] board, String word) {
// 把所有信息设置为全局变量
this.m = board.length;
this.n = board[0].length;
this.used = new boolean[m][n];
this.board = board;
this.word = word;
// 网格中的每一个字母 都有可能是word的第一个字母
for(int row = 0; row < m; row++){
for(int col = 0; col < n; col++){
// 发现网格中的字母 等于word的第一个字母,就开始按照规则搜索
if(board[row][col] == word.charAt(0)){
// dfs传入参数为 下标参数和word的index下标
boolean res = dfs(row,col,0);
// res 为搜索结果
if(res) return true;
}
}
}
// 整个网格都没有搜索到 就返回false
return false;
}
public boolean dfs(int row,int col,int index){
// 递归出口
if(index >= word.length()){
// 下标超过了 word长度
return true;
}
// 剪枝
if(row < 0 || row >= m || col < 0 || col >= n || used[row][col]){
// 越界了就不必要搜索 已经被使用过了就不必要搜索
return false;
}
// 传入的网格的字母和index位置的字母不一致
if(board[row][col] != word.charAt(index)){
return false;
}
// 设置该位置已经被访问了
used[row][col] = true;
// 搜索:上下左右,同时下标+1
boolean res = dfs(row+1,col,index+1) || dfs(row-1,col,index+1) || dfs(row,col+1,index+1) || dfs(row,col-1,index+1);
// 回溯后 该位置可以继续被使用
used[row][col] = false;
return res;
}
}
131. 分割回文串
本题每次都要判断是否回文,可以使用动态规划进行优化
class Solution {
List<List<String>> res = new ArrayList<>();
List<String> path = new ArrayList<>();
public List<List<String>> partition(String s) {
/**
分析:
这是一道典型的回溯树。横向循环遍历,纵向递归遍历。
第一次写的时候,递归条件判断出来,但是for循环(横向扩展)和递归遍历的逻辑(纵向扩展)没有想出来,这个是要利用字符串的substring api方法
*/
dfs(s,0);
return res;
}
public void dfs(String s,int index){
if( index == s.length()){
res.add(new ArrayList<>(path));
return;
}
for(int i = index; i < s.length(); i++){
// 定义横向的末尾指针,用于截取s
int endIndex = i;
// 截取的不是回文串,直接跳过 这里的区间是[index,endIndex]
if( !check(s,index,endIndex)) continue;
// 截取字符串,在截取之前就得先判断截取的字符串是不是回文串
// 这里的区间是[index,endIndex+1)
path.add(s.substring(index,endIndex+1));
dfs(s,i+1);
// 回溯
path.remove(path.size() - 1);
}
}
public boolean check(String s,int index,int endIndex){
while(index < endIndex){
if(s.charAt(index) != s.charAt(endIndex)) return false;
index++;
endIndex--;
}
return true;
}
}
401. 二进制手表
class Solution {
// 直接把二进制的问题转换为数组累加问题
int[] hours = new int[]{1,2,4,8,0,0,0,0,0,0};
int[] minutes = new int[]{0,0,0,0,1,2,4,8,16,32};
List<String> res = new ArrayList<>();
public List<String> readBinaryWatch(int turnedOn) {
// 方法二:采用回溯法
backTrack(turnedOn,0,0,0);
return res;
}
// 回溯的参数:num(需要点亮的灯,初始为turnedOn),index(点亮的下标)
// hour(小时数)minute(分钟数)
public void backTrack(int num,int index,int hour,int minute){
// 剪枝操作
if(hour > 11 || minute > 59){
return;
}
// 递归出口,当点亮到第0栈灯的时候,那么回溯所有数据
if(num == 0){
// 进行字符串拼接
StringBuilder sb = new StringBuilder();
sb.append(hour).append(':');
if(minute < 10){
sb.append('0');
}
sb.append(minute);
res.add(sb.toString());
//记得return终止掉
return;
}
// 这里是从下标开始递归遍历
for(int i = index; i < 10; i++){
backTrack(num - 1, i+1,hour+hours[i],minute+minutes[i]);
}
}
}
// 第一次接触回溯思想的题目(其实就是暴力遍历+剪枝)
// 方法一:调用Integer.bitCount() ->计算数字转换为二进制为1的个数
// 时钟限定在[0,11],分钟限定在[0,59],暴力循环
//定义结果集
// List<String> res = new LinkedList<>();
// for(int i = 0; i < 12; i++){
// for(int j = 0; j < 60; j++){
// // 调用Integer.bitCount()
// if(Integer.bitCount(i)+Integer.bitCount(j)==turnedOn){
// // 拼接、修改string ->StringBuilder()
// StringBuilder sb = new StringBuilder();
// // StringBuilder 遵循链式编程
// sb.append(i).append(":");
// // 若分钟数小于10,那么需要在前面拼接一个0
// if(j < 10){
// sb.append("0");
// }
// sb.append(j);
// res.add(sb.toString());
// }
// }
// }
// return res;
// }