回溯探索第二章!(LeetCode刷题!!!)
跟着carl哥的第X+1天
x天是因为在医院躺了快三周,回到学校开始学习!!!
缺的会开始补回来的,我向天发誓
回溯算法基础模板
void backtracking(参数){
//终止条件一般是当一个元素符合条件以后,将该元素存入结果集 然后返回到上一层再去向下遍历其他元素
if(终止条件){
存放结果;
return;
}
//for循环就是用来遍历数组的每一个元素(横向遍历)
for(选择:本层集合中元素(树中结点孩子的数量就是集合的大小)){
处理节点;
//递归向下遍历 在终止条件中判断是否符合条件
backtracking(路径,选择列表); //递归
//递归之后返回上一层后,再进行回溯处理,把原有下一层的元素去除 重新遍历该层中的其他元素
//这便是回溯法
回溯,撤销处理结果
}
}
回溯思路(回溯三部曲):
-
第一步:确定好递归后返回的参数(一般采用全局变量,不需要在回溯方法中传太多参数)
-
第二步:就是确定好回溯法的终止条件 (在这道题是存储单一答案的数组大小 == k值)
-
第三步:单层搜索的过程 (每一层我们应该怎么写 只看一层就不会乱 )
其实回溯法就是暴力搜索,然后运用递归实现n个for循环。横向遍历是数组宽度,纵向遍历是递归。
回溯算法第二层【组合问题+分割问题】
开头第一只远程哥布林:leetcode39. 组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
- 所有数字(包括 target)都是正整数。
- 解集不能包含重复的组合。
示例1:
- 输入:candidates = [2,3,6,7], target = 7
- 输出:[ [7], [2,2,3] ]
示例2:
- 输入:candidates = [2,3,5], target = 8
- 输出: [ [2,2,2,2], [2,3,3], [3,5] ]
思路(回溯三部曲):
因为candidates中的元素可以重复,所以idx要从本位置开始,不能从下一位开始,所以backTracking()中的i不能加1。 又因为从本元素开始,需要剪枝,防止时间复杂度过高。 然后定义一个sum来判断是否符合条件。
答案:::
public class Test01 {
static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
public void main(String[] args) throws IOException {
//读入一行,并按照空格拆分
String[] inStr = in.readLine().split(",");
int[] candidates = new int[inStr.length];
for (int i = 0; i < inStr.length; i ++) {
candidates[i] = Integer.parseInt(inStr[i]);
}
Integer target = Integer.valueOf(in.readLine());
List<List<Integer>> result = combinationSum(candidates, target);
out.write(result.toString());
out.flush();
out.close();
}
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(candidates);
if (candidates[0] > target) {
return res;
}
backtracking(res, new ArrayList<Integer>(), candidates, target, 0, 0);
return res;
}
public void backtracking(List<List<Integer>> res, List<Integer> path, int[] candidates, Integer target, int sum, int idx) {
if (sum > target) {
return;
}
if (sum == target) {
res.add(new ArrayList<Integer>(path));
return;
}
for (int i = idx; i < candidates.length && sum + candidates[i] <= target; i ++) {
path.add(candidates[i]);
sum += candidates[i];
backtracking(res, path, candidates, target, sum, i);
sum -= candidates[i];
path.remove(path.size() - 1);
}
}
}
又一只远程哥布林:leetcode40.组合总和II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明: 所有数字(包括目标数)都是正整数。 解集不能包含重复的组合。
示例1 :
输入: candidates = [10,1,2,7,6,1,5], target = 8
输出: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ]
示例2 :
输入: candidates = [2,5,2,1,2], target = 5
输出: [[1,2,2],[5]]
思路:这道题需要注意的就是去重,就是每一层上不能使用相同的元素,所以我们需要重新定义一个跟candidates大小相同的数组,用来记录candidates在该位置上是否有使用该元素
答案:
public class Test02 {
static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
static List<List<Integer>> res = new ArrayList<>();
static List<Integer> path = new ArrayList<>();
static boolean[] used;
static int sum = 0;
public static void main(String[] args) throws IOException {
//读入一行,并按照空格拆分
String[] inStr = in.readLine().split(",");
int[] candidates = new int[inStr.length];
for (int i = 0; i < inStr.length; i ++) {
candidates[i] = Integer.parseInt(inStr[i]);
}
Integer target = Integer.valueOf(in.readLine());
List<List<Integer>> result = combinationSum2(candidates, target);
out.write(result.toString());
out.flush();
out.close();
}
public static List<List<Integer>> combinationSum2(int[] candidates, int target) {
used = new boolean[candidates.length];
//加标志数组,用来辅助判断同层节点是否已经遍历
Arrays.fill(used, false);
//去重需要先排序,意在把相同的值放在一起
Arrays.sort(candidates);
backtracking(candidates, target, 0);
return res;
}
public static void backtracking(int[] candidates, Integer target, int idx) {
if (sum > target) {
return;
}
if (sum == target) {
res.add(new ArrayList<>(path));
return;
}
for (int i = idx; i < candidates.length && sum + candidates[i] <= target; i ++) {
// candidates[i] == candidates[i - 1]表示元素不等于同树层前一元素 (横向)
// 因为纵向也有 candidates[i] == candidates[i - 1],故我们需要一个used数组 用来记录同一层每一元素的使用情况
// 当 used[i - 1] == true 时,(纵向)可以使用,因为是不同层
// (横向)当到i时 由于used[i-1]等于false 且candidates[i] == candidates[i - 1] 说明现在的i位置元素已经是相同层的重复元素,可以跳过
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
continue;
}
path.add(candidates[i]);
sum += candidates[i];
used[i] = true;
backtracking(candidates, target, i + 1);
used[i] = false;
sum -= candidates[i];
path.remove(path.size() - 1);
}
}
}
Carl哥设计的used方法很巧妙,你首先要把回溯过程看成一棵树,试着理解
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false)
- 横向看:当到i时 由于used[i-1]等于false 且candidates[i] == candidates[i - 1] 说明现在的i位置元素已经是相同层的重复元素,可以跳过
- 纵向:因为used[i - 1] = true 表示的是 上一层的时候即使 用过相同元素,也无所谓,因为是在不同层
一只等级高一点的远程哥布林:leetcode131.分割回文串
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例 1:
输入:"aab"
输出:[ ["aa","b"], ["a","a","b"] ]
思路:
- 回文的意思就是 回文文回
- 其次你要怎么去切割字符串
- 最后你该怎么判断是否是回文
答案:
public class Test03 {
static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
static List<List<String>> res = new ArrayList<>();
static Deque<String> deque = new LinkedList<>();
public static void main(String[] args) throws IOException {
//读入一行,并按照空格拆分
String str = in.readLine();
List<List<String>> result = partition(str);
out.write(result.toString());
out.flush();
out.close();
}
private static List<List<String>> partition(String s) {
backtracking(s, 0);
return res;
}
public static void backtracking(String s, int startIndex) {
if (startIndex >= s.length()) {
res.add(new ArrayList<>(deque));
}
//aab
for (int i = startIndex; i < s.length(); i ++) {
if (isPalindrome(s, startIndex, i)) {
//substring是左闭右开 所以需要i+1
String str = s.substring(startIndex, i + 1);
deque.add(str);
} else {
continue;
}
backtracking(s, i + 1);
deque.removeLast();
}
}
//判断区间内的字符是不是回文子串(startIndex=0, i = 5)
private static boolean isPalindrome(String s, int startIndex, int i) {
int n = i - startIndex + 1;
char start = s.charAt(startIndex);
char end = s.charAt(i);
for (int j = 0; j < n / 2 + 1; j ++) {
if (start == end) {
start = s.charAt(startIndex ++);
end = s.charAt(i --);
} else {
return false;
}
}
return true;
}
}
第一只小Boss火焰哥布林:leetcode93.复原IP地址
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效的 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效的 IP 地址。
实例1:
- 输入:s = “25525511135”
- 输出:[“255.255.11.135”,“255.255.111.35”]
实例2:
- 输入:s = “0000”
- 输出:[“0.0.0.0”]
示例 3:
- 输入:s = “1111”
- 输出:[“1.1.1.1”]
示例 4:
- 输入:s = “010010”
- 输出:[“0.10.0.10”,“0.100.1.0”]
示例 5:
- 输入:s = “101023”
- 输出:[“1.0.10.23”,“1.0.102.3”,“10.1.0.23”,“10.10.2.3”,“101.0.2.3”]
提示:
0 <= s.length <= 3000
s 仅由数字组成
思路:
主要还是回溯三部曲
- 返回值
- 终止条件 因为是IP地址,所以我们可以去判断 . 的个数
- 单层搜索逻辑 如果回溯你要怎么处理 .
- 判断IP地址中的每一个 字串是否合法(不要用Integer.parseInt()去直接将字符串转换为数字 因为00会被判断成0)
答案
public class Test04 {
static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
static List<String> res = new ArrayList<>();
static StringBuffer path = new StringBuffer();
public static void main(String[] args) throws IOException {
//读入一行,并按照空格拆分
String s = in.readLine();
List<String> result = restoreIpAddresses(s);
out.write(result.toString());
out.flush();
out.close();
}
private static List<String> restoreIpAddresses(String s) {
if (s.length() > 12) {
return res;
}
backtracking(s, 0, 0);
return res;
}
/**
*
* 回溯三部曲:返回值 终止条件 单层搜索逻辑
*/
public static void backtracking(String s, int startIndex, int pointNum) {
//终止条件:逗点数量为3时,分割结束
if (pointNum == 3) {
// 判断第四段子字符是否合法,如果合法就放进result中
if (isValid(s, startIndex, s.length() - 1)) {
res.add(s);
}
return;
}
//aab
for (int i = startIndex; i < s.length(); i ++) {
if (isValid(s, startIndex, i)) {
//path.append(s.substring(0, i + 1) + "." + s.substring(i + 1));
s = s.substring(0, i + 1) + "." + s.substring(i + 1);
pointNum ++;
//插入逗点之后下一个子串的起始位置为i+2
backtracking(s, i + 2, pointNum);
//回溯
pointNum --;
//回溯删掉逗点
s = s.substring(0, i + 1) + s.substring(i + 2);
} else {
break;
}
}
}
//判断区间内的字符是不是IP地址(startIndex=0, i = 5)
//如果只有一个字符且为0 则直接返回true
//如果三个字符 则需要小于等于255
//如果超过三个字符则返回 false
private static boolean isValid(String s, int start, int end) {
if (start > end) {
return false;
}
if (s.charAt(start) == '0' && start != end) {
return false;
}
int num = 0;
for (int i = start; i <= end; i ++) {
if (s.charAt(i) > '9' || s.charAt(i) < '0') {
return false;
}
num = num * 10 + (s.charAt(i) - '0');
if (num > 255) {
return false;
}
}
return true;
}
}
自我总结
回溯好难啊 一道题做2小时还是错的 虽然想法对了 但是实现一直错误。。
少年仍需努力
朋友仍需努力 一起加油
兄弟萌冲啊!
大概就是这样,大家懂了吗 有什么不懂的评论区评论或者私信我吧!!