[题解]洛谷-P3384 模板题重链剖分-线段树

[题解]洛谷-P3384 模板题重链剖分-线段树

讲解

视频大约一个小时,需要加载半分钟左右。

原题和值得参考的博客

代码

代码有详细的注释,不理解可以看视频。

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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
/*
* © Copyright 2021 Xchkoo All rights reserved.
*
* AUTHOR: Xchkoo
* DATE: 2021-04-02
* LISENSE: CC v4.0 BY-SA https://creativecommons.org/licenses/by-sa/4.0/deed.zh
* Welcome to my blog -> https://Xchkoo.top/
*/
#include<iostream>
#include<string>
#include<algorithm>
#include<cstring>/*这个cstring里有个memset是不得不用啊*/

#define mid ((l+r)>>1)
#define lson rt<<1,l,mid
#define rson rt<<1|1,mid+1,r
#define len (r-l+1)
/* 这段主要是为了线段树,手敲太麻烦了 */
typedef long long ll;
typedef size_t s_t;
#define mem(a,b) memset(a,(b),sizeof(a))
using namespace std;


/* START global variate block */

const int MAXN = 100000+10;
int n, m, r, mod,cnt=1,dfs2_cnt,res=0;
int head[MAXN],segment_tree[MAXN<<1],value[MAXN],wt[MAXN],lazy[MAXN<<1],depth[MAXN],father[MAXN],node_size[MAXN],heavy_son[MAXN],array_top[MAXN],id[MAXN];

/* END global variate block */


/* START function and class block */

/* START tree part */

struct Node {
int next, to;
} e[MAXN<<1];

void add_edge(int u, int v) {
e[cnt].next = head[u];
e[cnt].to = v;
head[u] = cnt;
cnt++;
}

/* END */

/* START segment tree part */
/* 实际上我们把线段树函数单独拎出来,将线段树通过一个范围参数传入 */
void pushdown(int rt,int size)
/*
* 这块是懒标记,相比起lowbit这个其实好理解一点。
* 懒标记意味着它不会主动更新,它把这个操作储存到结点的lazy属性中(在c++中我们通过使用lazy数组来实现),
* 当有用到这个结点和这个结点的子树时(比如查询的dfs滚到这里了),我们会将懒标记打破,分配到每个它的子树,
* 当这个懒标记滚到叶子结点的时候,懒标记就转换成值加入叶子结点的值属性。
* ————————————————————————————————————————————————————————————————————
* 回到题目可能能更好理解一点,题目的操作3是要给指定的范围加值(1、2要用到hld我们先放着),
* 比方现在的范围是3-8,我们有一条1-10的线段并且我们在这个线段上建立了一棵线段树。
* 于是我们给3、8的lca(最近公共祖先)加个懒标记。
* 现在有个操作要查询3-4的数据,可以,我们于是从root开始向3-4出发
* 在这个时候我们发现root有一个懒标记属性,遵从定义我们把它打碎,分配给子结点
* (注意:我们是在把3-8共同加一个值吧!注意“共同”!)。
* 现在我们查找3-4时就会以这个顺序
* 1-10->|1-5 ->|1-3 ->|1-2-...
* | | |2-3 ->| 2 HERE
* | | | 3
* | |3-5 ->|3-4 HERE
* |5-10 -... |4-5-...
* 在同时我们会顺便把懒标记推下去,最终推到2 和 3-4
* 这里要一个转化:我们是在把3-8共同加一个值,共同意味着3-4这个区间其实要3加一次4加一次,
* 总共要加两次。所以在求和时要乘上子结点个数。
* 理解了的话就看代码吧:
*/
{
/* 左右子节点都加上父节点的lazy属性 */
lazy[rt<<1]+=lazy[rt];
lazy[rt<<1|1]+=lazy[rt];

/* 这里是左右结点把lazy打破的过程
* 注意:segment_tree是一个线段树结点的值的集合,
* 不是叶子结点的结点的值 是其子树所有结点的值的和。
* 所以在传递懒标记的过程中我们应当把这个结点的值也更新了。
*/
segment_tree[rt<<1]+=lazy[rt]*(size-(size>>1));
segment_tree[rt<<1|1]+=lazy[rt]*(size>>1);
segment_tree[rt<<1]%=mod;
segment_tree[rt<<1|1]%=mod;
lazy[rt]=0;
}

