2 树

2.1 树的定义

一个只有\(N-1\)条边,且任意两个点连通的图叫做树。通过这样定义的树往往是一棵无根树,而我们通常会任意选定一个根节点使其变成有根树。有根树可以定义“父亲和儿子”的层次关系,这往往有利于构造最优子结构,进行DP和搜索等操作。

特别的,如果在树上任意加上一条边,那么整个树上就会多出一个环。我们称这样的树是“基环树”。基环树不是树,但是它只有一个环。将整个环作为一个“广义根”,然后将根和连在环上的子树分开处理,同样可以套用树的许多算法。

2.2 树上的DP算法

通常选定一个根,然后用DFS计算。至于递归接口应该放在转移之前还是之后呢?那就看方程怎么写了。在写程序的时候,只要满足“已知推未知”的原则就行。

如果给定一棵无根树,答案要求给出最优的根使得某个值最优化,这时可以采用“换根法”。先任意选定一个根计算出规划值\(F_1\),然后从数学上推导出以任意点为根的规划值\(F_2\)。《进阶指南》上有相关的例题。

2.2.1 树的参量

子树大小size

最基础的量。转移方程简记为\(F(x)=1+\sum F(\text{son}(x))\)

树的重心

和size一样。如果子树\(x\)的大小是\(\text{size}(x)\),那么剩下树的大小就是\(N-\text{size}(x)\)。在求\(\text{size}\)时可以顺带求出。

树的直径

有一种DP方程,还有一种搜索方法。

第一种方法,设\(F_1(x)\)表示点\(x\)到它的子树最长的距离。有方程:

\[ F_1(x)=\max_{y \in \text{son}(x)}\{F(y)+d(x,y)\}
\]

然后以此推导出经过\(x\)的,在\(x\)子树内的最长链。设它为\(F_2(x)\)。

根据定义,我们在子树里面找出两条过\(x\)的路径,这两条路径最长即符合要求。有方程:

\[ F_2(x)=\max_{y_1,y_2\in \text{son}(x)}\{F_1(y_1)+d(x,y_1)+F_1(y_2)+d(x,y_2)\}
\]

这两个值,一个一定是最大值,一个一定是次大值。我们根据\(F_1\)的定义,得到:

\[ F_2(x)=\max_{y_2\in \text{son}(x),y_2 \neq y_1,F_1(x)=F(y_1)+d(x,y_1)}\{F_1(x)+F_1(y_2)+d(x,y_2)\}
\]

这样方程会相当麻烦。我们换一种思路:

设\(F_1(x)\)表示\(x\)到\(x\)子树叶子的最大距离,\(G_1(x)\)表示次大距离。这样我们有两个方程:

\[\begin{cases}
G_1(x)=F_1(x),F_1(x)=F_1(y)+d(x,y) & F_1(x) < F_1(y)+d(x,y)\\
G_1(x)=F_1(y)+d(x,y) & \text{else if }G_1(x)<F_1(y)+d(x,y)
\end{cases}\\
y \in \text{son}(x)
\]

这样\(F_2(x)=G_1(x)+F_1(x)\)就是原来所求了。枚举最大的\(F(i)\)即可求得答案。

也可以用两次BFS或DFS。先任意一个点\(root\),搜索出离\(root\)最远的点\(p_1\);然后再搜索出离\(p\)点最远的点\(p'\)。那么两点的距离\(pp'\)就是树的直径。

2.2.2 LCA

如果节点\(u\)既是\(x\)的祖先,又是\(y\)的祖先,则\(u\)是\(x,y\)的公共祖先。当这个公共祖先深度最深时,记\(u=LCA(x,y)\)是\(x,y\)的最近公共祖先。

求LCA有若干种方法:

向上标记法\(O(qN)\)

对于要求的\(LCA(x,y)\),我们先选一个节点\(x\)走到根节点,将路径上的点全部标记。然后,我们再让\(y\)同时往上走,\(y\)遇到的第一个被标记的点就是\(LCA(x,y)\)。

树上倍增法\(O(q\log N)\)

首先我们令\(F(x,i)\)表示\(x\)的\(2^i\)辈祖先,也就是\(x\)往上走\(2^i\)步得到的节点。初始时有\(F(x,0)=\text{father}(x)\),然后以\(i\)为阶段,以\(F(x,i)=F(F(x,i - 1), i - 1)\)为转移方程,就可以处理出所有的\(F(x,i)\)。

