---
title: '離線演算法'
disqus: hackmd
---
###### tags:`2023 NYCU PCCA Winter Camp`
離線演算法
===
[TOC]
# 離線演算法
## 在線v.s.離線
:::danger
* 現在想像你有一台機器,你有很多問題想請它回答。
* 在線:你問它一個問題,它會馬上告訴你答案。
* 離線:你必須把所有操作都先給它,它才能處理這些操作,最後一起輸出所有答案。
* 題目可以離線,代表你可以隨意決定處理操作的順序。
* 有些題目離線處理 Coding 複雜度會降低不少,通常可以避開持久化資結或更噁心的資結。
* 但碰到強制在線就只能乖乖寫又臭又長的資結。而題目要出成強制在線通常有兩種方法:
-- 互動式 I/O:你要輸出答案 judge 才會讓你讀下一筆詢問。
-- Hash:每筆詢問會跟上一筆詢問的答案 hash,judge 會給你 hash 後的結果。
:::
# 莫隊算法
#### 起源
* 2009 年由 IOI 中國集訓隊隊長莫濤發明並發揚光大。
* 莫隊 = (莫) 濤 + (隊) 長
* 優化區間詢問的演算法。
* 有一說其實在莫濤之前這個算法就已經出現,莫濤只是做了比較完整的統整。
#### 使用時機例子:區間查詢
有一個長度為 $N$ 的序列,接著有 $Q$ 筆詢問(區間和、區間眾數)。
每筆詢問給區間 $[ql, qr]$,求這個區間的答案,可以離線做。
* $N、Q$ 很大,時間複雜度$O(NQ)$會吃TLE。
* 方便起見,以下假設 $N、Q$ 是同個量級的,也就是說暴力是 $O(N^2)$。
## 仙貝知識1.答案窗口
### 核心概念
:::info
* 我們用另外一個角度看區間詢問。
* 試著維護一個「答案窗口」。
* 當這個窗口對到的區間是 $[l, r]$ 時,代表這個窗口所維護的值正好是詢問 $[l, r]$ 這個區間的答案。
* 假設現在答案窗口停留在 $[l, r]$ ,而我們要回答 $[ql, qr]$ 這個區間的答案。
Question:那我們要把這個窗口「滾動」到 $[ql, qr]$ 。
Answer:怎麼移動?方法有兩種:把窗口延伸或內縮。
實作方法
* 延伸(區間變大):窗口左界往左一格 $(l--)$ ,或是右界往右一格 $(r++)$ 。
* 內縮(區間變小):窗口左界往右一格 $(l++)$ ,或是右界往左一格 $(r--)$ 。
Answer:利用延伸和內縮滾動窗口,把窗口滾到詢問區間,如此依序處理完所有詢問。
:::
### 實作優化
:::success
* 瓶頸一:窗口延伸/內縮一格的時間。
* 瓶頸二:窗口延伸/內縮一格的次數。
先忽略瓶頸一,假設可以 O(1) 把窗口的邊界移動一格。
Question:如何降低移動窗口的次數?
Answer:窗口移動次數取決於你處理詢問區間的順序 ⇒ 降低窗口移動次數。
Question:如何安排區間的處理順序?
Answer:「先排左再排右」,也就是「把所有詢問先按左界排序,左界相同則按右界排序」?
但這最差情況還是 $O(N^2)$ 。反例:$[1, 1], [1, N], [2, 2], [2, N], [3, 3], [3, N], \cdots$,移動到下個詢問最差 $O(N)$ ,總共移動 $O(N)$ 次,最差 $O(N^2)$。
Answer:莫隊算法
:::
## 莫隊算法實作
### 實作流程
:::success
1. 把序列每 $\sqrt{N}$ 個分成一塊。
2. 把所有詢問先按「左界所在的塊」排序。
3. 若左界所在的塊相同,則按右界排序。
4. 滑動次數降為$O(N\sqrt{N})$。
:::
### 滑動次數證明(Q=N)
1. 假設塊的大小和數量都是 $O(\sqrt{N}$) 。
2. 窗口左界複雜度證明
* 當處理完此詢問,要滾動到下一個詢問時,窗口左界的指針移動情形有兩種:在同一塊內移動、移動到下一個塊。
* 因為塊的大小是 $O(\sqrt{N}$) ,所以這兩種情形都是 $O(\sqrt{N}$) 。
* 總共 O(N) 個詢問 ⇒ 窗口左界移動次數 $O(N\sqrt{N}$) 。
3. 窗口右界複雜度證明
4. 總時間複雜度$O(N\sqrt{N})$ 。
* 窗口左界、窗口右界移動次數都是 $O(N\sqrt{N})$ 。
* 移動一格的複雜度剛剛假設是 O(1)。
* 故總時間複雜度 $O(N\sqrt{N})$ 。
### 滑動次數證明(Q≠N)
1. 假設我們能 O(1) 將窗口伸縮一格、塊的大小為 B。
2. 窗口左界:移動到下個詢問只要 $O(B)$,所以總移動次數 $O(QB)$。
3. 窗口右界:在左界同塊的詢問之間移動均攤 $O(N)$ ,總共有 $O(NB)$ 塊,故總移動次數 $O(N^2B)$ 。
4. 合併兩者可得總複雜度 $O(QB+N^2B)$ 。
5. 根據算幾不等式, $B$ 取 $\frac{N}{\sqrt{Q}}$時,有最佳複雜度 O(N√Q)$ 。
### 一些細節
* 假設將窗口伸縮一格不是 $O(1)$ ,而是 $O(f(x))$ 。
* 那複雜度會變成 $O(f(x)N\sqrt{Q})$ ,就是多乘 $O(f(x))$ 就對了。
舉例:假設伸縮一格要 $O(log N)$ ,那複雜度就是 $O(N\sqrt{Q} log N)$ 。
* 莫隊只是幫你優化窗口的移動次數,拔掉一個根號,
* 想砸莫隊的話你得想辦法讓 $O(f(x))$ 盡量小,最好 $O(1)$ 或 $O(log N)$ 。
* 另外,莫隊算法受常數影響非常大,==理論最佳複雜度不代表執行時間最少==。
* 可利用 judge 或本機生大測資三分搜塊的大小。
## 例題一. 區間和
有一個長度為 $N$ 的序列 $⟨a_N⟩$,接下來有 $Q$ 筆詢問。
每筆詢問給 $[l, r]$,求 $\sum ^r_{i=l}a_i$ 。
限制
* $N, Q \leq 1e5$
## 例題一. AC code
```cpp=
#define ll long long
#define pll pair<ll,ll>
#define F first
#define S second
#define ql F.F
#define qr F.S
#define id S
const ll MAXN=2e5+5;
int N,Q,B;
vector<ll> a;
vector<pair<pll,ll>> qry;
ll ans[MAXN];
void init{
cin>>N>>Q;
B=N/max((ll)sqrt(Q),1);//B:塊的大小
a.clear();a.resize(N);
qry.clear();qry.resize(Q);
for(auto& i:a) cin>>i;
int cnt=0;
for(auto& i:qry){
cin>>i.ql>>i.qr;
i.ql--;i.qr--;//轉換成0-base
i.id=cnt++;//先用
}
}
void solve(){
sort(qry.begin(),qry.end(),
[&](const pair<pll,ll>& a,const pair<pll,ll>& b){
if(a.ql/B==b.ql/B) return a.qr<b.qr;
//左界區塊相同,按右界排
else return a.ql/B<b.ql/B;
//否則按左界的區塊來排序
});
ll l=0,r=-1;//答案窗口,初始化為[0, -1]
ll sum=0;////答案窗口維護的值(也就是窗口的區間和)
for(auto& i:qry){
while(l>i.ql) sum+=a[--l];//延伸左界
while(r<i.qr) sum+=a[++r];//延伸右界
while(l<i.ql) sum-=a[l++];//內縮左界
while(r>i.qr) sum-=a[r--];//內縮右界
ans[i.id]=sum;
}
for(ll i=0;i<Q;i++) cout<<ans[i]<<'\n';
}
```
### 實作細節
:::success
1. 因為會把詢問重新排序處理,因此要記錄一下詢問的時間點、開個 ans 陣列存,最後再一起輸出。
2. 順序上是先延伸再內縮,不然答案窗口 $[l, r]$ 可能會出現 l > r 的情況(在某些題目會出錯)。
:::
## 例題二.[區間數字種類數 CSES Distinct Values Queries](https://cses.fi/problemset/task/1734/)
給一個長度為 $N$ 的序列,接下來有 $Q$ 筆詢問。每筆詢問 $[l, r]$,求區間 $[l, r]$ 有幾種數字?
限制
* $N, Q \leq 2 \times 1e5$、$a_i \leq 1e9$
### 解題關鍵
:::info
1. 維護一個答案窗口。
2. 開一個變數 $ans$ 代表這個窗口對應到的答案。
3. 開一個陣列 $cnt$,$cnt[x]$ 紀錄 $x$ 出現多少次。
4. 加入一個數字 $x$(延伸窗口):$cnt[x]++$
5. 拔掉一個數字 $x$(延伸窗口):$cnt[x]--$
6. 當有數字的出現次數不再是 $0$ 或變成 $0$ 就要改變 $ans$。
7. $a_i \leq 1e9$ ⇒ 離散化
:::
### 例題二. AC code
:::success
```cpp=
#include <bits/stdc++.h>
using namespace std;
#define fastio ios::sync_with_stdio(false),cin.tie(0);
#define ll long long
#define pll pair<ll,ll>
#define eb emplace_back
#define F first
#define S second
#define ql F.F
#define qr F.S
#define id S
const ll MAXN=2e5;
ll N,Q,B;
vector<ll> a,dis;//dis存離散
vector<pair<pll,ll>> qry;
ll cnt[MAXN];
ll ans[MAXN];
void init(){
cin>>N>>Q;
B=N/max((int)sqrt(N),1);
a.clear(),qry.clear();
a.resize(N),qry.resize(Q);
for(auto& i:a) {
cin>>i;
dis.eb(i);
}
sort(dis.begin(),dis.end());
dis.resize(unique(dis.begin(),dis.end())-dis.begin());
for(ll i=0;i<N;i++){
a[i]=lower_bound(dis.begin(),dis.end(),a[i])-dis.begin();
}
ll cnt=0;//0-base
for(auto& i:qry){
cin>>i.ql>>i.qr;
i.ql--;i.qr--;
i.id=cnt++;
}
}
void solve(){
sort(qry.begin(),qry.end(),[&](const pair<pll,ll>& a,const pair<pll,ll> b){
if(a.ql/B==b.ql/B) return a.qr<b.qr;
else return a.ql/B<b.ql/B;
});
ll l=0,r=-1,total=0;//左閉右閉
for(auto& i:qry){
while(l>i.ql){
l--;
cnt[a[l]]++;
if(cnt[a[l]]==1) total++;
}
while(r<i.qr){
r++;
cnt[a[r]]++;
if(cnt[a[r]]==1) total++;
}
while(l<i.ql){
cnt[a[l]]--;
if(cnt[a[l]]==0) total--;
l++;
}
while(r>i.qr){
cnt[a[r]]--;
if(cnt[a[r]]==0) total--;
r--;
}
ans[i.id]=total;
}
for(ll i=0;i<Q;i++) cout<<ans[i]<<'\n';
}
int main() {
fastio
init();
solve();
return 0;
}
```
:::
## 例題三.[TIOJ 2122 區區區間眾數](https://tioj.ck.tp.edu.tw/problems/2122)
給一個長度為 $N$ 的序列,接下來有 $Q$ 筆詢問。每筆詢問 $[l, r]$,輸出區間 $[l, r]$ 眾數的出現次數。
限制
* $N, Q \leq 1e5$、$a_i \leq 1e5$
### 解題關鍵
:::info
1. 新的 $max$ 只有兩種可能:維持不變或減 $1$。
2. 維持不變表示有其他眾數存在。
3. 減 $1$ 表示只有被拔掉的那個數字的 $cnt$ 等於 $max$。
4. 開一個陣列 $cntcnt$,維護「出現次數的次數」。
5. 也就是在原本的 $cnt$ 陣列上套一層 $cnt$。
6. 舉例:$cntcnt[1] = 2$ 代表出現次數是 $1$ 的數字有 $2$ 種。
7. 維護好 $cnt$ 跟 $cntcnt$ ,就可以好好處理拔掉數字的情況了!
:::
### 例題三. AC code
:::success
```cpp=
#include <bits/stdc++.h>
using namespace std;
#define fastio ios::sync_with_stdio(false),cin.tie(0);
#define ll long long
#define pll pair<ll,ll>
#define ql first.first
#define qr first.second
#define id second
#define _ <<' '<<
const ll MAXN=1e5+5;
ll N,Q,B;
vector<ll> a;
vector<pair<pll,ll>> qry;
ll ans[MAXN],cnt[MAXN],cntcnt[MAXN];
void init(){
cin>>N>>Q;
B=N/(ll)sqrt(Q);
a.clear(),a.resize(N);
qry.clear(),qry.resize(Q);
for(auto& i:a) cin>>i;
ll cnt=0;
for(auto& i:qry){
cin>>i.ql>>i.qr;//0-base
i.ql--;i.qr--;
i.id=cnt++;
}
}
void solve(){
sort(qry.begin(),qry.end(),[&](const pair<pll,ll>& a,const pair<pll,ll>& b){
if(a.ql/B==b.ql/B) return a.qr<b.qr;
else return a.ql/B<b.ql/B;
});
ll l=0,r=-1,showmax=0;
for(auto& i:qry){
while(l>i.ql){
l--;
cntcnt[cnt[a[l]]]--;
cnt[a[l]]++;
cntcnt[cnt[a[l]]]++;
if(cntcnt[showmax+1]!=0) showmax++;
}
while(r<i.qr){
r++;
cntcnt[cnt[a[r]]]--;
cnt[a[r]]++;
cntcnt[cnt[a[r]]]++;
if(cntcnt[showmax+1]!=0) showmax++;
// cout<<'l' _ l _ 'r' _ r _ "showmax" _ showmax<<'\n';
}
while(l<i.ql){
cntcnt[cnt[a[l]]]--;
cnt[a[l]]--;
cntcnt[cnt[a[l]]]++;
if(cntcnt[showmax]==0) showmax--;
// cout<<'l' _ l _ 'r' _ r _ "showmax" _ showmax<<'\n';
l++;
}
while(r>i.qr){
cntcnt[cnt[a[r]]]--;
cnt[a[r]]--;
cntcnt[cnt[a[r]]]++;
if(cntcnt[showmax]==0) showmax--;
r--;
}
ans[i.id]=showmax;
}
for(ll i=0;i<Q;i++) cout<<ans[i]<<'\n';
}
int main() {
fastio
init();
solve();
return 0;
}
```
:::
## 練習題
* CF 136B Little Elephant and Array
* CF 86D Powerful Array
* CF 877F Ann and Books
* CF 617E XOR and Favorite Number
* CF 101138D Strange Queries (難)
* TIOJ 1699 害蟲決戰時刻
* TIOJ 1694 你的重生之旅
* ABC 242G Range Pairing Query
* ABC 238G Cubic?
# CDQ 分治
#### 起源
* 2008 年由中國 IOI 選手陳丹琦整理、總結
* 原文連結:
https://www.cs.princeton.edu/~danqic/papers/divide-and-conquer.pdf
* CDQ = 陳丹琦
* CDQ 分治是從 Merge Sort 的概念延伸出來的,解決問題的方法
* 具體可以解決哪些類型的問題不好總結,接下來就知道了 > <
# 二維偏序
* 接下來要介紹的是偏序問題,偏序問題的解法蘊含 CDQ 分治的核心思想
* 逆序數對是一種偏序問題,如果你寫過逆序數對 (用 Merge Sort 做),你基本上已經會 CDQ 分治了
## 名詞定義
* 序 = 集合裡面兩個東西之間的大小、順序關係
* 例如整數集合中,「1 < 2」、「3 < 4」
* 全序:任兩個東西之間一定可以互相比較
* 偏序:任兩個東西之間不一定可以互相比較
## 例題一. 二維偏序問題
* 有 $N$ 個數對,對於第 i 個數對 $(x_i, y_i)$,計算有多少個數對 $(x_k, y_k)$ 滿足 $x_k < x_i$ 且 $y_k < y_i$。
* (假設所有 xi 相異、所有 yi 相異)
## 實作流程
:::success
* 先把所有數對按照 $x$ 座標排序
---
* 接著想辦法算答案,也就是對每個 $(x_i, y_i)$ 計算有多少個 $(x_k, y_k)$ 滿足條件,你會發現滿足條件的 $k$ 一定在 $i$ 的前面 (因為按照 $x$ 座標排序)
* 從序列中間砍一半,$k$、 $i$ 可以分成三種情況
-- $k$ 在左邊,$i$ 在左邊
-- $k$ 在右邊,$i$ 在右邊
-- $k$ 在左邊,$i$ 在右邊
* 套用分治,前兩種情況可以遞迴解決
* 現在要處理第三種情況
----
* 現在要處理 $k$ 在左邊,,$i$ 在右邊的情況
* 重要觀察:左邊的所有 $x$ 座標都比右邊的小
* 計算 $k$ 在左邊,$i$ 在右邊的情況時,可以忽視 $x$ 座標!
* 現在把左右兩半邊都按照 $y$ 座標排序
* 接著枚舉右邊的每個數對 $i,,計算左邊有多少數對 $k$ 滿足條件
* 當枚舉 $i$ 的指針往右移動時,滿足條件的 $k$ 不會變少
* 雙指針把算到 $k$ 的數量累加在 $i$ 的答案
---
Question: 那要怎麼對y排序呢?
* 「把點按照 y 排序」那邊如果是用一般的 sort 排序,總
時間複雜度會是 $O(nlog^2n)$
* 但其實這可以套用 Merge Sort 的框架,對 y 座標
Merge Sort,總時間複雜度可以降為 $O(n log n)$
:::
## 實作方法
:::success
```cpp=
#define eb emplace_back
struct info{
ll x,y,id;//id是數對在原本陣列的位置
}
ll N;
vector<info> v;
vector<ll> ans;
void init(){
cin>>N;
v.clear();v.resize(N);
ans.resize(N);
ll cnt=0;
for(auto& i:v){
cin>>i.x>>i.y;
i.id=cnt++;
}
}
void cdq(ll L,ll R){
if(L==R) return;
ll mid=(L+R)/2;
cdq(L,mid),cdq(mid+1,R);
vector<info> temp;//存暫時排序的值
ll lptr=L,rptr=mid+1;
while(lptr<=mid && rptr<=R){
if(v[lptr].y<v[rptr].y) temp.eb(v[lptr++]);
else{
temp.eb(v[rptr]);
ans[v[rptr].id]+=(lptr-L);
rptr++;
}
}
while(lptr<=mid) temp.eb(v[lptr++]);
while(rptr<=R){
temp.eb(v[rptr]);
ans[v[rptr].id]+=(lptr-L);
rptr++;
}
for(ll pt=0,pv=L;pv<=R;pt++,pv++){
v[pt]=temp[pv]
}
}
int main(){
init();
sort(v.begin(),v.end(),
[&](const info& a,const info& b){
return a.x<b.x;
});
cdq(0,N-1);
return 0;
}
```
:::
# 三維偏序
* 有 $N$ 個數對,對於第 $i$ 個數對 $(x_i, y_i, z_i)$,計算有多少個數對 $(x_k, y_k, z_k)$ 滿足 $x_k < x_i$ 且 $y_k < y_i$且 $z_k < z_i$。
* (假設所有 $x_i$ 相異、所有 $y_i$ 相異、所有 $z_i$ 相異)
## 實作流程
:::success
* 解法其實是二維偏序的延伸,一樣先對 x 排序、對 y 套 Merge Sort。
* 在做雙指針的時候,我們已經考慮完了 x, y 兩個維度,想看剩下的 z 座標該怎麼處理?
1. 老樣子,先按照 $x$ 排序,這樣就解決掉 $x$ 這個維度了
2. 接著對 $y$ 套 Merge Sort,假設已經進行到最後一步合併
3. 現在目標:枚舉右邊的數對 $i$,計算左邊有多少數對 $k$ 滿足條件$y$ 這個維度一樣用 Merge Sort 的雙指針解決,那 $z$ 呢?
4. 很簡單,資料結構就行了!
* 目標:枚舉右邊的數對 $i$,計算左邊有多少數對 $k$ 滿足條件
* 相較於二維偏序,需要多一項工具幫我們處理第三個維度:值域資料結構 (BIT / 線段樹)
* 值域資結用來維護「數字的累計次數」
* 考慮 Merge Sort 過程中,接下來該放進新陣列的東西
* 如果是要放左半邊的東西,那就單點加值,把 $z_k$ 的次數 $+1$
* 如果是要放右半邊的東西,那就詢問 $1 ∼ z_i − 1$ 的次數總和
* 次數總和即是 對 $i$ 而言,左半邊有多少數對 $k$ 滿足 $x, y, z$ 三個維度都小於 $i$ ,正是我們要算的!
:::
## 實作方法
:::success
```cpp=
#define eb emplace_back
struct info{
ll x,y,z,id;//id是數對在原本陣列的位置
}
ll N;
vector<info> v;
vector<ll> ans;
void init(){
cin>>N;
v.clear();v.resize(N);
ans.resize(N);
ll cnt=0;
for(auto& i:v){
cin>>i.x>>i.y>>i.z;
i.id=cnt++;
}
}
void cdq(ll L,ll R){
if(L==R) return;
ll mid=(L+R)/2;
cdq(L,mid),cdq(mid+1,R);
vector<info> temp;//存暫時排序的值
ll lptr=L,rptr=mid+1;
//初始化BIT
while(lptr<=mid && rptr<=R){
if(v[lptr].y<v[rptr].y){
temp.eb(v[lptr]);
BIT.upd(v[lptr].z,1);
lptr++;
}
else{
temp.eb(v[rptr]);
ans[v[rptr].id]+=BIT.qry(v[rptr].z-1);
rptr++;
}
}
while(lptr<=mid){
temp.eb(v[lptr]);
BIT.upd(v[lptr].z,1);
lptr++;
}
while(rptr<=R){
temp.eb(v[rptr]);
ans[v[rptr].id]+=BIT.qry(v[rptr].z-1);
rptr++;
}
for(ll pt=0,pv=L;pv<=R;pt++,pv++){
v[pt]=temp[pv]
}
}
int main(){
init();
sort(v.begin(),v.end(),
[&](const info& a,const info& b){
return a.x<b.x;
});
cdq(0,N-1);
return 0;
}
```
:::
## 實作優化
:::danger
* 上面的 code 其實效率很差
* 瓶頸在於「初始化 BIT」,如果每次都要重新掃過整個 BIT 初始成 0,那這樣會 TLE
* 解決方法很簡單,只要記錄你在 BIT 做的操作,在 cdq() 函式結尾撤銷這些操作就好。
* 這樣時間複雜度就好很多,等等會分析具體複雜度是多少。
```cpp=
#define eb emplace_back
#define mkp make_pair
struct info{
ll x,y,z,id;//id是數對在原本陣列的位置
}
ll N;
vector<info> v;
vector<ll> ans;
void init(){
cin>>N;
v.clear();v.resize(N);
ans.resize(N);
ll cnt=0;
for(auto& i:v){
cin>>i.x>>i.y>>i.z;
i.id=cnt++;
}
}
void cdq(ll L,ll R){
if(L==R) return;
ll mid=(L+R)/2;
cdq(L,mid),cdq(mid+1,R);
vector<info> temp;//存暫時排序的值
vector<pll> BIT_op;
ll lptr=L,rptr=mid+1;
//初始化BIT
while(lptr<=mid && rptr<=R){
if(v[lptr].y<v[rptr].y){
temp.eb(v[lptr]);
BIT.upd(v[lptr].z,1);
BIT_op.eb(mkp(v[lptr].z,1));
lptr++;
}
else{
temp.eb(v[rptr]);
ans[v[rptr].id]+=BIT.qry(v[rptr].z-1);
rptr++;
}
}
while(lptr<=mid){
temp.eb(v[lptr]);
//小小常數優化:這兩行不需要
//想想看Merge Sort的過程就可以知道為什麼
//(答案不改變阿)
//BIT.upd(v[pl].z,1);
//BIT_op.ebk(mkp(v[pl].z,1));
lptr++;
}
while(rptr<=R){
temp.eb(v[rptr]);
ans[v[rptr].id]+=BIT.qry(v[rptr].z-1);
rptr++;
}
for(ll pt=0,pv=L;pv<=R;pt++,pv++){
v[pt]=temp[pv]
}
//撤銷所有操作,讓BIT變回初始狀態
for(auto& op:BIT_op){
BIT.upd(op.first,-op.second);//區間加值
}
}
int main(){
init();
sort(v.begin(),v.end(),
[&](const info& a,const info& b){
return a.x<b.x;
});
cdq(0,N-1);
return 0;
}
```
* Merge Sort 的複雜度:$O(n log n)$
* 操作一次 BIT 的複雜度:$O(log C)$,$C$ 是值域
* 總時間複雜度:$O(n log n log C)$
* 把所有維度先離散化的話,複雜度會變為 $O(n log^2n)$
:::
## 三維偏序—變形
### 第一種:< 變 >
非常簡單:
1. x 座標:由大到小排序
2. y 座標:對 y Merge Sort 時,就改成由大排到小
3. z 座標:值域 BIT / 線段樹,前綴詢問改成後綴
另解
* 把x,y,z乘上(-1),其餘code跟<相同
### 第二種:< 變 ≤
一樣很簡單,解決方式:
1. x 座標:維持一樣的方法
2. y 座標:對 y Merge Sort 時,合併時先放左邊的條件:if (左 < 右) 改成 if (左 $\leq$ 右)
3. z 座標:值域 BIT / 線段樹簡單改一下詢問區間就好
## 第三種:一樣是 <,只是一個維度會有相同數字出現
這個比較麻煩一點,解決方式:
1. 首先,先==把長得一模一樣的數對綁在一起==
2. x 座標:==x 小的在前,如果 x 相同那 y 大的在前,如果還是相同那 z 大的在前==
3. y 座標:對 y Merge Sort 時,合併時先放左邊的條件一定要是 if (左 < 右),確保 y 相同的時候,會是右邊先被放入
4. z 座標:維持一樣的方法
## 模板測試
* 嚴格偏序:CF 101485G
* 非嚴格偏序:洛谷 P3810 三维偏序模板題
## 偏序問題的啟發
* 剛剛偏序問題的解法其實就是 CDQ 分治的一種
* CDQ 分治的核心:利用 Merge Sort 的合併過程,製造單調性,從而降低需要考慮的維度
* 三維偏序的解法中:
-- 處理 x 這個維度的手段:一開始的排序
-- 處理 y 這個維度的手段:Merge Sort 的單調性 (雙指針)
-- 處理 z 這個維度的手段:BIT / 線段樹
* 如果不用 CDQ 的話,你會需要寫二維動態開點資料結構,實作繁雜常數又大
* 以剛才的例題,我們會說這是「對 x 分治,對 y 合併排序」
* 對誰分治、對誰合併排序,你可以任意選擇,選實作方便的就好
* 高維度的偏序一樣可以用 CDQ,只不過要嵌套,也就是 CDQ 套 CDQ
* 參考這篇四維偏序的教學:https://www.cnblogs.com/mlystdcall/p/6232324.html,寫得非常好懂,因為上課時間有限所以暫時不細講
* 一層 CDQ 可以幫你壓掉一個維度
* $k$ 維偏序需要壓掉 $k − 1$ 層維度,所以要套 $k − 1$ 層 CDQ時間複雜度會是 $O(n log^{k−1}n)$
* 其實你不一定要用 CDQ,你也可以寫$k − 1$ 維資料結構之類的,常數什麼的自己想辦法 OwO
# 操作分治