回溯算法
常见的一些问题都是用回溯算法去解答的,例如:八皇后、0-1背包、字符串匹配;感觉回溯算法就是去遍历每种情况的可能性。
纯暴力搜索,可以解决的问题:
- 组合问题:n个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按照一定规则有几种切割方式
- 子集:一个n个数的集合里有多少符合条件的子集
- 排列:n个数按照一定规则全排列,有几种排列方式
- 棋盘问题:n皇后、数独
如果把子集问题、组合问题、分割问题都抽象成一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找数据所有节点;
回溯模板
void backtracking(参数) {
if (终⽌条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩⼦的数量就是集合的⼤⼩)) {
处理节点;
backtracking(路径,选择列表);//递归
回溯,撤销处理结果
}
}
其中for循环为横向遍历,递归纵向遍历,回溯不断调整结果集
算法题目
1. 二进制手表
二进制手表顶部有 4 个 LED 代表 小时(0-11),底部的 6 个 LED 代表 分钟(0-59)。每个 LED 代表一个 0 或 1,最低位在右侧。
思路:需要去遍历各个情况,Integer.bitCount(i)实现的功能是计算一个(byte,short,char,int统一按照int方法计算)int,long类型的数值在二进制下“1”的数量。
class Solution {
public List<String> readBinaryWatch(int turnedOn) {
ArrayList<String> list = new ArrayList<String>();
for(int h = 0;h<12;h++){//0-11小时
for(int m = 0;m<60;m++){//0-59秒 枚举
if(Integer.bitCount(h)+Integer.bitCount(m)==turnedOn){
list.add(h+":"+((m<10)? "0":"")+m);
}
}
}
return list;
}
}
2. 找出所有自己的异或和再求和
一个数组的 异或总和 定义为数组中所有元素按位 XOR 的结果;如果数组为 空 ,则异或总和为 0 。
例如,数组 [2,5,6] 的 异或总和 为 2 XOR 5 XOR 6 = 1 。
给你一个数组 nums ,请你求出 nums 中每个 子集 的 异或总和 ,计算并返回这些值相加之 和 。
注意:在本题中,元素 相同 的不同子集应 多次 计数。
数组 a 是数组 b 的一个 子集 的前提条件是:从 b 删除几个(也可能不删除)元素能够得到 a 。
思路:遍历nums中的每个数,每个数都有两种可能,或者取或者不取,最终将所有结果求和,是0-1背包问题的简化
class Solution {
int sum=0;int res=0;
public int subsetXORSum(int[] nums) {
yihuo(nums,0,sum);
return res;
}
public void yihuo(int[]nums,int i,int sum){
if(i==nums.length){
res+=sum;
return;
}
yihuo(nums,i+1,sum);
sum = sum^nums[i];
yihuo(nums,i+1,sum);
}
}
子集问题
3.子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
思路:感觉就是在遍历所有的可能出现的情况
class Solution {
List<List<Integer>> list = new ArrayList<List<Integer>>();
List<Integer> li = new ArrayList<Integer>();
public List<List<Integer>> subsets(int[] nums) {
backtracking(nums,0);
return list;
}
public void backtracking(int[] nums,int start){
list.add(new ArrayList<Integer>(li));//收割结果的位置,需要收割各个节点的值,而不是叶子节点的值
if(start>=nums.length){
return;
}
for(int i =start;i<nums.length;i++){
li.add(nums[i]);
backtracking(nums,i+1);
li.remove(li.size()-1);
}
}
}
组合问题
4. 组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
思路:
lass Solution {
List<List<Integer>> list = new ArrayList<List<Integer>>();
List<Integer> li = new ArrayList<Integer>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n,k,0);
return list;
}
public void backtracking(int n,int k,int start){
if(li.size() == k){
//收割结果
list.add(new ArrayList<Integer>(li));
return;
}
//单层处理逻辑
for(int i = start;i<n;i++){
li.add(i+1);
backtracking(n,k,i+1);
li.remove(li.size()-1);
}
}
}
5. 组合总和III
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
思路:组合问题的延伸,上一题基础上增加了一个判断条件
class Solution {
List<List<Integer>> list = new ArrayList<List<Integer>>();
List<Integer> li = new ArrayList<Integer>();
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(k,n,0,0);
return list;
}
public void backtracking(int k,int n,int start,int sum){
if(li.size()==k){
if(sum == n){
list.add(new ArrayList<Integer>(li));
}
return;
}
//单层处理逻辑
for(int i = start;i<9;i++){
li.add(i+1);
sum += i+1;
backtracking(k,n,i+1,sum);
sum = sum -i-1;
li.remove(li.size()-1);
}
}
}
6. 组合总和
给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。
candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是唯一的。
对于给定的输入,保证和为 target 的唯一组合数少于 150 个。
思路:本题中的结束条件和之前有所不同,同时注意for循环开始的条件,同时这几个题都没有用到剪枝,如果考虑效率问题,可以添加上剪枝
class Solution {
List<List<Integer>> list = new ArrayList<List<Integer>>();
List<Integer> li = new ArrayList<Integer>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtracking(candidates,target,0,0);
return list;
}
public void backtracking(int[] candidates, int target,int sum,int start){
if(sum == target){
//获取结果
list.add(new ArrayList<Integer>(li));
return;
}
if(sum>target){
return;
}
//单层循环
for(int i=start;i<candidates.length;i++){
li.add(candidates[i]);
sum +=candidates[i];
backtracking(candidates,target,sum,i);
sum -= candidates[i];
li.remove(li.size()-1);
}
}
}
7.组合总和II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
注意:解集不能包含重复的组合。
思路:此题需要注意,candidates内会有重复元素,但是只能取一次,要做到同一层只能取一次但是每一个分支上的可以取
对应题目
代码随想录,回溯算法精讲v1.2.pdf
分割问题
8. 分割回文串
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]
思路:需要注意终止条件、以及每一层中如何去选择
class Solution {
List<List<String>> list = new ArrayList<List<String>>();
List<String> li = new ArrayList<String>();
public List<List<String>> partition(String s) {
backtracking(s,0);
return list;
}
public void backtracking(String s,int start){
if(start>=s.length()){
list.add(new ArrayList<String>(li));
return;
}
for(int i =start;i<s.length();i++){
if(ishuiwen(s,start,i)){
String ss = s.substring(start,i+1);
li.add(ss);
backtracking(s,i+1);
li.remove(li.size()-1);
}else{
continue;
}
}
}
public boolean ishuiwen(String s,int l,int r){
boolean flag = true;
while(l<r){
if(s.charAt(l)!=s.charAt(r)){
flag = false;
break;
}
l++;
r--;
}
return flag;
}
}
9.复原ip地址
给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 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 地址。
思路:需要在每层循环中判断是否满足条件,将满足条件的数保存,同时如果不满足条件,则直接break;
class Solution {
List<String> list = new ArrayList<String>();
List<String> li = new ArrayList<String>();
public List<String> restoreIpAddresses(String s) {
backtracking(s,0);
return list;
}
public void backtracking(String s,int start){
if(li.size()==4){
if(start>=s.length()){
//收割结果
list.add(zhunhuan(li));
return;
}else{
return;
}
}
for(int i =start;i<s.length()&&li.size()<4;i++){
if(isLegal(s,start,i)){
String str = s.substring(start,i+1);
li.add(str);
backtracking(s,i+1);
li.remove(li.size()-1);
}else{
//添加条件
break;
//continue;
}
}
}
public boolean isLegal(String s,int l,int r){
if(l==r){//只有一位,即使是0也无所谓
return true;
}else{
if(s.charAt(l)=='0'){//大于一位的,如果0开头的就排除
return false;
}else{//判断是否小于等于255
int num =0;
for(int i = l;i<=r;i++){
int shuzi = Character.getNumericValue(s.charAt(i));
num = num *10+shuzi;
}
if(num<=255){
return true;
}else{
return false;
}
}
}
}
public String zhunhuan(List<String> list){
String str = "";
for(int i =0;i<list.size();i++){
str =str+list.get(i)+".";
}
return str.substring(0,str.length()-1);
}
}
排序问题
10.全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
思路:
每层都需要从0开始搜索,而不是从start开始搜索
需要保留叶子节点的值
需要使用used数组来记录哪些元素已经使用过
class Solution {
List<List<Integer>> list = new ArrayList<List<Integer>>();
List<Integer> li = new ArrayList<Integer>();
public List<List<Integer>> permute(int[] nums) {
boolean[]used = new boolean[nums.length];
backtracking(nums,used);
return list;
}
public void backtracking(int[] nums,boolean[] used){
if(li.size()==nums.length){
list.add(new ArrayList<Integer>(li));
return;
}
for(int i = 0;i<nums.length;i++){
if(used[i]==true){
continue;
}
used[i]=true;
li.add(nums[i]);
backtracking(nums,used);
li.remove(li.size()-1);
used[i]=false;
}
}
}
11.全排列II
给定一个可包含重复数字的序列nums,按任意属性返回所有不重复的全排列
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
思路:本题和上题的不同点在于这个是包含重复数字的序列,所以需要提前排序,然后在本次循环内进行判断,如果本次待加入的数字和前面的数字一样,则跳过本次,否则就重复了~~
class Solution {
List<List<Integer>> list = new ArrayList<List<Integer>>();
List<Integer> li = new ArrayList<Integer>();
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
backtracking(nums,used);
return list;
}
public void backtracking(int[] nums,boolean[] used){
if(li.size()==nums.length){
list.add(new ArrayList<Integer>(li));
return;
}
for(int i =0;i<nums.length;i++){
if(used[i]==true||(i>0&&nums[i]==nums[i-1]&&used[i-1]==false)){//used[i]==true 在一个树枝中向下搜索如果已经用过了则跳过;
//(i>0&&nums[i]==nums[i-1]&&used[i-1]==false) 后面的数和前面的数一样,同时前面的那一条分支已经走过了used[i-1]==false,所以此个分支也应该跳过,如果走的化和上个一样的结果,所以需要continue;
continue;
}
li.add(nums[i]);
used[i]=true;
backtracking(nums,used);
used[i]=false;
li.remove(li.size()-1);
}
}
}