---
slideOptions:
theme: sky
---
## 真.資料結構EX
#### 成為十萬序列大師
###### wangyenjen
----
## WHOAMI
喵
----
#### JAW
- Johnchen902:字串、噁心模擬題、小品、DP、慘題
- nonamefour0210:數學、DP、吉祥物!
- 我:打雜、資結、dp優化、幾何
---
### 前備知識
- 線段樹和BIT
- 線段樹和BIT
- 線段樹和BIT
---
CONTENT
- 穿越時空的線段樹
- 切塊、Mo's、分case
- 擁抱分治,向資結說不
----
遺珠之憾...
- O(1) 求斐波那契數列
- 把PI、e存在月球上
---
### 小複習
----
## 線段樹
![Imgur](https://i.imgur.com/Cr00XrO.png)
----
### 區間開根號
問題:
給你 $N$ 個數字 $a_i(1 \le i \le N)$,跟 $Q$ 個操作,操作有兩種:
1. ```query x y```: 查詢序列 $a$ 在區間 $[x, y]$ 的和。
2. ```sqrt x y```: 把 $a_x, a_{x + 1}, ..., a_y$ 的值個別開根號後取高斯。
----
### 區間開根號
問題:
給你 $N$ 個數字 $a_i(1 \le i \le N)$,跟 $Q$ 個操作,操作有兩種:
1. ```query x y```: 查詢序列 $a$ 在區間 $[x, y]$ 的和。
2. ```sqrt x y```: 把 $a_x, a_{x + 1}, ..., a_y$ 的值個別開根號後取高斯。
$1 \le N, Q \le 10^5$
$1 \le a_i \le 10^9$
---
## 穿越時空的線段樹
----
先來看看一個問題:
給定一張 $N$ 個點的無向圖,一開始圖上有 $M$ 條邊。
現在有 $Q$ 個操作,每個操作會是以下兩種中的一種:
1. 增加一條連接著編號 $A_i$ 與編號 $B_i$ 的邊。
2. 刪除一條連接著編號 $A_i$ 與編號 $B_i$ 的邊(保證這條邊是存在的)。
每次操作完後請輸出當前的連通塊數量。
$1 \le N, Q \le 10^5$
$1 \le A_i, B_i \le N$
----
當發現一個問題太難的時候,我們往往可以從較簡單的版本著手,試著找出解決難題的方向。
----
把兩種操作分開來看看吧!
----
### easy version 1.
給定一張 $N$ 個點的無向圖,一開始圖上有 $M$ 條邊。
現在有 $Q$ 個操作,每個操作會是以下形式:
- 增加一條連接著編號 $A_i$ 與編號 $B_i$ 的邊。
每次操作完後請輸出當前的連通塊數量。
$1 \le N, Q \le 10^5$
$1 \le A_i, B_i \le N$
----
### 並查集(Disjoint Set)的標準應用
並查集可以...
1. 合併兩個連通塊
2. 詢問每個連通塊有多少點
3. 詢問當前有多少連通塊
etc...
----
### easy version 2.
給定一張 $N$ 個點的無向圖,一開始圖上有 $M$ 條邊。
現在有 $Q$ 個操作,每個操作會是以下形式:
- 刪除一條連接著編號 $A_i$ 與編號 $B_i$ 的邊(保證這條邊是存在的)。
每次操作完後請輸出當前的連通塊數量。
$1 \le N, Q \le 10^5$
$1 \le A_i, B_i \le N$
----
似乎沒有現成的資料結構可以維護(?
----
當題目沒要求強制在線時,往往有簡單的離線做法!
----
## 時光倒流
----
把所有操作先存起來
對於最終的Graph,如果把操作到回去做?
----
變成只有加邊的問題了!
----
#### 回到原問題
給定一張 $N$ 個點的無向圖,一開始圖上有 $M$ 條邊。
現在有 $Q$ 個操作,每個操作會是以下兩種中的一種:
1. 增加一條連接著編號 $A_i$ 與編號 $B_i$ 的邊。
2. 刪除一條連接著編號 $A_i$ 與編號 $B_i$ 的邊(保證這條邊是存在的)。
每次操作完後請輸出當前的連通塊數量。
$1 \le N, Q \le 10^5$
$1 \le A_i, B_i \le N$
----
#### 還是不會做
----
### 時間線段樹!
----
時間線段樹,顧名思義就是將線段樹的每個節點當成對應的時間區間,並存放著該時間區間對應的所有>
操作,即在這邊操作是有時效性的。
----
使用上會把詢問與具有時效性的操作一起當成時間點,將這些時間點所形成的區間視為線段樹上的節點。
![Imgur](https://i.imgur.com/Cr00XrO.png)
----
```C++11=
void insert_event(vector<Event> tr[], int idx,
int lb, int rb, int ql, int qr, Event e) {
// tr[] := 時間線段樹存事件所使用的陣列
// idx := 當前在時間線段樹上哪個節點
// 當前節點對應到的時間區間為 [lb, rb]
// 當前要在時間區間 [ql, qr] 插入一個事件e
// 當前區間 [lb, rb] 完全被 事件區間 [ql, qr] 包含
if (ql <= lb && rb <= qr) {
// 插入事件至時間線段樹對應的節點
tr[idx].push_back(e);
return;
}
int m = (lb + rb) / 2;
// 事件區間 [ql, qr] 完全位於 mid 左邊
if (qr <= mid)
insert_event(tr, idx*2, lb, mid, ql, qr, e);
// 事件區間 [ql, qr] 完全位於 mid 右邊
else if (mid < ql)
insert_event(tr, idx *2+1, mid+1, rb, ql, qr, e);
// 事件區間 [ql, qr] 跨過 mid 這個時間點
else {
insert_event(tr, idx*2, lb, mid, ql, qr, e);
insert_event(tr, idx*2+1, mid+1, rb, ql, qr, e);
}
}
```
----
```C++11=
void traversal(vector<Event> tr[], int idx, int lb, int rb)
{
int cnt = 0; // 用來記錄在這一層做了多少事情
for (auto e : tr[idx])
if (do_things(e))
cnt++;
if (lb == rb) // 記錄在這個時間點的答案
else {
int mid = (lb + rb) / 2;
// 遍歷時間線段樹的左子樹
traversal(tr, idx * 2, lb, mid);
// 遍歷時間線段樹的右子樹
traversal(tr, idx * 2 + 1, mid + 1, rb);
}
while (cnt--)
undo(); // 將所做的事情進行undo操作
}
```
----
如果對於每條邊,我們考慮它存活的時間...
----
#### 時間線段樹的undo?
----
#### 並查集again
----
#### 支援undo操作的並查集
----
```C++11=
int cc_cnt; // 當前連通塊數量
int sz[MAX_N]; // 用來記錄每個連通塊大小
int par[MAX_N]; // 用來記錄每個節點所指向的父節點
stack<pair<int*, int> > stk_sz; // 用來記錄每次操作前的sz
stack<pair<int*, int> > stk_par; // 用來記錄每次操作前的par
```
----
```C++11
void init(int n) {
while (!stk_sz.empty())
stk_sz.pop();
while (!stk_par.empty())
stk_par.pop();
cc_cnt = n;
for (int i = 0; i < n; i++) {
par[i] = i;
sz[i] = 1;
}
}
```
----
不能路徑壓縮!
```C++11=
int find(int x) { // 找根
while (x != par[x])
x = par[x];
return x;
}
```
----
不能路經壓縮,所以一定要啟發式合併!
```C++11=
bool merge(int x, int y) {
x = find(x);
y = find(y);
if(x == y)
return 0;
if (sz[x] < sz[y]) // 啟發式合併
swap(x , y);
// 記錄操作前的sz
stk_sz.push(make_pair(&sz[x], sz[x]));
// 記錄操作前的par
stk_par.push(make_pair(&par[y], par[y]));
cc_cnt--;
sz[x] += sz[y];
par[y] = x;
return 1;
}
```
----
```C++11=
void undo() {
pair<int*, int> p_par = stk_par.back();
pair<int*, int> p_sz = stk_sz.back();
stk_par.pop();
stk_sz.pop();
*p_par.first = p_par.second; // 將par回復成操作前的par
*p_sz.first = p_sz.second; // 將sz回復成操作前的sz
cc_cnt++;
}
```
----
至此,對於可undo的並查集,每次merge操作的時間複雜度為$O(\log N)$,每次undo操作為$O(1)$
----
- 單次插入事件$O(\log (M + Q))$
- merge操作$O(\log N)$
- undo操作$O(1)$
- traversal $O((M + Q))$
總時間複雜度為$O((M + Q) \log N + (M + Q) \log (M + Q))$
----
#### 總結
總結一下,通常題目只要滿足以下兩個條件便可以考慮使用時間線段樹了:
- 操作具有時效性
- 搭配的資料結構支援undo操作(一般的資料結構幾乎都可以支援)
----
#### 小習題 - BZOJ 4025 二分圖
給定一張$n$個點的圖,有$m$條邊與$T$個時間點,每條邊只存在於$(st, ed]$這些時間點,求每個時間點時這張圖是否為二分圖。
- $1 \le n \le 10^5$
- $1 \le m \le 2 \times 10^5$
- $1 \le T \le 10^5$
- $0 \le st \le ed \le T$
---
## 切塊
----
#### 先來看個問題吧!
給你一個長度 $N$ 的序列 $A[0], A[1], ..., A[N - 1]$
緊接著 $Q$ 筆詢問,每筆詢問是一個數對 $x, y$
請你回答 $min${ $A[x], A[x + 1], ..., A[y]$ }
A = {5, 2, 8, 1, 7}
Query = {(0, 2), (1, 4), (2, 2)}
答案:2 1 8
----
```C++11=
int ans = A[x];
for (int i = x + 1; i <= y; i++)
ans = min(ans, A[i]);
```
時間複雜度 $O(QN)$
特性:$min(a, b, c) = min(a, min(b, c))$
可以對一部分數字先求最小值,再從每個部份的最小值中取最小的!
----
一部分?
每連續 $k$ 個分一組,先把每組的最小值算好並存下來
對於詢問
![Imgur](https://i.imgur.com/6of1f0B.png)
綠色每塊都先算好了,時間花費是塊數:最多 $\frac{N}{k}$
紅色部份每個數字跑一遍,時間花費是一塊大小:最多 $2k$
----
每次詢問 $O(\frac{N}{k} + k)$
取個好的 $k$?
算幾不等式:$\frac{N}{k} + k \le 2 \times sqrt(\frac{N}{k} * k) = 2 \times sqrt(N)$
在 $\frac{N}{k} = k$ 也就是 $k = sqrt(N)$ 時最小
所以取 $k$ 為 $sqrt(N)$ 就有了每次詢問 $O(sqrt(N))$ 的解法了
----
### 分塊的好處
1.大部分的區間詢問問題都需要用某種平衡二元搜尋樹來維護,若能用set,map,或是簡單的線段樹維護那還好,但如果性質很難維護或是要用到複雜的資料結構,寫起來會比較久也比較難debug。
2.分塊時跟陣列一樣只有一層,比較好想我們要維護什麼
3.code易懂
----
### IOI 2011 Elephants
有 $N$ 隻可愛的大象在舞台上排成一條線跳舞。
接下來有$M$次操作,每次操作可以把 左邊數來第$a$隻大象移動到數線上的位置$b$,每次操作完後請輸出最少需要幾張照片才可以拍到所有的大象。
(每張照片可以cover一個長度為$L$的區間)
$1 \le N,M \le 150000 , 0 \le b \le 10^9$
----
### 如果大象不會移動位置?
Greedy!
----
### 將大象序列切成$K$塊吧!
對於每一隻大象我們可以紀錄如果從他開始,我們要幾台相機才能蓋滿該塊的所有大象,且最後會蓋到哪個位置。
----
### 操作
對某一塊加一隻大象,對某一塊刪除一隻大象
----
### 如果某塊大象太多?
令一塊大小為$X$,如此修改時間為$O(X \log X)$,查詢時間便是$O(K \log X)$,
但悲劇的是每次操作後每一塊大小並不是固定的,
在經過$Q$次操作後,我們也許會獲得$X = \frac{N}{K} + Q \le N$
----
### 打掉重練!
當有一塊過大時(可以定為$\frac{2N}{K}$),我們便整個打掉重新分塊
----
我們令$K=\sqrt{N}$
建表時間為$O(N \log N)$,
每次修改與查詢的時間複雜度為$O(\sqrt{N} \log \sqrt{N})$
重建表總時間為$O\big(\frac{Q}{\sqrt{N}} \times N \log N\big) = O(Q \sqrt{N} \log N)$
---
### mo's
----
### 經典問題 - 區間眾數
給定一個序列$S_1 , \ldots , S_N$,接著有$Q$組詢問,
每次詢問$[L_i, R_i]$出現最多的數字出現幾次。
$(1 \le N,Q \le 10^5)$
----
$app[x] :=$ 數字$x$出現幾次
$cnt[y] :=$ 有$cnt[y]$個數字出現過$y$次
$cnt[0]$初始為$N$。
```C++11=
void add(int x) {
int now = ++app[x];
cnt[now - 1]--;
cnt[now]++;
curMax = max(curMax, now);
}
void sub(int x) {
int now = --app[x];
cnt[now + 1]--;
cnt[now]++;
if (!cnt[curMax])
curMax--;
}
```
----
```C++11=
for (int i = 1, curL = 1, curR = 0; i <= Q; i++) {
while (curR < qry[i].R) {
curR++;
add(S[curR]);
}
while (curR > qry[i].R) {
sub(S[curR]);
curR--;
}
while (curL > qry[i].L) {
curL--;
add(S[curL]);
}
while (curL < qry[i].L) {
sub(S[curL]);
curL++;
}
answer[qry[i].qid] = curMax;
}
```
----
### 對於左界
左界屬於同一塊的詢問,每次都不會移動超過$\frac{N}{K}$次;
而左界屬於不同塊的操作,移動次數加起來也不超過$N$次。
所以左界移動的時間複雜度為$O(\frac{NQ}{K})$。
----
### 對於右界
相同塊:因為$R_i$是遞增的,所以對於同一塊最多花費$O(N)$的時間。
對於不同塊因為交界最多只有$O(K)$次,每次最多也花費$O(N)$,故維護右界的時間複雜度為$O(KN)$。
----
取$K = \sqrt{N}$,於是我們得到一個時間複雜度為$O(\frac{NQ}{K} + KN) = O(Q\sqrt{N} + N\sqrt{N})$的做法
---
## 分case
----
<font color="red" style="font-size: 200%">遺珠QQ</font>
---
## 擁抱分治,向資結說不
----
### WHY 離線?
- 在線作法不好做
- 在線太久太滿太胖
---
### 整體二分
----
#### METEOR
長度 $M$ 的序列,一開始都是$0$
$N$ 個人瓜分這個序列,每個人有他的目標 $g_i$
$K$ 次操作,每次都是序列某個區間全部加上 $x_i > 0$
問每個人什麼時候達到目標
也就是擁有的元素加起來 $\ge g_i$
----
每個人二分搜何時達到目標
用可持久線段樹維護每個操作做完的序列長相
----
時間 $O((M+N) \log K \log M)$
空間 $O(K \log M)$
----
<font color="red" style="font-size: 200%">MLE</font>
mempry limit: 64 mb
----
開 $K$ 棵線段樹真的太虧了
看來不能每個人各自二分搜了
----
```C++11=
typedef vector< int > VI;
void totBS( int s , int e , VI nation ) {
if(s > e || nation.size() == 0)
return ;
int mid = (s + e) / 2;
do_things( s , mid );
VI ok, notok;
split( mid , nation , ok , notok );
undo_things( s , mid );
totBS( s , mid-1 , ok );
totBS( mid+1 , e , notok );
}
```
----
看那些人可以在 mid 之前達到目標
可以的丟去二分搜 $[s,mid−1]$
不行的丟去二分搜 $[mid+1,e]$
----
把 [s,mid] 的操作作用一番
BIT 維護之
```C++11=
void do_things( int s , int mid ) {
for( int i = s ; i <= mid ; i++ ) {
int nl = in[ i ].l , nr = in[ i ].r;
ll nnu = in[ i ].nu;
bit.upd(nl, nnu);
bit.upd(nr+1, -nnu);
}
}
```
----
看那些人在 mid 前可以被滿足
並且更新他們的目標 (把 [s,mid] 對他們貢獻扣掉)
```C++11=
void split( int mid , VI& nation , VI& ok , VI& notok ) {
for( int nat : nation ) {
tem[nat] = 0ll;
for( int it : satel[ nat ] ) {
tem[nat] += bit.qry(it);
if(tem[nat] >= need[nat])
break;
}
if(tem[nat] >= need[nat]) {
ok.push_back(nat);
if(ans[nat] > 0) ans[nat] = min(ans[nat], mid);
else ans[nat] = mid;
}
else {
need[nat] -= tem[nat];
notok.push_back(nat);
}
}
// 真的把 nation 佔的記憶體釋放掉
VI ().swap( nation );
}
```
----
把 BIT 還原
還給子孫乾淨的 BIT
```C++11=
void undo_things( int s , int mid ) {
for(int i = s; i <= mid; i++) {
int nl = in[i].l, nr = in[i].r;
ll nnu = in[i].nu;
bit.upd(nl, -nnu);
bit.upd(nr+1, nnu);
}
}
```
----
時間: $O((N+M+K) \log M \log K)$
空間: $O(M)$ (這寫法空間真的是線性嗎^^)
----
<font color="green" style="font-size: 200%">MLE</font>
----
<font color="green" style="font-size: 200%">AC</font>
其實這作法好好寫可以不用 BIT
時間少一個 O(logM)
空間也是好好線性
---
## 操作分治
----
在線好難做
把操作們整理成方便處理的順序
變得好做
----
#### MACHINE WORKS
給你 $N,t_i,g_i,r_i,p_i$
$1 \le N \le 10^5$ , $t_i$ 遞增
計算下列 DP 式 :
$dp[i]=max_{j<i}$ {$g_j \times t_i+(r_j−p_j+dp[j]−(t_j+1)\times g_j)$}
----
阿不就斜率優化 DP
deque 做過去就沒啦
----
可是 $g_j$ 沒有單調性
<font color="red" style="font-size: 200%">慘</font>
----
用平衡樹維護下凸包
算好一個 $dp[i]$ 後
把 $y = g_i \times x + (r_j − p_j + dp[i] − (t_j + 1) \times g_j)$ 塞進去
把附近斜率不必要的線拔掉
一堆邊界條件好難寫
----
### 好希望 $g_j$ 有單調性阿~~
----
想想分治
先把 $dp[1 \dots \frac{N}{2}]$ 做完
按照 $g_j$ 排好
那就可以輕鬆做 $dp[\frac{N}{2} + 1 \dots N]$ 了耶
----
所以可以得到長這樣的作法
```C++11=
void solve( int l , int r ) {
if( l == r ) return ;
int mid = ( l + r ) / 2;
solve( l , mid );
// 把 [l, mid] 整理好
// 算 [l, mid] 對 [mid+1, r] 答案的影響
solve( mid+1 , r );
}
```
----
整理好就只是
先把線做出來,排序好
```C++11=
vector< Line > lns , stk;
lns.clear();
for( int i = l ; i <= mid ; i++ )
if( in[ i ].dp >= in[ i ].P ) {
Ln l1{
in[ i ].G ,
in[ i ].dp-in[ i ].P+in[ i ].R
-( in[ i ].D+1ll )*in[ i ].G
};
lns.push_back( l1 );
}
sort( lns.begin() , lns.end() );
```
----
然後做出下凸包,把不必要的線拿走
```C++11=
stk.clear();
for( Ln l1 : lns ) {
while( stk.size()
&& stk.back().m == l1.m
&& stk.back().b < l1.b )
stk.pop_back();
if( stk.size() && stk.back().m == l1.m ) continue ;
while( stk.size() >= 2
&& !ok( stk[ stk.size() - 2 ] ,
stk[ stk.size() - 1 ] , l1 ) )
stk.pop_back();
stk.push_back( l1 );
}
```
----
## 不必要的線?
像圖中紅色的線就是爛線
![Imgur](https://i.imgur.com/4z06l8V.png =487x487)
----
接下來就是要去更新 $[mid+1,r]$ 的 $dp$ 值了
```C++11=
int j = 0;
for( int i = mid+1 ; i <= r ; i++ ) {
ll x = in[ i ].D;
while( j + 1 < (int)stk.size()
&& stk[ j ]( x ) < stk[ j+1 ]( x ) )
j++;
in[ i ].dp = max( in[ i ].dp , stk[ j ]( x ) );
}
dq( mid + 1 , r );
```
----
這裡只處理 $[l,mid]$ 對 $dp[i]$ 的影響
$[mid+1,i−1]$ 的影響透過 $dq(mid+1, r)$ 遞迴搞定
----
好好 merge sort 線的斜率的話
時間 : $O(N \log N)$
----
#### 矩形面積和
在一個二維平面上有 $n$ 個點,大家都知道,任兩個點可以形成一個矩形,而現在要你求出所有任兩點形成的矩形(共 $n * (n - 1) / 2$ 個矩形)面積和。
$2 \le n \le 100000$
$0 \le x_i, y_i \le 10000$
----
#### ADA Farm
在ADA農場中有 $N$ 隻貓咪,第 $i$ 貓咪位於 $(x_i, y_i)$。
貓咪如果上下左右移動的話,會耗費$1$單位時間。
貓咪如果走日字形移動的話,會耗費$2$單位時間。
定義 $D(i, j)$ 代表第$j$隻貓咪走到第$i$隻貓咪所在位置的最小花費時間
對於每隻貓咪$i$,請求出$D(i, 1) + D(i, 2) + \dots + D(i, N)$
----
```C++11=
int calc(int dx, int dy) {
if (dx < dy) swap(dx, dy);
if (dx >= 2 * dy) return dx;
else return 2 * (dx + dy + 1) / 3;
}
```
---
## Q&A