# LIS(最長遞增子序列) ## 簡介 LIS是要計算一個序列中最長的遞增子序列,例如 `[10, 9, 2, 5, 3, 7, 101, 18]` 這個序列中的最長遞增子序列可以是`[2, 3, 7, 101]`,LIS的答案可以不為一,可能有很多種答案,因此我們要求的是他的最長長度。而我將會介紹$O(N^2)$和$O(NlogN)$的做法! ## $O(N^2)$的作法 我們可以直接窮舉先前每項數字的LIS,並去計算哪一種的長度最長。 定義 $dp[i]$ 表示以$i$為結尾的最長遞增子序列。 因此要計算 $dp[i]$ 需要去檢查小於$i$的所有位置,假設$j$是我們要去檢查的位置,如果 $j < i$ 並且 $arr[j] > arr[i]$ 則可以把 $arr_j$ 為結尾的LIS接上來,最長遞增子序列的值就會變成 $dp[j] + 1$,當然啦我們是要求最長的,所以即使有非常多種答案,我們最終要取最好的,因此我們可以得到轉移式 - $dp[i] = max(dp[j] + 1, dp[i])\qquad if\quad j < i \quad \&\& \quad arr[j] < arr[i]$ ### $O(N^2)$ code ```cpp= #include <bits/stdc++.h> #define IOS ios_base::sync_with_stdio(false);cin.tie(0);cout.tie(0); using namespace std; typedef long long ll; int main(){ IOS int arr[100]; int n; cin >> n; for(int i = 0;i < n;i++) cin >> arr[i]; int dp[100]; for(int i = 0;i < n;i++) dp[i] = 1; for(int i = 0;i < n;i++){ for(int j = 0;j < i;j++){ if(arr[i] > arr[j]) dp[i] = max(dp[j] + 1, dp[i]); } } int ans = 1; for(int i = 0;i < n;i++) ans = max(ans, dp[i]); cout << ans << '\n'; return 0; } ``` 那在leetcode的300題(<a href="https://leetcode.com/problems/longest-increasing-subsequence/description/">題目</a>)就是LIS的裸題,可以去練習看看。 :::spoiler leetcode 300. Longest Increasing Subsequence參考解答 ```cpp= class Solution { public: int lengthOfLIS(vector<int>& nums) { int dp[3000]; for(int i = 0;i < nums.size();i++) dp[i] = 1; for(int i = 0;i < nums.size();i++){ for(int j = 0;j < i;j++){ if(nums[i] > nums[j]) dp[i] = max(dp[j] + 1, dp[i]); } } int ans = 1; for(int i = 0;i < nums.size();i++) ans = max(ans, dp[i]); return ans; } }; ``` ::: <hr> 那接下來做做看這一題<a href="https://cses.fi/problemset/task/1145/">CSES 1145: Increasing Subsequence</a>,當你很開心的寫下下面這個code :::spoiler CSES 1145: Increasing Subsequence ```cpp= #include <bits/stdc++.h> #include <ext/pb_ds/assoc_container.hpp> #include <ext/pb_ds/tree_policy.hpp> #define IOS ios_base::sync_with_stdio(false);cin.tie(0);cout.tie(0); #define INF 0x3f3f3f3f #define int long long #define lowbit(x) x&(-x) using namespace std; using namespace __gnu_pbds; typedef long long ll; const int N = 2e5 + 5; int n, dp[N], arr[N]; signed main(){ IOS cin >> n; for(int i = 0;i < n;i++) cin >> arr[i]; for(int i = 0;i < n;i++) dp[i] = 1; for(int i = 0;i < n;i++){ for(int j = 0;j < i;j++){ if(arr[i] > arr[j]) dp[i] = max(dp[i], dp[j] + 1); } } int ans = 1; for(int i = 0;i < n;i++) ans = max(ans, dp[i]); cout << ans << '\n'; return 0; } ``` ::: 發現竟然吃了TLE,可以看到這一題的限制是$1≤n≤2⋅10^5$,顯然$O(N^2)$不會過,因此我要來介紹$O(NlogN)$的解法 ## $O(NlogN)$ 接下來要介紹$O(NlogN)$的作法,$O(NlogN)$的做法我會兩種,一種是用二分搜去優化,另一種是使用資料結構做優化。 ### 二分搜優化 這個作法是利用二分搜去優化。 首先我們先用$arr[]$讀入測資,接著創建一個vector,稱作$v[]$好了,存儲目前為止找到的最長遞增子序列,我們要利用$v[j]$紀錄第$j$個位子的最小值,也就是說$v[j]$是要紀錄在$j$的時候最小的$arr$,並更有潛力的建構出LIS。接著再利用二分搜去更新我們的$v[j]$。 我拿上面那個例子示範一次,`arr = [10, 9, 2, 5, 3, 7, 101, 18]`。 1. 一開始我們將$10$推進$v$裡面,此時`v={10}` 2. 再來尋找$9$在$v$的位置,可以發現$9$小於$10$,因此$10$替換成$9$,此時`v={9}` 3. $2$小於$9$,因此$9$替換成$2$,此時`v={2}` 4. 處理$5$,由於$5$大於$2$,所以把$5$推進$v$,此時`v={2, 5}` 5. 接著$3$小於$5$,因此$5$替換成$3$,此時`v={2, 3}` 6. 處理$7$,由於$7$大於$3$,所以把$7$推進$v$,此時`v={2, 3, 7}` 7. $101$大於$7$,所以把$101$推進$v$,此時`v={2, 3, 7, 101}` 8. $18$小於$101$,因此$101$替換成$18$,此時`v={2, 3, 7, 18}` 9. 完成 在建構LIS的過程中可以發現$v$裡面的數字具有單調性,因此在尋找的時候可以使用二分搜。 :::spoiler leetcode 300. Longest Increasing Subsequence參考解答 ```cpp= class Solution { public: int lengthOfLIS(vector<int>& nums) { vector<int> v; int n = nums.size(); for(int i = 0;i < n;i++){ int p = lower_bound(v.begin(), v.end(), nums[i]) - v.begin(); if(p == v.size()) v.push_back(nums[i]); else v[p] = nums[i]; } return v.size(); } }; ``` ::: 那用這個方法解CSES那一題就留給大家做做看,因為那一題我想用下一個做法示範一下,嘿嘿。 ### 資料結構優化 在這個方法中我使用BIT(Binary Indexed Tree)來做優化,BIT通常是處理區間問題,BIT可以用$O(logN)$的時間做查詢(query),和$O(logN)$的時間做更新(update),利用這個特性可以在$O(NlogN)$下找出LIS。 首先定義$dp[i]$是區間$[0, i]$的LIS長度,接著列出轉移式: $$dp[i] = max(dp[1], dp[2], dp[3], ...., dp[i - 1]) + 1$$ 會發現$max(dp[1], dp[2], dp[3], ...., dp[i - 1]) + 1$其實在求的是前綴最大值,發現到這間件事情之後便可以巧妙的利用BIT去優化。 這邊我們BIT是要對值域做更新和查詢,記得是「值域」喔,所以每一次做更新的時候我們要先找比該元素小1的值做查詢,查看比該元素小的LIS長度是多少,同樣使用這個例子`[10, 9, 2, 5, 3, 7, 101, 18]`。 - 處理第一個元素,查詢BIT,找到比10小的位置中最大的LIS長度:query(10 - 1) = 0,並將這個回傳值加上1,更新BIT和答案:update(10, 1),$dp[10]=1$。 - 處理第二個元素,查詢BIT,找到比9小的位置中最大的LIS長度:query(9 - 1) = 0,並將這個回傳值加上1,更新BIT和答案:update(9, 1),$dp[9]=1$。 - 處理第三個元素,查詢BIT,找到比2小的位置中最大的LIS長度:query(2 - 1) = 0,並將這個回傳值加上1,更新BIT和答案:update(2, 1),$dp[2]=1$。 - 處理第四個元素,查詢BIT,找到比5小的位置中最大的LIS長度:query(5 - 1) = 1,並將這個回傳值加上1,更新BIT和答案:update(5, 1),$dp[10]=2$。 - 以此類推,最後可以計算到LIS為4。 那一樣我先放上LeetCode的那一題的參考做法 ```cpp= class Solution { public: const static int N = 3000; vector<int> b; int arr[N + 5], dp[N + 5]; int lowbit(int x){ return x&(-x); } int query(int idx){ int ans = 0; for(int i = idx;i > 0;i-=lowbit(i)){ ans = max(dp[i], ans); } return ans; } void update(int idx, int val){ for(int i = idx;i <= N;i+=lowbit(i)){ dp[i] = max(dp[i], val); } } int lengthOfLIS(vector<int>& nums) { int n = nums.size(); for(int i = 0;i < n;i++) b.push_back(nums[i]); sort(b.begin(), b.end()); b.resize(unique(b.begin(), b.end()) - b.begin()); for(int i = 0;i < n;i++) nums[i] = lower_bound(b.begin(), b.end(), nums[i]) - b.begin() + 1; for(int i = 0;i <= N;i++) dp[i] = 0; for(int i = 0;i < n;i++){ int leng = query(nums[i] - 1) + 1; update(nums[i], leng); } int ans = 1; for(int i = 0;i <= N;i++) ans = max(ans, dp[i]); return ans; } }; ``` <hr> 再來就是我們的重頭戲了CSES 1145: Increasing Subsequence的$O(NlogN)$的做法 :::spoiler CSES 1145: Increasing Subsequence optimized by BIT ```cpp= #include <bits/stdc++.h> #include <ext/pb_ds/assoc_container.hpp> #include <ext/pb_ds/tree_policy.hpp> #define IOS ios_base::sync_with_stdio(false);cin.tie(0);cout.tie(0); #define INF 0x3f3f3f3f #define int long long #define lowbit(x) x&(-x) using namespace std; using namespace __gnu_pbds; typedef long long ll; const int N = 2e5 + 5; vector<int> b; int arr[N + 5], dp[N + 5], lis = 1, n; int query(int idx){ int ans = 0; for(int i = idx;i > 0;i-=lowbit(i)){ ans = max(dp[i], ans); } return ans; } void update(int idx, int val){ for(int i = idx;i <= N;i+=lowbit(i)){ dp[i] = max(dp[i], val); } } signed main(){ IOS cin >> n; for(int i = 1;i <= n;i++){ cin >> arr[i]; b.push_back(arr[i]); } sort(b.begin(), b.end()); b.resize(unique(b.begin(), b.end()) - b.begin()); for(int i = 1;i <= n;i++) arr[i] = lower_bound(b.begin(), b.end(), arr[i]) - b.begin() + 1; for(int i = 0;i <= N;i++) dp[i] = 0; for(int i = 1;i <= n;i++){ int leng = query(arr[i] - 1) + 1; update(arr[i], leng); } int ans = 1; for(int i = 0;i <= N;i++) ans = max(ans, dp[i]); cout << ans << '\n'; return 0; } ``` ::: 我稍微講一下這一題,Constraints是$1≤n≤2⋅10^5$,$1≤x_i≤10^9$,由於是對值域做查詢和更新,按照題目的限制$1≤x_i≤10^9$陣列會爆,因此要先做離散化,做完離散化之後就按照上面的步驟處理,最後找尋$max(dp[1], dp[2], dp[3], ...., dp[N])$就是我們的答案了! 優化後的做法真的好神奇好美妙喔,希望大家都能一起欣賞這個優化的美,太喜歡了~~~ 太開心了我寫完這篇LIS的介紹了! ## Reference - https://yuihuang.com/dp-lis/ - https://www.csie.ntu.edu.tw/~sprout/algo2021/homework/hand10.pdf