貪婪演算法 與 動態規劃演算法 短視近利 與 深謀遠慮 國立中央大學 資訊工程學系 江振瑞 教授 3.1 貪婪演算法基本概念 2 貪婪解題策略 貪 婪 演 算 法 (greedy algorithm) 使 用 貪 婪 策 略 (greedy strategy)解決問題。 假設一個問題可以藉由一系列的選擇(或決策)來解決,貪 婪演算法的特性為每一次選擇皆採取區域最佳解(locally optimal solution),而透過每一個區域最佳解最後綜合成 為全域最佳解(globally optimal solution)而將問題解決。 換句話說,貪婪演算法一步步地建構出一個問題的完整解 答。其每一步都藉由貪婪解題策略選擇當下最好的部份解 答加入完整解答中以解決問題。 3 使用貪婪解題策略的演算法 背包(Knapsack)演算法 Huffman編碼演算法 Kruskal最小含括樹演算法 Prim最小含括樹演算法 Dijkstra最短路徑演算法 4 3.2 背包演算法 5 背包演算法背景介紹 背包演算法(knapsack algorithm)使用貪婪 解題策略解決背包問題(knapsack problem) 或稱為零碎背包問題(fractional knapsack problem) 以下我們先定義背包問題 6 定義 -- 背包問題 給定一個最大載重容量(capacity)為m的背 包,以及n個可以放入背包的物品,其中第i 個物品的重量為wi>0,價格為pi>0 目標: 找出x1,…,Xn以最大化 w x i 1i n i i m 限制條件為 1in 其中 0xi1, 1 i n i p x 7 背包演算法 Algorithm 背包演算法 Input: 背包的最大容量m,以及可以放入背 包的n個物品的非負重量wi與價格pi Output: 介於0與1之間的x1,…,xn分別代表第1 個,…,第n個物品放入背包中的零碎部份。可 wixi m 。 i 以最大化 pi x,並且滿足 1i n 1i n 1: 將pi/wi由大至小排序。 2: 根據此排序來將物品依序盡可能地放入背 8 包中,直至背包容量m用完為止。 背包演算法時間複雜度 行1: 依pi/wi由大至小排序: O(n log n) 行2: 將物品依序放入背包: O(n) 總時間複雜度: O(n log n) 9 背包演算法範例 給定: n = 3, m = 5, (w1, w2, w3) = (1, 2, 3) (p1, p2, p3) = (20, 60, 45) 貪婪策略解答: p1/w1 = 20/1 = 20 p2/w2 = 60/2 = 30 p3/w3 = 45/3 = 15 最佳解: x2 = 1, x1 = 1, x3 = 2/3 最大總價值: 601+201+45(2/3)=110 10 定義 -- 0/1背包問題 給定一個最大載重容量(capacity)為m的背 包,以及n個可以放入背包的物品,其中第i 個物品的重量為wi>0,價格為pi>0 目標: 找出x1,…,Xn以最大化 w x i 1i n i i m 限制條件為 1in 其中 xi=0 or xi=1, 1 i n i p x 11 0/1背包演算法範例 給定: n = 3, m= 5, (w1, w2, w3) = (1, 2, 3) (p1, p2, p3) = (20, 60, 45) 貪婪策略解答: p1/w1 = 20/1 = 20 p2/w2 = 60/2 = 30 p3/w3 = 45/3 = 15 解答: x2 = 1, x1 = 1, x3 = 0 總價值: 601+201+450=80 最佳解: x1 = 0, x2 = 1, x3 = 1 總價值: 200+601+451=105 12 背包演算法與 0/1背包演算法範例圖示 13 3.3 Huffman編碼演算法 14 Huffman編碼 字元編碼(character coding)可以分為 固定長度編碼: 如ACSII、Unicode 可變長度編碼: Huffman code Huffman編碼以字首碼(prefix code)方式達到 字元編碼最佳資料壓縮(optimal data compression) 字首碼 (prefix code): 任何字元編碼一定不是其他 字元編碼的字首(prefix)。 可以使用二元樹來呈現,達到簡單編碼(encoding) 15 與解碼(decoding)的功能。 Huffman編碼範例 假設給定一個僅用到a, b, c, d, e五個字元的文件, 現在欲針對五個字元進行編碼,以下是可能的固定 長度編碼與可變長度的Huffman字首碼。 字首碼讓出現頻率較高字元的編碼較短,以達到使 用最少位元就可以將所有資料儲存的目標。 a b c 14% 17% 23% 6% 40% 固定長度編碼 000 001 010 011 100 可變長度編碼 1111 110 10 1110 0 出現頻率 d e 16 對應不同編碼的樹及其成本 樹T的成本( 單一字元編碼成本) Cost(T) f (c)d T (c) cC Cost(T)=3 Cost(T)=2.17 17 Huffman編碼演算法 Algorithm Huffman編碼演算法 Input: 字元集合C與每個字元的出現頻率f Output: Huffman編碼樹 1. 2. 3. 4. 5. 6. 7. 8. 9. n |C| //C: the set of n characters Q C //Q: 優先佇列,以字元頻率為優先次序 for i 1 to n – 1 //n個字元(節點)欲合併成一個節點,每迭代合併一次可少一節點 配置一個新的樹節點u u.left x GetMin(Q) u.right y GetMin(Q) u.f x.f + y.f Insert u into Q return GetMIN(Q) 作為Huffman編碼樹的樹根 18 Huffman編碼演算法時間複雜度 行2: O(n)建立優先佇列Q 行3-8: for迴圈一共執行n-1次,而且迴圈中 的優先佇列操作均為O(log n)複雜度,因此 整個迴圈具有O(n log n)的複雜度 總時間複雜度: O(n log n) 19 Huffman編碼演算法的執行範例 20 Huffman編碼演算法的執行範例(續) 21 Huffman編碼演算法的執行範例(續) 22 Huffman編碼演算法的執行範例(續) 23 Huffman編碼演算法的執行範例(續) (4) 24 3.4 Kruskal最小含括樹演算法 25 最小含括樹 最小含括樹(Minimum Spanning Tree, MST)可以定義在歐氏空間(Euclidean space) 或者一個圖(graph)上。 給定一個加權連通無向圖(weighted connected undirected graph) G = (V, E) 含括樹(spanning tree) H= (V, T), T E, 是 一個無向樹(undirected tree),它是G的子圖, 包含G的所有節點 最小含括樹MST是一個擁有最小(minimum)總 加權(weight)或總成本(cost)的含括樹。 26 最小含括樹範例 圖G的最小含括樹(非唯一) 一個圖G 27 Kruskal最小含括樹演算法概念 Kruskal最小含括樹演算法是一個貪婪演算 法(greedy algorithm) 它採取貪婪解題策略產生給定圖G=(V, E) 的最小含括樹H=(V, T),每次都是挑選最 小成本且不形成cycle的邊加入目前的最小 含括樹的邊集合T之中 因為n個節點的樹具有n-1個邊,因此,經 過n-1次邊的挑選之後,就可以形成累積 成本最小的含括樹。 28 Kruskal最小含括樹演算法 Algorithm Kruskal最小含括樹演算法 Input: 無向加權圖G=(V, E),其中|V|=n Output: G的最小含括樹(MST)H=(V, T) 1. T← //T為MST的邊集合,一開始設為空集合 2. while T包含少於n-1個邊 do 3. 選出邊(u, v),其中(u, v)E,且(u, v)的加權(weight)最小 4. E←E-(u, v) 5. if ( (u, v)加入T中形成循環(cycle) ) then 將(u, v)丟棄 6. else T←T(u, v) 7. return H=(V, T) 29 Kruskal最小含括樹演算法執行範例 30 Kruskal最小含括樹演算法討論 Q: 我們如何檢查加入新的邊是否會形成循環? A: 使用 集合 (SET) 尋找與 聯集 (UNION)操作 使用 集合 (SET) 與 聯集 (UNION)操作。 考慮樹的節點集合: 一開始產生n個包含單一節點的集合;也就是說若 V={v1,…,vn} ,則產生{v1}, {v2},…,{vn} 加入邊(u, v)是否會形成循環: 找出u,v所屬的集合,若 u, v 在相同的集合, 則加入邊(u, v) 會形成循環。反之,若uS1 , vS2 ,而且 S1S2則加入邊(u, v)不會形成循環,此時應對 S1 與 S2 進行聯集操作。 31 Kruskal演算法的時間複雜度 時間複雜度: O(|E| log|E|) 排序 : O(|E| log|E|) 行2-6 迴圈 (幾乎每個邊都要檢查) O(|E|) 找出元素所在的集合 O(log |V|) 聯集兩集合 O(log |V|) : O(|E| log |V|) O(|E| log|E|) |E| |V|2 =O(|V|2 log |V|) =O(n2 log n) 32 3.5 Prim最小含括樹演算法 33 Prim最小含括樹演算法概念 Prim最小含括樹演算法是一個貪婪演算法(greedy algorithm)。 它採取貪婪解題策略產生給定圖G=(V, E)的最小含 括樹H=(V, T)。此演算法先隨意挑一個節點加入集 合X中,此後每次都挑選一個一端的節點在X中, 而另一端的節點在(V-X)中的最小成本的邊。如此, 可保證將所挑選的邊加入T之後不會形成循環 (cycle),這代表H=(V, T)是一棵樹(tree)。 等挑完n-1個邊之後,H=(V, T) 就是最小含括樹 (MST)。 34 Prim最小含括樹演算法 Algorithm Prim最小含括樹演算法 Input: G=(V, E)為無向加權圖,其中|V|=n Output:G的最小含括樹(MST)H=(V, T) 1. T← //T為MST的邊集合,一開始設為空集合 2. X←{v} //隨意選擇一個節點v加入集合X中 3. while T包含少於n-1個邊 do 4. 選出(u, v)E,其中uX且vV-X,且(u, v)的加權(weight)最小 5. T←T(u, v) //(u, v)是一個邊 6. X←X{v} 7. return H=(V, T) 35 Prim最小含括樹演算法執行範例 36 Prim最小含括樹演算法時間複雜度 總時間複雜度: O(n2),因為 外層的while迴圈(行3-6): n-1 O(n) 內層迴圈(行4): 在(u, v)中選擇最小加 權,其中u屬於X,v屬於V-X O(n) (藉著使用Prim提出的兩個向量C1和C2) (Ref: R. C. Prim, “Shortest connection networks and some generalizations,” Bell System Technical Journal, 36(1389–1401), 1957.) 比較: 如果 |E|<<n2 ,則採用Kruskal演算法 (複雜度O(|E| log|E|)效能較佳 37 3.6 Dijkstra最短路徑演算法 38 圖的最短路徑 由圖(graph)中的某個節點(vertex or node)v到圖中的另一節 點u,若v到u之間存在一條路徑(path),則路徑中所經過的 邊(edge)的加權(weight)的總合稱為路徑的成本(cost)或距離 (distance)。所有路徑中具有最小成本的稱為最短路徑 (shortest path)。 由於最短路徑具有許多應用,因此有許多求取最短路徑的 演算法,著名的演算法包括: (1) Dijkstra演算法(使用貪婪解題策略) (2) Bellman-Ford演算法(使用動態規劃解題策略) (3) Floyd-Warshall演算法(使用動態規劃解題策略) 39 Dijkstra最短路徑演算法設計者 E. W. Dijkstra(1930年5月11日-2002 年8月6日)生於荷蘭鹿特丹 在1972年獲得圖靈獎(Turing Award) 2002 年 , Dijkstra 獲 得 了 ACM PODC (Principles of Distributed Computing) 最具影 響力 論文 獎(Influential Paper Award) , 以 表 彰 他 在 分 散 式 計 算 (distributed computing)領域中關於自 我穩定(self stabilization)計算模式的 貢獻。為了紀念他,這個每年一度獎 項 也 在 此 後 被 更 名 為 Dijkstra 獎 (Dijkstra Prize) Source: http://en.wikipedia.org/wiki/Edsger_W._Dijkstra Creative Commons Attribution-Share Alike 3.0 Unported Author:Hamilton Richards 40 Dijkstra最短路徑演算法 是喝咖啡時20分鐘想出的發明 “One morning I was shopping in Amsterdam with my young fiancée, and tired, we sat down on the café terrace to drink a cup of coffee and I was just thinking about whether I could do this, and I then designed the algorithm for the shortest path. As I said, it was a 20-minute invention. In fact, it was published in 1959, three years later.” Thomas J. Misa (Editor), "An Interview with Edsger W. Dijkstra," Communications of the ACM 53 (8): 41–47, 2010. Attribution 2.0 Generic (CC BY 2.0) Elliott Brown Source: https://www.flickr.com/photos/ell-rbrown/14165662691/in/photolist- 41 Dijkstra最短路徑演算法介紹 Dijkstra演算法: Dijkstra演算法屬於求取單一(single)(來)源 (source)(節)點至全部(all)(目)標(destination)(節)點的單源點 至全標點之一至全(one-to-all)最短路徑演算法。 Dijkstra演算法只能用在所有的邊都是非負邊(non-negative weighted edge)的圖。因為負邊有可能產生負循環,因而無 法產生正確的最短路徑,而Dijkstra演算法並無法檢查給定 的圖是否有負循環。 Dijkstra最短路徑演算法採用貪婪策略解決問題,每次都挑 選一個目前可以由源節點抵達距離(累積邊加權)最小的節點 往外調整其鄰居節點的最短路徑距離。在經過n次(n為節點 個數)的節點選擇之後,則所有的節點都可以求得由單一源 節點可以抵達的最短路徑距離。 42 Dijkstra最短路徑演算法 Algorithm Dijkstra最短路徑演算法 Input:給定一個非負加權有向圖(non-negative weighted digraph)G=(V, E),及一個來源 (source)節點s。G各邊的加權值以w[x][y]表示,其中x 及y為邊的二個節點。 Output:對每一個節點u而言,傳回一個由s到u的最短路徑距離(累積邊加權)d[u]。 1. d[s]←0; d[u]←∞ for each u≠s 2. 將每一個節點加入優先佇列Q 3. while Q≠ do 4. 自Q中移出具有最小d[u]值之節點u 5. for 每一個與u相鄰之節點x do 6. if d[x] > d[u]+w[u][x] then 7. d[x]←d[u]+w[u][x] 8. return d 43 Dijkstra最短路徑演算法 如何記錄所有路徑經過的節點? 若要讓Dijkstra演算法也能夠求出每一條最短路徑所經過的每 一個節點,則我們要將每一節點在最短路徑中的前一節點紀 錄下來,其作法為增加一個陣列p(代表predecessor,前行節點) 來記錄最短路徑中的每一個節點的前一節點。 Dijkstra演算法之if敘述修改如下: if (d[x] > d[u]+w[u][x]) then d[x]←d[u]+w[u][x] p[x]←u //此為新加敘述,代表在最短路徑中節點x的前一節點為u 44 Dijkstra演算法執行範例 45 Dijkstra最短路徑演算法 時間複雜度 假設G一共有n個節點,m個邊(也就是|V|=n, |E|=m) 行2將每一個節點加入優先佇列Q,因此Q具有n個元素 行3的while迴圈每次迭代會自Q中次移出一個節點,因此會 執行n次迭代 行4使用O(log n)時間自Q中移出最小d[u]值之節點u 行5的for迴圈在整個演算法的執行過程中一共執行m次迭代 行7使用O(log n)的時間根據新的d[u]值更新u在Q中的位置( 先刪除u再新增u) 因此總時間複雜度為O((n+m) log n)=O( (|V|+|E|) log |V|) 46 3.7 動態規劃演算法基本概念 47 動態規劃解題策略 動態規劃演算法(dynamic programming algorithm)使用動態 規劃策略(dynamic programming strategy)解決問題。它將 原問題分解成一系列相依子問題(subproblems),並作出一 系列的決策解決子問題以解決原問題。 然而這些子問題是相依的,因此一個子問題的決策需要遞迴 地參考許多不同子問題的決策。為避免一再地解重複的子問 題,一旦確認解出子問題的最佳解答(solution),即會將其存 在表格(或陣列)中。當需要用到某一子問題的解答時,即直 接從表格中取出其解答以節省遞迴計算時間,是一個「系統 化」的、「節省不必要計算」的、「以空間換取時間」的演 算法。 48 動態規劃解題策略(續) 一般動態規劃演算法的設計原則如下: 決定需要建構的表格 決定遞迴關係 決定填表格順序,用以填完表格而解決問題 一 般 依 照 遞 迴 關 係 中 的 邊 界 條 件 (boundary condition)將初始值(initial value)填入表格,然 後以由底而上(bottom-up)的方式持續運行直至 填完整個表格而求出原問題解答為止。 49 動態規劃與貪婪演算法之比較 比較: 二者都是透過一系列的決策以解決問題,但是有以下的 不同點: 在貪婪演算法中,任何決策都是獨立(independent)的,都 只要考慮區域最佳解(locally optimal)。 而這些區域最佳解最後可以匯總為全域最佳解 (globally optimal solution)。 在動態規劃演算法中,決策是相依的(dependent),也就是 說每個決策不能只是考慮區域最佳解。 若需要執行n次決策,則第i次決策與第1次到第i-1次連續決策所產 生的狀態是相依的;或是說第i次決策與第i+1次到第n次連續決策所 產生的狀態是相依的。 50 使用動態規劃解題策略的演算法 多階圖最小成本路徑演算法 0/1背包演算法 最長共同子序列演算法 矩陣鏈乘積演算法 Bellman-Ford最短路徑演算法 Floyd-Warshall最短路徑演算法 51 3.8 多階圖最小成本路徑演算法 52 多階圖最小成本路徑問題簡單範例 輸入: 一個多階圖 輸出: 節點S到節點T的 最小成本路徑 就像分三期付款以取得商品的產權一樣。有的付款方式第一期要 繳1萬,有的要繳3萬,有的要繳6萬。但是依照不同的第一期繳 法,則在第二期及第三期有不同的繳款選擇,而造成繳款總額的 不同。我們當然想要選擇最少繳款總額的方式。 貪婪演算法無法解決此問題: S A D T cost = 1+2+17 = 20. 實際最短路徑: S C F T cost = 6+3+1 = 10. 53 多階圖定義 一個k階(k-stage),k2,多階圖(multi-stage graph) G=(V,E)是一個加權有向圖(weighted digraph),其節 點被分割置入k個互斥集合(disjoint sets) V1, V2, …,Vk。 其中每一個集合Vi, 1 i k, 被定義為圖中的第i階(ith stage),而且V1 和 Vk 都僅包含一個節點(node or vertex)。V1 中的節點稱為源點(source),而Vk中的節 點稱為標點(target或destination)。 此外,若<u,v>是E的邊,則uVi 且 vVi+1,1 i <k, 而每個邊都有一個加權(weight)(或稱為成本或距離)。 54 多階圖範例 以下是一個4階多階圖 55 多階圖最小成本路徑問題 多階圖最小成本路徑問題(multi-stage graph minimum-cost path problem)定義: 給定一個k階多階圖G=(V,E),找出從 V1 中的源點 (source) v1(或s) 到 Vk 中的標點(target) vk(或t) 的最小成本路徑(minimum-cost path)。 56 多階圖最小成本路徑問題範例 範例:從以下多階圖(multi-stage graph)中找出v1 到v4 的 最短路徑 貪婪演算法解答: v1v2,2v3,1v4 cost = 1+1+29=31 最佳解: v1v2,3v3,4v4 cost = 6+3+1=10 貪婪演算法無法解決此問題,這是因為不同階(stage)的決 策會影響到其他階的決策,它們是相依的。 57 使用動態規劃演算法解決 多階圖最小成本路徑問題 決定表格結構: 我們首先決定使用陣列d[x]來儲存節點x 到標點的最短路徑距離(或成本),使用陣列p[i]來儲存最 短路徑在第i階所經過的節點。 然後我們決定表格間不同元素的遞迴關係(如下頁所示)。 最後我們由底而上地(由第k, 第k-1,…,到第1階)填完表格 而得到解答。 58 動態規劃遞迴關係 動態規劃遞迴關係: d[S, T] = min{1+d[A, T], 3+d[B, T], 6+d[C, T]} 59 動態規劃遞迴關係(續) d[A, T] = min{2+d[D, T], 10+d[E, T]} = min{2+(17+d[T,T]), 10+(12+d[T,T])} = min{2+17+0, 10+12+0}=19. 邊界條件: d[T, T]=0 60 多階圖最小成本路徑演算法 Algorithm 多階圖最小成本路徑演算法 Input: 具n個節點的k階多階圖G(V, E), V= i=1..k Vi , ViVj= for ij, V1={1},Vk={n}, <x,y>E (xVi yVi+1), <x,y>的加權為w[x,y] Output: 具k個節點由節點1到節點n的最小成本路徑(記錄在p[1..k]中)及其最 小成本(記錄在d[1]中) 1. d[n]=0; d[1..n-1]=; //陣列d[x]儲存節點x到標點t的最小距離(distance) 2. for ik-1 to 1 do 3. for every node x in Vi do 4. for every edge <x, y>E do 5. if (d[x]>w[x,y]+d[y]) do 6. d[x]=w[x,y]+d[y] 7. s[x]=y //s代表節點的下節點(successor) 8. p[1]=1;p[k]=n; //陣列p[i]儲存最短路徑在第k階的節點 9. for i2 to k-1 do p[i]s[p[i-1]]; 61 10. return p,d[1] 多階圖最小成本路徑演算法 時間複雜度 行1: 設定d[0],d[1..n]啟始值: O(n) 行2-4: 三層for迴圈實際執行|E|次迭代: O(|E|) 行5-7: 迴圈內if敘述執行需要常數時間 行8: 2個設定敘述 行9: for迴圈執行k-2次迭代: O(k) 行10: 回傳演算法執行結果需要常數時間 總時間複雜度: O(n + |E| + k) O( |E| ) 62 3.9 0/1背包演算法 63 定義 -- 0/1背包問題 給定一個最大載重容量(capacity)為m的背 包,以及n個可以放入背包的物品,其中第i 個物品的重量為wi>0,價格為pi>0 目標: 找出x1,…,xn以最大化 限制條件為 w i x i m p x 1i n i i 1i n 其中 xi=0 or xi=1, 1 i n (令 S={1,…,n}且T={i | xi=1}S) 64 使用暴力法解決0/1背包問題 0/1背包問題是一個最佳化問題(optimization problem)。 我們可以測試S集合的所有2n個子集合來解這問題, 其時間複雜為O(2n)。 Q: 有任何比暴力法更好的演算法嗎? A: 有。我們可以利用動態規劃(dynamic programming)策略,用空間(表格或陣列)以換取 時間(trade space for time)。 65 0/1背包演算法 建構表格(陣列) 我們建構一個陣列P[0..n, 0..m]。 元素P[i, w], 1in, 0wm, 用以儲存物品集 合{1,2,…,i}的所有可能子集合中的物品最大總 價值,而這些物品的總重量小於等於w。 如果我們可以計算出陣列中的所有元素,那麼 元素P[n, m]儲存的值為物品集合{1,2,…,n}的 所有可能子集合中的物品最大總價值,而這些 物品的總重量小於等於m。 66 0/1背包演算法 確定遞迴關係 初始化設定(initialization): P[0, w]=0 for 0wm 對所有的i與w(0in, 0wm)而言: P[i, w]= max(P[i-1, w], pi+P[i-1, w-wi]), if wiw P[i, w]=P[i-1, w], otherwise 67 0/1背包演算法 由底而上的逐一將表格填完 i, w由小而大,或稱由底而上(bottom up),根據 上頁的遞迴關係,逐一計算P[i, w]的值,將表格 填完。 P[i, w] i=0 1 2 w=0 0 1 0 2 0 … 0 m 0 … n 68 0/1背包演算法 Algorithm 0/1背包演算法 Input:背包的最大容量m,以及可能放入背包的n個物品(編號為1,…,n)的非 負重量wi與價格pi, 1in。 Output: 以全放或全不放的方式容納在背包中所有物品的最大總價值 1. for w←0 to m do P[0, w]=0 2. for i←1 to n do 3. for w←0 to m do 4. if wiw then 5. P[i,w]=max(P[i-1, w], pi+P[i-1, w-wi]) 6. else 7. P[i,w]=P[i-1, w] 8. return P[n,m] 69 0/1背包演算法範例 Input: n=3, m=8 i wi pi 1 5 20 2 3 30 3 4 10 Table: P[i,w] w=0 i=0 0 1 0 2 0 3 0 1 0 0 0 0 2 0 0 0 0 3 0 0 30 30 4 0 0 30 30 5 0 20 30 30 6 0 20 30 30 7 0 20 30 40 8 0 20 50 50 Output: p[3,8]=50 70 0/1背包演算法 時間複雜度 行1: 設定P[0,1..m]初始值: O(m) 行2: 外層for迴圈執行n次迭代: O(n) 行3: 內層for迴圈執行m次迭代: O(m) 行4-7: 迴圈內敘述執行需要常數時間 行8: 回傳演算法執行結果需要常數時間 總時間複雜度: O(n m) 71 0/1背包演算法時間複雜度討論 0/1背包演算法時間複雜度為O(nm) Q: 這是多項式時間嗎? A: 實際上這個演算法是屬於偽多項式時間(pseudopolynomial time)。 如果一個演算法的時間複雜度是輸入數值(numeric value of the input)的多項式,但卻是輸入長度(the length of the input)的指數函數,那麼該演算法屬於偽多項式時間複雜度。 例如,若m以長度為K位元的無正負號的二進位數字來表示, 則表格p有2k-1個直欄,而0/1背包演算法時間複雜度為 O(n2k),為輸入長度k的指數函數。 72 0/1背包演算法 如何找出哪些物品該放在背包中? 前面提到的0/1背包演算法只計算P[i,w], 也就是背包中物品的最高總價值,並沒有 記錄哪些物品所形成的子集合T(TS)可以 達成最高總價值的最佳解。 為了計算出實際達成最佳解的子集合,我 們增加一個輔助的布林陣列Q[1..n, 1..m] 。若P[i,w]得最佳解答時,第 i 個物品在 子集合T中,則Q[i,w]設為 1;否則Q[i,w] 設為 0。 73 0/1背包演算法 如何找出哪些物品該放在背包中?? 若Q[n, m]為1,則nT,我們可以遞迴地檢查Q[n-1, m-wn] 若Q[n, m]為0,則 nT,我們可以遞迴地檢查Q[n-1, m] 因此,以下的碼可以找出T中的元素。 k=m T for in to 1 do if Q[i, k]=1 then TT{i} k=k-wi 下頁中之完整演算法可以找出正確的集合T 與T所對應的物品最大總價值P[n,m] 74 Algorithm 0/1背包演算法 Input:背包的最大容量m,以及可能放入背包的n個物品的非負重量wi與價格pi Output: T與p,其中T為物品子集,T中物品總重小於m而且可以產生最大總價值p 1. for w←0 to m do P[0, w]=0 2. for i←1 to n do 3. for w←0 to m do 4. if (wiw) ((pi+P[i-1, w-wi])>P[i-1, w]) then 5. P[i,w]= pi+P[i-1, w-wi] 6. Q[i,w]=1 7. else 8. P[i,w]=P[i-1, w] 9. Q[i, w]=0 10. k=m 更完整的0/1背包演算法 11. T 12. for in to 1 do 13. if Q[i, k]=1 then 14. T=T{i} 15. k=k-wi 16. return T, P[n,m] 75 3.10 最長共同子序列演算法 76 最長共同子序列 以下我們說明最長共同子序列(Longest Common Subsequence, LCS or LCSS)相關背景知識 令X為一個由若干符號依序排列組成的序列(sequence), 則X的子序列(subsequence)為從X刪除(不必要為連續性 的)0個或多個符號的序列 例: 令X = b a c a d,則ad, ac, bac, acad, bacad, bcd等 與空序列都是X的子序列 例: 序列X = b a c a d 與 Y = a c c b a d c b 的共同子序 列(common subsequence)有: ad, ac, bac, acad等 例: 序列X與Y的最長共同子序列(longest common subsequence)為: a c a d 77 最長共同子序列應用: DNA序列比對 DNA = {A|C|G|T}* (A: 腺嘌呤; C: 胞嘧啶; G: 鳥嘌呤; T: 胸腺嘧啶) S1=ACCGGTCGAGTGCGGCCGAAGCCGGCCGAA S2=GTCGTTCGGAATGCCGTTGCTGTAAA Q: S1與S2是否為相似的DNA序列? A: 這問題可以由找出S1與S2的最長共同子序列 來解決。 78 最長共同子序列應用: 化身路徑群組 化身路徑群組(Avatar Path Clustering): 由於相似的個性,興趣或習慣,網路虛擬環境(Networked Virtual Environment, NVE)或巨量多 人線上遊戲(Massively Multiplayer Online Game, MMOG)的用戶或化身(avatar)可能具有相似的 行為模式,導致在虛擬世界中有著類似的化身路徑。 我們希望將類似的化身歸類為一個群組,並為他們找到代表性路徑(representative path, RP)。 參考論文: Jehn-Ruey Jiang, Ching-Chuan Huang, and Chung-Hsien Tsai, “Avatar Path Clustering in Networked Virtual Environments," in Proc. of the 4th International Workshop on Peer-to-Peer Networked Virtual Environments (P2PNVE 2010), 2010. 下圖來源: Huiguang Liang, Ransi Nilaksha Silva, Wei Tsang Ooi, Mehul Motani, “Avatar mobility in user-created networked virtual worlds: measurements, analysis, and implications,” Multimedia Tools and Applications, v.45 n.1-3, p.163-190, October 2009. 79 最長共同子序列應用: 化身路徑群組(續) 在第二人生(Second Life, SL)贈品島(Freebies Island)的兩條路徑有多相似? 80 最長共同子序列應用: 化身路徑群組(續) 將化身路徑轉為序列: 將虛擬世界切割為方格 (grid cell),並針對化身 路徑每隔固定時間取樣, 找出其所在方格編號形成 序列。 SeqA:C60.C61.C62.C63.C55.C47.C39.C31.C32 81 最長共同子序列應用: 化身路徑群組(續) SeqA :C60.C61.C62.C63.C55.C47.C39.C31.C32 SeqB :C60.C61.C62.C54.C62.C63.C64 LCSSAB :C60.C61.C62. C63 找出兩條路徑對應的 最長共同子序列以衡 量其相似程度。 82 最長共同子序列問題 以下我們定義最長共同子序列(Longest Common Subsequence, LCS)問題: 給定兩個序列X = <x1,x2,...,xm>, Y = <y1,y2,...,yn>,找出X和Y的最長共同子序 列長度 83 最長共同子序列問題 暴力法時間複雜度 產生序列X(或Y)的所有子序列,然後檢查每 個子序列是否也是序列Y(或X)的子序列,然 後儲存下最長的子序列並輸出。複雜度為: n 2m = O(2m) 或 m 2n = O(2n ) Q1: 如何產生一個序列的所有子序列? Q2: 如何檢查一個序列是否為另一個序列的 84 子序列? 最長共同子序列動態規劃演算法 建構表格 我們定義Xi = < x1,x2,...,xi > 及Yj = <y1,y2,...,yj>。 我們建構表格c[0..m,0..n],令c [i, j]紀錄Xi 和Yj 的LCS的長度 則c[m, n]紀錄X = <x1,x2,...,xm>和Y = <y1,y2,...,yn>的LCS的長度 85 最長共同子序列動態規劃演算法 遞迴關係 如前頁所定義,Xi = < x1,x2,...,xi >及Yj = <y1,y2,...,yj>,c [i, j]記錄Xi 和Yj 的LCS的長度, 則我們有以下遞迴關係: if i=0 or j=0 0 if i,j>0 and xi=y c[i -1, j -1] +1 c[i, j] max{c[i, j -1], c[i -1, j]} if i,j>0 and x i y j j 86 最長共同子序列動態規劃演算法 由底而上的逐一將表格填完 i, j由小而大,或稱由底而上(bottom up),根 據上頁的遞迴關係,逐一計算c[i, j]的值, 將表格填完。 c[i, j] i=0 1 2 … m j=0 0 0 0 1 0 2 0 … 0 n 0 0 0 87 最長共同子序列演算法 Algorithm 最長共同子序列演算法 Input: 兩個序列X = <x1,x2,...,xm>, Y = <y1,y2,...,yn> Input: X和Y的最長共同子序列長度 1. m |X| //m記錄序列X的長度 2. n |Y| //n記錄序列Y的長度 3. for i 1 to m do c[i, 0] 0 4. for j 1 to n do c[0, j] 0 本演算法參考以下書籍改寫: CORMEN, Thomas H., et al. Introduction to algorithms. Cambridge: MIT press, 2001. 88 最長共同子序列演算法(續) 我們同時建構陣列 5. for i 1 to m do d[1..m, 1..n],並使 6. for j 1 to n do 用d[i,j]記錄c[i,j]最 7. if xi = yj then 大值的參考由來 8. c[i, j] c[i-1, j-1]+1 9. d[i, j] “” //對應i-1及 j-1 10. else if c[i–1, j] c[i, j-1] then 11. c[i, j] c[i-1, j] 12. d[i, j] “” //對應i-1 13. else 14. c[i, j] c[i, j-1] 15. d[i, j] “” //對應 j-1 16. return c and d 89 最長共同子序列演算法 時間複雜度 行5的外層for迴圈一共有m次迭代 行6的內層for迴圈一共有n次迭代 行7-15的if敘述需要常數時間 因此總時間複雜度為O(mn) ,相對於暴力法的 O(2m)或 O(2n),有非常大的改善。 90 最長共同子序列演算法 執行範例 X=DBCBE Y=BACDBA 91 最長共同子序列演算法 如何找出最長共同子序列 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. LCS //LCS初始值為空序列 im; jn while |LCS| <c[m, n] do // |LCS| 表示LCS的長度 if d[i, j] = “” then LCS xi || LCS // || 表示序列連結 i--; j--; 行18-25while迴圈時間複雜 else if d[i, j] = “” then 度為O(m+n),因為當i由m i-減為1時且/或j由n減為1時 else // d[i, j] = “” 一定會產生正確的LCS j-return LCS, |LCS| (也就是c[m, n]) 92 3.11 矩陣鏈乘積演算法 93 二個矩陣相乘所需的純量乘法數 假設 M 是一個xy的矩陣,N 是一個yz的矩 陣, 則 MN需要xyz次的純量乘法計算。 a d 1 2 3 例如: M = 4 5 6 2x3 , N = b e c f 3x2 1a + 2b + 3c 則MN = 4a + 5b + 6c 1d + 2e + 3f 4d + 5e + 6f 2x2 94 矩陣相乘執行順序非常關鍵 假設M1是個5200的矩陣,M2是一個20010 的矩陣,M3是一個10100的矩陣。 那麼算出((M1M2)M3)需要 520010+1010100=5000+10000= 15000次的純量乘法計算。 然而算出(M1(M2M3))卻需要20010100 + 5200100=200000+100000= 300000次的純量乘法計算。 95 矩陣鏈乘積問題 定義: 矩陣鏈乘積問題(matrix-chain product problem) 給定一個含n個矩陣的矩陣鏈乘積 M1M2…Mn,其中矩陣Mi, 1in, 的維度為 ei-1ei 。 找 出 一 種 M1M2…Mn 的 完 全 括 號 (fully parenthesized)方式,以便可以使用最 少的純量乘法求出M1M2…Mn的乘積。 96 完全括號矩陣鏈乘積 若 一 個 矩 陣 鏈 乘 積 為 完 全 括 號 (fully parenthesized),則其為單一矩陣或為兩個完 全括號矩陣鏈乘積相乘並加括號。 例如: X、Y是完全括號矩陣鏈乘積,(XY)也 是完全括號矩陣鏈乘積 又例如: ((XY)Z)與(X(YZ))都是完全括號矩 陣鏈乘積 97 矩陣鏈乘積問題 窮舉所有不同的完全括號方式 長度為n的矩陣鏈乘積不同的完全括號方式的總數P(n): 相當於將矩陣鏈乘積在第k個矩陣之後與第k+1個矩陣 之前,使用括號分為二組後再計算其乘積,而1kn-1 。我們可得: 1 if n 1 P(n) n -1 P(k)P(n - k) if n 2 k 1 實際上,P(n)為對應到n-1的卡塔蘭數(Catalan number)=Cn-1 =O(2n) 98 卡塔蘭數(Catalan number) 卡塔蘭數是以比利時的數學家卡塔蘭(Eugène Charles Catalan, 1814–1894)命名。 卡塔蘭數的一般項公式為 n 2n 1 4 Cn n + 1 n O( n 3/2 ) 99 矩陣鏈乘積演算法 建構表格 使用表格m[1..n, 1..n],定義 m[i, j] = 計算 Mi…Mj所需的最小純量相乘數,其中Mi 的維度為ei-1 ei,1 i, j n。 目標為求得m[1, n] 除了表格m之外,矩陣鏈乘積演算法利用 另一個輔助表格s[1..n, 1..n]來記錄矩陣鏈 分割(split)的位置。s[i, j]記錄哪一個分割 位置造就了m[i, j]的最小純量相乘數。 100 矩陣鏈乘積演算法 遞迴關係 定義 m[i, j]= 計算Mi…Mj所需的最小相乘數 ,其中Mi維度為ei-1ei,1 i, j n。 0 i j m[i, j]= minik j{m[i, k] + m[k + 1, j] + ei-1e k e j} i j 101 矩陣鏈乘積演算法 由底而上的逐一將表格填完 因為邊界條件為i=j,因此我們根據11,22,…,nn, 12,23,…,(n-1)n, 13,24,(n-2)n, …, 1n的方式填表,也就是 根據 j-i的差值d由0到n-1,或稱由底而上(bottom up),根 據上頁的遞迴關係,逐一計算m[i, j]的值,將表格填完。 d=n-1 d=… 因為ji,因此 表格中有一半 的格子不使用。 d=2 d=1 d=0(符合邊界條件的初始值)102 矩陣鏈乘積演算法 本演算法參考以下書籍改寫: CORMEN, Thomas H., et al. Introduction to algorithms. Cambridge: MIT press, 2001. Algorithm 矩陣鏈乘積演算法 Input: 矩陣鏈乘積M1M2…Mn Output: 計算矩陣鏈乘積M1M2…Mn所需最少的純量乘法數 1. for i 1 to n do 2. m[i, i] 0 3. for d 1 to n – 1 do 4. for i 1 to n – d do 5. ji+d 6. m[i, j] 7. for k i to j – 1 do 8. t m[i, k] + m[k+1, j]+ ei-1ekej 9. if t < m[i, j] then 10. m[i, j] t 11. s[i, j] k 12. return m and s 103 矩陣鏈乘積演算法 時間複雜度 行3的外層for迴圈一共有O(n)次迭代 行4的中層for迴圈一共有O(n)次迭代 行7的內層for迴圈一共有O(n)次迭代 行8-11的敘述需要常數時間計算 因此總時間複雜度為O(n3) 104 矩陣鏈乘積演算法 印出最佳全完括號矩陣鏈乘積 Algorithm 印出完全括號矩陣鏈乘積(s, i, j) Input: 矩陣鏈乘積演算法輸出的表格s,整數i與j, ij Output: 將完全括號矩陣鏈乘積印出 藉由呼叫印出完全括號矩陣鏈乘積(s, 1, n)就可 1. if i=j then 以印出M M …M 的完全括號方式。因為陣列s 2. print “M” ||i 是矩陣鏈乘積演算法產生的,用以記錄產生最 少純量乘法數的完全括號矩陣鏈乘積,因此呼 3. else 叫印出完全括號矩陣鏈乘積(s, 1, n)印出的是 M M …M 的最佳完全括號方式。 4. print “(“ 5. 印出完全括號矩陣鏈(s, i, s[i,j]) 6. 印出完全括號矩陣鏈(s, s[i,j]+1, j) 本演算法參考以下書籍改寫: 7. print “)” 1 1 2 2 n n CORMEN, Thomas H., et al. Introduction to algorithms. Cambridge: MIT press, 2001. 105 3.12 Bellman-Ford 最短路徑演算法 106 Bellman-Ford最短路徑演算法 介紹 與Dijkstra演算法相同,Bellman-Ford演算 法也是屬於求取單一源點(single source)至 全部標點(all destination)的一至全(one-toall)最短路徑演算法。 但是與Dijkstra演算法不同的是,BellmanFord演算法可以在具有負加權邊(negativeweight edge)的圖也可以正確的執行,並且 可 以 檢 查 圖 是 否 有 負 加 權 循 環 (negativeweight cycle)。 107 Bellman-Ford最短路徑演算法 介紹(續) Bellman-Ford最短路徑演算法採用動態規劃 策略解決問題,找出由源點s到其他全部節點 的最短路徑。 Bellman-Ford演算法一開始在第1次迭代先求 出所有屬於1-邊路徑(1-edge path)的最短路徑 ,並將其最短路徑距離儲存在陣列中。 所謂1-邊路徑,就是由源點s開始,只能經過 1個邊所造成的路徑。 所謂k-邊路徑,就是由源點s開始,只能經過 k個邊所造成的路徑。 108 Bellman-Ford最短路徑演算法 介紹(續) 然後Bellman-Ford演算法基於儲存在陣列中 的1-邊最短路徑結果,在第2次迭代針對每個 邊(u, x),由u往外調整到x的最短路徑距離, 可以得出所有屬於2-邊路徑(2-edge path)的最 短路徑。 依此類推則在第n-1次迭代可以求出所有屬於 (n-1)-邊路徑((n-1)-edge path)的最短路徑。 因為具n個節點的圖的最長路徑最多具有n-1 個邊,因此第n-1次迭代求出的路徑已經是最 終的正確結果了。 109 Bellman-Ford最短路徑演算法 Algorithm Bellman-Ford最短路徑演算法 Input: 給定一個加權有向圖(weighted digraph)G=(V, E),及一個來源(source)節點s。G各邊的 加權值以w[x][y]表示,其中x 及y為邊的二個節點。 Output: 對每一個頂點u而言,傳回一個由s到u的最短路徑距離(累積邊加權)d[u],及每個節 點u最短路徑上的前節點(predecessor)p[u]。 1. d[s]←0; d[u]←∞ for each u≠s 2. for i←1 to |V|-1 do 3. for 每一個G的邊(u, x) do 4. if d[x] > d[u] + w[u][x] then 5. d[x]← d[x] + w[u][x] 6. p[x]← u 7. for 每一個G的邊(u, x) do //檢查有無負循環(negative-weight cycle) 8. if d[x] > d[u] + w[u][x] then exit //代表有負循環,無法產生正確結果 9. return d, p 110 Bellman-Ford最短路徑演算法 時間複雜度 假設G一共有n個節點,m個邊(也就是|V|=n, |E|=m) 行2-6的外層for迴圈一共有O(n)次迭代 行3-6的內層for迴圈一共有O(m)次迭代 行4-6為內層if指令,針對每個邊(u, x)依據目前的d[u]值調 整d[x],其執行需要常數時間 行7-8的for迴圈在求出(n-1)邊路徑之後再針對每個邊(u, x) ,檢查目前的d[u]值,一共有O(m)次迭代 因此總時間複雜度為O(n m)= O(|V| |E|) 111 Bellman-Ford最短路徑演算法 執行範例 112 3.13 Floyd-Warshall 最短路徑演算法 113 Floyd-Warshall最短路徑演算法 介紹 與Dijkstra演算法與Bellman-Ford演算法不同的是, Floyd-Warshall演算法可以求出全部節點配對的最 短路徑,是一個全配對最短路徑(all-pair shortest path)演算法。 Floyd-Warshall演算法可以處理具有負加權邊的圖 ,但是不能用以檢查一個圖是否具有負加權循環。 114 Floyd-Warshall最短路徑演算法 介紹(續) Floyd-Warshall演算法採用動態規劃策略解決問題,利 用一個n×n(n為節點總數)的二維陣列d來記錄每一節點 配對間的最短路徑成本或距離(distance) 。 在啟始(initial)狀況時, d[i][j]=w[i][j], for each i and j。 ( 注 意 : w[i][j]=0, for i=j; w[i][j]=, for (i, j)E; w[i][j]=the weight of (i, j), for (i, j) E) Floyd-Warshall演算法執行時會不斷的更新陣列d。在第 k次更新陣列d時,表示d中所紀錄的最短路徑是經由編 號小於或等於k的節點當作中間節點所造成的。因此, 當第n次更新陣列d時,則表示d中所紀錄的最短路徑是 可以經由所有節點當作中間節點所造成的,這也就是 演算法所需要的結果。 115 Floyd-Warshall最短路徑演算法 Algorithm Floyd-Warshall最短路徑演算法 Input:一個加權有向圖(weighted digraph)G,其中節點編號為1,…,n,而 各邊的加權值以w[x][y]表示,x 及y為邊的二個節點 Output:G中的每一個節點配對的最短路徑距離d[x][y],及對應的路徑前節 點p[x][y],其中x及y為邊的二個節點 1. d[i][j]←w[i][j], for each i and j 2. for k←1 to n do //針對每個編號由1至n的節點 3. for i←1 to n do //針對每個編號由1至n的節點 4. for j←1 to n do //針對每個編號由1至n的節點 5. if d[i][j] > d[i][k]+d[k][j] then 6. d[i][j]←d[i][k]+d[k][j] 7. p[i][j]←p[k][j] 8. return d, p 116 Floyd-Warshall演算法討論 前節點(predecessor)陣列p紀錄每個節點 在最短路徑上的前節點。在進行p[i][j]初 始化時,若i=j或(i,j)∉E則p[i][j]初始化為 NIL(),否則(也就(i,j)E)p[i][j]初始化 為i。 演算法執行完畢之後,則可藉由前節點陣 列p來找出由任意節點到其他任意節點最 短路徑上經過的所有節點。 117 Floyd-Warshall最短路徑演算法 範例: 陣列初始化 G(V, E) s a b c d s 0 a 6 0 2 1 b 2 0 3 6 c 3 4 0 d 1 5 0 d[i][j] s a b c d s a s c d b a c d c s a d b c p[i][j] 118 Floyd-Warshall最短路徑演算法 時間複雜度 假設G一共有n個節點(也就是|V|=n) 行2的外層for迴圈一共有n次迭代 行3的中層for迴圈一共有n次迭代 行4的內層for迴圈一共有n次迭代 行5-7迴圈內if敘述的執行為常數時間 因此總時間複雜度為O(n3)=O(|V|3) 119 The End 120