接下来我们选择一个点往上跳(设这个点为\(x\))。可以进行交换使得\(\text{deep}(x)\geq \text{deep}(y)\)。依次尝试让\(x\)向上走\(2^{\log N},\cdots,2^2,2^1,2^0\)步,使得每一步都恰好满足\(\text{deep}(x) \geq \text{deep}(y)\)。最后一步应有\(\text{deep}(x)=\text{deep}(y)\)。如果有\(x=y\),那么\(LCA(x,y)=y\)。

否则,我们让\(x\),\(y\)同时向上跳\(2^{\log N},\cdots,2^2,2^1,2^0\)步,使得每一步都有\(x \neq y\)。在最后一步的时候,一定有\(\text{father}(x)=\text{father}(y)=LCA(x,y)\)。

树上倍增法有非常广泛的应用。举个例子,有道题就需要维护树上路径的最大值,这时就可以用“树上ST表”。这个结构就是树上倍增法的体现。

树链剖分\(O(q\log N)\)

重点是用两次dfs处理出top数组,即每条树链的顶端。每次询问时,如果\(x\)和\(y\)都在同一条链上,则\(LCA(x,y)\)就是深度较小的那个节点。否则,我们就让深度较大的节点往它的上面的链跳,即令\(x=\text{father}(\text{top}(x))\)。

实现起来代码长度较长,会有一定常数,但是在时间复杂度上面应该还是略优于倍增法。且这种方法可扩展性强,可以配套其他的操作。

LCA的Tarjan算法\(O(q+N)\)

用并查集对向上标记法的优化。

在DFS搜索一棵树时,每个节点有三个状态:UNMARKEDTRAVERSALBACKTRACKED,分别表示“未标记”,“已遍历”,“已回溯”。

当一个节点\(x\)正处于TRAVERSAL状态时,其沿着父亲至根节点一定有一条TRAVERSAL链。此时对于任意一个处于BACKTRACKED状态的节点\(y\),\(LCA(x,y)\)就是\(y\)沿着父亲路径,遇到的第一个TRAVERSAL节点。这是向上标记法的实质。

这里对于UNMARKED的\(y\)是不成立的,因为TRAVERSAL链和BACKTRACKED链的交点相当于一个不同时刻决策的分支,是第一个使得\(x\)和\(y\)分立的节点。否则,沿着\(y\)的一段路径会标记成TRAVERSAL,而不是BACKTRACKED

在这里,我们用一个并查集来维护这个路径。对于一个BACKTRACKED节点,我们定义它的支点\(top(y)\)表示节点\(y\)沿着父亲路径,向上遇到的第一个TRAVERSAL节点。

当一个节点\(x\)由TRAVERSAL变成BACKTRACKED时,它的父亲一定是TRAVERSAL的(根据遍历回溯的顺序可以得到)。这时一定有\(top(x)=top(\text{father}(x))\)。

当我们在访问\(top(y)\)时,可以顺便进行路径压缩,即\(top(y)=top(top(y))\)。这对答案是没有影响的。这一点和并查集类似,可以用并查集的get操作完成。

总结一下,对于每一个UNMARKED节点\(x\),我们先标记为TRAVERSAL,并遍历它的儿子\(y\),然后令\(top(y)=x\)。随后,对于每一个和\(x\)有关的询问\(LCA(u_i,x)\),如果\(u_i\)是BACKTRACKED的,我们可以直接由get(u[i])得到答案。

将询问离线处理,预处理和每个节点有关的询问,然后运行这个算法。时间复杂度\(O(q+N)\),其中并查集合并的时间复杂度可以忽略不计。


unsigned short state[MAXN];
#define UNMARKED 0
#define TRAVERSAL 1
#define BACKTRACKED 2 int top[MAXN];
inline int get(int cur)
{
if(cur == top[cur])
return cur;
return top[cur] = get(top[cur]);
}
inline void init()
{
for(rg int i = 1; i <= N; ++ i)
top[i] = i;
} void DFS(int cur)
{
state[cur] = TRAVERSAL;
for(rg int e = head[cur]; e; e = edge[e].next)
{
int to = edge[e].to;
if(state[to] != UNMARKED)
continue;
DFS(to);
top[to] = cur;
} for(rg int i = 0; i < queryNode[cur].size(); ++ i)
{
int node = queryNode[cur][i], rank = queryRank[cur][i];
if(state[node] != BACKTRACKED)
continue;
ans[rank] = get(node);
}
state[cur] = BACKTRACKED;
}

2.3 最小生成树(MST)

