Home
探索 Uedu
學生控制台
註冊會員/登入
研究知情同意中心
教師控制台
課程設定
支援與訊息
Uptime 數據

UeduGPTs

--

Jupyters

4

UG26 CISOSE26
臺北 AQI 46 · 臺中 AQI 28 · 臺南 AQI 24 · 高雄 AQI 33

AI 回覆桌面通知

AI 助教回覆完成時顯示桌面通知

聊天訊息通知

同學在討論區發送訊息時通知

聲音通知

每當有新通知時播放提示音

演算法與複雜度

演算法與複雜度(進階):如何證明「夠快的演算法根本不存在」?

從遞迴式與主定理、決策樹下界,到歸約、NP-完全與繞過難題的近似、隨機與參數化策略

如果「夠快的演算法根本不存在」,你要怎麼證明這件事?

你已經會分析一支演算法跑多快了。但有一個更深的問題:給定一個問題,最好的演算法能有多快?這不是問「我寫的這支」,而是問「任何人、用任何聰明手段、現在或未來寫出來的所有演算法裡,最快的那支」。

這聽起來像是不可能回答的——你怎麼可能對「還沒被發明出來的演算法」說三道四?但計算機科學最迷人的成就之一,正是我們確實能對問題本身的難度下界(lower bound)做出嚴格證明。比較排序(comparison sort)不可能比 $O(n \log n)$ 更快、旅行推銷員問題(TSP)若有多項式解則幾乎所有困難問題都會跟著崩塌——這些不是「目前還沒人做到」,而是「在某個明確模型下被證明做不到,或被證明牽一髮動全身」。入門篇教你量一支演算法;這一篇要教你對整個問題的難度說話:遞迴式怎麼解、下界怎麼證、難度怎麼在問題之間傳遞,以及當問題真的太難時,我們用什麼策略繞過它。

演算法與複雜度進階概念示意圖

遞迴式:分治演算法的成本方程式

許多高效演算法走的是分治(divide and conquer)路線:把問題切成幾個子問題、遞迴解決、再合併。它們的成本沒辦法用一條迴圈數出來,而是寫成一條遞迴式(recurrence relation)——成本用「自己更小的版本」來定義。

合併排序(merge sort)是教科書範例。它把陣列切兩半、各自排序、再 $O(n)$ 線性合併:

def merge_sort(a):
    if len(a) <= 1:
        return a
    mid = len(a) // 2
    left = merge_sort(a[:mid])      # T(n/2)
    right = merge_sort(a[mid:])     # T(n/2)
    return merge(left, right)       # O(n)

def merge(x, y):
    result, i, j = [], 0, 0
    while i < len(x) and j < len(y):
        if x[i] <= y[j]:
            result.append(x[i]); i += 1
        else:
            result.append(y[j]); j += 1
    result.extend(x[i:]); result.extend(y[j:])
    return result

它的成本方程式是:

$$T(n) = 2\,T(n/2) + \Theta(n)$$

意思是「解一個大小為 $n$ 的問題 = 解兩個一半大的子問題 + 花線性時間合併」。問題剩下:這條方程式展開後是多少?

動手算一下:用遞迴樹展開

把遞迴一層一層畫出來,就是遞迴樹(recursion tree)。每一層代表一次遞迴深度,我們算「每一層的合併成本總和」:

子問題個數 每個大小 每個合併成本 該層總成本
0 1 $n$ $n$ $n$
1 2 $n/2$ $n/2$ $n$
2 4 $n/4$ $n/4$ $n$
$\vdots$ $\vdots$ $\vdots$ $\vdots$ $\vdots$
$k$ $2^k$ $n/2^k$ $n/2^k$ $n$

關鍵觀察:每一層的總成本都是 $n$(子問題變多,但每個變小,乘起來剛好抵消)。樹要切到子問題大小為 1,需要 $\log_2 n$ 層。所以總成本是「層數 × 每層成本」:

