(Divide & Conquer)分治和回溯本质上就是特殊的递归, 找重复性 重复性有最近重复性(分治,回溯),和最优重复性(动态规划);
不论分治,递归,回溯本质都是找重复性, 分解问题, 最后组合每个子问题的结果.
将一个字符串转为大写字符串
Python 递归代码模板
def recursion(level, param1, param2, ...):
#recursion terminator
if level > MAX_LEVEL:
process_result
return
# process logic in current level
process(level, data..)
# drill down
self.recursion(level + 1, par1, ...)
#reverse the current level status if needed
分治 代码模板
def divide_conquer(problem, param1, param2, ...):
# recursion terminator
if problem is None:
print_result
return
# prepare data
data = prepare_Data(problem)
subproblems = split_problem(problem, data)
处理当前层逻辑:
如果是求n的阶乘这里就是写成n * fac(n - 1),
如果是斐波那契写成n - 1的递归结果加上n - 2的递归结果,
如果是组合左右括号,组装左括号存在一个变量中,或者组装右括号存起来,当然要判断左右括号是否用完
# conquer subproblems
subresult1 = self.divide_conquer(subproblem[0], p1, ...)
subresult1 = self.divide_conquer(subproblem[1], p1, ...)
subresult1 = self.divide_conquer(subproblem[2], p1, ...)
调用函数下探到下一层解决更细节的子问题
# process and generate the final result
result = process_result(subresult1, subresult2, subresult3, ...)
#reverse the current level states
回溯:
1、什么是“树形问题”?为什么为什么是在树形问题上使用“深度优先遍历”?不用深度优先遍历我们还可以用什么?
2、什么是“回溯”?为什么需要回溯?
3、不回溯可以吗?
首先介绍“回溯”算法的应用。“回溯”算法也叫“回溯搜索”算法,主要用于在一个庞大的空间里搜索我们所需要的问题的解。我们每天使用的“搜索引擎”就是帮助我们在庞大的互联网上搜索我们需要的信息。“搜索”引擎的“搜索”和“回溯搜索”算法的“搜索”意思是一样的。
“回溯”指的是“状态重置”,可以理解为“回到过去”、“恢复现场”,是在编码的过程中,是为了节约空间而使用的一种技巧。而回溯其实是“深度优先遍历”特有的一种现象。之所以是“深度优先遍历”,是因为我们要解决的问题通常是在一棵树上完成的,在这棵树上搜索需要的答案,一般使用深度优先遍历。
Pow(x, n)
一 暴力 时间复杂度 O(n)
result = 1
for i: 0 -> n {
result *= x;
}
二 分治
// template:
1 terminator
2 process(split problem)
3 drill down(subproblems), merge(sub result)
4 reverse states
思想:要算x的n次方结果,比如算2^10,不需要一个2一个2的乘起来乘10次,可以一分为二,只要算2的5次方就够了,将2的5次方乘以它自己,最后就变成2的10次方了,注意2的5次方乘以2的5次方是指数相加2的10次方,并不是2的25次方, 还有注意点就是2的5次方的算法,一分为二,分为2的2次方,那么2的2次方乘以它自己为2的4次方,发现漏掉一个2,所以如果指数是一个奇数的时候将其一分为二乘以它自己会漏掉一个自己,这时要根据奇偶性补一个x,所以这个题由求问题 化解为求其子问题
x^n -- > 2^10 : 2^5 -- > (2^2)*2
pow(x, n):
subproblem: subresult = pow(x, n/2)
merge:
if n % 2 == 1 {
// 奇数odd
result = subresult * subresult * x;
}else {
// 偶数even
result = subresult * subresult;
}
时间复杂度变为了log(N)
所以如果N = 1024次的话,只需要10次,因为1024减半512,减半256减半…
方法二 分治 :时间 O(logN)
double myPow(double x, int n) {
long long N = n;
if (N < 0) {
x = 1 / x;
N = -N;
}
return fastPow(x, N);
}
double fastPow(double x, long long n){
if (n == 0) return 1.0;
double half = fastPow(x, n / 2);
if (n % 2 == 0) {
return half * half;
}else {
return half * half * x;
}
}
上面写报错的原因:Java 代码中 int32, n∈[−2147483648, 2147483647] , 因此当 n = -2147483648时 执行 n = − n 得到n = −2147483648 时 会因越界而赋值出错
出现 刚才那个问题,我试了下,n = -2147483648 时, n= -n 不起作用了,从而导致无限递归
// n = -2147483648 时 导致无限递归
public double myPow(double x, int n) {
if (n < 0) {
return (1.0 / myPow(x, -n));
}
}
,然后抛出stackoverflow的错误了,
确实是因为越界的问题,但是,赋值的时候不会出错,只是改不了值了,是的,不是不起作用,最小值时-n就是等于n的,所以死循环了,@杨**-北京-Java [强]
这里的 最小值时-n就是等于n 的理解
我们先来看整数-1在计算机中如何表示。
假设这也是一个int类型,那么:
1、先取1的原码:00000000 00000000 00000000 00000001
2、得反码: 11111111 11111111 11111111 11111110
3、得补码: 11111111 11111111 11111111 11111111
可见,-1在计算机里用二进制表达就是全1。16进制为:0xFFFFFF。
对于 n = -n 赋值时 当n为最小值 n = -2147483648 那么
Integer.MIN_VALUE: 10000000000000000000000000000000 先减1 变为
Integer.MAX_VALUE: 01111111111111111111111111111111 在取反码
Integer.MIN_VALUE: 10000000000000000000000000000000 又变成了它自己
所以一直死循环
// n = -2147483648 时 导致无限递归
public double myPow(double x, int n) {
if (n < 0) {
return (1.0 / myPow(x, -n));
}
}
/*
[底层] 为什么Integer.MIN_VALUE-1会等于Integer.MAX_VALUE
Integer.MIN_VALUE-1 = Integer.MAX_VALUE
Integer.MAX_VALUE+1 = Integer.MIN_VALUE
实际上这里是计算机底层的位运算法则问题[1]
计算机底层采用了补码来进行加减乘除的运算,好处是符号位参与运算.
举上面两个例子来说明问题。
Integer.MIN_VALUE: 10000000000000000000000000000000
Integer.MAX_VALUE: 01111111111111111111111111111111
-1: 11111111111111111111111111111111
一、
Integer.MIN_VALUE - 1 = Integer.MIN_VALUE + (-1)
10000000000000000000000000000000
+ 11111111111111111111111111111111
---------------------------------------------------
1,01111111111111111111111111111111
舍弃最高位的进位,所以得到的就是Integer.MAX_VALUE
二、
Integer.MAX_VALUE + 1= Integer.MIN_VALUE
01111111111111111111111111111111
+ 00000000000000000000000000000001
---------------------------------------------------
10000000000000000000000000000000
除了补码之外,还有反码、移码等等。
1、反码
整数是它本身,负数是符号位不变数值位求反
2、移码
主要用于浮点数的表示当中的阶码, 相应的补码符号位求反就是移码
*/
把int 改成long 是可以用同样逻辑的
感觉下面这个n < 0 的情况 1/ resu应该就没用上,因为上面如果n < 0的话已经将n变为正数了,是吧
实战 子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
方法一 :递归分治思想
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
if (nums == null) {return ans;}
dfs(ans, nums, new ArrayList<Integer>(), 0);
}
private void dfs(List<List<Integer>> ans, int[] nums, List<Integer> list, int index) {
// teminator
if (index == nums.length) {
ans.add(new ArrayList<Integer>(list));
return;
}
// not pick the number at this index,list不需要发生改变
dfs(ans, nums, list, index + 1);
list.add(nums[index]);
// pick the number at this index list发生改变了
dfs(ans, nums, list, index + 1);
// reverse the current state
/*这里为什么要reverse,因为我们在上面add给了list的一个数,改变了list参数,这个地方随着递归一直在变,它不是本层的本地变量,不是本层的局部变量Local variable(每一层是隔开的),但这个参数改变了会影响上面几层的函数,所以这里就把刚才添加到List中的数去掉,达到一致的.也可以不用reverse the current state 但是要把结果的每一层都复制出来通过这种办法每次改变当前层list不会影响到上下面一层,因为是每层拷贝一份出来的,这样的话,因为List为容器相等于把里面的每一个元素的reference都浅拷贝即指针,引用拷贝了一次类似于下面伪代码
开始递归时
[] [] []
[] [] [3]
当list = [3]时,如果没有进行 list.remove(list.size() - 1),就会出现无法回退到list = []的状态,无法回退到[]状态就无法进行当index = 1时 list = [2]的状态,...,所以递归添加完成后必须进行remove操作,但是如果用引用拷贝的话,就不需要remove了,因为拷贝的值不会改变当前参数的状态
*/
list.remove(list.size() - 1);
}
绿色箭头为最终答案
伪代码
private void dfs(List<List<Integer>> ans, int[] nums, List<Integer> list, int index) {
// teminator
if (index == nums.length) {
ans.add(new ArrayList<Integer>(list));
return;
}
// not pick the number at this index,不需要发生改变
dfs(ans, nums, list.clone(), index + 1);
list.add(nums[index]);
// pick the number at this index 发生改变了
dfs(ans, nums, list.clone(), index + 1);
}
方法二:迭代思想:
一开始是空数组,之后都是往之前已经产生过的子集合里面加,
/*
开始res添加了一个[]
[1] [1, 2] [1, 2, 3]
[1, 3]
[2] [2, 3]
[3]
*/
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
backtrack(0, nums, res, new ArrayList<Integer>());
return res;
}
private void backtrack(int i, int[] nums, List<List<Integer>> res, ArrayList<Integer> tmp) {
res.add(new ArrayList<>(tmp));
for (int j = i; j < nums.length; j++) {
tmp.add(nums[j]);
backtrack(j + 1, nums, res, tmp);
tmp.remove(tmp.size() - 1);
}
}
电话号码的字母组合
public List<String> letterCombinations(String digits) {
if (digits == null || digits.length() == 0) return new ArrayList();
Map<Character, String> map = new HashMap<Character, String>();
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");
List<String> res = new LinkedList<String>();
search("", digits, 0, res, map);
return res;
}
private void search(String s, String digits, int i, List<String> res, Map<Character, String> map) {
if (i == digits.length()) {
res.add(s);
return;
}
String letter = map.get(digits.charAt(i));
for (int j = 0; j < letter.length(); j++) {
search(s + letter.charAt(j), digits, i + 1, res, map);
}
}