void build(int rt,int l,int r)
/*
* 此函数是在建立一棵线段树,
* 因为我们在处理操作3、4时并不需要树链剖分,
* 直接在原来树的基础上就可以了。
*/
{
if(l == r)
/*
* 当左範圍等于右範圍,
* 它的范围就是一,也就是一个叶子结点,
* 这个条件意味着可以结束递归了。
*/
{
segment_tree[rt] = wt[l];
if(segment_tree[rt]>mod) segment_tree[rt]%=mod;
/*
* 别忘了题目有取模的操作。
* 另外取模有个法则 (a+b) mod c = (a mod c + b mod c)mod c
* 可以用阿贝尔群证明。
*/
return;
}
build(lson);/* 递归 lson和rson定义在宏里 */
build(rson);
/*
* 线段树是一棵二叉树,
* 可以想象、用纸推一下这个性质,
* 左子树序号一定是父节点的两倍,右子树一定是父节点的两倍加1
* (在这里因为左子树序号是个偶数,所以它的二进制末位一定是0,
* 用|位操作符来|1其实就是加1,用位操作符比用+更快,是一个优化)
*/
segment_tree[rt]=(segment_tree[rt<<1] + segment_tree[rt<<1|1])%mod;
}

void query(int rt,int l,int r,int L,int R)
/* 查询函数 小写l、r 是查询范围,大写L、R是线段树的范围*/
{
if(L<=l&&r<=R) { /* 如果在范围内 加上这个结点的值*/
res+=segment_tree[rt];
res%=mod;
return;
}
/*
* #define lson rt<<1,l,mid
* #define rson rt<<1|1,mid+1,r
*/
else /* 如果是在跨范围的就分成两块递归,如果这个结点还有懒标记属性,就先打破懒标记 */
{
if(lazy[rt])pushdown(rt,len);
if(L<=mid)query(lson,L,R);
if(R>mid)query(rson,L,R);
}
}

void update(int rt,int l,int r,int L,int R,int k)//1 1 5 0
/* 区间更新函数 大至同理于区间查询函数*/
{
if(L<=l&&r<=R) {
lazy[rt]+=k;/* 这里就是应该用懒标记的地方 */
segment_tree[rt]+=k*len; /* 乘以个数在pushdown函数中有解释 */
}
else {
if(lazy[rt]) pushdown(rt,len);/* 如果有懒标记打破 */
/* 跨区间就分开处理 */
if(L<=mid) update(lson,L,R,k);
if(R>mid) update(rson,L,R,k);
segment_tree[rt]=(segment_tree[rt<<1]+segment_tree[rt<<1|1])%mod;
}
}

/* END */

/* START execute part */
/*
* 这一部分是针对操作1234的实际操作部分,
* 3、4就是基本的线段树
* 接下来着重讲1、2操作。
*
*/
int query_lca(int x,int y){
int ans=0;
while(array_top[x]!=array_top[y]){//当两个点不在同一条链上
if(depth[array_top[x]]<depth[array_top[y]])swap(x,y);//把x点改为所在链顶端的深度更深的那个点
res=0;
query(1,1,n,id[array_top[x]],id[x]);//ans加上x点到x所在链顶端 这一段区间的点权和
ans+=res;
ans%=mod;//按题意取模
x=father[array_top[x]];//把x跳到x所在链顶端的那个点的上面一个点
}
//直到两个点处于一条链上
if(depth[x]>depth[y])swap(x,y);//把x点深度更深的那个点
res=0;
query(1,1,n,id[x],id[y]);//这时再加上此时两个点的区间和即可
ans+=res;
return ans%mod;
}

void update_lca(int x,int y,int k){//同上
k%=mod;
while(array_top[x]!=array_top[y]){
if(depth[array_top[x]]<depth[array_top[y]])swap(x,y);
update(1,1,n,id[array_top[x]],id[x],k);
x=father[array_top[x]];
}
if(depth[x]>depth[y])swap(x,y);
update(1,1,n,id[x],id[y],k);
}

int query_segment_tree(int x){
res=0;
query(1,1,n,id[x],id[x]+node_size[x]-1);//子树区间右端点为id[x]+siz[x]-1
return res;
}

void update_segment_tree(int x,int k){//同上 //4 2
update(1,1,n,id[x],id[x]+node_size[x]-1,k); //1 1 5 0 0 2
}

/* END */

/* START Heavy-light Decomposition part */