$$T(n) = \underbrace{n}_{\text{每層}} \times \underbrace{\log_2 n}_{\text{層數}} = \Theta(n \log n)$$

這就是為什麼合併排序是 $O(n \log n)$——不是背下來的,而是從成本方程式推出來的。遞迴樹法的威力在於它讓你「看見」成本花在哪裡:合併排序成本均勻分布在每一層,而其他演算法可能成本集中在樹根或樹葉,行為完全不同。

主定理:一個查表就能解遞迴式的工具

每次都畫遞迴樹很累。對於形如下式的遞迴,有一個現成的「查表公式」叫主定理(Master Theorem)

$$T(n) = a\,T(n/b) + f(n), \quad a \ge 1,\ b > 1$$

這裡 $a$ 是子問題個數、$b$ 是每個子問題縮小的倍率、$f(n)$ 是分割與合併的成本。定理的核心是比較兩股力量:一股是「葉子總工作量」$n^{\log_b a}$(遞迴展開到底,所有葉子加起來的成本),另一股是「根節點工作量」$f(n)$。誰主宰,誰就決定答案:

  • 情況一(葉子贏):若 $f(n) = O(n^{\log_b a - \varepsilon})$($f$ 比葉子成本還小一截),則 $T(n) = \Theta(n^{\log_b a})$。
  • 情況二(平手):若 $f(n) = \Theta(n^{\log_b a})$(兩者同階),則 $T(n) = \Theta(n^{\log_b a} \log n)$,多乘一個 $\log n$。
  • 情況三(根贏):若 $f(n) = \Omega(n^{\log_b a + \varepsilon})$ 且滿足正則條件 $a\,f(n/b) \le c\,f(n)$($c < 1$),則 $T(n) = \Theta(f(n))$。

套用幾個例子立刻有感:

合併排序:  T(n) = 2 T(n/2) + n
  a=2, b=2 → n^(log₂2) = n¹ = n,而 f(n)=n → 同階(情況二)
  → T(n) = Θ(n log n)  ✓

二分搜尋:  T(n) = 1 T(n/2) + O(1)
  a=1, b=2 → n^(log₂1) = n⁰ = 1,f(n)=1 → 同階(情況二)
  → T(n) = Θ(log n)  ✓

Karatsuba 大數乘法:T(n) = 3 T(n/2) + O(n)
  a=3, b=2 → n^(log₂3) ≈ n^1.585,f(n)=n 較小(情況一)
  → T(n) = Θ(n^1.585)  ← 比小學直式乘法的 O(n²) 快!

最後那個 Karatsuba 例子值得停一下:小學教的兩個 $n$ 位數相乘是 $O(n^2)$,但用分治把乘法次數從 4 次減到 3 次(代價是多幾次加法),複雜度就掉到 $n^{1.585}$。這正是「改變遞迴結構就能改變漸近階」的鮮活示範,也是為什麼設計分治演算法時,每一個 $a$、$b$ 的選擇都在跟複雜度討價還價。

主定理的邊界:它不是萬能的。當 $f(n)$ 落在情況一與情況二之間的「灰色地帶」(差距不到一個多項式因子,例如 $f(n) = n \log n$ 對上 $n$),三種情況都套不上,要改用更強的 Akra–Bazzi 方法。子問題大小不等(如 $T(n) = T(n/3) + T(2n/3) + n$)時也得另尋他法。

下界:證明「不可能更快」的藝術

到目前為止我們都在算某支演算法多快,這是上界(upper bound)——「存在一支這麼快的演算法」。但開頭那個問題要的是下界——「任何演算法都至少要這麼慢」。這兩者方向相反:上界靠「給出一支演算法」就成立,下界要靠「對所有可能的演算法論證」。

最經典的下界證明是「比較排序不可能比 $O(n \log n)$ 快」,用的工具是決策樹(decision tree)。把任何「只靠兩兩比較來排序」的演算法想成一棵二元樹:每個內部節點是一次比較(問「$a_i < a_j$ 嗎?」),根據答案往左或往右走,每片葉子對應一種最終排序結果。

