iT邦幫忙

2026 iThome 鐵人賽

DAY 20
2

前幾篇,我們一路從 dependency、propagation 談到 Graph 本身也可能隨著系統執行而改變。
到這裡,我們已經不只是在描述資料結構,也開始面對更接近現實系統的問題。

而接下來要換一個角度:

當可以做的選擇不只一個時,我們到底該選哪一個?

前面的搜尋問題裡,我們經常是在找:

  • 有沒有路?
  • 哪條路最短?
  • 哪些東西會被影響?

但很多現實問題不只要求「找到一個答案」。
我們還希望:

這個答案夠好

今天先從一個非常生活化的例子開始:

找零錢

https://ithelp.ithome.com.tw/upload/images/20260907/20129020qKTsBdkWRu.png


找到答案,和找到比較好的答案,是兩回事

假設現在要找 67 元,手上可以使用的硬幣面額是:

50、10、5、1

如果問題只是:

能不能湊出 67 元?

其實答案非常多。

例如:

50 + 10 + 5 + 1 + 1

可以。
但:

10 + 10 + 10 + 10 + 10 + 10 + 5 + 1 + 1

也可以。
甚至全部使用 1 元:

1 + 1 + 1 + ... + 1

一共 67 枚,也能完成。
所以我們想解的通常不是怎麼湊出 67 元,而是:

在可以湊出 67 元的前提下,盡可能減少使用的硬幣數量

這時,問題裡就多了一個東西:

最佳化目標(objective)

也就是:

我們到底想讓什麼變得更好?

在這個問題裡:

  • 衡量方式:使用的硬幣數量
  • 最佳化目標:硬幣數量越少越好

人通常會怎麼找零?

如果你真的拿著:

50、10、5、1

這些面額,要湊出 67 元,你大概不會先把所有可能的找零方式全部寫在紙上。

我們很自然會這樣想:

67 元

先拿一枚不超過 67 的最大硬幣:

50

剩下 17
接著,在不超過 17 的硬幣裡選最大的:

10

剩下 7
再選:

5

剩下 2
接著就是:

1
1

整個過程變成:

剩下 67 → 選 50
剩下 17 → 選 10
剩下 7  → 選 5
剩下 2  → 選 1
剩下 1  → 選 1
剩下 0  → 完成

最後得到:

50 + 10 + 5 + 1 + 1

總共 5 枚硬幣,這種思考方式非常自然。
因為我們每一步都只問一個很簡單的問題:

現在看起來,哪個選擇最好?


這就是 Greedy 的基本直覺

這類策略通常稱為:

Greedy Algorithm,貪婪演算法

Greedy 的核心想法其實非常直白:

每一步,都做目前看起來最好的選擇

在找零問題裡目前最好可以理解成:

在不超過剩餘金額的前提下,先拿最大的硬幣。

所以:

67 → 50
17 → 10
7  → 5
2  → 1
1  → 1

我們沒有先知道完整答案。
也沒有先列出所有可能的硬幣組合,再從裡面挑最好的。

我們只是:

做一個選擇
↓
問題變小
↓
再做下一個選擇
↓
問題再變小

直到問題被解完。


當下這一步,要怎麼選?

Greedy 裡有一個非常重要的概念:

局部選擇(local choice)

也就是:

只根據目前的狀態,選擇眼前最好的選項

例如現在剩下 17,我們不會先思考後面所有可能:

10 + 5 + 1 + 1
5 + 5 + 5 + 1 + 1
10 + 1 + 1 + 1 + 1 + 1 + 1 + 1
...

而是直接說:

目前最大的可用硬幣是 10

所以選 10,接著再處理剩下的 7,這就是所謂的局部選擇:

現在的狀態
↓
選現在最好的
↓
進入下一個狀態

Greedy 並不是站在終點把整條路全部看完,再決定第一步。
它比較像是:

一路往前做看起來最划算的決定


為什麼這種方法這麼符合人類直覺?

因為我們平常做決策時,其實很少真的把「所有可能」全部展開。

想像今天午餐附近有100 家餐廳
理論上,你可以研究:

  • 價格
  • 距離
  • 評價
  • 等待時間
  • 親子友善
  • 優惠

甚至把所有可能全部比較完再做決定。
但現實中很少有人真的這麼做。

我們更可能是:

不要太遠
↓
看起來不錯
↓
價格可以接受
↓
就這間

因為:

找出絕對最好的答案,本身也需要成本

這裡的午餐例子不是在說這就是 Greedy,Greedy 是在這個前提下,進一步給出一套明確規則:

每一步都按照某個局部標準,選擇目前最好的選項


Greedy 的吸引力:不用先展開所有可能

假設有很多個選擇:

A
B
C
D
E
...

每做完一個選擇,又會產生下一層選擇。
如果我們真的試圖把所有可能全部列出來,可能會變成:

        Start
      /   |   \
     A    B    C
    /|\  /|\  /|\
   ...  ...  ...

選擇越多,可能性也可能快速增加。
Greedy 的思考方式則完全不同,它不會把整棵可能性全部展開。
而是:

Start
  ↓
當下最好
  ↓
當下最好
  ↓
當下最好
  ↓
答案

因此 Greedy 最吸引人的地方就是:

我們不需要先把所有可能答案找出來,才開始做選擇。

每一步只需要處理目前的狀態。


