becaido
講師介紹
因為想學習離線
所以來當離線講師
Mo's Algorithm
詢問區間眾數
\(N,Q\leq 10^5\)
暴力做法
維護兩個指針 \(L,R\)
每次詢問把指針一格一格移到詢問的 \(l,r\)
每次加入 / 刪除元素可以
\(O(1)\) 維護目前眾數是多少
複雜度 \(O(NQ)\)
想減少指針移動的次數
每 \(B\) 個元素分一塊
對詢問的區間排序
離線處理
排序時先比較兩個詢問的 \(l\) 在哪一塊
如果不同塊可以小的排前面
如果一樣則比較 \(r\)
按照排序好的區間移動指針
複雜度分析
對於所有 \(l\) 在同一塊的詢問
左指針 \(L\) 每次詢問只會移動 \(O(B)\) 次
右指針 \(R\) 會遞增,加起來最多移動 \(O(N)\)
共 \(\frac{N}{B}\) 塊
複雜度 \(O(QB+\frac{N^2}{B})\)
\(B 取 \frac{N}{\sqrt{Q}}\) 最好
複雜度 \(O(N\sqrt{Q})\)
莫隊使用時機
可以很快速計算加入 / 刪除的元素
對整體的貢獻
區間詢問子區間 xor 最大值
\(N,Q\leq 10^5,a_i\leq 10^9\)
先轉成前綴 xor
\(p_i=a_1\oplus a_2\oplus\dots\oplus a_i\)
詢問 \(l,r\) 相當於求
\(l\leq i\leq j\leq r\)
\(p_j\oplus p_{i-1}\) 的最大值
先輩知識
01-Trie \(O(\log C)\) 求 xor 極值
想使用莫隊
但是刪除元素時不知道最大值會變什麼
改變莫隊的方法
一樣先排序好區間
對於在 \(l,r\) 在同一塊的詢問
可以 \(O(B\log C)\) 直接算
對於 \(l\) 都在同一塊的詢問
把每個區間分成左右兩邊
在 \(k\) 右邊是遞增的
每次先加入他們
在 \(k\) 左邊則是加完右邊後暴力加
每次回答完後要回溯操作
把在 \(k\) 左邊的人都刪掉
把最大值調回來
複雜度 \(O(QB\log C+\frac{N^2}{B}\log C)\)
\(B\) 一樣取 \(\frac{N}{\sqrt{Q}}\)
共 \(O(N\sqrt{Q}\log C)\)
此種技巧稱為回滾莫隊
練習題:ZJ i531
使用時機:
加入元素很好維護答案
刪除元素時很難維護答案
有 \(n\) 個人,每個人的目標金額是 \(V_i\)
有 \(m\) 塊土地,第 \(i\) 塊土地的主人是 \(a_i\)
接下來會有 \(Q\) 次拍照,每次拍區間 \([L_i,R_i]\) 的土地
只要自己有任何一個土地被拍到就可以增加 \(C_i\) 的金錢
若一個人有多個土地只會加一次
問每個人會在第幾次拍照後達成目標
看到詢問最早會想到二分搜
但每個人都二分搜一次太慢了
試著讓全部人一起
來看 Code
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 5;
int n, m, q;
int ans[N];
int a[N], V[N];
vector<int> p[N];
int L[N], R[N], C[N];
int last[N];
ll sum[N];
ll bit[N];
void upd(int pos, int x) {
for (; pos <= m; pos += pos & -pos) bit[pos] += x;
}
ll que(int pos) {
ll re = 0;
for (; pos; pos -= pos & -pos) re += bit[pos];
return re;
}
ll que(int l, int r) {
return que(r) - que(l - 1);
}
void divide(int l, int r, vector<int> &ask) {
if (l == r || ask.size() == 0) {
for (int i : ask) ans[i] = l;
return;
}
int mid = (l + r) / 2;
vector<tuple<int, int, int>> op;
vector<int> pos;
for (int i = l; i <= mid; i++) {
op.emplace_back(L[i], L[i], C[i]);
op.emplace_back(R[i] + 1, L[i], -C[i]);
}
for (int i : ask) pos.insert(pos.end(), p[i].begin(), p[i].end());
sort(op.begin(), op.end());
sort(pos.begin(), pos.end());
int cur = 0;
for (int i : pos) {
while (cur < op.size() && get<0>(op[cur]) <= i) {
upd(get<1>(op[cur]), get<2>(op[cur]));
cur++;
}
sum[a[i]] += que(last[a[i]] + 1, i);
last[a[i]] = i;
}
vector<int> askl, askr;
for (int i : ask) {
if (sum[i] >= V[i]) askl.emplace_back(i);
else {
V[i] -= sum[i];
askr.emplace_back(i);
}
last[i] = sum[i] = 0;
}
ask.clear();
for (int i = 0; i < cur; i++) upd(get<1>(op[i]), -get<2>(op[i]));
divide(l, mid, askl);
divide(mid + 1, r, askr);
}
int main() {
ios::sync_with_stdio(false), cin.tie(0);
cin >> n >> m >> q;
for (int i = 1; i <= m; i++) cin >> a[i], p[a[i]].emplace_back(i);
for (int i = 1; i <= n; i++) cin >> V[i];
for (int i = 1; i <= q; i++) cin >> L[i] >> R[i] >> C[i];
vector<int> ask(n);
iota(ask.begin(), ask.end(), 1);
divide(1, q + 1, ask);
for (int i = 1; i <= n; i++) {
if (ans[i] == q + 1) ans[i] = -1;
cout << ans[i] << '\n';
}
}
練習題:TIOJ 1840
有一個長度 \(N\) 的陣列 \(a_1\sim a_N\)
有 \(Q\) 個操作
每個操作可能為單點改值或區間求和
可以用線段樹或 BIT
不過有可以不用資結的方法
暴力做法
建好前綴和
每次問區間和時先算出初始的答案
然後把之前全部改值都跑一遍
\(O(N+Q^2)\)
改成每 \(K\) 個詢問用這個做法
做完重新建前綴和
\(O(QK+\frac{NQ}{K})\)
\(K\) 取 \(\sqrt{N}\) 複雜度可以到 \(Q\sqrt{N}\)
有一棵樹,樹上有紅點跟藍點
一開始點 \(1\) 是紅點,其他點都是藍點
有 \(Q\) 個操作,把某個點設成紅點或是詢問一點到紅點的最短距離
可以用操作分塊的思想
每 \(K\) 個操作分一塊
可以先對目前的紅點跑一次多點源 bfs
算出每個點到紅點的最短距離
之後的詢問可以跟在他之前設成紅點的人用 lca 算距離
如果預處理 lca 可以 \(O(1)\) 求
複雜度 \(O(N\log N+Q\sqrt{N})\)
TIOJ 2202:King Game
有一個長度 \(N\) 的陣列 \(a_1\sim a_N\)
\(Q\) 個詢問,每次給 \(L,R,x,y\)
可以花 \(w_i\) 把 \(a_i\) 改成任何數字
想讓 \(a_L\sim a_R\) 裡有 \(z\) 種數字
且 \(x\leq z\leq y\)
問最小花費
\(N,Q,a_i,w_i\leq 10^5\)