文章目录
Pow(x,n)(分治)
分治法
采用分治的思想,求x的n次方,可以先求x的n/2次方
如果n为偶数x^n = x^(n/2)* x^(n/2)
如果n为奇数x^n = x^(n/2)* x^(n/2) *x
public class Main {
public double myPow(double x, int n) {
if (x == 1) return 1;//特殊用例,加快速度
if (x == -1) return n % 2 == 0 ? 1 : -1;//特殊用例,加快速度
if (n == -2147483648) return 0;//一个特殊的坑人的用例
if (n < 0) {//负数次方处理
n = -n;
x = 1 / x;
}
return pow(x, n);
}
double pow(double x, int n) {
if (n == 0) return 1;
//求n/2次方的值
double half = myPow(x, n / 2);
return n % 2 == 0 ? half * half : half * half * x;
}
}
为运算表达式设计优先级(分治)
分治算法典型应用
分治法
class Solution {
public List<Integer> diffWaysToCompute(String input) {
List<Integer> ways = new ArrayList<>();
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
if (c == '+' || c == '-' || c == '*') {
List<Integer> left = diffWaysToCompute(input.substring(0, i));
List<Integer> right = diffWaysToCompute(input.substring(i + 1));
for (int l : left) {
for (int r : right) {
switch (c) {
case '+':
ways.add(l + r);
break;
case '-':
ways.add(l - r);
break;
case '*':
ways.add(l * r);
break;
}
}
}
}
}
//只有数字没有符号的情况
if (ways.size() == 0) {
ways.add(Integer.valueOf(input));
}
return ways;
}
}
子集(回溯)
回溯法
解法一:
每次遍历加一个大于原来数下标的数到集合,把集合加入到结果集
class Solution {
List<List<Integer>> res;
public List<List<Integer>> subsets(int[] nums) {
res = new ArrayList<>();
if (nums == null) return res;
dfs(nums, new ArrayList<Integer>(), 0);
return res;
}
private void dfs(int[] nums, List<Integer> list, int start) {
res.add(new ArrayList<>(list));
for (int i = start;i<nums.length;i++){
list.add(nums[i]);
dfs(nums, list, i + 1);
//reserve
list.remove(list.size() - 1);
}
}
}
解法二:
将添加值的问题分为加值
和不加值
两种
public class Main {
List<List<Integer>> res;
public List<List<Integer>> subsets(int[] nums) {
res = new ArrayList<>();
if (nums == null) return res;
dfs(nums, new ArrayList<Integer>(), 0);
return res;
}
/**
* @param nums 入参数组
* @param list 传入每层的数组
* @param level 层数
*/
private void dfs(int[] nums, List<Integer> list, int level) {
if (level == nums.length) {
res.add(new ArrayList<>(list));//因为list是传入的引用,所以需要new一个list加入
return;
}
//不需要这个参数的集合
dfs(nums, list, level + 1);
//将值加入列表,再进行下探
list.add(nums[level]);
dfs(nums, list, level + 1);
//reserve
list.remove(list.size() - 1);//真的list 引用,操作完了之后必须reserve
}
}
迭代法
这个题还有一个比较经典的玩法就是迭代法
遍历入参集合,每次都把已有的集合复制一份,然后把值加入到复制的集合中
public class Main {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
if (nums == null) return res;
res.add(new ArrayList<>());
for (int i = 0; i < nums.length; i++) {
List<List<Integer>> tmp_list = new ArrayList<>();//使用tmp_list,保证遍历时取的数都是上一轮的
for (List list : res) {
List<Integer> curlist = new ArrayList<>(list);
curlist.add(nums[i]);
tmp_list.add(curlist);
}
res.addAll(tmp_list);
}
return res;
}
}
子集II(回溯)
思路:
- 数组排序
- 每次遍历加一个大于原来数下标的数到集合,把集合加入到结果集
- 如果两个数的值相同,第二次就不用加入遍历了,避免重复
public class Solution {
List<List<Integer>> res;
public List<List<Integer>> subsetsWithDup(int[] nums) {
res = new ArrayList<>();
if (nums == null) return res;
Arrays.sort(nums);
dfs(nums, new ArrayList<Integer>(), 0);
return res;
}
private void dfs(int[] nums, List<Integer> list, int start) {
res.add(new ArrayList<>(list));
for (int i = start;i<nums.length;i++){
//和上一个数字相同,跳过
if (i>start&&nums[i]==nums[i-1])continue;
list.add(nums[i]);
dfs(nums, list, i + 1);
//reserve
list.remove(list.size() - 1);
}
}
}
电话号码的字母组合(回溯)
回溯法
public class Main {
Map<Character, String> map;
List<String> res;
public List<String> letterCombinations(String digits) {
if (digits.length() == 0 || digits == null) return new ArrayList<>();
//初始化
map = new HashMap<>();
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");
res = new LinkedList<>();
search("", digits, 0);
return res;
}
private void search(String s, String digits, int level) {
//退出条件,长度够了
if (level == digits.length()) {
res.add(s);
return;
}
//获取第level个数字对应的字母
String cur = map.get(digits.charAt(level));
//遍历字母并依次下探
for (int i = 0; i < cur.length(); i++) {
search(s + cur.charAt(i), digits, level + 1);
}
}
}
单词搜索(回溯)
回溯法DFS解法
搜索到以word的第一个字母开头的位置,然后进行深度优先遍历
public class Solution {
int m;
int n;
public boolean exist(char[][] board, String word) {
if (word == null || word.length() == 0) return true;
if (board == null || board.length == 0 || board[0].length == 0) return false;
m = board.length;
n = board[0].length;
boolean[][] visited = new boolean[m][n];
for (int r = 0; r < m; r++)
for (int c = 0; c < n; c++)
if (board[r][c] == word.charAt(0) && dfs(0, r, c, visited, board, word))
return true;
return false;
}
private boolean dfs(int curlen, int r, int c, boolean[][] visited, char[][] board, String word) {
if (curlen == word.length()) return true;
if (r == -1 || c == -1 || r == m || c == n
|| board[r][c] != word.charAt(curlen) || visited[r][c])
return false;
visited[r][c] = true;
if (dfs(curlen + 1, r + 1, c, visited, board, word)) return true;
if (dfs(curlen + 1, r - 1, c, visited, board, word)) return true;
if (dfs(curlen + 1, r, c + 1, visited, board, word)) return true;
if (dfs(curlen + 1, r, c - 1, visited, board, word)) return true;
//reserve
visited[r][c] = false;
return false;
}
}
组合(回溯)
回溯法
public class Main {
List<List<Integer>> res;
public List<List<Integer>> combine(int n, int k) {
res = new ArrayList<>();
if (n <= 0 || k <= 0 || n < k) {
return res;
}
findCombinations(n, k, 1, new Stack<>());
return res;
}
private void findCombinations(int n, int k, int begin, Stack<Integer> pre) {
//当数目够了k就加入结果集并退出
if (pre.size() == k) {
res.add(new ArrayList<>(pre));
return;
}
//进一步下探,添加一个数,并下探它后面的数继续进行添加
for (int i = begin; i <= n; i++) {
pre.add(i);
findCombinations(n, k, i + 1, pre);
pre.pop();
}
}
}
for 循环里 i 从 start 到 n,其实没必要到 n。比如,n = 5,k = 4,temp.size( ) == 1,此时代表我们还需要(4 - 1 = 3)个数字,如果 i = 4 的话,以后最多把 4 和 5 加入到 temp 中,而此时 temp.size() 才等于 1 + 2 = 3,不够 4 个,所以 i 没必要等于 4,i 循环到 3 就足够了。
所以,需要对下探条件进行修改,对整个递归树进行剪枝,减去图中绿色的数(图自题解)
推算出剪枝规则是i <= n - (k -temp.size()) + 1
于是优化后的代码如下
public class Main {
List<List<Integer>> res;
public List<List<Integer>> combine(int n, int k) {
res = new ArrayList<>();
if (n <= 0 || k <= 0 || n < k) {
return res;
}
findCombinations(n, k, 1, new Stack<>());
return res;
}
private void findCombinations(int n, int k, int begin, Stack<Integer> pre) {
//当数目够了k就加入结果集并退出
if (pre.size() == k) {
res.add(new ArrayList<>(pre));
return;
}
//进一步下探,添加一个数,并下探它后面的数继续进行添加
for (int i = begin; i <= n - (k - pre.size()) + 1; i++) {
pre.add(i);
findCombinations(n, k, i + 1, pre);
pre.pop();
}
}
}
组合总和(回溯)
public class Solution {
List<List<Integer>> res;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
res = new ArrayList<>();
backtrack(new ArrayList<>(), 0, target, candidates);
return res;
}
private void backtrack(List<Integer> tempCombination, int start, int target, int[] candidates) {
if (target == 0) {
res.add(new ArrayList<>(tempCombination));
return;
}
for (int i = start; i < candidates.length; i++) {
if (candidates[i] <= target) {
//当前层
tempCombination.add(candidates[i]);
//下探
backtrack(tempCombination, i, target - candidates[i], candidates);
//reserve
tempCombination.remove(tempCombination.size() - 1);
}
}
}
}
组合总和II(回溯)
与上一题相比,要做好防重复的准备
首先可以先到加一个标记位,然后每次检查下就好了,但是重复数字可能造成重复结果,且样例少不了变态的几十个重复的。
所以,先对入参进行排序,如果发现重复的,判断用不用得上,用不上就依次跳过
public class Solution {
List<List<Integer>> res;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
res = new ArrayList<>();
Arrays.sort(candidates);
boolean[] visited = new boolean[candidates.length];
backtrack(new ArrayList<>(), 0, target, candidates, visited);
return res;
}
private void backtrack(List<Integer> tempCombination, int start, int target, int[] candidates, boolean[] visited) {
if (target == 0) {
res.add(new ArrayList<>(tempCombination));
return;
}
for (int i = start; i < candidates.length; i++) {
//当前节点等于上一个节点,且上一个节点没有被访问过,直接跳过(如果上一个都没用上,当前这个就更没用了)
if (i != 0 && candidates[i] == candidates[i - 1] && !visited[i - 1]) continue;
if (candidates[i] <= target) {
//当前层
tempCombination.add(candidates[i]);
visited[i] = true;
//下探(因为不能重复,所以i要+1)
backtrack(tempCombination, i + 1, target - candidates[i], candidates, visited);
//reserve
visited[i] = false;
tempCombination.remove(tempCombination.size() - 1);
}
}
}
}
组合总和III(回溯)
class Solution {
List<List<Integer>> res;
public List<List<Integer>> combinationSum3(int k, int n) {
res = new ArrayList<>();
List<Integer> tmp = new ArrayList<>();
backtrack(k, n, tmp, 1);
return res;
}
private void backtrack(int k, int n, List<Integer> tmp, int start) {
//k,n都刚好终止则符合
if (k == 0 && n == 0) {
res.add(new ArrayList<>(tmp));
return;
}
//单个条件终止,说明这条路径不行
if (k == 0 || n == 0) return;
for (int i = start; i <= 9; i++) {
//当前层
tmp.add(i);
//下探
backtrack(k - 1, n - i, tmp, i + 1);
//reserve
tmp.remove(tmp.size() - 1);
}
}
}
全排列(回溯)
这个题组合一样,都是标准的回溯的模板解决
public class Main {
List<List<Integer>> res;
public List<List<Integer>> permute(int[] nums) {
res = new ArrayList<>();
backtrack(new ArrayList<>(), nums);
return res;
}
private void backtrack(List<Integer> tmpList, int[] nums) {
if (tmpList.size() == nums.length) {
res.add(new ArrayList<>(tmpList));//切记要新new一个加入
return;
}
for (int i = 0; i < nums.length; i++) {
if (tmpList.contains(nums[i])) continue;//已经存在了,就跳过
//当前层业务
tmpList.add(nums[i]);
//下探
backtrack(tmpList, nums);
//reserve
tmpList.remove(tmpList.size() - 1);
}
}
}
全排列II(回溯)
与上一个题全排列相比,本题存在两个新的问题
if(tempList.contains(nums[i])) continue;
这句代码是通过比较值不同跳过的,而现在值可以相同- 如果有两个相同的值,则会产生两次一样的结果
针对上述两个问题,进行如下的升级
首先将nums
进行排序,重新定义个一个old集合存放访问过的值的下标,使即使值相同也能判断是否访问过
针对重复现象,改为如下代码去重
//判断上一个元素和自己是不是一样的,一样说明上次就加进去了,不用重复操作了
// !old.contains(i - 1)是为了保证前一个数是因为回溯被还原了的才跳过,否则不能跳过
if (i > 0 && !old.contains(i - 1) && nums[i - 1] == nums[i]) {
continue;
}
视频讲解剪枝原理参考大佬的题解
public class Solution {
List<List<Integer>> res;
public List<List<Integer>> permuteUnique(int[] nums) {
res = new ArrayList<>();
if (nums == null || nums.length == 0) return res;
Arrays.sort(nums);
boolean[] visited = new boolean[nums.length];
backtrack(nums, visited, new ArrayList<>());
return res;
}
private void backtrack(int[] nums, boolean[] visited, List<Integer> list) {
if (list.size() == nums.length) {
res.add(new ArrayList<>(list));
return;
}
for (int i = 0; i < nums.length; i++) {
if (visited[i]) continue;
//判断上一个元素和自己是不是一样的,一样说明上次就加进去了,不用重复操作了
if (i > 0 && nums[i] == nums[i - 1] && !visited[i - 1]) continue;
visited[i] = true;
list.add(nums[i]);
backtrack(nums, visited, list);
list.remove(list.size() - 1);
visited[i] = false;
}
}
}
N皇后(回溯)
超经典回溯题,思路见代码注解
public class Main {
List<List<String>> res;
//标记列是否被攻击
int[] rows;
//标记次对角线是否被攻击
int[] secondMain;
//标记主对角线是否被攻击
int[] main;
//存储皇后的位置
int[] queens;
//n皇后
int n;
public List<List<String>> solveNQueens(int n) {
//初始化
res = new ArrayList<>();
rows = new int[n];
secondMain = new int[2 * n - 1];
main = new int[2 * n - 1];
queens = new int[n];
this.n = n;
//回溯算法从第0行开始查找皇后的位置
backtrack(0);
return res;
}
//回溯算法放置皇后
private void backtrack(int row) {
if (row >= n) return;
//从第一列开始尝试放皇后
for (int col = 0; col < n; col++) {
//检测是否会被攻击,如果没有攻击再进行下一步操作
if (isNotUnderAttrack(row, col)) {
//在当前位置放置皇后
placeQueen(row, col);
// 放置完成判断现在是否是最后一行,如果是就存储这个方案
if (row == n - 1) addSolution();
//下探 在下一行放皇后
backtrack(row + 1);
//回溯,将上一个皇后去掉
removeQueen(row, col);
}
}
}
//回溯,去掉皇后和皇后的攻击范围
private void removeQueen(int row, int col) {
//移除row行的皇后
queens[row] = 0;
//更新列攻击范围
rows[col] = 0;
//更新主对角线攻击范围
main[row - col + n - 1] = 0;
//更新次对角线攻击范围
secondMain[row + col] = 0;
}
//将满足条件的皇后位置放入结果集
private void addSolution() {
List<String> solution = new ArrayList<>();
for (int i = 0; i < n; i++) {
//记录当前行皇后位置
int col = queens[i];
StringBuilder builder = new StringBuilder();
for (int j = 0; j < col; j++) builder.append(".");
builder.append("Q");
for (int j = 0;j<n-col-1;j++)builder.append(".");
solution.add(builder.toString());
}
res.add(solution);
}
//指定位置放置皇后
private void placeQueen(int row, int col) {
//存放皇后
queens[row] = col;
//更新列攻击范围
rows[col] = 1;
//更新主对角线攻击范围
main[row - col + n - 1] = 1;
//更新次对角线攻击范围
secondMain[row + col] = 1;
}
//检验位置是否会被攻击
private boolean isNotUnderAttrack(int row, int col) {
//对于列,直接判断列位置是否为0
//对于主对角线,row的值恒为col-n+1,所以将row - col + n - 1=0表示
//对于次对角线,row+col的值可以表示一条线
return rows[col] + main[row - col + n - 1] + secondMain[row + col] == 0;
}
}