論證如下:$n$ 個相異元素有 $n!$ 種可能的排列,每一種都必須對應到至少一片葉子(否則演算法對該輸入會出錯)。一棵高度為 $h$ 的二元樹最多有 $2^h$ 片葉子。所以必須:

$$2^h \ge n! \quad\Longrightarrow\quad h \ge \log_2(n!)$$

而樹的高度 $h$ 就是最壞情況下要做的比較次數。用 Stirling 近似 $\log_2(n!) = \Theta(n \log n)$,得到:

$$h \ge \log_2(n!) = \Theta(n \log n)$$

這個下界對「所有」比較排序都成立——合併排序、堆積排序、快速排序,再聰明的也逃不掉。它不是說「我們還沒找到更快的」,而是說「在只用比較的模型裡,$n \log n$ 是天花板,找下去也是白費」。

這同時解釋了一個常見困惑:計數排序(counting sort)、基數排序(radix sort)為什麼能做到 $O(n)$?因為它們不只用比較——它們直接拿元素的值當索引去存取,跳出了決策樹模型,因此不受這個下界約束。下界永遠是「在某個計算模型下」成立的;換模型,天花板就變了。這是讀任何下界結果時都要追問的第一個問題:它假設的是哪個模型?

當問題本身就難:歸約與 NP-完全

有些問題我們連 $O(n \log n)$ 都奢望不起,連「有沒有多項式時間演算法」都不知道。旅行推銷員問題(TSP)、布林可滿足性(SAT)、子集合加總(subset sum)——幾十年來無數聰明人嘗試,沒人找到多項式解,但也沒人能證明它不存在。這就是 $P$ 與 $NP$ 之爭的舞台。

入門篇提過 $P$(多項式時間可解)與 $NP$(多項式時間可驗證)。進階的關鍵機制是歸約(reduction):把問題 $A$ 「翻譯」成問題 $B$,使得「解出 $B$ 就等於解出 $A$」。如果這個翻譯本身只花多項式時間,我們寫 $A \le_p B$,讀作「$A$ 多項式歸約到 $B$」。它的含義很強:

如果 $A \le_p B$,那麼「$B$ 容易」會推出「$A$ 也容易」;反過來,「$A$ 難」會推出「$B$ 至少一樣難」。

歸約讓難度可以在問題之間傳遞。一個問題被稱為 NP-困難(NP-hard),如果 $NP$ 裡每一個問題都能歸約到它;如果它自己又恰好在 $NP$ 裡,就叫 NP-完全(NP-complete)。NP-完全問題是「$NP$ 裡最難的一群」——它們彼此之間都能互相歸約,命運綁在一起:只要有任何一個 NP-完全問題找到多項式解,所有 $NP$ 問題就全部跟著有了(也就證明了 $P = NP$)。

1971 年 Cook 與 Levin 證明 SAT 是第一個 NP-完全問題(Cook–Levin 定理),這是個了不起的起點——它從零建立了第一個「最難」的錨點。此後要證明新問題 $X$ 是 NP-完全,就不必再從頭,只要:

證明 X 是 NP-完全的標準兩步:
  1. 證 X ∈ NP        ── 給一個「猜測解」,能在多項式時間驗證它對不對
  2. 挑一個已知 NP-完全問題 Y,證 Y ≤ₚ X
                       ── 把 Y 的任意輸入,多項式時間改造成 X 的輸入

注意歸約的方向:要證 $X$ 難,是把已知很難的 $Y$ 歸約 $X$($Y \le_p X$),意思是「連 $Y$ 都能靠 $X$ 解掉,可見 $X$ 至少跟 $Y$ 一樣難」。初學者最常把方向寫反——記住「拿難的去壓你要證的」就不會錯。透過這套機制,Karp 在 1972 年一口氣證明了 21 個經典問題都是 NP-完全,從此這份清單滾到了數千個問題。

