树的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
#include <bits/stdc++.h>
using namespace std;
int n,x,y,z,pre[110],k;
struct node{
int to,len,nxt;
}a[220];
void add(int u,int v,int l){
a[++k] = {v,l,pre[u]};
pre[u] = k;
}
void dfs(int x,int fa){
for(int i = pre[x];i;i = a[i].nxt){
if(a[i].to != fa) dfs(a[i].to,x);
}
}
int main(){
cin>>n;
for(int i = 1;i < n;i++){
cin>>x>>y>>z;
add(x,y,z);
add(y,x,z);
}
dfs(1,0/*!Tips!*/);
}

P1364 医院设置

  • 我们定义 int dfs(int x,int fa,int dis) 计算路径和。disdis 代表从起点到当前节点的边权和。
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
#include <bits/stdc++.h>
using namespace std;
int n, x, y, z, pre[110], val[110], k;
struct node {
int to, len, nxt;
} a[220];
void add(int u, int v, int l) {
a[++k] = {v, l, pre[u]};
pre[u] = k;
}
int dfs(int x, int fa, int dis) {
int res = dis * val[x];
for (int i = pre[x]; i; i = a[i].nxt) {
if (a[i].to != fa) {
res += dfs(a[i].to, x, dis + a[i].len);
}
}
return res;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
int lc, rc; //left connect,right connect
cin >> val[i] >> lc >> rc;//猎奇输入
if (lc) {
add(i, lc, 1);
add(lc, i, 1);
}
if (rc) {
add(i, rc, 1);
add(rc, i, 1);
}
}
int ans = INT_MAX;
for (int i = 1; i <= n; i++) {
ans = min(ans, dfs(i, 0, 0));
}
cout<<ans<<endl;
}

树的直径

做法 1. 两次 DFS

过程

首先从任意节点 yy 开始进行第一次 DFS,到达距离其最远的节点,记为 zz,然后再从 zz 开始做第二次 DFS,到达距离 zz 最远的节点,记为 zz',则 δ(z,z)\delta(z,z') 即为树的直径。

显然,如果第一次 DFS 到达的节点 zz 是直径的一端,那么第二次 DFS 到达的节点 zz' 一定是直径的一端。我们只需证明在任意情况下,zz 必为直径的一端。

定理:在一棵树上,从任意节点 yy 开始进行一次 DFS,到达的距离其最远的节点 zz 必为直径的一端。

证明

OI Wiki

image

Warning 负权边

上述证明过程建立在所有路径均不为负的前提下。如果树上存在负权边,则上述证明不成立。故若存在负权边,则无法使用两次 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <bits/stdc++.h>
using namespace std;

struct node {
int to, nxt;
} a[4001010];

int dep[2000010];
int pre[2000010], k, n;
int x, y;
void add(int u, int v) {
a[++k] = {v, pre[u]};
pre[u] = k;
}
void dfs(int x, int fa) {

for (int i = pre[x]; i; i = a[i].nxt) {
int to = a[i].to;
if (to != fa) {
dep[to] = dep[x] + 1;
dfs(to, x);
}
}
}

int main() {
cin >> n;
for (int i = 1; i < n; i++) {
cin >> x >> y;
add(x, y);
add(y, x);
}
dep[1] = 0;
dfs(1, 0);
int ma = 0, mai;
for (int i = 1; i <= n; i++) {
if (dep[i] > ma) {
ma = dep[i];
mai = i;
}
}
dep[mai] = 0;
dfs(mai, 0);
int maxx = 0;
for (int i = 1; i <= n; i++) {
maxx = max(maxx, dep[i]);
}
cout << maxx;
}

求先序序列

  • 递归水过,后序的最后一个字母就是当前的根节点,然后在中序中以根节点为中心分成两个子树再进行递归即可。
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
#include <bits/stdc++.h>
using namespace std;

struct node {
int to, nxt;
} a[4001010];

int dep[2000010];
int pre[2000010], k, n;
int x, y;
void add(int u, int v) {
a[++k] = {v, pre[u]};
pre[u] = k;
}
void dfs(int x, int fa) {

for (int i = pre[x]; i; i = a[i].nxt) {
int to = a[i].to;
if (to != fa) {
dep[to] = dep[x] + 1;
dfs(to, x);
}
}
}

int main() {
cin >> n;
for (int i = 1; i < n; i++) {
cin >> x >> y;
add(x, y);
add(y, x);
}
dep[1] = 0;
dfs(1, 0);
int ma = 0, mai;
for (int i = 1; i <= n; i++) {
if (dep[i] > ma) {
ma = dep[i];
mai = i;
}
}
dep[mai] = 0;
dfs(mai, 0);
int maxx = 0;
for (int i = 1; i <= n; i++) {
maxx = max(maxx, dep[i]);
}
cout << maxx;
}

