cover

回溯法就是「系統化地試所有可能」。跟暴力的差別?暴力是傻試,回溯會在發現走不通時提前回頭(剪枝)。排列、組合、子集、N 皇后——全是這個套路。

先講結論

回溯法的模板就三行:做選擇 → 遞迴 → 撤銷選擇。排列、組合、子集的差別只在「怎麼控制不重複」。搞懂這個模板,你就能解七八成的回溯題。

模板

void backtrack(路徑, 選擇列表) {
    if (滿足結束條件) {
        result.add(路徑的副本);
        return;
    }
    for (選擇 : 選擇列表) {
        if (!isValid(選擇)) continue;  // 剪枝
        path.add(選擇);                // 做選擇
        backtrack(路徑, 選擇列表);      // 遞迴
        path.remove(last);             // 撤銷選擇
    }
}

關鍵是最後那行「撤銷選擇」。遞迴回來之後,你要把狀態還原,才能嘗試下一個選擇。忘記這行 = bug。

排列 vs 組合 vs 子集

問題[1,2,3] 的結果去重方式
排列[1,2,3], [1,3,2], [2,1,3]… (6個)used[] 陣列
組合 C(3,2)[1,2], [1,3], [2,3] (3個)start 參數
子集[], [1], [1,2], [1,2,3]… (8個)start 參數

排列:每個位置都可以放任何未用過的元素

void permute(int[] nums, boolean[] used, List<Integer> path) {
    if (path.size() == nums.length) {
        result.add(new ArrayList<>(path));
        return;
    }
    for (int i = 0; i < nums.length; i++) {
        if (used[i]) continue;
        used[i] = true;
        path.add(nums[i]);
        permute(nums, used, path);
        path.remove(path.size() - 1);
        used[i] = false;
    }
}

組合:只能往後選(避免重複)

void combine(int n, int k, int start, List<Integer> path) {
    if (path.size() == k) {
        result.add(new ArrayList<>(path));
        return;
    }
    // 剪枝:剩餘元素不夠就不選了
    for (int i = start; i <= n - (k - path.size()) + 1; i++) {
        path.add(i);
        combine(n, k, i + 1, path);
        path.remove(path.size() - 1);
    }
}

子集:每個節點都是答案

void subsets(int[] nums, int start, List<Integer> path) {
    result.add(new ArrayList<>(path)); // 不用等到葉子
    for (int i = start; i < nums.length; i++) {
        path.add(nums[i]);
        subsets(nums, i + 1, path);
        path.remove(path.size() - 1);
    }
}

三個問題的程式碼結構幾乎一樣,差別只在迴圈起點和收集答案的位置。

剪枝:回溯法的靈魂

不剪枝的回溯就是暴力搜尋。剪枝讓你跳過「肯定不行」的分支。

常見剪枝技巧:

  1. 排序 + 跳過重複nums[i] == nums[i-1] 時 skip
  2. 餘量不足:剩餘元素不夠填滿結果
  3. 超過限制:當前和已經超過 target,後面的(排序後更大)肯定也超過

好的剪枝可以把指數級的搜尋空間砍掉大半。

N 皇后:回溯法的招牌題

4 皇后的一個解:
. Q . .
. . . Q
Q . . .
. . Q .

放 queen 時檢查三個方向:同列、左上對角線、右上對角線。一旦衝突就剪枝。

檢查衝突:
queens[row] = col
同列:queens[i] == col
對角線:|queens[i] - col| == |i - row|

4 皇后有 2 組解,8 皇后有 92 組解。暴力要檢查 N^N 種放法,剪枝後實際搜尋空間小非常多。

回溯的時間複雜度

問題複雜度
全排列O(n!)
組合 C(n,k)O(C(n,k))
子集O(2^n)
N 皇后O(n!)

指數級或階乘級。回溯法本質上就是暴力搜尋的優化版,不要期待多項式時間。但在搜尋空間不大的問題上(n < 20 左右),它完全夠用。


回溯法教你一個人生道理:走錯路不可怕,可怕的是走錯了還不回頭。剪枝教你另一個:有些路根本不用走,用看的就知道是死路。