太難就繞過去:近似、隨機與參數化

如果一個問題是 NP-完全的,而你又真的得在實務上解它(物流排程、晶片佈線天天都在解 TSP 的近親),難道就投降嗎?不。複雜度理論的價值不只在「判死刑」,更在於它告訴你該往哪個方向繞

近似演算法(approximation algorithm):放棄「最佳解」,改求「保證不差太多的解」。對某些問題,我們能在多項式時間內找到「至多比最佳解差 $\rho$ 倍」的解,$\rho$ 稱為近似比(approximation ratio)。例如頂點覆蓋(vertex cover)有簡單的 2-近似演算法。下面是它的精髓——反覆挑一條還沒被覆蓋的邊,把兩個端點都放進答案:

def vertex_cover_2approx(edges):
    """2-近似:結果保證 ≤ 2 倍最佳解"""
    cover = set()
    remaining = set(edges)
    while remaining:
        u, v = next(iter(remaining))     # 任取一條未覆蓋的邊
        cover.add(u); cover.add(v)       # 兩端點都收進來
        # 移除所有被 u 或 v 覆蓋掉的邊
        remaining = {(a, b) for (a, b) in remaining
                     if a not in (u, v) and b not in (u, v)}
    return cover

為什麼保證 2 倍?因為每條被我們挑中的邊,最佳解至少要選它兩個端點中的一個;而我們選了兩個。所以我們的解最多是最佳解的兩倍——這個保證對任何輸入都成立,是寫死在邏輯裡的,不是平均表現。

隨機化演算法(randomized algorithm):引入隨機性來避開最壞情況或簡化設計。隨機化快速排序(randomized quicksort)隨機選 pivot,把「對手構造的最壞輸入」變成「機率極低的壞運氣」,期望時間 $O(n \log n)$ 不依賴輸入長相。這類演算法又分兩派:Las Vegas(答案永遠正確、時間是隨機變數,如隨機化快排)與 Monte Carlo(時間有保證、答案有小機率出錯,如 Miller–Rabin 質數測試)。

參數化複雜度(parameterized complexity):把「難」拆得更細。一個問題可能在輸入規模 $n$ 上是指數的,但若某個關鍵參數 $k$(如要找的解的大小)很小,就能寫成 $f(k) \cdot n^{O(1)}$——指數爆炸被關進 $k$ 裡,對 $n$ 仍是多項式。這叫固定參數可解(fixed-parameter tractable, FPT)。頂點覆蓋找大小 $\le k$ 的解就是 FPT 的代表:當 $k$ 小(現實中往往如此),即使圖很大也能快速解出精確答案。這給了「NP-完全」一個更細膩的回答——不是所有實例都一樣難,難度可能集中在某個參數上。

重點回顧

  • 分治演算法的成本寫成遞迴式;用遞迴樹展開可看見成本分布,合併排序每層成本皆為 $n$、共 $\log n$ 層,故為 $\Theta(n \log n)$。
  • 主定理是解 $T(n) = a\,T(n/b) + f(n)$ 的查表工具,比較葉子成本 $n^{\log_b a}$ 與根成本 $f(n)$ 決定三種情況;它有灰色地帶與不適用情形(改用 Akra–Bazzi)。
  • 下界證明「沒有更快的演算法」:比較排序的決策樹論證($n!$ 種排列需 $\ge \log_2(n!) = \Theta(n \log n)$ 高度)說明 $n \log n$ 是天花板;下界永遠相對於某個計算模型而言。
  • 歸約($\le_p$) 讓難度在問題間傳遞;NP-完全問題是 $NP$ 中最難的一群,命運相連——任一個有多項式解就推出 $P = NP$。證明 NP-完全要把已知難問題歸約目標問題。
  • 面對 NP-完全問題的實務出路:近似演算法(保證近似比)、隨機化演算法(Las Vegas / Monte Carlo)、參數化複雜度(把指數關進小參數 $k$,達成 FPT)。

