基礎算法-學習

回溯

回溯法解決的問題

回溯法,一般可以解決如下幾種問題:

  • 組合問題: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 階 + 1 階
  2. 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;
}
Tags: