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;
}
}
子集
分治法
將添加值的問題分爲加值
和不加值
兩種
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;
}
}
電話號碼的字母組合
回溯法
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);
}
}
}
組合
回溯法
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 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 Main {
List<List<Integer>> res;
public List<List<Integer>> permuteUnique(int[] nums) {
res = new ArrayList<>();
Arrays.sort(nums);
List<Integer> old = new ArrayList<>();
backtrack(new ArrayList<>(), nums, old);
return res;
}
private void backtrack(List<Integer> tmpList, int[] nums, List<Integer> old) {
if (tmpList.size() == nums.length) {
res.add(new ArrayList<>(tmpList));//切記要新new一個加入
return;
}
for (int i = 0; i < nums.length; i++) {
//當前下標的值已經被使用過了
if (old.contains(i)) {
continue;
}
//判斷上一個元素和自己是不是一樣的,一樣說明上次就加進去了,不用重複操作了
// !old.contains(i - 1)是爲了保證前一個數是因爲回溯被還原了的才跳過,否則不能跳過
if (i > 0 && !old.contains(i - 1) && nums[i - 1] == nums[i]) {
continue;
}
//當前層業務
old.add(i);
tmpList.add(nums[i]);
//下探
backtrack(tmpList, nums, old);
//reserve
old.remove(old.size() - 1);
tmpList.remove(tmpList.size() - 1);
}
}
}
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;
}
}