深入探討(研究所視角)

把上面的線索往上延伸,會通向複雜度理論的核心地景與幾個仍然開放的深水區。

複雜度類別的層級:$P$ 與 $NP$ 只是冰山一角。$NP$ 的「對偶」是 $\text{co-NP}$(補問題可在多項式時間驗證「否」的證據),$NP \cap \text{co-NP}$ 裡住著像「整數因數分解」這類「兩個方向都有簡短證書、卻疑似不在 $P$」的問題。再往上是多項式層級(polynomial hierarchy, PH),用交錯的 $\exists/\forall$ 量詞堆出 $\Sigma_k^p$、$\Pi_k^p$ 一層層;$P = NP$ 會讓整個 PH 塌縮成一層。空間維度有 $\text{PSPACE}$(多項式空間,可解到量詞布林公式 QBF 這種 PSPACE-完全問題)、$L$(對數空間)與 $NL$,已知 $NL = \text{co-NL}$(Immerman–Szelepcsényi 定理),而 $L \overset{?}{=} NL$ 仍未解。一條被嚴格證明的稀有「分離」是時間層級定理(time hierarchy theorem):嚴格更多的時間確實能解嚴格更多的問題,所以 $P \subsetneq \text{EXP}$ 是已知為真——這提醒我們,「下界很難證」不代表「下界不存在」。

為什麼 $P \overset{?}{=} NP$ 這麼難證:已經有後設定理在解釋障礙本身。相對化(relativization):存在神諭 $A$、$B$ 使 $P^A = NP^A$ 但 $P^B \neq NP^B$(Baker–Gill–Solovay),所以任何「對神諭無感」的證明手法(包括樸素對角線法)都不可能分開 $P$ 與 $NP$。自然證明(natural proofs):Razborov–Rudich 指出,若單向函數存在,一大類「自然」的電路下界證明策略注定失敗。代數化(algebrization)又擋掉了一批繞過相對化的手法。這三道牆是為什麼這個千禧年大獎問題懸宕至今——不是還沒夠聰明,而是已知的工具被證明都不夠用。

精細複雜度(fine-grained complexity):對於已經在 $P$ 裡的問題,近年的研究把刀magnify到「$n^2$ 還是 $n^{1.99}$」這種級別。許多問題(編輯距離、最長共同子序列、圖直徑)卡在 $O(n^2)$ 幾十年降不下來,於是人們提出條件下界:假設強指數時間假說(Strong Exponential Time Hypothesis, SETH)為真,則編輯距離不存在 $O(n^{2-\varepsilon})$ 演算法。這是一種「相對難度」——把 $P$ 內部問題之間用細粒度歸約串起來,形成和 NP-完全平行的、但發生在多項式尺度上的難度網。

與機器學習與量子的交界:複雜度的觸角還在延伸。$\text{BQP}$(量子多項式時間)容納了 Shor 因數分解演算法,而因數分解疑似不在 $P$ 卻在 $\text{BQP}$——這正是量子計算威脅 RSA 的理論根源,也讓 $\text{BQP}$ 與經典類別的關係成為熱點。另一邊,學習理論用 PAC 框架問「一個概念類別需要多少樣本、多少計算才能學會」,把樣本複雜度與計算複雜度綁在同一張地圖上。本文教你分析一個演算法、論證一個問題的難度;研究所之後的旅程,是去探索難度本身的結構——哪些牆是真的、哪些只是我們還沒找到梯子,以及在量子與學習這些新模型裡,整張地圖會被重畫成什麼樣子。

AI 共讀助教正在陪你讀:演算法與複雜度(進階):如何證明「夠快的演算法根本不存在」?
嗨!我是這篇文章的共讀助教,只根據〈演算法與複雜度(進階):如何證明「夠快的演算法根本不存在」?〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。