題目的難度顏色使用 Luogu 上的分級,由簡單到困難分別為 🔴🟠🟡🟢🔵🟣⚫。

🔗 CF2121H Ice Baby

Problem Statement

題目簡述

給定兩個長度為 nn 的陣列 llrr
對於每個 k[1,n]k \in [1, n],考慮所有長度為 kk 的陣列 aa,滿足 liairil_i \le a_i \le r_i (1ik1 \le i \le k)。
請求出在所有可能的陣列 aa 中,最長非遞減子序列 (LNDS) 的最大長度。

Constraints

約束條件

  • 1t1041 \le t \le 10^4
  • 1n21051 \le n \le 2 \cdot 10^5
  • 1liri1091 \le l_i \le r_i \le 10^9
  • n2105\sum n \le 2 \cdot 10^5

思路:資料結構維護 LIS 狀態

這是一道典型的利用資料結構優化動態規劃的題目。我們可以從最長遞增子序列(LIS)的經典解法出發,思考如何將其擴展到區間的情況。

狀態定義

在經典的 LIS 問題中,我們通常會維護一個陣列,其中第 jj 個元素表示「長度為 jj 的遞增子序列中,結尾元素的最小值」。
我們將這個概念套用到本題:定義一個狀態陣列,其第 jj 個元素代表長度為 jj 的非遞減子序列中,結尾元素的最小值

狀態的單調性

顯然,這個狀態陣列必定是一個單調不遞減的序列。長度越長的子序列,其結尾元素的最小值必然大於等於較短子序列的結尾元素最小值。

狀態轉移分析

當我們依序處理每個區間 [li,ri][l_i, r_i] 時,考慮它能如何更新我們的狀態陣列。
對於任意長度 jj,我們可以嘗試從長度為 j1j-1 的子序列擴展而來。為了讓新的結尾元素盡可能小,我們應該在區間 [li,ri][l_i, r_i] 中挑選一個大於等於前一個元素的最小值。

具體來說,假設前一個元素的值為 prevprev

  1. 如果 prevliprev \le l_i,我們可以直接選擇區間的下界 lil_i
  2. 如果 li<prevril_i < prev \le r_i,我們必須選擇 prevprev 本身(因為要滿足非遞減的條件)。
  3. 如果 prev>riprev > r_i,則無法從這個狀態擴展(因為區間內的所有數都太小了)。

綜合起來,我們挑選的新結尾元素會是 max(prev,li)\max(prev, l_i),前提是 prevriprev \le r_i
接著,我們將這個新結尾元素與原本長度為 jj 的結尾元素進行比較,取較小值來更新狀態。

轉移的整體影響

由於狀態陣列是單調不遞減的,我們可以根據當前區間的邊界 lil_irir_i,將狀態陣列的值劃分為三個部分來觀察變化:

  1. li\le l_i 的部分
    這些狀態即使接上新區間的數,得到的新結尾元素至少也是 lil_i。因為它們原本的值就已經小於等於 lil_i 了,所以取最小值更新後,這部分的值完全不會改變
  2. >li> l_iri\le r_i 的部分
    • 這部分的值可以由前一個狀態轉移而來。具體來說,這段的第一個元素會被更新為 lil_i,後續元素則會被更新為前一個元素的值。
    • 整體來看,這部分的變化相當於將整段數值向右平移一格,並在最前面插入了 lil_i
  3. >ri> r_i 的部分
  • 這部分的第一個元素(也就是 >ri> r_i 的最小元素),會因為第二部分的平移操作而被覆蓋。
  • 而這部分的後續元素,由於前一個狀態已經大於 rir_i,無法滿足非遞減條件進行有效的區間轉移,因此值不會改變
核心結論

綜合上述三個部分的變化,從整體的角度來看,狀態陣列的改變非常單純:

  1. 插入了一個新元素 lil_i
  2. 原本在第三部分的第一個元素(也就是大於 rir_i 的最小元素)被擠掉了,相當於被刪除

如果陣列中所有元素都小於等於 rir_i,則沒有元素會被擠掉,這意味著最長非遞減子序列的長度成功增加了 1。

因為狀態陣列只關心值的集合而不保留順序索引,平移效果會自然地由後續的插入與刪除操作體現。

資料結構維護

但在陣列中插入元素的複雜度是 O(n)\mathcal{O}(n),如果直接模擬整個狀態陣列的變化,對於 nn 個區間來說,總複雜度將達到 O(n2)\mathcal{O}(n^2),這在 nn 高達 21052 \cdot 10^5 的情況下是不可行的。

既然我們只需要進行「插入元素」和「尋找並刪除大於某個值的最小元素」這兩種操作,我們可以使用支援動態排序的資料結構來維護這個狀態陣列。

  • 在 C++ 中,可以使用 std::multiset
  • 在 Python 中,可以使用 sortedcontainers.SortedList(或自行實作平衡樹等替代方案)。

複雜度分析

  • 時間複雜度:O(nlogn)\mathcal{O}(n \log n),對於每個區間,我們在 multiset 中進行常數次查詢、插入和刪除操作,每次操作的時間複雜度為 O(logn)\mathcal{O}(\log n)
  • 空間複雜度:O(n)\mathcal{O}(n)multiset 最多儲存 nn 個元素。

類題


Code

Warning

這份 Python 程式碼使用 sortedcontainers.SortedList 方便測試,但其實 Codeforces 不支援這個套件。
若要正式提交,可以直接複製 SortedList 的原始碼,也能自行實作可支援插入、刪除、排名查詢的有序結構。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from sortedcontainers import SortedList

def solve():
n = int(input())

ans = []
f = SortedList()
for _ in range(n):
l, r = map(int, input().split())
idx = f.bisect_right(r)
if idx != len(f):
f.pop(idx)
f.add(l)
ans.append(len(f))
print(*ans)

if __name__ == "__main__":
t = int(input())
for _ in range(t):
solve()

寫在最後

PROMPT

這裡什麼都沒有。