在刷了几十道回溯算法之后,回溯大致可以解决如下几类问题(公认的分类):组合、子集、分割、排列、棋盘问题,其实在我看来分割和子集是同一类问题,就像一份吐司面包,分割是需要切成n份,而组合就是把切好的面包片重新组合,将第一块和第五块放在一起。如果把二进制数当成切吐司的刀,那么所有的问题就变得简单了例如:1011010,1就代表该位置需要切一刀,0就不需要切;
对于分割问题
只要能够搞明白n位二进制有多少种变换组合,就能够将分割的情况全部考虑到,显然n位二进制有2的n次幂的组合种类,一下就是打印所有组合种类的回溯算法,也是解决子集问题和分割问题的核心:
//将n位的二进制组合全部打印出来
public class allCombine {
List<Integer> path;
List<List<Integer>> results;
public static void main(String[] args) {
allCombine al = new allCombine();
al.path=new ArrayList<>();
al.results= new ArrayList<>();
al.retuall(3);//暂定有3位
System.out.println(al.results);
}
void retuall(int s){
if(path.size()==s){
results.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < 2; i++) {
path.add(i);
retuall(s);
path.remove(path.size()-1);
}
}
}
输出结果:
[[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]]
这样就可以拿输出的结果判定需要切割的字符串或数组是否需要在第i位进行切割
例如字符串“abc” 当二进制排序为[0,0,0]就可以代表该字符串一刀不切,
当数组为[1,0,1]就可以将字符串映射为{"a","c"},大概就是这个意思,二进制每一个输出的数组结果都可以映射为对字符串的处理方式;
对于子集问题
无非是把分割后的元素进行组合,就拿力扣的78题做一个演示
代码如下:
class Solution {
List<List<Integer>> results = new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
int s = nums.length;
retuall(s, 0);//返回数组位数的二进制所有组合
for (List<Integer> li : results) {//用每个组合映射出一种答案
List<Integer> pa = new ArrayList<>();
for (int i = 0; i < li.size(); i++) {
if (li.get(i) == 1) {//当二进制组合中某位是1时,数组的响应位置的数字就会被保留
pa.add(nums[i]);
}
}
res.add(new ArrayList<>(pa));
}
return res;
}
void retuall(int s, int index) {
if (path.size() == s) {
results.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < 2; i++) {
path.add(i);
retuall(s, index - 1);
path.remove(path.size() - 1);
}
}
}
总结:用二进制的所有情况去映射答案是一种很死板的算法,可以说是傻瓜式的解决问题的方式,如果对回溯算法掌握的不行的情况下,或者不考虑效率的情况下,就可以照搬,文章写的食用性不是特别强,这种思想的拓展性应该很强,由于时间问题,暂时没有扩展,后期有时间会重写一篇,如果只想速成的同学,建议多思考阅读几遍,应该也不算难理解。