void hld_dfs1(int x, int fa, int deep)
/*
* 这个dfs1用来进行重链剖分的第一次递归,
* 找到重子结点,并把它的heavy son通过数组记录。
* 接下来就可以进行dfs2把heavy son连成heavy_edge。
*/
{
depth[x] = deep;/* lca */
father[x] = fa;/* 这里的father数组实际上是重链连接起来的关键,详见dfs2 */
node_size[x] = 1;
int heavy_son_size = -1;
for(int i=head[x];i;i=e[i].next) {
int to = e[i].to;
if(to == fa) continue;/*若为父亲则跳过*/
hld_dfs1(to, x, deep+1);/*dfs它的儿子*/
node_size[x] += node_size[to];/*把它的儿子size加到它身上,遍历完后就会得到它的size*/
if(node_size[to] > heavy_son_size) {
heavy_son[x]=to;
heavy_son_size=node_size[to];
/* tips:一个父亲只有一个heavy son,
* 如果有两个大小一样的heavy son,
* 那就任选一个,不会对结果造成影响。
*/
}
}
}

void hld_dfs2(int x,int top)
/*
* 这个dfs是用来把heavy son连成heavy_edge的。
* 一个heavy_edge一般有1个属性————顶点
* 我们会记录它,因为链的dfs序是连续的,所以跳的过程也遵循dfs序,
* 所以在接下来的lca中我们可以通过顶点的父亲来跳链。
*/
{
/* 接下来这两步是在对原先的序号重编为dfs序
*(因为这段在一个dfs中,所以dfs2_cnt的自增会保证它是一个dfs序)。
* top就是我们说的链顶。 -> https://oi-wiki.org/graph/hld/
*/
id[x]=++dfs2_cnt;
wt[dfs2_cnt]=value[x];
array_top[x] = top;
if(!heavy_son[x]) return;
/* 没有heavy_son意味着这一条重链我们已经遍历到尾了 */
/* 按先处理重儿子,再处理轻儿子的顺序递归处理 */
hld_dfs2(heavy_son[x],top);/* 重儿子 */
for(int i=head[x];i;i=e[i].next)/* 轻儿子 */
{
int to = e[i].to;
if(to==father[x]||to==heavy_son[x]) continue;
hld_dfs2(to,to);
}
}

/* END */

/* END function and class block */


/* START main block */

int main() {
//freopen("p3384.in", "r", stdin);
//freopen("p3384.out", "w", stdout);
ios::sync_with_stdio(false);
/* n: num of node
* m: num of the execution
* r: root's serial number
* p: the mod num
*/
cin >> n >> m >> r >> mod;
for(int i=1;i<=n;i++)
cin >> value[i];
int a,b;
for(int i=1;i<n;i++) {
cin >> a >> b;
add_edge(a,b);
add_edge(b,a);
}
hld_dfs1(r,0,1); // 2 0 1
hld_dfs2(r,r);
/* for exec 1,2 */
build(1,1,n);
/* for exec 3,4 */
while(m--) {
int x,y,w,exec;
cin >> exec;
/* 对操作进行处理:
* 一共有四个操作
* 操作1:格式:1 x y z 表示将树从 x 到 y 结点最短路径上所有节点的值都加上 z。
* 操作2:格式:2 x y 表示求树从 x 到 y 结点最短路径上所有节点的值之和。
* 操作3:格式:3 x z 表示将以 x 为根节点的子树内所有节点值都加上 z。
* 操作4:格式:4 x 表示求以 x 为根节点的子树内所有节点值之和
*/
if(exec == 1){
cin >> x >> y >> w;
update_lca(x,y,w);
}
else if(exec == 2) {
cin >> x >> y;
cout << query_lca(x,y)<<endl;
}
else if(exec == 3) {
cin >> x >> w;
update_segment_tree(x,w);//4 2
}
else if(exec == 4) {
cin >> x;
cout << query_segment_tree(x)<<endl;
}
}
cin.get();
}

/* END main block */
//(rt=1, l=1, r=5, L=0, R=-1, k=2)
//Breakpoint 1, update_segment_tree (x=4, k=2) at p3384.cpp:208
//208 update(1,1,n,id[x],id[x]+node_size[x]-1,k);
//(gdb) p id
//$1 = {0, 0, 1, 0 <repeats 100007 times>}
//(gdb) p node_size
//$2 = {0, 0, 1, 0 <repeats 100007 times>}

http://xchkoo.github.io/p3384_2021-04-05/ 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。