樹上莫隊
括號序樹上莫隊
一般的莫隊只能處理線性問題,我們要把樹強行壓成序列。
我們可以將樹的括號序跑下來,把括號序分塊,在括號序上跑莫隊。
具體怎麼做呢?
過程
dfs 一棵樹,然後如果 dfs 到 x 點,就 push_back(x),dfs 完 x 點,就直接 push_back(-x),然後我們在挪動指針的時候,
- 新加入的值是 x --->
add(x)
- 新加入的值是 - x --->
del(x)
- 新刪除的值是 x --->
del(x)
- 新刪除的值是 - x --->
add(x)
這樣的話,我們就把一棵樹處理成了序列。
例題
例題 「WC2013」糖果公園
題意:給你一棵樹,樹上第 \(i\) 個點顏色為 \(c_i\),每次詢問一條路徑 \(u_i\),\(v_i\), 求這條路徑上的
\(\sum_{c}val_c\sum_{i=1}^{cnt_c}w_i\)
其中:\(val\) 表示該顏色的價值,\(cnt\) 表示顏色出現的次數,\(w\) 表示該顏色出現 \(i\) 次後的價值
過程
先把樹變成序列,然後每次添加/刪除一個點,這個點的對答案的的貢獻是可以在 \(O(1)\) 時間內獲得的,即 \(val_c\times w_{cnt_{c+1}}\)
發現因為他會把起點的子樹也掃了一遍,產生多餘的貢獻,怎麼辦呢?
因為掃的過程中起點的子樹裏的點肯定會被掃兩次,但貢獻為 0。
所以可以開一個 \(vis\) 數組,每次掃到點 x,就把 \(vis_x\) 異或上 1。
如果 \(vis_x=0\),那這個點的貢獻就可以不計。
所以可以用樹上莫隊來求。
修改的話,加上一維時間維即可,變成帶修改樹上莫隊。
然後因為所包含的區間內可能沒有 LCA,對於沒有的情況要將多餘的貢獻刪除,然後就完事了。
實現
參考代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154 | #include <algorithm>
#include <cmath>
#include <cstdio>
using namespace std;
const int maxn = 200010;
int f[maxn], g[maxn], id[maxn], head[maxn], cnt, last[maxn], dep[maxn],
fa[maxn][22], v[maxn], w[maxn];
int block, index, n, m, q;
int pos[maxn], col[maxn], app[maxn];
bool vis[maxn];
long long ans[maxn], cur;
struct edge {
int to, nxt;
} e[maxn];
int cnt1 = 0, cnt2 = 0; // 時間戳
struct query {
int l, r, t, id;
bool operator<(const query &b) const {
return (pos[l] < pos[b.l]) || (pos[l] == pos[b.l] && pos[r] < pos[b.r]) ||
(pos[l] == pos[b.l] && pos[r] == pos[b.r] && t < b.t);
}
} a[maxn], b[maxn];
void addedge(int x, int y) {
e[++cnt] = (edge){y, head[x]};
head[x] = cnt;
}
void dfs(int x) {
id[f[x] = ++index] = x;
for (int i = head[x]; i; i = e[i].nxt) {
if (e[i].to != fa[x][0]) {
fa[e[i].to][0] = x;
dep[e[i].to] = dep[x] + 1;
dfs(e[i].to);
}
}
id[g[x] = ++index] = x; // 括號序
}
int lca(int x, int y) {
if (dep[x] < dep[y]) swap(x, y);
if (dep[x] != dep[y]) {
int dis = dep[x] - dep[y];
for (int i = 20; i >= 0; i--)
if (dis >= (1 << i)) dis -= 1 << i, x = fa[x][i];
} // 爬到同一高度
if (x == y) return x;
for (int i = 20; i >= 0; i--) {
if (fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
}
return fa[x][0];
}
void add(int x) {
if (vis[x])
cur -= (long long)v[col[x]] * w[app[col[x]]--];
else
cur += (long long)v[col[x]] * w[++app[col[x]]];
vis[x] ^= 1;
}
void modify(int x, int t) {
if (vis[x]) {
add(x);
col[x] = t;
add(x);
} else
col[x] = t;
} // 在時間維上移動
int main() {
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= m; i++) scanf("%d", &v[i]);
for (int i = 1; i <= n; i++) scanf("%d", &w[i]);
for (int i = 1; i < n; i++) {
int x, y;
scanf("%d%d", &x, &y);
addedge(x, y);
addedge(y, x);
}
for (int i = 1; i <= n; i++) {
scanf("%d", &last[i]);
col[i] = last[i];
}
dfs(1);
for (int j = 1; j <= 20; j++)
for (int i = 1; i <= n; i++)
fa[i][j] = fa[fa[i][j - 1]][j - 1]; // 預處理祖先
int block = pow(index, 2.0 / 3);
for (int i = 1; i <= index; i++) {
pos[i] = (i - 1) / block;
}
while (q--) {
int opt, x, y;
scanf("%d%d%d", &opt, &x, &y);
if (opt == 0) {
b[++cnt2].l = x;
b[cnt2].r = last[x];
last[x] = b[cnt2].t = y;
} else {
if (f[x] > f[y]) swap(x, y);
a[++cnt1] = (query){lca(x, y) == x ? f[x] : g[x], f[y], cnt2, cnt1};
}
}
sort(a + 1, a + cnt1 + 1);
int L, R, T; // 指針座標
L = R = 0;
T = 1;
for (int i = 1; i <= cnt1; i++) {
while (T <= a[i].t) {
modify(b[T].l, b[T].t);
T++;
}
while (T > a[i].t) {
modify(b[T].l, b[T].r);
T--;
}
while (L > a[i].l) {
L--;
add(id[L]);
}
while (L < a[i].l) {
add(id[L]);
L++;
}
while (R > a[i].r) {
add(id[R]);
R--;
}
while (R < a[i].r) {
R++;
add(id[R]);
}
int x = id[L], y = id[R];
int llca = lca(x, y);
if (x != llca && y != llca) {
add(llca);
ans[a[i].id] = cur;
add(llca);
} else
ans[a[i].id] = cur;
}
for (int i = 1; i <= cnt1; i++) {
printf("%lld\n", ans[i]);
}
return 0;
}
|
真·樹上莫隊
上面的樹上莫隊只是將樹轉化成了鏈,下面的才是真正的樹上莫隊。
由於莫隊相關的問題都是模板題,因此實現部分不做太多解釋
詢問的排序
首先我們知道莫隊的是基於分塊的算法,所以我們需要找到一種樹上的分塊方法來保證時間複雜度。
條件:
- 屬於同一塊的節點之間的距離不超過給定塊的大小
- 每個塊中的節點不能太多也不能太少
- 每個節點都要屬於一個塊
- 編號相鄰的塊之間的距離不能太大
瞭解了這些條件後,我們看到這樣一道題 「SCOI2005」王室聯邦。
在這道題的基礎上我們只要保證最後一個條件就可以解決分塊的問題了。
思路
令 lim 為希望塊的大小,首先,對於整個樹 dfs,當子樹的大小大於 lim 時,就將它們分在一塊,容易想到:對於根,可能會剩下一些點,於是將這些點分在最後一個塊裏。
做法:用棧維護當前節點作為父節點訪問它的子節點,當從棧頂到父節點的距離大於希望塊的大小時,彈出這部分元素分為一塊,最後剩餘的一塊單獨作為一塊。
最後的排序方法:若第一維時間戳大於第二維,交換它們,按第一維所屬塊為第一關鍵字,第二維時間戳為第二關鍵字排序。
指針的移動
過程
容易想到,我們可以標記被計入答案的點,讓指針直接向目標移動,同時取反路徑上的點。
但是,這樣有一個問題,若指針一開始都在 x 上,顯然 x 被標記,當兩個指針向同一子節點移動(還有許多情況)時,x 應該不被標記,但實際情況是 x 被標記,因為兩個指針分別標記了一次,抵消了。
如何解決呢?
有一個很顯然的性質:這些點肯定是某些 LCA,因為 LCA 處才有可能被重複撤銷導致撤銷失敗。
所以我們每次不標記 LCA,到需要詢問答案時再將 LCA 標記,然後再撤銷。
實現
| // 取反路徑上除LCA以外的所有節點
void move(int x, int y) {
if (dp[x] < dp[y]) swap(x, y);
while (dp[x] > dp[y]) update(x), x = fa[x];
while (x != y) update(x), update(y), x = fa[x], y = fa[y];
// x!=y保證LCA沒被取反
}
|
對於求 LCA,我們可以用樹剖,然後我們就可以把分塊的步驟放到樹剖的第一次 dfs 裏面,時間戳也可以直接用第二次 dfs 的 dfs 序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 | int bl[100002], bls = 0; // 屬於的塊,塊的數量
unsigned step; // 塊大小
int fa[100002], dp[100002], hs[100002] = {0}, sz[100002] = {0};
// 父節點,深度,重兒子,大小
stack<int> sta;
void dfs1(int x) {
sz[x] = 1;
unsigned ss = sta.size();
for (int i = head[x]; i; i = nxt[i])
if (ver[i] != fa[x]) {
fa[ver[i]] = x;
dp[ver[i]] = dp[x] + 1;
dfs1(ver[i]);
sz[x] += sz[ver[i]];
if (sz[ver[i]] > sz[hs[x]]) hs[x] = ver[i];
if (sta.size() - ss >= step) {
bls++;
while (sta.size() != ss) bl[sta.top()] = bls, sta.pop();
}
}
sta.push(x);
}
// main
if (!sta.empty()) {
bls++; // 這一行可寫可不寫
while (!sta.empty()) bl[sta.top()] = bls, sta.pop();
}
|
時間複雜度
重點到了,這裏關係到塊的大小取值。
設塊的大小為 \(unit\):
- 對於 x 指針,由於每個塊中節點的距離在 \(unit\) 左右,每個塊中 x 指針移動 \(unit^2\) 次(\(unit\times dis_{\max}\)),共計 \(n\times unit\) 次(\(unit^2 \times (\frac{n}{unit})\));
- 對於 y 指針,每個塊中最多移動 \(O(n)\) 次,共計 \(\frac{n^2}{unit}\) 次(\(n \times (\frac{n}{unit})\))。
加起來大概在根號處取得最小值(由於樹上莫隊塊的大小不固定,所以不一定要嚴格按照)。
例題「WC2013」糖果公園
由於多了時間維,塊的大小取到 \(n^{0.6}\) 的樣子就差不多了。
參考代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152 | #include <bits/stdc++.h>
using namespace std;
int gi() {
int x, c, op = 1;
while (c = getchar(), c < '0' || c > '9')
if (c == '-') op = -op;
x = c ^ 48;
while (c = getchar(), c >= '0' && c <= '9')
x = (x << 3) + (x << 1) + (c ^ 48);
return x * op;
}
int head[100002], nxt[200004], ver[200004], tot = 0;
void add(int x, int y) {
ver[++tot] = y, nxt[tot] = head[x], head[x] = tot;
ver[++tot] = x, nxt[tot] = head[y], head[y] = tot;
}
int bl[100002], bls = 0;
unsigned step;
int fa[100002], dp[100002], hs[100002] = {0}, sz[100002] = {0}, top[100002],
id[100002];
stack<int> sta;
void dfs1(int x) {
sz[x] = 1;
unsigned ss = sta.size();
for (int i = head[x]; i; i = nxt[i])
if (ver[i] != fa[x]) {
fa[ver[i]] = x, dp[ver[i]] = dp[x] + 1;
dfs1(ver[i]);
sz[x] += sz[ver[i]];
if (sz[ver[i]] > sz[hs[x]]) hs[x] = ver[i];
if (sta.size() - ss >= step) {
bls++;
while (sta.size() != ss) bl[sta.top()] = bls, sta.pop();
}
}
sta.push(x);
}
int cnt = 0;
void dfs2(int x, int hf) {
top[x] = hf, id[x] = ++cnt;
if (!hs[x]) return;
dfs2(hs[x], hf);
for (int i = head[x]; i; i = nxt[i])
if (ver[i] != fa[x] && ver[i] != hs[x]) dfs2(ver[i], ver[i]);
}
int lca(int x, int y) {
while (top[x] != top[y]) {
if (dp[top[x]] < dp[top[y]]) swap(x, y);
x = fa[top[x]];
}
return dp[x] < dp[y] ? x : y;
}
struct qu {
int x, y, t, id;
bool operator<(const qu a) const {
return bl[x] == bl[a.x] ? (bl[y] == bl[a.y] ? t < a.t : bl[y] < bl[a.y])
: bl[x] < bl[a.x];
}
} q[100001];
int qs = 0;
struct ch {
int x, y, b;
} upd[100001];
int ups = 0;
long long ans[100001];
int b[100001] = {0};
int a[100001];
long long w[100001];
long long v[100001];
long long now = 0;
bool vis[100001] = {0};
void back(int t) {
if (vis[upd[t].x]) {
now -= w[b[upd[t].y]--] * v[upd[t].y];
now += w[++b[upd[t].b]] * v[upd[t].b];
}
a[upd[t].x] = upd[t].b;
}
void change(int t) {
if (vis[upd[t].x]) {
now -= w[b[upd[t].b]--] * v[upd[t].b];
now += w[++b[upd[t].y]] * v[upd[t].y];
}
a[upd[t].x] = upd[t].y;
}
void update(int x) {
if (vis[x])
now -= w[b[a[x]]--] * v[a[x]];
else
now += w[++b[a[x]]] * v[a[x]];
vis[x] ^= 1;
}
void move(int x, int y) {
if (dp[x] < dp[y]) swap(x, y);
while (dp[x] > dp[y]) update(x), x = fa[x];
while (x != y) update(x), update(y), x = fa[x], y = fa[y];
}
int main() {
int n = gi(), m = gi(), k = gi();
step = (int)pow(n, 0.6);
for (int i = 1; i <= m; i++) v[i] = gi();
for (int i = 1; i <= n; i++) w[i] = gi();
for (int i = 1; i < n; i++) add(gi(), gi());
for (int i = 1; i <= n; i++) a[i] = gi();
for (int i = 1; i <= k; i++)
if (gi())
q[++qs].x = gi(), q[qs].y = gi(), q[qs].t = ups, q[qs].id = qs;
else
upd[++ups].x = gi(), upd[ups].y = gi();
for (int i = 1; i <= ups; i++) upd[i].b = a[upd[i].x], a[upd[i].x] = upd[i].y;
for (int i = ups; i; i--) back(i);
fa[1] = 1;
dfs1(1), dfs2(1, 1);
if (!sta.empty()) {
bls++;
while (!sta.empty()) bl[sta.top()] = bls, sta.pop();
}
for (int i = 1; i <= n; i++)
if (id[q[i].x] > id[q[i].y]) swap(q[i].x, q[i].y);
sort(q + 1, q + qs + 1);
int x = 1, y = 1, t = 0;
for (int i = 1; i <= qs; i++) {
if (x != q[i].x) move(x, q[i].x), x = q[i].x;
if (y != q[i].y) move(y, q[i].y), y = q[i].y;
int f = lca(x, y);
update(f);
while (t < q[i].t) change(++t);
while (t > q[i].t) back(t--);
ans[q[i].id] = now;
update(f);
}
for (int i = 1; i <= qs; i++) printf("%lld\n", ans[i]);
return 0;
}
|
本页面最近更新:,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:StudyingFather, Backl1ght, countercurrent-time, Ir1d, greyqz, MicDZ, ouuan, Linky
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用