---
# System prepended metadata

title: LIS DP in a nutshell
tags: [zeena's educational shitposts, data structures, shitpost, binary search]

---

# 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))$.