如果用 JavaScript 表達,甚至非常直覺

以前面的找零問題來說:

// 假設硬幣已經按照面額由大到小排列
const coins = [50, 10, 5, 1];

function makeChange(amount) {
  const result = [];

  for (const coin of coins) {
    while (amount >= coin) {
      result.push(coin);
      amount -= coin;
    }
  }

  return result;
}

console.log(makeChange(67));

結果:

[50, 10, 5, 1, 1]

這段程式真正值得看的並不是語法。
而是它背後的決策規則:

從最大的硬幣開始
↓
能拿就拿
↓
再處理剩下的金額

我們甚至沒有建立 所有可能的找零方式 這份資料。
因為 Greedy 根本沒有打算比較所有完整答案。


Greedy 其實是一種「承諾」

當我們一開始拿下 50 面額之後,我們並沒有打算再回頭說:

等一下,也許剛剛不要拿 50 比較好。

我們直接接受這個選擇,然後繼續解:

17

也就是說,典型的 Greedy 思路是:

做出眼前這一步的選擇
↓
接受這個選擇
↓
繼續處理剩下的問題

它不像某些搜尋策略會:

試一下
↓
發現不好
↓
退回去
↓
換另一條路

而是傾向於:

做完選擇,就繼續往前

這也是它為什麼簡單。
但這件事同時也埋下了一個很重要的問題。


Greedy 不只是「選最大的」

看到找零問題之後,很容易把 Greedy 記成:

每次選最大的

但這並不精確,Greedy 真正的概念是:

每一步選擇目前看起來最符合目標的選項

在找零問題裡,我們希望:硬幣數量越少越好
因此「優先拿大面額硬幣」看起來很合理。
但換一個問題,Greedy 每一步採用的選擇規則可能完全不同。

例如,如果我們希望:成本最低
那可能每次選:目前最便宜的

如果我們重視的是:越早完成越好
可能優先選:最快能完成的

如果我們想要:取得最大收益
眼前這一步的選擇又可能變成:目前收益最高的

所以 Greedy 不是某一個固定規則。
更準確地說,它是一種解題策略:

先定義什麼叫「更好」
↓
據此決定每一步要怎麼選
↓
每一步選當下最好的

從「找到答案」開始進入「選擇答案」

到這裡,我們其實已經慢慢離開前幾篇主要討論的世界了。
前面的問題,很多時候是在問:

  • 能不能到?
  • 哪些地方會被走訪?
  • 哪些東西會受到影響?

但接下來,我們會越來越常遇到另一類問題:

有很多可行答案
↓
但它們並不是一樣好
↓
那我們應該怎麼選?

這時候,問題的重點開始從 找到答案 走向 根據某個目標選擇答案


Greedy 為什麼這麼常見?

因為它有幾個非常實際的優點。

  1. 簡單:我們不需要同時維護大量可能答案。
  2. :只要每一步的選擇規則很容易判斷,做決定通常就很直接。
  3. 容易理解:很多 Greedy 的規則甚至可以直接用「每次選最XX的」句型概括。
  4. 可能直接得到最優解:在某些問題裡,不需要展開所有可能,一連串局部選擇就能得到整體最佳答案。

有些問題甚至真的可以靠一連串當下看起來最好的選擇,得到整體最好的答案。
所以 Greedy 的核心精神是:

如果問題的結構允許我們只看眼前,就沒有必要把所有可能性全部找出來

真正困難的是去判斷:

我們怎麼知道這個問題允不允許?


當下最好的,不一定代表全局最好

現在回到我們找零的過程:

67 → 50
17 → 10
7  → 5
2  → 1
1  → 1

每一步看起來都非常合理。
因為當下最大的硬幣,似乎都能讓剩餘金額下降得最快。
也就是:

每一步,我們都做了當下最好的選擇

但是我們真正想達成的整體目標是:

最後使用的硬幣總數最少

這是最後用來衡量整個答案好壞的標準。
於是 Greedy 最核心的問題也出現了:

當下最佳
    ↓
當下最佳
    ↓
當下最佳
    ↓
     ?
整體最佳

我們現在其實偷偷做了一個很大的假設:

如果每一步都選得最好,最後的答案應該也會最好。

聽起來非常合理,甚至合理到我們平常根本不會懷疑它。
但演算法真正有趣的地方,往往就是從這些「看起來理所當然」的地方開始。


今天真正要記住的,不是找零

找零只是讓 Greedy 的直覺非常容易被看見。
真正重要的是這個思考模式:

有很多可能答案
↓
先定義什麼叫「更好」
↓
不想把所有答案全部列出來
↓
所以每一步選目前最好的
↓
繼續處理剩下的問題

這就是 Greedy 最核心的精神:

透過一連串當下看起來最好的選擇,希望得到一個好的整體結果

它之所以重要,就是因為:

簡單、快速,而且非常符合人的決策直覺

但也正因如此,我們很容易忽略那個最重要的問題:

每一步都選眼前最好的,看起來很合理;但這樣得到的結果,真的永遠是整體最好嗎?


上一篇
Day 18|當 Graph 本身也會改變,問題有什麼不同?
下一篇
Day 20|每一步都選最好,為什麼最後可能不是最好?
系列文
生活中的資料結構與演算法:30 天學會把現實問題變成可推理的模型21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言