ROSALIND | Longest Increasing Subsequence (LGIS)
給一正整數列,求其最長的遞增與遞減子數列。例如數列 5, 1, 4, 2, 3 的最長遞增數列為 1, 2, 3,最長遞減數列則為 5, 4, 3。
Given: A positive integer n≤10000 followed by a permutation π of length n.
Return: A longest increasing subsequence of π , followed by a longest decreasing subsequence of π.
(https://rosalind.info/problems/lgis/)
知識點
Synteny block 是物種基因體間基因順序彼此一致的片段。這些片段在演化過程中沒有因為重排(genome arrangement)而分離或反轉,暗示著坐落其中的基因不只位置相鄰,功能也可能彼此關聯。
由於基因體動輒包含數億對鹼基,即使比較的對象是親緣關係相近的物種,仍有可觀的計算量與複雜性。相較之下,synteny block 的數量遠少於鹼基數,卻同樣能對基因體的功能和結構提供一些線索。因此,透過比較 synteny block 來研究推演物種的演化過程也不失為一種可行的方式。
舉例來說,假設甲物種與乙物種共享 5 個 synteny blocks,並且依照它們在甲基因體的排列順序命名為 1, 2, 3, 4, 5。接著標記乙基因體上對應的 synteny blocks,便能概括兩者基因體的差異程度,或是推測基因體的結構變化。
1 | 甲:1, 2, 3, 4, 5 |
儘管以符號標記 synteny blocks 可直觀呈現基因體差異,不過隨著比較對象和內容增加,可以使用距離指標來簡化基因體差異分析。
(人類與老鼠的染色體 synteny blocks 分布圖,https://en.wikipedia.org/wiki/Synteny)
在 “Counting Point Mutations” 當中已介紹 Hamming distance 來評估發生點突變的序列間的差異。在這題則引進最長遞增/遞減子數列的概念,透過 synteny block 的順序來定量基因體間的差異。最長遞增/遞減子數列的長度越長,表示兩基因體越相似;長度越短,則表示可能發生較多次基因體重排等結構上的變化。
題解
由於找最長遞增子數列的方式也能用在找最長遞減子數列,所以我將專注討論最長遞增子數列的解法。
假設數列皆如 1, 2, 3, 4,… 嚴格遞增,那麼尋找最長遞增數列並不困難。然而,對於數列元素順序凌亂的狀況,如 2, 5, 3, 1, 4, …,隨著數列長度增加,會越來越難直觀看出答案。
造成困難的主因在於,在檢查某項數字時,我們難以確定它是延續某個既有的遞增子數列,還是作為新的遞增子數列的開端。在檢查完所有數字之前,也無從得知當前檢查的數字是否為最終答案的一部份。
以數列 2, 5, 3, 1, 4 為例,2 能與 5 或 3 組成遞增子數列,但該怎麼判斷要納入何者,以組成最長的遞增子數列?又或在數列 3, 5, 1, 2, 4 的案例中,該依據什麼資訊來決定放棄包含 3, 5 的子數列,而轉為建立新的子數列 1, 2, 4?
綜上所述,可以將讓我們左右為難的因素歸納如下,
- 當前數字大於先前數字時,不確定要忽略它還是將其延伸至既有的遞增子數列。
- 當前數字小於先前數字時,不確定要忽略它還是以它為起點建立新的遞增子數列。
為了解決這問題,可以使用當前數列的最長遞增子數列長度為依據,來衡量納入當前數字和建立新數列的效益,假如行動能增加遞增子數列的長度則為之。
由於每次檢查都可能推翻先前的答案,所以要比對整個數列後才能得知最終結果。以數列 3, 5, 1, 2, 4 為例,由左至右檢查數字來構築最長遞增子數列:
檢查數字 | 當前最長遞增子數列 | 遞增數列長度 | 判斷 | 行為 |
---|---|---|---|---|
3 | [3] | 1 | 起始數字 | 納入當前數字 |
5 | [3, 5] | 2 | 5 > 3,延長遞增數列 | 納入當前數字 |
1 | [3, 5] | 2 | 1 < 5,不能延長現有遞增數列 | 忽略當前數字 |
2 | [3, 5], [1, 2] | 2 | 2 > 1,形成新的遞增數列,與現有數列等長 | 建立當前數列 |
4 | [1, 2, 4] | 3 | 4 > 2,延長新建立的遞增數列,長度大於 [3, 5] 與 [1, 2] | 納入當前數字 |
為了實現這套算法,需要紀錄以下資訊
lis_len
:紀錄截至當前數列位置,最長遞增子數列的長度,用於衡量是否要納入新數字或建立新數列。prev
:該數字在遞增子數列的前一項的位置,用於追蹤遞增子數列的元素。
1 | lis_len = [1] * len(l) # 因為每個數字自成數列,所以在開始計算前,遞增子數列的長度至少為 1。 |
假設現在檢查第 i 個數字,那麼我們需要把它與數列之前所有數字(l[j]
, i > j)比較,才能涵蓋到所有可能性。
1 | for i in range(1, len(l)): |
簡言之,在當前數字大於先前數字時(l[i] > l[j]
),有兩種可能的行動:
- 將當前數字
l[i]
納入以l[j]
結尾的遞增子數列,形成長度為lis_len[j] + 1
的遞增子數列。 - 忽略先前的數字,不更動
l[i]
所屬的遞增子數列,讓長度維持lis_len[i]
為了找到最長的遞增子數列,應該選擇能增加子數列長度的行動。換句話說,若 lis_len[j] + 1 > lis_len[i]
,那麼就該把 l[i]
納入以 l[j]
結尾的遞增子數列,並且更新 lis_len[i] = lis_len[j] + 1
。
與此同時,也透過 prev
紀錄當前數字 l[i]
在遞增子數列的前一項的位置 prev[i] = j
,以利在檢查全部數字之後,從輸入的數列重建出最長的遞增子數列。
透過這樣的比較,可以確保 lis_len
每一項都記錄了截至當前位置能獲得的最常遞增子數列長度。因此,lis_len
的最大值便是整個數列的最長遞增子數列長度;該值在 lis_len
的位置,也對應到遞增子數列最後一項的位置。
1 | final_pos = max(range(len(l)), key=lambda x: lis_len[x]) |
由於prev陣列記錄了當前位置的數字在最長遞增子數列中前一項的索引位置,因此我們需要依序讀取prev中的值l[prev[i]],並將讀取出的數列反轉,才能獲得正確的最長遞增子數列順序。
由於 prev
記錄了當前位置的數字在最長遞增子數列中前一項的位置,所以回溯 prev
得到的數列順序與正確結果相反。仍需將讀取出的數列反轉,才能得到最長遞增子數列。
1 | output = [] |
完整的功能實現如下,
1 | def lgis(l): |
只要將數列倒序輸入這個副程式,再反轉回傳結果,即可獲得最長遞減子數列,因此不用額外寫一個專門的副程式。由於題目提供的輸入為純文字檔,所以在執行程式前要記得將文字轉換為整數,並在輸出答案時再將整數轉換回文字以利後續排版。若省略型態轉換的步驟,則程式會依照字母排序,結果自然會與預期有所出入。
1 | def rosalind_lgis(path): |
討論
這道題的解法是動態規劃的案例。對我來說,最困難的不是熟悉動態規劃的概念,而是判斷適用動態規劃的情境,還有選擇合適的資訊存儲和判斷式。雖然與資訊科學無關(但跟生物有點關係),練習題目時,一直想起高中生物老師耳提面命的話,
蓋棺才能論定
每個人多少有想追求的目標與想累積的經歷,當中免不了要面對兩難的人生抉擇:接受新的職位,還是待下去?繼續打拼,還是止損停利?與他人合作,還是獨行江湖?即使擁有預知能力,可以判斷當下決策帶來的影響,也很難保證長期而言,會帶來最高的效益。
於是,我們也只能依照當下的條件,選擇最契合個人方向與渴望的行動,直到人生告一段落再也沒有任何可能性的時候,才有機會判斷哪些舉動與決策對於當初訂立的目標最有幫助。只是知道了又如何?雖然其他人可以從這些經驗得到啟發,可是當事人也沒機會用到自己一生濃縮的最佳解。
何況每個階段的追求也可能大相逕庭,處心積慮規劃了老半天,才在某個陽光明媚的早晨發現,自己渴望 B 而不是 A 的情況應該也不少見。
因此,當初生物老師在聯考後第一堂課不檢討考卷,而花了老半天講些莫名其妙的話的時候,可能不是在勉勵我們「別因挫折而氣餒,能否達成目標還說不定呢!」(對,我們班上很多人沒考好)。他也許是期望我們就用力活著,去體驗挫敗、毫無意義、徒勞無功之類說不上是好是壞的情感,不用煩惱纏繞腦中的時程、途徑、機會、指標、最佳化等有的沒的東西。