P1827 [USACO3.4] 美国血统 American Heritage

  • 跟上一题同理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <bits/stdc++.h>
using namespace std;
void dfs(string s1, string s2) {
if (s1.size() <= 0) return ;
char mid = s2[0];//root

int div = s1.find(mid);
dfs(s1.substr(0, div), s2.substr(1, div));
dfs(s1.substr(div+1), s2.substr(div+1));
cout << mid;
}
int main() {
string s1, s2;
cin >> s1 >> s2;
dfs(s1, s2);
}//

[!Important] P2680 [NOIP 2015 提高组] 运输计划

  • 如果你没有思路建议看 这个题解 ,这个写的很有实力。

关于题解的梳理和总结: By DS

问题描述

给定一棵 nn 个节点的树和 mm 条运输路径,要求选择一条边将其权值置零,使得所有路径的最大耗时最小化。

算法思路

  1. 二分答案框架
  • 设当前检查的答案为 TT,判断是否存在一条边,置零后所有路径长度 T\leq T
  • 关键性质:所有长度 >T> T 的路径必须共用被置零的边。
  1. 可行性检查(check(T)
  • 步骤1:统计超限路径(长度 >T> T)的数量 kk
  • 步骤2:用树上差分标记这些路径覆盖的边。
  • 步骤3:检查是否存在边满足:
  • 被所有超限路径覆盖(覆盖次数 =k= k)。
  • 边权 wmax(路径长度)Tw \geq \max(\text{路径长度}) - T
  1. 技术实现
  • LCA计算:倍增法预处理,单次查询 O(logn)O(\log n)
  • 路径长度公式

dis(u,v)=dist[u]+dist[v]2×dist[LCA(u,v)]\text{dis}(u, v) = \text{dist}[u] + \text{dist}[v] - 2 \times \text{dist}[\text{LCA}(u, v)]

  • 树上差分
  • 对路径 (u,v)(u, v)cnt[u]++, cnt[v]++, cnt[LCA(u,v)] -= 2
  • DFS汇总后,cnt[x] 表示边 (父节点,x)(\text{父节点}, x) 被覆盖的次数。

复杂度分析

  • 预处理
  • LCA倍增表:O(nlogn)O(n \log n) 时间,O(nlogn)O(n \log n) 空间。
  • 路径长度计算:O(mlogn)O(m \log n)
  • 单次 checkO(n+m)O(n + m)
  • 总复杂度O((n+m)logT)O((n + m) \log T),其中 TT 为最大路径长度。

关键优化

  1. 二分边界
  • 下界 L=0L = 0,上界 R=max(路径长度)R = \max(\text{路径长度})
  • 每次调整:

{R=mid1if check(mid)=true,L=mid+1otherwise.\begin{cases} R = \text{mid} - 1 & \text{if } \text{check}(\text{mid}) = \text{true}, \\ L = \text{mid} + 1 & \text{otherwise}. \end{cases}

  1. 差分数组重置
  • 每次 check 前清零,避免全局数组反复初始化。

正确性证明

  • 充分性:若存在满足条件的边,置零后所有路径长度 T\leq T
  • 必要性:若 TT 可行,则必须存在这样的边。

注意事项

  1. 根节点选择:任意选根(如节点1),不影响LCA和路径计算。
  2. 边权处理:通过子节点存储父边的权值(dist[v] - dist[fa[v]])。
  3. 数据范围
  • n,m3×105n, m \leq 3 \times 10^5,需使用高效输入输出(快读快写)。

卡常方法

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
/*
程序执行流程概述(核心逻辑:二分答案 + 树上差分 + LCA):
1. 输入处理:读取树的节点数、路径数,构建邻接表。
2. 预处理阶段:
a. 第一次DFS(dfs):计算节点深度、父节点(倍增基础),并将边权"下沉"到子节点(用子节点唯一标识边)。
b. 构建倍增表(f数组):预处理每个节点的2^j级祖先,用于快速查询LCA。
c. 第二次DFS(dfs_dist):计算根节点到所有节点的距离(distRoot数组),用于快速计算路径长度。
3. 预计算路径信息:读取每条路径的起点/终点,计算其LCA和原始长度(pathLen数组),记录最大路径长度(用于二分边界)。
4. 二分答案:通过二分法寻找**最小的最大路径长度T**,使得存在一条边置零后,所有路径长度≤T。
a. 可行性检查(check函数):
i. 统计超限路径(长度> T的路径)。
ii. 用树上差分标记这些路径覆盖的边。
iii. 汇总差分数组,得到每条边的覆盖次数。
iv. 检查是否存在边被所有超限路径覆盖,且边权足够大(置零后可使最大超限路径≤T)。
5. 输出结果:输出最小的T。
*/
/*
关于卡常:
#1 头文件
#2 for int -> for register int
#3 void -> inline void
#4 [!Important] 改二分逻辑(左边界)。见 https://images.cnblogs.com/cnblogs_com/blogs/838391/galleries/2441761/o_250722010513_1.png
*/
#include "bits/stdc++.h" //卡常#1
using namespace std;
const int N = 3e5 + 10; // 最大节点数(适配题目3e5的数据范围)
const int L = 20; // 倍增深度(2^20=1e6,足够覆盖3e5节点的深度)

// 全局变量定义(按功能分类)
// 1. LCA与树结构
int f[N][L]; // 倍增数组:f[i][j]表示i的2^j级祖先
int pre[N]; // 邻接表头指针:pre[i]指向i的第一条边
int lg[N]; // 预处理log2值:lg[i] = floor(log2(i)),用于快速计算倍增层级
int n, k, q; // n:节点数;k:边计数;q:路径数
int dep[N]; // 节点深度:dep[i]表示i的深度(根节点1的深度为1)

// 2. 路径与差分
int st[N], ed[N]; // 路径的起点/终点:st[i]是第i条路径的起点,ed[i]是终点
long long pathLen[N];// 路径原始长度:pathLen[i]是第i条路径的长度(未置零任何边)
int pathLCA[N]; // 路径的LCA:pathLCA[i]是第i条路径的起点与终点的LCA
int mark[N]; // 差分数组:用于标记路径覆盖的边,汇总后表示边的覆盖次数

// 3. 边权与距离
int base[N]; // 边权下沉数组:base[i]表示i到父节点的边的权值(根节点1无父边,base[1]无意义)
long long distRoot[N];// 根到节点的距离:distRoot[i]是根节点1到i的距离

// 快读快写模板
namespace FastIO {
char buf[1 << 20], *p1 = buf, *p2 = buf;
inline char gc() {
if (p1 == p2) p2 = (p1 = buf) + fread(buf, 1, 1 << 20, stdin);
return p1 == p2 ? EOF : *p1++;
}
template<typename T>
inline void read(T &x) {
x = 0;
bool neg = 0;
char ch = gc();
while (!isdigit(ch)) neg |= (ch == '-'), ch = gc();
while (isdigit(ch)) x = (x << 3) + (x << 1) + (ch ^ 48), ch = gc();
if (neg) x = -x;
}
inline void read(char *s) {
char ch = gc();
while (isspace(ch)) ch = gc();
while (!isspace(ch)) *s++ = ch, ch = gc();
*s = '\0';
}
char pbuf[1 << 20], *pp = pbuf;
inline void push(const char &c) {
if (pp - pbuf == (1 << 20)) fwrite(pbuf, 1, 1 << 20, stdout), pp = pbuf;
*pp++ = c;
}
template<typename T>
inline void write(T x) {
static int sta[40], top = 0;
if (x < 0) push('-'), x = -x;
do {
sta[top++] = x % 10;
} while (x /= 10);
while (top) push(sta[--top] + '0');
}
inline void flush() {
fwrite(pbuf, 1, pp - pbuf, stdout);
pp = pbuf;
}
} using namespace FastIO;

// 邻接表节点结构体(存储边信息)
struct Node {
int to; // 边的目标节点
int nxt; // 下一条边的索引(链式存储)
int cost; // 边的权值
} a[N * 2]; // 邻接表数组(无向树,每条边存两次,大小为2*N)

// 添加边到邻接表(无向边,所以添加两次)
// 参数:u(起点)、v(终点)、t(权值)
inline void add(int u, int v, int t) {
a[++k] = {v, pre[u], t}; // 创建新边:目标v,下一条边是pre[u],权值t
pre[u] = k; // 更新u的邻接表头指针为当前边的索引
}

// 第一次DFS(建树):计算深度、父节点(f[i][0])、边权下沉(base数组)
// 参数:x(当前节点)、fa(当前节点的父节点)
inline void dfs(int x, int fa) {
dep[x] = dep[fa] + 1; // 当前节点深度=父节点深度+1(根节点1的父节点是0,dep[0]=0,所以dep[1]=1)
f[x][0] = fa; // 当前节点的直接父节点(2^0级祖先)是fa
// 遍历x的所有邻接边(邻接表链式存储)
for (register int i = pre[x]; i; i = a[i].nxt) {
int to = a[i].to; // 边的目标节点
if (to == fa) continue; // 跳过父节点(避免循环)
base[to] = a[i].cost; // 边权下沉:将边(x→to)的权值存到子节点to中(用to唯一标识这条边)
dfs(to, x); // 递归处理子节点to,父节点是x
}
}

// 第二次DFS(计算距离):计算根节点1到所有节点的距离(distRoot数组)
// 参数:x(当前节点)、fa(当前节点的父节点)
inline void dfs_dist(int x, int fa) {
// 遍历x的所有邻接边
for (register int i = pre[x]; i; i = a[i].nxt) {
int to = a[i].to; // 边的目标节点
if (to == fa) continue; // 跳过父节点(避免循环)
// 子节点to的距离=父节点x的距离 + 边(x→to)的权值(base[to])
distRoot[to] = distRoot[x] + base[to];
dfs_dist(to, x); // 递归处理子节点to,父节点是x
}
}

// 倍增法查询LCA(最近公共祖先):快速找到u和v的最近公共祖先
// 参数:u、v(要查询的两个节点)
// 返回值:u和v的LCA
int lca(int u, int v) {
// 步骤1:调整u和v的深度,使u的深度≥v的深度(如果u比v浅,交换两者)
if (dep[u] < dep[v]) swap(u, v);
// 步骤2:将u提升到与v同一深度(用倍增法快速提升)
int depth_diff = dep[u] - dep[v]; // u和v的深度差
for (register int i = lg[depth_diff]; i >= 0; i--) { // 从最大的2^i开始尝试
if (dep[f[u][i]] >= dep[v]) { // 如果u的2^i级祖先的深度≥v的深度,就提升u
u = f[u][i];
}
}
// 步骤3:如果提升后的u等于v,说明v是u的祖先,直接返回v
if (u == v) return u;
// 步骤4:同步提升u和v,直到它们的祖先相同(此时祖先就是LCA)
for (register int i = L - 1; i >= 0; i--) { // 从最大的2^i开始尝试
if (f[u][i] != f[v][i]) { // 如果u和v的2^i级祖先不同,就提升它们
u = f[u][i];
v = f[v][i];
}
}
// 步骤5:u和v的直接父节点(2^0级祖先)就是它们的LCA
return f[u][0];
}

// 差分汇总DFS:自底向上累加差分数组mark,得到每条边的覆盖次数
// 参数:x(当前节点)、fa(当前节点的父节点)
// 说明:mark[x]表示边(fa[x]→x)被覆盖的次数(因为边权下沉到x)
inline void dfs_sum(int x, int fa) {
// 遍历x的所有邻接边
for (register int i = pre[x]; i; i = a[i].nxt) {
int to = a[i].to; // 边的目标节点
if (to == fa) continue; // 跳过父节点(避免循环)
dfs_sum(to, x); // 先递归处理子节点to(自底向上)
mark[x] += mark[to];// 将子节点to的mark值累加到x的mark值(边(to的父节点→to)的覆盖次数是mark[to],而to的父节点是x)
}
}

// 可行性检查函数:判断是否存在一条边,置零后所有路径长度≤mid
// 参数:mid(当前要检查的最大路径长度上限)
// 返回值:true(存在这样的边)/ false(不存在)
bool check(long long mid) {
memset(mark, 0, sizeof(mark)); // 重置差分数组(每次检查都要重新标记)
int cnt_over = 0; // 超限路径计数:长度>mid的路径数量
long long max_l = 0; // 最大超限路径长度:用于计算需要减少的长度

// 步骤1:标记所有超限路径(长度>mid的路径)
for (register int i = 1; i <= q; i++) {
if (pathLen[i] > mid) { // 如果第i条路径的长度超过mid
cnt_over++; // 超限路径数量+1
max_l = max(max_l, pathLen[i]); // 更新最大超限路径长度
// 树上差分标记:路径(st[i]→ed[i])的起点、终点+1,LCA-2
// 原理:路径u-v的覆盖边是u到LCA和v到LCA的边(除了LCA本身),通过差分标记,汇总后mark[x]表示边(fa[x]→x)的覆盖次数
mark[st[i]]++; // 起点st[i]的mark值+1
mark[ed[i]]++; // 终点ed[i]的mark值+1
mark[pathLCA[i]] -= 2; // LCA的mark值-2(修正重复计数)
}
}

// 步骤2:判断是否有超限路径(没有的话,mid可行)
if (cnt_over == 0) {
return true;
}

// 步骤3:汇总差分数组,得到每条边的覆盖次数
dfs_sum(1, 0); // 从根节点1开始,自底向上汇总mark数组

// 步骤4:检查是否存在符合条件的边
// 遍历所有节点(从2开始,因为根节点1没有父边)
for (register int i = 2; i <= n; i++) {
// 条件1:边(fa[i]→i)被所有超限路径覆盖(mark[i] == cnt_over)
// 条件2:边的权值足够大(base[i] >= max_l - mid)
// 解释:max_l是最大超限路径长度,需要将其减少到<=mid,所以需要减少的长度是max_l - mid。如果边的权值≥这个值,置零该边后,max_l会减少base[i],从而≤mid。
if (mark[i] == cnt_over && base[i] >= max_l - mid) {
return true; // 存在这样的边,mid可行
}
}

// 没有找到符合条件的边,mid不可行
return false;
}

// 主函数(程序入口)
int main() {
long long max_edge = 0; // 最长边
// 步骤1:输入处理
read(n); // 读取节点数n
// 预处理lg数组(log2值):lg[i] = floor(log2(i))
// 例如:lg[1] = 0,lg[2] = 1,lg[3] = 1,lg[4] = 2,依此类推
for (register int i = 2; i <= n; i++) {
lg[i] = lg[i >> 1] + 1; // i >> 1等价于i/2(整数除法)
}
read(q); // 读取路径数q
// 读取树的边信息,构建邻接表
for (register int i = 1; i < n; i++) { // 树有n-1条边
int x, y, t;
read(x); // 读取边的节点x
read(y); // 读取边的节点y
read(t); // 读取边的权值t
max_edge = max(max_edge, 1ll * t); // 更新最长边
add(x, y, t); // 添加边x→y(权值t)
add(y, x, t); // 添加边y→x(权值t,无向树)
}

// 步骤2:预处理阶段
dep[0] = 0; // 虚拟根节点0的深度为0(根节点1的父节点是0)
dfs(1, 0); // 第一次DFS:建树,处理深度、父节点、边权下沉
// 构建倍增表f[i][j]:f[i][j] = f[f[i][j-1]][j-1](i的2^j级祖先等于i的2^(j-1)级祖先的2^(j-1)级祖先)
for (register int i = 1; i < L; i++) { // 遍历倍增的层级(从1到L-1)
for (register int j = 1; j <= n; j++) { // 遍历所有节点
f[j][i] = f[f[j][i - 1]][i - 1]; // 计算j的2^i级祖先
}
}
distRoot[1] = 0; // 根节点1到自身的距离为0
dfs_dist(1, 0); // 第二次DFS:计算根到每个节点的距离

// 步骤3:预计算路径信息
long long max_len = 0; // 所有路径中的最大长度(用于设置二分的右边界)
for (register int i = 1; i <= q; i++) {
read(st[i]); // 读取第i条路径的起点st[i]
read(ed[i]); // 读取第i条路径的终点ed[i]
pathLCA[i] = lca(st[i], ed[i]); // 计算第i条路径的LCA
// 计算第i条路径的长度:distRoot[st[i]] + distRoot[ed[i]] - 2 * distRoot[pathLCA[i]]
// 原理:路径st[i]-ed[i]的长度等于st[i]到根的距离加上ed[i]到根的距离,减去两倍的LCA到根的距离(LCA到根的距离被计算了两次)
pathLen[i] = distRoot[st[i]] + distRoot[ed[i]] - 2 * distRoot[pathLCA[i]];
max_len = max(max_len, pathLen[i]); // 更新最大路径长度
}

// 步骤4:二分答案(寻找最小的T)
long long l = max_len - max_edge; // 二分左边界(最小可能的T),详见 https://images.cnblogs.com/cnblogs_com/blogs/838391/galleries/2441761/o_250722010513_1.png
long long r = max_len; // 二分右边界(最大可能的T,即未置零任何边时的最大路径长度)
while (l <= r) { // 当左边界小于右边界时继续循环
long long mid = (l + r) >> 1; // 取中点(等价于(l + r)/2,位运算更快)
if (check(mid)) { // 如果mid可行(存在边置零后所有路径≤mid)
r = mid - 1; // 缩小右边界,尝试寻找更小的T
} else { // 如果mid不可行(不存在这样的边)
l = mid + 1; // 扩大左边界,寻找更大的T
}
}

// 步骤5:输出结果(此时l == r,是最小的T)
write(l); // 输出最小的最大路径长度
flush();
return 0; // 程序结束
}