2.3.1 最小生成树算法

最小生成树是某个图的子图。它是一棵树,且边权之和最小。在构造最小生成树时,我们可以尽可能贪心地选取边权小的边。这样就有了第一个算法:

Kruskal算法

简单来说,我们每次在整个图中选取未被选取的,局部权值最小的边。如果加入这条边,可以让原来不连通的两个森林连通,就把这条边加入到生成树中。否则,我们就跳过这条边,继续检查下一条边。

可以对边先快速排序,然后用并查集维护点和点的连通性。时间复杂度\(O(M\log M)\)。

Prim算法

设\(S\)和\(T\)分别表示待选集合和已选集合。在最开始时,\(T=\{1\}\)。

当\(i\in S\)我们设\(dis_i\)是点\(i\)到集合\(T\)中最近点的距离。每次我们选取最小的\(dis_i\),然后将\(i\)选入\(T\)中,同时用\(i\)更新其他点的\(dis\)值。

2.3.2 衍生算法

Kruskal重构树

来源自一个非常简单的模型:

  • 求无向连通图中,两点之间所有简单路径的最大边权的最小值。

也就是说,两点之间有若干条路径,而每条路径上都有一个最大边权。求这些最大边权中的最小值。

在执行Kruskal算法时,我们会依次选取边权最小的边,然后将边对应的两个连通块合并。在这里,我们不是直接合并,而是设定一个“虚点”,让两个连通块都指向这个虚点。虚点的权值就是原来的边权。这样,两点之间最小的最大边权就是合并两个点所属连通块的所需边权,重构后,也就是两点的\(LCA\)的点权。

这样,上面这道题就可以用\(O(\log N)\)的时间处理每一个询问了。

可以结合NOI2018 归程来具体理解一下。这里是这道题的题解

如果你认为这道题有点难,不妨试一下这道题:NOIP2013 货车运输。这道题可以通过构造一个最大生成树的重构树直接完成。

2.4 树链剖分

这部分内容最好结合《一本通》上面的图理解。这里只快速捋一捋知识点。

树链剖分其实在考场上是一种非常有风险的算法,因为代码量大,会消耗大量的时间和精力。不过,有些题目还是必须得用这种方法求解。

树链剖分的主要方式为重链剖分。当然,长链剖分也是一种形式。所谓的重链就是对于每一个节点,连接子树大小最大的儿子,从而形成一条链。剖分的时候,所有的“重儿子-父亲”边会形成一条条链,这些链按顺序排列就会形成一个区间。所谓的重儿子,就是指子树大小最大的儿子。

在区间上就可以套用区间数据结构了。当然,这里主要是介绍“线段树+树链剖分”的方法。

在树链剖分时,我们先要用DFS预处理出所有的点参量,包括father depth size son,即父亲,深度,子树大小,重儿子。这部分的代码如下:

inline void getSon(int cur, int curFather)
{
father[cur] = curFather;
depth[cur] = depth[curFather] + 1;
size[cur] = 1;
son[cur] = 0;
for(rg int e = head[cur]; e; e = edge[e].next)
{
int to = edge[e].to;
if(to == curFather)
continue; getSon(to, cur);
size[cur] += size[to];
if(son == 0 || size[to] > size[son[cur]])
son[cur] = to;
}
}

接下来,我们给每个节点打上时间戳,并且对时间戳和节点建立一一对应的关系。用dfn[x]表示点x的时间戳,用rev[i]表示时间戳为i的节点编号。

同时,我们预处理出每个节点x所在的重链的顶端top[x]。对于重儿子,有top[son[x]]=top[x];对于其他的儿子,它们单独作为一条新链的开端,有top[to]

inline void getTop(int cur, int curFather)
{
dfn[cur] = ++ timeStamp;
rev[timeStamp] = cur; if(son[cur])
{
top[son[cur]] = top[cur];
getTop(son[cur], cur);
} for(rg int e = head[cur]; e; e = edge[e].next)
{
int to = edge[e].to;
if(to == curFather || to == son[cur])
continue;
top[to] = to;
getTop(to, cur);
}
}

在每一条链上,深度depth大的节点时间戳一定大。这样,树上的每个点就被一一对应到[1,timeStamp]的闭区间里了。

接下来以询问最大值为例,演示一下如何询问树上两点间路径上的信息。

为了方便封装,我们假定我们定义了这样的一个结构体:

SegmentTree sgt;

