# LIS DP in a nutshell **in a nutshell = ultra-oversimplified** ###### Author: zeena (i2225 PTNK) ###### Resources: [cp-algorithms](https://cp-algorithms.com/sequences/longest_increasing_subsequence.html) :::success :bulb: Mình **khuyến khích** bạn nên đọc song song shitpost này và tài liệu mình liệt kê ở trên. ::: --- ## wtf is DP? read [this shit](https://usaco.guide/gold/intro-dp?lang=cpp) ## wut is LIS? LIS (Longest Increasing Subsequence) là một trong những dạng DP khá kinh điển. Như cái tên, LIS là dãy con đơn điệu tăng dài nhất. **Ví dụ:** cho mảng $A =$ $[1, 5, 2, 4, 2]$, thì LIS là $[1, 2, 4]$ và nó có độ dài là 3. ## how does it work? Cho bài toán kinh điển: [VNOJ](https://oj.vnoi.info/problem/liq) Ta gọi $f(i)$ là độ dài LIS với phần tử kết thúc cuối cùng thuộc vị trí $i$. Nhận xét: Với mỗi $i$ đang xét, ta cần phải tìm các phần tử có vị trí $j$ từ $1$ tới $i-1$ sao cho $A_j < A_i$. Khi đấy, ta sẽ lấy $f(i)=$ $\max\limits_{j = 1, A_j<A_i}^{i-1} {(f(j)+1)}$ :::spoiler Implementation (C++) ```cpp! int dp[n+1] = {}; dp[1] = 1; int LIS = 1; for (int i=2; i<=n; ++i) { for (int j=1; j<i; ++j) { if (a[i]>a[j]) dp[i] = max(dp[i], dp[j]+1); } LIS = max(LIS, dp[i]); } return LIS; ``` ::: Độ phức tạp cuối cùng sẽ là $O(n^2)$. ## xtension Bây giờ, ta nâng $n$ lên, cho đến khi nào cách cũ bị **TLE** thì thôi. [Cũng là VNOJ](https://oj.vnoi.info/problem/lis) ### "Tham Lam" Ta có thể lấy mảng $dp$ là 1 mảng chứa LIS. $dp$ ban đầu chỉ có phần tử $A_1$. Với mọi $i$ $(1 < i \le n)$, ta sẽ tìm vị trí $j$ nhỏ nhất trong $dp$ sao cho $dp_j \ge A_i$. Do vậy ta sẽ có 2 trường hợp: - Nếu có: đặt phần tử $A_i$ ở vị trí tìm được trong mảng $dp$. - Nếu không có: ta thêm vào mảng $dp$ từ sau cùng. Từ đấy ta có nhận xét như sau: - Mảng $dp$ luôn chứa các phần tử được xếp tăng dần. $(*)$ - Nhờ vào $(*)$, ta có thể chặt nhị phân trong mảng $dp$ để kiểm tra. :::spoiler Vì sao mình lại làm như vậy? **"Tham lam"** ::: <br /> :::spoiler Implementation (C++) ```cpp! vector<int> dp; dp.push_back(a[1]); for (int i=2; i<=n; i++) { auto j = lower_bound(dp.begin(), dp.end(), a[i]); if (j==dp.end()) dp.push_back(a[i]); else *j = a[i]; } return dp.size(); ``` ::: <br /> **Lưu ý:** Có một số biến thể của LIS sẽ có một số trường hợp cách này không giải được mà ta sẽ phải dùng CTDL để giải. ### Dùng CTDL Quay về cách đầu tiên, ta thấy $f(i) = f(j) + 1$ với điều kiện là $A_j < A_i$. Như vậy, ta sẽ gọi 1 mảng $t$ nào đó sao cho $t[A_i] = f(i)$. Khi đấy, bài toán để tìm $f(i)$ trở thành bài toán trở thành tìm $\max$ của mảng $t$ trong đoạn $[1, A_i)$ (tùy vào giới hạn bé nhất của $A_i$). Dễ dàng nhận thấy, ta có thể quản lý hiệu quả và truy cập nhanh đoạn này bằng CTDL như Fenwick Tree, Segment Tree... :::spoiler Fenwick Tree Implementation (FPC) ```cpp var bit: array[1..100005] of longInt; n, i, a: longInt; function max(a, b: longInt): longInt; begin if (a<b) then exit(b) else exit(a) end; procedure upd(x, v: longInt); begin while (x <= n) do begin bit[x] := max(bit[x], v); inc(x, x and -x); end; end; function query(x: longInt): longInt; var res: longInt; begin res := 0; while (x > 0) do begin res := max(res, bit[x]); dec(x, x and -x) end; exit(res) end; begin readln(n); for i := 1 to n do begin read(a); upd(a, query(a)+1); end; writeln(query(n)); end. ``` ::: <br /> **Lưu ý:** Cách này chỉ dùng được khi giới hạn của $A_i$ không vượt quá khoảng $10^7$. Do đó mà cách này không thể dùng được cho bài trên của VNOJ. Độ phức tạp cuối cùng của cả 2 cách trên sẽ là $O(n.log(n))$.