longest increasing subsequence
sequence
「數列」。一串數字。
例如1 3 5 2 9,由五個數字組成的數列。
「序列」。一串字元。字元其實是數字。
例如a c e b i,由五個字元組成的序列。
subsequence
「子序列」。sequence之中,依照由左到右的順序,挑幾個數字出來,就是subsequence。其中sub-的意思是「附屬、次要」。
例如1 3 5 2 9的其中一個子序列是3 9。
例如1 3 5 2 9的其中一個子序列是1 5 2 9。
空集合(不取)、原序列(全取),都是子序列!
increasing
「遞增」。數字不斷增加。
例如3 9是遞增子序列。
例如1 5 2 9不是遞增子序列。
遞增的數學性質很強,可以設計高速演算法。
longest increasing subsequence(LIS)
「最長遞增子序列」。所有子序列當中,遞增的、最長的子序列,可能有許多個。
例如1 3 5 2 9的LIS是1 3 5 9。
找出其中一個LIS,有快速的演算法,接下來將為各位介紹。
找出所有的LIS,以上述演算法作為基礎。
longest increasing subsequence: dynamic programming
演算法
錯誤的做法:紀錄每種前綴的LIS。online演算法,一次讀取一個數字,窮舉先前每種前綴的LIS,看看哪一種接得最長。
正確的做法:紀錄每項數字為結尾的LIS。online演算法,一次讀取一個數字,窮舉先前每項數字為結尾的LIS,看看哪一種接得最長。
時間複雜度O(N²),N是序列長度。
sequence : -7 10 9 2 3 8 8 1 sequence :(-7)10 9 2 3 8 8 1 LIS ends with s[0] : -7 sequence : -7(10) 9 2 3 8 8 1 LIS ends with s[0] : -7 LIS ends with s[1] : -7 10 sequence : -7 10 (9) 2 3 8 8 1 LIS ends with s[0] : -7 LIS ends with s[1] : -7 10 LIS ends with s[2] : -7 9 sequence : -7 10 9 (2) 3 8 8 1 LIS ends with s[0] : -7 LIS ends with s[1] : -7 10 LIS ends with s[2] : -7 9 LIS ends with s[3] : -7 2 sequence : -7 10 9 2 (3) 8 8 1 LIS ends with s[0] : -7 LIS ends with s[1] : -7 10 LIS ends with s[2] : -7 9 LIS ends with s[3] : -7 2 LIS ends with s[4] : -7 2 3 sequence : -7 10 9 2 3 (8) 8 1 LIS ends with s[0] : -7 LIS ends with s[1] : -7 10 LIS ends with s[2] : -7 9 LIS ends with s[3] : -7 2 LIS ends with s[4] : -7 2 3 LIS ends with s[5] : -7 2 3 8 sequence : -7 10 9 2 3 8 (8) 1 LIS ends with s[0] : -7 LIS ends with s[1] : -7 10 LIS ends with s[2] : -7 9 LIS ends with s[3] : -7 2 LIS ends with s[4] : -7 2 3 LIS ends with s[5] : -7 2 3 8 LIS ends with s[6] : -7 2 3 8 sequence : -7 10 9 2 3 8 8 (1) LIS ends with s[0] : -7 LIS ends with s[1] : -7 10 LIS ends with s[2] : -7 9 LIS ends with s[3] : -7 2 LIS ends with s[4] : -7 2 3 LIS ends with s[5] : -7 2 3 8 LIS ends with s[6] : -7 2 3 8 LIS ends with s[7] : -7 1
UVa 111 231 437 497 10131 10534
計算longest increasing subsequence的長度
這裡提供兩支程式碼,效果一樣!第一支程式碼是看s[i]能接在哪些數字後面。第二支程式碼是看s[i]後面能接上哪些數字。
longest increasing subsequence: Robinson–Schensted–Knuth algorithm
演算法
紀錄每個數字在LIS當中的位置。位置越後面越好,以便接得更長。以binary search加速。
時間複雜度O(NlogL),N是序列長度,L是LIS長度。
首先找到每個數字在LIS當中的最合適位置position[]。
sequence : -7 10 9 2 3 8 8 1 temp LIS : position : sequence :(-7)10 9 2 3 8 8 1 temp LIS : -7 position : 1 // -7 在 LIS 的第一個位置 sequence : -7(10) 9 2 3 8 8 1 temp LIS : -7 10 position : 1 2 // 10 在 LIS 的第二個位置,以此類推。 sequence : -7 10 (9) 2 3 8 8 1 temp LIS : -7 9 position : 1 2 2 /* 9 成為 LIS 的潛力比 10 大, 所以以 9 代替 10 */ sequence : -7 10 9 (2) 3 8 8 1 temp LIS : -7 2 position : 1 2 2 2 /* 2 成為 LIS 的潛力比 9 大, 所以以 2 代替 9 */ sequence : -7 10 9 2 (3) 8 8 1 temp LIS : -7 2 3 position : 1 2 2 2 3 sequence : -7 10 9 2 3 (8) 8 1 temp LIS : -7 2 3 8 position : 1 2 2 2 3 4 sequence : -7 10 9 2 3 8 (8) 1 temp LIS : -7 2 3 8 position : 1 2 2 2 3 4 4 /* 8 成為 LIS 的潛力比 8 大, 所以以 8 代替 8 */ sequence : -7 10 9 2 3 8 8 (1) temp LIS : -7 1 3 8 position : 1 2 2 2 3 4 4 2 /* 1 成為 LIS 的潛力比 2 大, 所以以 1 代替 2 */
接下來從position[]之中找到LIS。從尾端往回找,先找到的就是正確的。因為LIS長度為4,所以先找到position[]之中的4。
sequence : -7 10 9 2 3 8 (8) 1 position : 1 2 2 2 3 4 (4) 2 LIS : - - - 8 /* search 4th, 8 is fourth LIS element */ sequence : -7 10 9 2 (3) 8 8 1 position : 1 2 2 2 (3) 4 4 2 LIS : - - 3 8 /* search 3rd, 3 is third LIS element */ sequence : -7 10 9 (2) 3 8 8 1 position : 1 2 2 (2) 3 4 4 2 LIS : - 2 3 8 /* search 2nd, 2 is second LIS element */ sequence :(-7)10 9 2 3 8 8 1 position : (1) 2 2 2 3 4 4 2 LIS : -7 2 3 8 /* search 1st, -7 is first LIS element */
最後得到其中一個LIS為-7 2 3 8,是最後出現的LIS。
順帶一提,如果想要得到最先出現的LIS,將原本序列前後顛倒,做longest decreasing subsequence。
UVa 481
非嚴格遞增
請先看看這個例子。
sequence : -7 10 9 2 3 8 (8) 1 temp LIS : -7 2 3 8 position : 1 2 2 2 3 4 4 /* 8 成為 LIS 的潛力比 8 大, 所以以 8 代替 8 */
這個時候就不能用8來代替8,而要用8去代替比8稍大的數字。如果找不到比8稍大的數字,則直接將數字加在後面。上面的例子修改過後,就變成這樣。
sequence : -7 10 9 2 3 8 (8) 1 temp LIS : -7 2 3 8 8 position : 1 2 2 2 3 4 5 /* 8 可以接在 8 後面 */
宏觀視角
這個演算法找出了所有的子序列,依照長度排列。
greedy method,隨時記錄優良的子序列,逐步更新子序列,越串越長,求出LIS。
sequence 5 2 9 4 8 3 =========================== read 5 | 1 | 5 // 長度1 (以5結尾最長的) --------------------------- read 2 | 2 | 2 // 長度1 (以3結尾最長的) --------------------------- read 9 | 3 | 9 // 長度1 | 4 | 2 9 // 長度2,接第二串 | 5 | 5 9 // 長度2,接第一串(以9結尾最長的) --------------------------- read 4 | 6 | 4 // 長度1 | 7 | 2 4 // 長度2,接第二串(以4結尾最長的) --------------------------- read 8 | 8 | 8 // 長度1 | 9 | 4 8 // 長度2,接第六串 | 10 | 2 8 // 長度2,接第二串 | 11 | 5 8 // 長度2,接第一串 | 12 | 2 4 8 // 長度3,接第七串(以8結尾最長的) --------------------------- read 3 | 13 | 3 // 長度1 | 14 | 2 3 // 長度2,接第二串(以3結尾最長的)
read 5 | 5 read 2 | 2 read 9 | 2 9 read 4 | 2 4 // 同時記錄了第4串和第7串 read 8 | 2 4 8 read 3 | 2 3 8 // 同時記錄了第12串和第14串
box stacking problem
box stacking problem
有一堆尚未封裝的紙箱,把紙箱一個一個套起來存放。然而每個紙箱都有不同的長寬高。如果一個紙箱的三邊長,皆小於另一個紙箱的三邊長,這個紙箱才放得進去。請問最多能套幾層?
UVa 103
延伸閱讀
這個問題等價於有向無環圖的最長路徑,請見「single source shortest paths in DAG: topological sort」。
N個點 <---> N個箱子 一條有向邊 <---> 箱子可以放入另一個箱子 有向無環圖 <---> 兩兩箱子之間的放入關係 最長路徑 <---> 箱子疊越多越好
演算法
箱子實施字典排序,然後尋找LIS。時間複雜度O(NlogN)。
box stacking problem
box stacking problem
有一堆封裝完畢的紙箱,把紙箱一個一個疊起來存放。然而每個紙箱都有不同的長寬高。如果一個紙箱的底部兩邊長,超過下方紙箱的頂部兩邊長,這個紙箱就會凸出一塊,這是我們不樂見的。請問最多能疊多高?
演算法
這個問題可以利用LIS的概念來解決。時間複雜度O(NlogN)。
一種變形問題:紙箱可以任意面朝上。這個問題是NP-complete問題。
一種變形問題:每種尺寸的紙箱都有無限多個,紙箱可以任意面朝上,兩邊長不可完全密合。此時這個問題可以利用LIS解決。時間複雜度O(NlogN)。
Sloane's box stacking problem
Sloane's box stacking problem
有一堆封裝完畢的紙箱,把紙箱一個一個疊起來存放。然而每個紙箱都有不同的重量、不同的抗壓力量。如果一個紙箱上方的總重量,超過這個紙箱的抗壓力量,這個紙箱就會被壓傷,這是我們不樂見的。請問最多能疊多少個紙箱?又有多少種避免壓傷紙箱的疊法?
UVa 10154
延伸閱讀
這個問題等價於單機排程的逾時工作盡量少,請見「single machine scheduling: Moore–Hodgson algorithm」。
N個工作要完成 <---> N個箱子要疊 工作的處理時間 <---> 箱子的內容物重量 工作的完工期限 <---> 箱子的抗壓力量+箱子的內容物重量=承重能力 如期完工的工作越多越好 <---> 箱子疊越多越好
演算法:由上往下疊,累計重量越少越好。
一、依照承重能力由小到大排序。 二、依序疊紙箱,由上往下疊,求出LIS。
在一種合理的疊法當中,任意兩個緊鄰的紙箱,如果上方紙箱的承重能力比下方紙箱還強,那麼交換這兩個紙箱,仍是一種合理的疊法。也就是說,一種合理的疊法,經由多次兩兩交換緊鄰紙箱,使得由上往下來看,紙箱的承重能力恰好是遞增的。也就是說,我們可以依照承重能力排序紙箱,再來求LIS。
由上往下疊紙箱,累計重量,越小越好,讓下方更容易疊放紙箱。
時間複雜度O(NlogN + NL)。
演算法:由下往上疊,剩餘力量越多越好。
一、依照抗壓力量由大到小排序。 二、依序疊紙箱,由下往上疊,求出LIS。
這個方法與前一個方法剛好互補。
由下往上疊紙箱,統計剩餘抗壓力量,越大越好,讓上方更容易疊放紙箱。
時間複雜度O(NlogN + NL)。