数据结构sgt是一棵建立在区间[1,timeStamp]上的线段树,可以通过成员函数getMax(int left, int right)访问区间[left, right]的最大值。对于路径的两个端点u v,我们进行这样的操作:

  1. uv在同一条链上,我们先交换使得depth[u]<=depth[v],然后我们直接用sgt.getMax(dfn[u], dfn[v])来更新答案,随后结束过程。
  2. 否则,我们每次选择链深度大的点(假定我们交换两点使得depth[top[u]] >= depth[top[v]] ),然后用sgt.getMax(dfn[top[u]], dfn[u])更新答案。之后我们让u = father[top[u]]使u转至下一条链上。如果此时``u v` 仍不在同一条链上,则执行2。反之,则执行1。

这部分代码如下:

number cur = -INF;
int u = read(1), v = read(1);
int fu = top[u], fv = top[v];
while(fu != fv)
{
if(depth[fu] < depth[fv])
{
swap(u, v);
swap(fu, fv);
}
checkMax(cur, sgt.getMax(dfn[fu], dfn[u]));
u = father[fu];
fu = top[u];
}
if(depth[u] > depth[v])
swap(u, v);
checkMax(cur, sgt.getMax(dfn[u], dfn[v])); printf("%lld\n", cur);

这里张贴ZJOI 2008 树的统计的代码。这道题要求支持动态查询树上两点间的点权和和点权最大值。代码如下:

#include <cstdio>
#include <cctype>
#include <cstring>
using namespace std;
#define rg register
#define fre(z) freopen(z".in", "r", stdin), freopen(z".out", "w", stdout)
#define customize template<class type> inline
typedef long long number;
const number INF = 0x3f3f3f3f3f3f3f3f;
customize type read(type sample)
{
type ret = 0, sign = 1; char ch = getchar();
while(! isdigit(ch))
sign = ch == '-' ? -1 : 1, ch = getchar();
while(isdigit(ch))
ret = ret * 10 + ch - '0', ch = getchar();
return sign == -1 ? -ret : ret;
} const int MAXN = 30010; int N, Q;
int timeStamp = 0; int head[MAXN];
struct Edge{
int next;
int front, to;
}edge[MAXN << 1];
int tot = 0;
inline void append(int front, int to)
{
++ tot;
edge[tot] = (Edge){head[front], front, to};
head[front] = tot;
}
inline void connect(int front, int to)
{
append(front, to);
append(to, front);
} int dfn[MAXN], rev[MAXN];
int father[MAXN], depth[MAXN], size[MAXN], top[MAXN], weight[MAXN], son[MAXN]; inline void getSon(int cur, int curFather)
{
father[cur] = curFather;
depth[cur] = depth[curFather] + 1;
size[cur] = 1;
son[cur] = 0;
for(rg int e = head[cur]; e; e = edge[e].next)
{
int to = edge[e].to;
if(to == curFather)
continue; getSon(to, cur);
size[cur] += size[to];
if(son == 0 || size[to] > size[son[cur]])
son[cur] = to;
}
} inline void getTop(int cur, int curFather)
{
dfn[cur] = ++ timeStamp;
rev[timeStamp] = cur; if(son[cur])
{
top[son[cur]] = top[cur];
getTop(son[cur], cur);
} for(rg int e = head[cur]; e; e = edge[e].next)
{
int to = edge[e].to;
if(to == curFather || to == son[cur])
continue;
top[to] = to;
getTop(to, cur);
}
} customize type max(type a, type b)
{
return a > b ? a : b;
}
customize type checkMax(type &var, type value)
{
return var = var > value ? var : value;
}
customize void swap(type &a, type &b)
{
type temp = a;
a = b;
b = temp;
} struct SegmentTree{
struct Node{
int left, right;
int lc, rc;
struct Data{
number max;
number sum;
}data;
number add;
inline int len()
{
return right - left + 1;
}
}node[MAXN << 2];
int size;
int root;
SegmentTree()
{
size = 0;
}
inline int newNode(int left, int right, int val)
{
++ size;
node[size] = (Node){left, right, 0, 0, (Node::Data){val, val}, 0};
return size;
}
inline int build(int left, int right)
{
if(left == right)
{
return newNode(left, right, weight[rev[left]]);
} int mid = (left + right) >> 1;
int cur = newNode(left, right, 0);
node[cur].lc = build(left, mid);
node[cur].rc = build(mid + 1, right);
node[cur].data.sum = node[node[cur].lc].data.sum + node[node[cur].rc].data.sum;
node[cur].data.max = max(node[node[cur].lc].data.max, node[node[cur].rc].data.max);
return cur;
}
inline void init(int left, int right)
{
root = build(left, right);
}
inline void spread(int cur)
{
if(node[cur].add)
{
number curAdd = node[cur].add;
node[node[cur].lc].data.sum += node[node[cur].lc].len() * curAdd;
node[node[cur].rc].data.sum += node[node[cur].rc].len() * curAdd;
node[node[cur].lc].data.max += curAdd;
node[node[cur].rc].data.max += curAdd;
node[node[cur].lc].add = curAdd;
node[node[cur].rc].add = curAdd;
node[cur].add = 0;
}
}
inline number searchMax(int cur, int left, int right)
{
if(left <= node[cur].left && node[cur].right <= right)
{
return node[cur].data.max;
}
spread(cur);
number temp = -INF;
if(left <= node[node[cur].lc].right)
{
checkMax(temp, searchMax(node[cur].lc, left, right));
}
if(right >= node[node[cur].rc].left)
{
checkMax(temp, searchMax(node[cur].rc, left, right));
}
return temp;
}
inline number getMax(int left, int right)
{
return searchMax(root, left, right);
}
inline number searchSum(int cur, int left, int right)
{
if(left <= node[cur].left && node[cur].right <= right)
{
return node[cur].data.sum;
}
spread(cur);
number temp = 0;
if(left <= node[node[cur].lc].right)
{
temp += searchSum(node[cur].lc, left, right);
}
if(right >= node[node[cur].rc].left)
{
temp += searchSum(node[cur].rc, left, right);
}
return temp;
}
inline number getSum(int left, int right)
{
return searchSum(root, left, right);
}
inline void change(int cur, int pos, number val)
{
if(node[cur].left == node[cur].right)
{
node[cur].data = (Node::Data){val, val};
return;
}
spread(cur);
if(pos <= node[node[cur].lc].right)
{
change(node[cur].lc, pos, val);
}
if(pos >= node[node[cur].rc].left)
{
change(node[cur].rc, pos, val);
}
node[cur].data.max = max(node[node[cur].lc].data.max, node[node[cur].rc].data.max);
node[cur].data.sum = node[node[cur].lc].data.sum + node[node[cur].rc].data.sum;
}
inline void change(int pos, number val)
{
change(root, pos, val);
}
}sgt; int main()
{
N = read(1);
for(rg int i = 1; i < N; ++ i)
{
int u = read(1), v = read(1);
connect(u, v);
}
for(rg int i = 1; i <= N; ++ i)
{
weight[i] = read(1ll);
} getSon(1, 0); top[1] = 1;
getTop(1, 0); sgt.init(1, timeStamp); Q = read(1);
for(rg int i = 1; i <= Q; ++ i)
{
char op[10];
scanf("%s", op);
if(op[0] == 'C')
{
int u = read(1);
number val = read(1ll);
sgt.change(dfn[u], val);
}
else
{
if(op[1] == 'M')
{
number cur = -INF;
int u = read(1), v = read(1);
int fu = top[u], fv = top[v];
while(fu != fv)
{
if(depth[fu] < depth[fv])
{
swap(u, v);
swap(fu, fv);
}
checkMax(cur, sgt.getMax(dfn[fu], dfn[u]));
u = father[fu];
fu = top[u];
}
if(depth[u] > depth[v])
swap(u, v);
checkMax(cur, sgt.getMax(dfn[u], dfn[v])); printf("%lld\n", cur);
}
else
{
number cur = 0;
int u = read(1), v = read(1);
int fu = top[u], fv = top[v];
while(fu != fv)
{
if(depth[fu] < depth[fv])
{
swap(fu, fv);
swap(u, v);
}
cur += sgt.getSum(dfn[fu], dfn[u]);
u = father[fu];
fu = top[u];
}
if(depth[u] > depth[v])
swap(u, v);
cur += sgt.getSum(dfn[u], dfn[v]); printf("%lld\n", cur);
}
}
}
return 0;
}

2.5 树上差分

“差分”这个概念源于序列操作。部分区间问题需要对整段区间进行大量的修改,而最后进行询问。如果采用暴力做法,修改的时间复杂度是\(O(N)\)的,而查询的时间复杂度仅为\(O(1)\)。对于只询问一次的题目,这种做法是非常不平衡的。

我们先来看一个模型:

给定\(Q\)个操作,每次标记一个区间\([l, r]\)。最后询问每个位置\(i\)被标记的次数。

假设\(A_i\)表示\(i\)位置被标记的次数,\(A_0 = 0\),那么我们可以令\(\Delta A_i = A_i - A_{i - 1}\)。这样做的好处在于修改的时间复杂度大大降低:假设\(\Delta A_i\) 原来均为\(0\),现在我们在\([l, r]\)上打标记,那么仅有\(\Delta A_l = A_l - A_{l - 1} = 1\)和\(\Delta A_{r+1} = A_{r + 1} - A_r = -1\)两个位置发生了改变。令\(\Delta A_l = 1\),\(\Delta A_{r+1} = -1\)即可。

在询问的时候,直接查询差分数组的前缀和\(A_i = \sum_{j = 1}^{i} \Delta A_j\)即可。

我们把序列上的差分推广到树上,可以得到这样的模型:

给定多个操作,每次在树的两点之间的路径进行标记。求每个点(边)被标记的次数。

树上的前缀和比区间要稍微复杂一点。我们以点为例,介绍一下如何进行树上差分。

我们设\(F(x)\)表示当前节点被经过的次数。如果我们标记一条路径,我们可以把这个路径拆成

两段:\(u \rightarrow LCA(u,v) \rightarrow v\)。当然,这其中也包含了\(LCA(u,v) \in \{u,v\}\)的情况。这条路径上每一个\(F(x)\)都要自增1。

我们考虑一种最暴力的更改做法:令\(F(u)\)向上走到\(F(root)\)的部分均自增1,然后让\(F(v)\)向上走到\(F(root)\)的部分也均自增1。由于LCA被重复计算了一次,所以\(F(LCA)\)向上走到\(F(root)\)的部分均自减1。而\(LCA\)以上的部分不能被计算,所以令\(F(\text{father}(LCA))\)到\(F(root)\)的部分均自减1。

这样做的好处是易于差分。我们令\(\Delta F(x) = F(x) - \sum_{\text{son } y}F(y)\)。

当每次加入一条树上的路径\((u,v)\)时,我们令\(\Delta F(u)++\),\(\Delta F(v)++\), \(\Delta F(LCA)--\), \(\Delta F(\text{father}(LCA))--\)。

这样就和原来的暴力做法一一对应起来了。在最后统计答案时,我们只要DFS统计子树的\(\Delta F\)和,就可以得到原来的\(F\)了。

可以结合这份代码片段理解一下:

for(rg int i = 1; i <= K; ++ i)
{
int u = read(1), v = read(1);
int lca = LCA(u, v);
++ DF[u]; ++ DF[v];
-- DF[lca]; -- DF[father[lca]];
}
getMax(1, 0);
printf("%d", ans);

DFS部分:

inline void getMax(int cur, int curFather)
{
sum[cur] = DF[cur];
for(rg int e = head[cur]; e; e = edge[e].next)
{
int to = edge[e].to;
if(to == curFather)
continue;
getMax(to, cur);
sum[cur] += sum[to];
}
checkMax(ans, sum[cur]);
}

2.6 点分治

点分治是树分治的一种。它的核心原理在于:

  • 根的选取对于答案没有影响
  • 可以划分出子结构,递归计算

比较典型的一类问题是统计树上的路径数。直接暴力枚举显然是不可取的,我们考虑路径的种类:

  1. 端点存在家族关系,即某个点是另一个点的祖父
  2. 其他

对于这两种情况,我们都可以找到一个根节点,使得路径经过这个根节点。这样一来,我们就把“统计整棵树的答案”这个问题划分成了“统计子树的答案”。

如何递归地进行求解呢?我们可以考虑每次先计算父节点,然后依次计算各个子节点。这样一来,整个计算的顺序就会呈现出一个树形结构,时间复杂度就会从\(N\)往\(\log N\)级别靠近。

当然,如果树退化成了一条链,而我们恰好又从端点开始计算。解决方案是对于当前子树,我们先找到这棵树的重心,然后以重心为根计算这棵子树的答案。

不过点分治确实对于我来说太难了。我对着各种题解看了好几天,可能我的代码实现水平还不够高吧。这里就直接张贴洛谷模板题的代码了。这道题要求询问树上是否存在长度为\(K\)的路径。

#include <cstdio>
#include <cstring>
#include <cctype>
#include <vector>
using namespace std;
#define rg register
#define fre(z) freopen(z".in", "r", stdin), freopen(z".out", "w", stdout)
#define customize template<class type> inline
typedef long long number;
const number INF = 0x3f3f3f3f3f3f3f3f;
customize type read(type sample)
{
type ret = 0, sign = 1; char ch = getchar();
while(! isdigit(ch))
sign = ch == '-' ? -1 : 1, ch = getchar();
while(isdigit(ch))
ret = ret * 10 + ch - '0', ch = getchar();
return sign == -1 ? -ret : ret;
} const int MAXN = 100010;
const int MAXM = 110; int N, M; number K[MAXM];
int head[MAXN];
struct Edge{
int next;
int front, to;
number len;
}edge[MAXN << 1];
int tot = 0;
inline void append(int front, int to, number len)
{
++ tot;
edge[tot] = (Edge) {head[front], front, to, len};
head[front] = tot;
}
inline void connect(int front, int to, number len)
{
append(front, to, len);
append(to, front, len);
} int size[MAXN];
bool used[MAXN];
bool exist[10000010];
bool sat[MAXM]; int root;
int minSize; inline void getRoot(int cur, int father, int curSize)
{
size[cur] = 1;
int maxPart = 0;
for(rg int e = head[cur]; e; e = edge[e].next)
{
int to = edge[e].to;
if(used[to] || to == father)
continue;
getRoot(to, cur, curSize);
size[cur] += size[to];
if(size[to] > maxPart)
maxPart = size[to];
}
if(maxPart > curSize - size[cur])
maxPart = curSize - size[cur];
if(maxPart <= minSize)
{
root = cur;
minSize = maxPart;
}
} number dis[MAXN];
vector<number> subdis;//the distance in the current subtree
vector<int> curdis;//the distance that have been calculate in the current dividing procedure inline void getDis(int cur, int father)
{
subdis.push_back(dis[cur]);
for(rg int e = head[cur]; e; e = edge[e].next)
{
int to = edge[e].to;
if(used[to] || to == father)
continue;
dis[to] = dis[cur] + edge[e].len;
getDis(to, cur);
}
} inline void calc(int cur)
{
curdis.clear();
for(rg int e = head[cur]; e; e = edge[e].next)
{
int to = edge[e].to;
subdis.clear();
if(used[to])
continue;
dis[to] = edge[e].len;
getDis(to, cur); for(rg int i = subdis.size() - 1; i >= 0; -- i)
for(rg int j = 1; j <= M; ++ j)
sat[j] |= exist[(int) K[j] - subdis[i]]; //ask whether there's a path exist in the current subtree.
//we must make sure that the endpoints of the path don't both belong to the same subtree. for(rg int i = 0; i < subdis.size(); ++ i)
{
curdis.push_back(subdis[i]);
exist[(int) subdis[i]] = true;
}
}
for(rg int i = 0; i < curdis.size(); ++ i)
exist[curdis[i]] = 0;
}
inline void devide(int cur)
{
used[cur] = 1;//lable this node, representing that this node has been deleted.
exist[0] = 1;// the current node has a path leading to itself with the length 0
calc(cur);//calculate and get the statiscal information
for(rg int e = head[cur]; e; e = edge[e].next)
{
int to = edge[e].to;
if(used[to])
continue;
root = 0; minSize = N + 1;
getRoot(to, 0, size[to]);//find the center of gravity of the root.
devide(to);//solve with the depth-first order
}
} int main()
{
fre("tree");
N = read(1); M = read(1);
for(rg int i = 1; i < N; ++ i)
{
int u = read(1), v = read(1); number l = read(1ll);
connect(u, v, l);
}
for(rg int i = 1; i <= M; ++ i)
K[i] = read(1ll); minSize = N;
getRoot(1, 0, N);
devide(root); for(rg int i = 1; i <= M; ++ i)
{
if(sat[i])
puts("AYE");
else
puts("NAY");
}
return 0;
}

图论小专题B的更多相关文章

  1. 图论小专题A

    大意失荆州.今天考试一到可以用Dijkstra水过的题目我竟然没有做出来,这说明基础还是相当重要.考虑到我连Tarjan算法都不太记得了,我决定再过一遍蓝皮书,对图论做一个小的总结.图论这个部分可能会 ...

  2. 图论小专题C

    3 负环及其应用 3.1 判定算法 判断负环只能用"边松弛"算法,也就是Bellman-Ford和SPFA算法.这两个算法都是\(O(NM)\)级别的.因为负环中一定存在一条负边, ...

  3. NOIp 图论算法专题总结 (1):最短路、最小生成树、最近公共祖先

    系列索引: NOIp 图论算法专题总结 (1) NOIp 图论算法专题总结 (2) NOIp 图论算法专题总结 (3) 最短路 Floyd 基本思路:枚举所有点与点的中点,如果从中点走最短,更新两点间 ...

  4. 【总结】图论小总结【题解】P1330封锁阳关大学

    [题解][总结]P1330 封锁阳光大学 &&图论小总结 这道题其实有一点点难度,不过我能经过思考做出来说明还是没有普及组\(D1T1\)难度的. 考虑一条边的两边要有且仅有一个点被选 ...

  5. NOIp 图论算法专题总结 (2)

    系列索引: NOIp 图论算法专题总结 (1) NOIp 图论算法专题总结 (2) NOIp 图论算法专题总结 (3) 树链剖分 https://oi-wiki.org/graph/heavy-lig ...

  6. NOIp 图论算法专题总结 (3):网络流 & 二分图 简明讲义

    系列索引: NOIp 图论算法专题总结 (1) NOIp 图论算法专题总结 (2) NOIp 图论算法专题总结 (3) 网络流 概念 1 容量网络(capacity network)是一个有向图,图的 ...

  7. 「日常训练」「小专题·图论」Domino Effect(1-5)

    题意 分析 这题几乎就是一条dijkstra的问题.但是,如何考虑倒在中间? 要意识到这题求什么:单源最短路的最大值.那么有没有更大的?倒在中间有可能会使它更大. 但是要注意一个问题:不要把不存在的边 ...

  8. 「日常训练」「小专题·图论」 Cow Contest (1-3)

    题意 分析 问题是要看出来这是个floyd闭包问题.我没看出来- - 分析之后补充. 代码 // Origin: // Theme: Graph Theory (Basic) // Date: 080 ...

  9. 「日常训练」「小专题·图论」 Frogger (1-1)

    题意 分析 变形的dijkstra. 分析题意之后补充. 代码 // Origin: // Theme: Graph Theory (Basic) // Date: 080518 // Author: ...

随机推荐

  1. # 双值Hash

    双值Hash 简单介绍 Hash的应用:Hash其实就像一个加密过程,很多加密算法都会用到Hash,像GitHub中生成的token值也是Hash的结果. Hash冲突:简单来说就是不同的数映射到了同 ...

  2. python学习-7 条件语句 while循环 + 练习题

    1.死循环 while 1 == 1: print('ok') 结果是一直循环 2.循环 count = 0 while count < 10: print(count) count = cou ...

  3. 轻松搭建CAS 5.x系列(2)-搭建HTTPS的SSO SERVER端

    概要说明 CAS要求,必须使用HTTPS的服务,否则就只等实现登录,无法实现单点登录.科普下HTTPS,网站有HTTP和HTTPS两种协议.HTTP是浏览器到网站之间是明文传输,比如你输入帐号名和密码 ...

  4. C# switch语句的使用

    1  今天我们来学习switch 语句的使用,switch 语句和if else 类似 switch 语句主要的作用是用于来判断在规定条件下   根据你的选择来执行switch 语句下面case :的 ...

  5. 泛型和DataTable的属性

    泛型转DataTable public DataTable ToDataTable<TResult>(this IEnumerable<TResult> value) wher ...

  6. vue runtime报错问题

    Webpack中导入vue和普通网页中导入vue的区别1. 普通网页导入vue方式 <script></script> 2. Webpack导入vue方式 Import Vue ...

  7. Win Server 2012 配置运行 .net core 环境

    今天拿到一台 全新的win 2012 服务器配置服务器环境 记录一下 首先装好IIS 打开服务器管理器  - 添加功能和角色     好 安装完IIS 看一下服务器有没有安装 core的运行环境(全新 ...

  8. str 文本函数的调用

    方法 说明 S.isdigit() 判断字符串中的字符是否全为数字 S.isalpha() 判断字符串是否全为英文字母 S.islower() 判断字符串所有字符是否全为小写英文字母 S.isuppe ...

  9. SSH配置(同一服务器不同用户)

    一.cxwh用户ssh免密登陆至xtjk用户 1.cxwh用户执行 ssh-keygen -t rsa -N "" -f /home/cxwh/.ssh/id_rsa cp /ho ...

  10. Maven基本概念——根目录、项目创建、坐标

    转载来自:https://www.cnblogs.com/zjfjava/p/6817793.html 尊重原创! (一)Maven 基本概念——根目录.项目创建.坐标    1. MavenProj ...