離線演算法

 

建國中學 賴昭勳

離線是什麼

 

 

OFFLINE?

 

處理有多筆詢問的問題的時候

 

  • 在線:對每個詢問分別處理,詢問之間沒有任何關聯

 

  • 離線:先讀入所有詢問,可以透過排序詢問等方法在不同詢問之間取得共同的資訊

莫隊演算法

有一種問題長這樣...

給你一個長度為\(n\)的整數序列,有\(Q\)筆詢問,每次詢問區間\([l, r]\)的某種東西(數量/最大最小值...)

  • 看到\(n\)沒有到超級無敵大的時候(\( \leq 10^5\))
  • 想要用奇怪資料結構做卻沒有辦法/不會的時候...
  • 想要唬爛的時候

莫隊(Mo's Algorithm)是什麼

假設我已知\([l, r]\)的答案,能夠快速求出\([l, r + 1], [l, r - 1], [l + 1, r], [l - 1, r]\)的答案的時候

+1
-1
+1
-1

我們的目標

找到一種處理詢問的順序,使得各詢問之間的總移動量最少。

做法:對詢問分塊!

分塊想法

假設每塊大小為 \(k\)

  1. 先將詢問依照左界分組

  2. 把每塊內的詢問按照右界排序
  3. 沒了

具體會發生什麼事

在每一組裡面,右界會遞增,左界則會在大小為\(k\)的範圍內亂跳。

也就是說,假設有\(x\)個東西在同一組,則總移動量為

\(O(kx + n)\)

 

那這樣的複雜度分析是多少?

複雜度分析

假設移動一次需要\(O(m)\)的時間,則

分塊的時間是\(O(q)\)

排序的總時間是\(O(qlogq)\)

每塊處理移動\(O((kx + n)*m), \sum x = q\)

兩塊之間的移動\(O(nm)\)

總共有\(O(n / k)\)塊

 

所以總複雜度為 \(O(qlogq + (n^2/ k + qk)m)\)

算幾又來啦

由算幾不等式可得\(n^2/k + qk \geq 2\sqrt {n^2q}\)

故當\(n^2/k = qk \rightarrow k \approx \sqrt{n}\)時

\(n^2/k + qk\)的最小值為\(O((n + q)\sqrt{n})\)

 

所以最佳總複雜度為 \(O(qlogq + (n + q)\sqrt{n}\cdot m)\)

 

 

註:在這裡我們假設\(n \approx q\)

這樣好像就能好好做了?

回到莫隊的使用時機...

 

「假設我已知\([l, r]\)的答案,能夠快速求出\([l, r + 1], [l, r - 1], [l + 1, r], [l - 1, r]\)的答案的時候」

 

也就是說,要對一個題目使用莫隊,必須先找出方法快速移動一個區間的答案

來看看例題

https://tioj.ck.tp.edu.tw/problems/2122

 

給你一個長度為\(n\)的序列,有\(q\)筆詢問\([l, r]\),求\([l, r]\)區間眾數出現的次數

 

\(n, q \leq 10^5, 0 <a_i \leq 10^5\)

對於一個區間,我要存下什麼資訊?

當我移動一格的時候,這個資訊又會怎麼改變?

Hint: 眾數->出現最多次的數字,你必須知道每個數字出現幾次,還有?

Ans: 紀錄兩個陣列,一個為每數字在目前的區間中出現幾次,另一個為每個出現次數有幾個數字

當我要移動的時候...

設 \(a[i]\)代表 數字 \(i\)出現的次數,

\(b[i]\)代表有幾種數字在目前的區間出現\(i\)次。

則:

a[0] a[1] a[2]
1 2 2
b[0] b[1] b[2]
0 1 2

移除一個 \(1\)的時候:

  • \(b[2]--\)
  • \(a[1]--\)
  • \(b[1]++\)
+1
-1
-1

最後紀錄答案

另外紀錄一個變數代表「目前最大的\(x\),使\(b[x] > 0\)」

因為每次新增/減少一個東西時,\(x\)至多改變一,所以可在\(O(1)\)之內進行莫隊的「移動」

由前面的複雜度分析可得知,時間複雜度為

\(O(qlogq + (n + q)\sqrt{n})\)

實作時間!

實作這題的時候要注意一些細節:

  • 詢問的排序:要在每筆詢問額外紀錄他的\(id\),最後按照順序輸出
  • 移動的順序:在移動時必須保證左界\(\leq\)右界,所以應該先讓左界往左/右界往右,再讓左界往右/右界往左

 

附上code: https://pastebin.com/T0V5vvTf

其他例題

區間逆序數對數,\(n \leq 23000, q \leq 2\times10^5\)

Hint:按我獲取提示

 

搭配資料結構,考慮新增一個數字時會多形成多少對逆序數對

植物大戰殭屍 2

https://tioj.ck.tp.edu.tw/problems/1922

 

區間詢問:如果\(a_i\)出現\(k\)次,則會獲得價值\(a_i \times k^2\),求每個區間的價值和

Hint: 

當\(a_i\)的個數增加一的時候,價值會如何增加?

給你一個數列\(a_i\)和數字\(k\),每次詢問一個區間有多少個子區間 xor 起來是 \(k\)

 

\(n, q \leq 10^5, 0 \leq a_i, k \leq 10^6\)

Hint: 

子區間xor -> 前綴xor

增加一個\(a_i\)時,只有一種數字和他xor起來是\(k\)

整體二分搜

大家有聽過二分搜吧?

正常二分搜的時候,可以把拿來二分搜的東西想成一個區間,每次查詢答案是在區間的左半還是右半,然後更新區間走下去。

那假設我要一次搜很多的詢問呢?

整體二分搜:

  1. 將區間切成兩半

  2. 對每個詢問+元素看要去左半還是右半
  3. 遞迴下去做

 

原因:可以改變詢問順序,一次處理多筆詢問

直接看例題:Coding Days

https://tioj.ck.tp.edu.tw/problems/1840

給你一個序列,支援三種操作:

1. 查詢 \([l, r]\)第 \(k\)小的值

2. 改變一個元素的值​

3. 輸出 7122

 

\(n \leq 50000, q \leq 10000\)

這是什麼鬼

先考慮靜態(沒有第二種操作)的版本

看看有沒有方法用二分搜在 \(O(n\log C)\)之內找到答案

這裡要特別注意二分搜的做法,因為有一個小小優化,之後處理多筆詢問時將大大的改變複雜度

對答案二分搜吧

當前有個答案的可能區間\([l, r)\),每次檢查\(mid = (l + r) / 2\),看有幾個數字小於等於他

如果個數大於\(k\),則答案在\([l, mid)\),否則在\([mid, r)\)

 

這裡遞迴下去的時候,注意到每個數字也能被分到左邊右邊

(如果答案大於\(mid\),只需算\(mid \leq a_i < r\)的數字)

 

一筆詢問時,這樣複雜度不變:\(O(n\log C)\)

但是多筆詢問呢?

全部一起二分搜

目前有一個序列,每筆詢問有\(l, r, k\),現在要對每筆詢問看他的答案是否小於\(mid\)

 

位置維護一個 BIT,如果\(a_i \leq mid\)就把位置 \(i\)增加一,這樣即可在\(O(\log n)\)的時間內查詢在一個區間\( \leq mid\)的個數


這樣就可以將每個詢問分成兩邊,且依照每個\(a_i\)也分成兩邊

那修改怎麼辦啊www

把修改也當成一種詢問!

處理詢問時維持原本的詢問順序。把「將\(x\)改為\(y\)」看成「刪掉一個\(x\),加上一個\(y\)」,同樣能用BIT好好做。

 

而且這樣的話一個修改操作也可以被分到不同的兩塊!

時間複雜度是多少?

每一層的遞迴中,所有詢問,所有元素皆只出現一次,而每次處理需要\(O((n + q) \log n)\)的時間,至多有\(O(\log C)\)層

 

總複雜度 \(O((n + q) \log n \log C)\)

澂逝嘜

#pragma GCC optimize("Ofast")
#pragma loop_opt(on)
#include <iostream>
#include <algorithm>
#include <utility>
#include <vector>
#define maxn 100005
#define ll long long
using namespace std;
int cnt = 0;
struct query {
	int type = 0, ind = 0;
	int l = 0, r = 0, k = 0;
	query(int t, int a, int b, int c) {
		type = t, l = a, r = b, k = c;
		ind = cnt++;
	}
};
int ans[maxn];
struct BIT {
	int arr[maxn];
	void modify(int ind, int n, int val) {
		for (;ind <= n;ind += ind & (-ind)) arr[ind] += val;
	}
	int query(int ind) {
		int ret = 0;
		for (;ind;ind -= ind & (-ind)) ret += arr[ind];
		return ret;
	}
}bit;
int a[maxn];
int n;
void solve(vector<query> que, vector<int> ind, ll low, ll up) { //[low, up)
	//cout << low << " " << up << " " << mid << endl;
	if (up <= low || que.size() == 0) return;
	if (up - low == 1) {
		for (query i:que) {
			ans[i.ind] = low;
		}
		return;
	}
	ll mid = (low + up) / 2;
	//cout << low << " " << up << " " << mid << " " << endl;
	vector<int> lind, rind, bm;
	for (int i:ind) {
		if (a[i] < mid) {
			bit.modify(i, n, 1), bm.push_back(i);
			lind.push_back(i);
		} else {
			rind.push_back(i);
		}
	}
	/*
	for (int i:lind) cout << i << " ";
	cout << endl;
	for (int i:rind) cout << i << " ";
	cout << endl;
	*/
	vector<query> left, right;
	for (auto q:que) {
		//cout << q.ind << " ";
		int tol = 0;
		if (q.type == 1) {
			//cout << "  " << q.ind << " " << bit.query(q.r) - (q.l - 1 ? bit.query(q.l - 1) : 0) << " " << q.k<< endl;
			int val = bit.query(q.r) - (q.l - 1 ? bit.query(q.l - 1) : 0);
			if (val >= q.k) tol = 1;
			else q.k -= val;
		} else if (q.type == 2) {
			if (q.l < 0) {
				if (q.k < mid) {
					//cout << " " << -q.l << ' ' << q.k << endl;
					bit.modify(-q.l, n, -1), bm.push_back(q.l);
					//cout << "   " << bit.query(1) << endl;
					tol = 1;
				}
			} else {
				if (q.k < mid) {
					//cout << " " << q.l << ' ' << q.k << endl;
					bit.modify(q.l, n, 1), bm.push_back(q.l);
					tol = 1;
				}
			}
		}
		if (tol) left.push_back(q);
		else right.push_back(q);
	}
	//cout << endl;
	for (int i:bm) {
		if (i > 0) bit.modify(i, n, -1);
		else bit.modify(-i, n, 1);
	}
	solve(left, lind, low, mid);
	solve(right, rind, mid, up);
}
int main() {
	ios_base::sync_with_stdio(0);cin.tie(0);
	int t;
	cin >> t;
	while (t--) {
		int q;
		cin >> n >> q;
		int cop[n];
		vector<int> index;
		for (int i = 1;i <= n;i++) cin >> a[i], cop[i] = a[i], index.push_back(i);
		vector<query > que;
		for (int i = 0;i < q;i++) {
			int type;
			cin >> type;
			if (type == 1) {
				int l, r,k;
				cin >> l >> r >> k;
				que.push_back(query(type, l, r, k));
			} else if (type == 2) {
				int ind, v;
				cin >> ind >> v;
				que.push_back(query(type, -ind, 0, cop[ind]));
				//cout << -ind << " " << cop[ind] << endl;
				cop[ind] = v;
				que.push_back(query(type, ind, 0, v));
				//cout << ind << " " << v << endl;
			} else {
				int x, v;
				cin >> x >> v;
				que.push_back(query(type, 0, 0, 7122));
			}
		}
		solve(que, index, -(1LL<<31), 1LL<<31);
		for (query q:que) {
			if (q.type == 3) {
				cout << "7122" <<  "\n";
			} else if (q.type == 1) {
				cout << ans[q.ind] << "\n";
			}
		}
	}

}

這題滿分解需要用到我們還沒教過的東西

大家可以先想想第三個Subtask (\(a_i\)相異)怎麼做

Hint: 

對天數(答案)整體二分

對每筆詢問紀錄他還距離目標還有多遠

CDQ 分治

還記得分治嗎?

merge sort 的時候,把區間切成兩半解決之後合併結果。

 

這個想法套用在離線的框架上可以做什麼事情?

操作分治!

假設我們有一系列的操作,可能是修改/詢問目前的東西

 

那我們可以把操作看成是按照「時間」順序排!

處理詢問的時候必須考慮「時間」在他前面的所有修改操作

操作分治,就是對這個「時間」的序列/維度進行分治

回顧分治

假設處理好(遞迴)前半操作對前半詢問的影響,

後半操作對後半詢問的影響,

那麼我們只剩下前半操作對後半詢問的影響了!

紅對紅,綠對綠都已做完

只剩下紅對綠

T

用修改與查詢看逆序數對

https://tioj.ck.tp.edu.tw/problems/1080

把一個數字\(a_i\)看成:

  • 詢問比\(a_i\)小的數字有幾個
  • 新增一個\(a_i\)

 

此時,我們能在\(O(n)\)內找到左半數字對右半查詢的影響

中國競程圈流傳著一個名言...

CDQ 分治可以解決高維偏序問題

給你一個數列,有\(m\)次操作。

每次給一個\(x\),把所有為\(x\)的數字都刪除。

請輸出每次操作後的逆序數對數。

先將問題抽象化

每個數字有三個屬性:\(i, a_i, t_i\),其中\(t_i\)代表這個元素會被刪除的時間

問題變成,對於每一個\(i\),有多少\(j\)符合

\((i < j, a_i > a_j \ or \ i > j, a_i < a_j), t_i > t_j\)

當我們套分治的時候,目的是希望把一個維度做掉,這時候可以把哪個維度去除呢?

考慮把操作倒回來做,每次增加一些元素,看這些元素加進來時會多多少逆序數對

套上CDQ分治

對\(index\),也就是\(i < j\)的維度做分治。

 

每次把當前的序列切成左右兩半,遞迴處理好各自的答案後,只需要處理左邊和右邊之間的點對就好。

左邊

\(i\)

\(a_i\)

\(t_i\)

右邊

\(j\)

\(a_j\)

\(t_j\)

<

左邊

\(j\)

\(a_j\)

\(t_j\)

右邊

\(i\)

\(a_i\)

\(t_i\)

<

或是

>
>
>
<

套上CDQ分治

對時間開一個BIT

用merge sort 的方式,對於一個\(a_i\),所有小於他的\(a_j\)全部都被用\(t_j\)丟到BIT裡面,再查詢當前比\(t_i\)小的元素有幾個,就可以去更新第\(i\)項的答案

 

這樣的總複雜度就是\(O(n {log}^2 n)\)

左邊

\(i\)

\(a_i\)

\(t_i\)

右邊

\(j\)

\(a_j\)

\(t_j\)

<
>
>

例題:

No Judge: 給你一個二維平面,處理兩種操作

  1. 在\(x_i, y_i\)的點上加入權重\(w_i\)
  2. 詢問以\(0, 0\)為左下角,\(x_i, y_i\)為右上角的矩形所覆蓋的權重和

 

\(q \leq 10^5, 0 \leq x_i, y_i \leq 10^9\)

Hint: 

只考慮前半對後半的影響的話

你就可以把詢問和修改的一個維度(\(x_i\)或\(y_i\))排序然後用merge sort做

謝謝大家

 

離線真的很難

這裡面的內容我也是最近才弄懂

大家加油><

離線演算法 (資讀)

By justinlai2003

離線演算法 (資讀)

  • 1,121