回溯
回溯法解決的問題
回溯法,一般可以解決如下幾種問題:
- 組合問題:N個數里面按一定規則找出k個數的集合
- 切割問題:一個字符串按一定規則有幾種切割方式
- 子集問題:一個N個數的集合里有多少符合條件的子集
- 排列問題:N個數按一定規則全排列,有幾種排列方式
- 棋盤問題:N皇后,解數獨等等
另外,什么是排列?
組合是不強調元素順序的,排列是強調元素順序。
解決一個回溯問題,實際上就是一個決策樹的遍歷過程。你只需要思考 3 個問題:
1、路徑:也就是已經做出的選擇。
2、選擇列表:也就是你當前可以做的選擇。
3、結束條件:也就是到達決策樹底層,無法再做選擇的條件。
模板
void backtracking(參數) {
if (終止條件) {
存放結果;
return;
}
for (選擇:本層集合中元素(樹中節點孩子的數量就是集合的大小)) {
處理節點;
backtracking(路徑,選擇列表); // 遞歸
回溯,撤銷處理結果
}
}
溯和遞歸是相輔相成的。
接着提到了回溯法的效率,回溯法其實就是暴力查找,並不是什么高效的算法。
組合
給定兩個整數 n 和 k,返回 1 ... n 中所有可能的 k 個數的組合。
示例:
輸入: n = 4, k = 2
輸出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

每次從集合中選取元素,可選擇的范圍隨着選擇的進行而收縮,調整可選擇的范圍。
圖中可以發現n相當於樹的寬度,k相當於樹的深度。
那么如何在這個樹上遍歷,然后收集到我們要的結果集呢?
圖中每次搜索到了葉子節點,我們就找到了一個結果。
相當於只需要把達到葉子節點的結果收集起來,就可以求得 n個數中k個數的組合集合。
lass Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
combineHelper(n, k, 1);
return result;
}
/**
* 每次從集合中選取元素,可選擇的范圍隨着選擇的進行而收縮,調整可選擇的范圍,就是要靠startIndex
* @param startIndex 用來記錄本層遞歸的中,集合從哪里開始遍歷(集合就是[1,...,n] )。
*/
private void combineHelper(int n, int k, int startIndex){
//終止條件
if (path.size() == k){
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){
path.add(i);
combineHelper(n, k, i + 1);
path.removeLast();
}
}
}
剪枝優化
我們說過,回溯法雖然是暴力搜索,但也有時候可以有點剪枝優化一下的。
來舉一個例子,n = 4,k = 4的話,那么第一層for循環的時候,從元素2開始的遍歷都沒有意義了。 在第二層for循環,從元素3開始的遍歷都沒有意義了。

貪心
什么是貪心
貪心的本質是選擇每一階段的局部最優,從而達到全局最優。
貪心一般解題步驟
貪心算法一般分為如下四步:
- 將問題分解為若干個子問題
- 找出適合的貪心策略
- 求解每一個子問題的最優解
- 將局部最優解堆疊成全局最優解
分發餅干
假設你是一位很棒的家長,想要給你的孩子們一些小餅干。但是,每個孩子最多只能給一塊餅干。
對每個孩子 i,都有一個胃口值 g[i],這是能讓孩子們滿足胃口的餅干的最小尺寸;並且每塊餅干 j,都有一個尺寸 s[j] 。如果 s[j] >= g[i],我們可以將這個餅干 j 分配給孩子 i ,這個孩子會得到滿足。你的目標是盡可能滿足越多數量的孩子,並輸出這個最大數值。
示例 1: 輸入: g = [1,2,3], s = [1,1] 輸出: 1 解釋: 你有三個孩子和兩塊小餅干,3個孩子的胃口值分別是:1,2,3。 雖然你有兩塊小餅干,由於他們的尺寸都是1,你只能讓胃口值是1的孩子滿足。 所以你應該輸出1。
這里的局部最優就是大餅干喂給胃口大的,充分利用餅干尺寸喂飽一個,全局最優就是喂飽盡可能多的小孩。
class Solution {
// 思路1:優先考慮餅干,小餅干先喂飽小胃口
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int start = 0;
int count = 0;
for (int i = 0; i < s.length && start < g.length; i++) {
if (s[i] >= g[start]) {
start++;
count++;
}
}
return count;
}
}
動態規划
什么是動態規划
動態規划,英文:Dynamic Programming,簡稱DP,如果某一問題有很多重疊子問題,使用動態規划是最有效的。
動態規划算法就是將待求解問題分解成若干子問題,
先求解子問題並保存子問題的答案避免重復計算,然后從這些子問題的解得到原問題的解。
而如何斷定一個問題是否可以用動態規划來解決,就需要掌握動態規划的兩個基本要素,「最優子結構性質」 和「重疊子問題性質」 。
爬樓梯
假設你正在爬樓梯。需要 n 階你才能到達樓頂。
每次你可以爬 1 或 2 個台階。你有多少種不同的方法可以爬到樓頂呢?
注意:給定 n 是一個正整數。
示例 1: 輸入: 2 輸出: 2 解釋: 有兩種方法可以爬到樓頂。
- 1 階 + 1 階
- 2 階
class Solution {
public int climbStairs(int n) {
if(n <= 2) return n;
int a = 1, b = 2, sum = 0;
for(int i = 3; i <= n; i++){
sum = a + b;
a = b;
b = sum;
}
return b;
}
}
// 常規方式
public int climbStairs(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
// 用變量記錄代替數組
public int climbStairs(int n) {
int a = 0, b = 1, c = 0; // 默認需要1次
for (int i = 1; i <= n; i++) {
c = a + b; // f(i - 1) + f(n - 2)
a = b; // 記錄上一輪的值
b = c; // 向后步進1個數
}
return c;
}