【知识点复习】树的直径与最近公共祖先
前言
快一年没写过正经博客了……已经不会排版辽。。(虽然以前也不太会就是说)
参考链接: 0x63.图论 - 树的直径与最近公共祖先
定义:
直径:树上两点间距离最远的路径长度。
公共祖先:两个点到顶点的路径上的最早交点。
求法:
一、求直径
树形 \(DP\)
\(dis[x]\) 为 节点 \(x\) 到其子树节点的最远距离;
\(ans\) 存直径
不解释,看代码就能看懂
void dp(int x) {
vis[x] = 1;
int ver;
for(int i = head[x]; i; i = nex[i]) {
ver = to[i];
if(vis[ver]) continue;
dp(ver);
ans = max(ans, dis[ver] + dis[x] + w[i]);
dis[x] = max(dis[x], dis[ver] + w[i]);
}
}
两次 \(BFS/DFS\)
先从任意一点开始 \(DFS\),求出当前点所能到达的最远距离,该点记作 \(P\)。再从 \(P\) 开始, \(DFS\) 求出 \(P\) 所能达到的最远点,记作 \(Q\)。所求 \(P\) \(Q\) 就是直径的端点,两点间距离就是直径。
很好证明,可以手画一棵树推一推。就不赘述了,看代码。
\(dis[x]\) 为从出发点到该点的距离;
\(ans\) 为直径。
void dfs(int u,int &ed){
if(dis[u] > ans)ans = dis[u],ed = u;
vis[u] = 1;
for(int i = head[u];~i;i = nex[i]){
int v = ver[i],w = edge[i];
if(vis[v])continue;
dis[v] = dis[u] + w;
dfs(v,ed);
}
return ;
}
void solve(){
dfs(1,p);
ans = dis[p] = 0;
memset(vis,0,sizeof vis);
dfs(p,q);
}
二、求 LCA
其实求 \(LCA\) 的核心思想都是在树上跳,先跳到同深度,再跳到同祖先。
这里介绍两个常用的方法,如果很感兴趣,可以去洛谷模板题的题解区逛逛。
直接看代码吧,具体过程找度娘(
倍增
void dfs(int x,int f) {
dep[x] = dep[f] + 1;
fa[x][0] = f;
for(int i = 1; i < 20; i ++) fa[x][i] = fa[fa[x][i - 1]][i - 1];
for(int i= head[x]; i; i = e[i].nex) {
int k = e[i].to;
if(k != f) dfs(k,x);
}
}
int lca(int x,int y) {
if(dep[x] < dep[y]) swap(x,y);
for(int i = 20; i >= 0; i --) {
if(dep[fa[x][i]] >= dep[y]) 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 dfs1(int p,int dep,int father)//统计子树大小、深度、父亲和重儿子。
{
depth[p]=dep;
fa[p]=father;
int maxv=-1;
siz[p]=1;
for(int i=head[p];i;i=e[i].next)
if(e[i].to!=father)
{
dfs1(e[i].to,dep+1,p);
siz[p]+=siz[e[i].to];
if(siz[e[i].to]>maxv)
{
maxv=siz[e[i].to];
son[p]=e[i].to;
}
}
return ;
}
void dfs2(int p,int tp)//统计链顶
{
top[p]=tp;
if(!son[p])
return ;
dfs2(son[p],tp);
for(int i=head[p];i;i=e[i].next)
if(e[i].to!=fa[p]&&e[i].to!=son[p])
dfs2(e[i].to,e[i].to);
return ;
}
int query(int x,int y)//核心代码
{
while(top[x]!=top[y])//判断是否在一条重链上
if(depth[top[x]]>=depth[top[y]])//链顶深度大的往上跳
x=fa[top[x]];
else
y=fa[top[y]];
return depth[x]<depth[y]?x:y;//返回深度小的节点编号
}
应用:
直径
模板题
题意:有 \(N\) 个农田 \(M\) 条路,求两农田(任意的)间的路径距离最大值。
思路:直接求树的直径就好。
点击查看代码
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 5e5 + 5;
int n, m, tot, ans, head[N], nex[N], to[N], w[N], dis[N];
bool vis[N];
void add(int x, int y, int z) {
to[++tot] = y, nex[tot] = head[x], w[tot] = z, head[x] = tot;
to[++tot] = x, nex[tot] = head[y], w[tot] = z, head[y] = tot;
}
void dp(int x) {
vis[x] = 1;
int ver;
for(int i = head[x]; i; i = nex[i]) {
ver = to[i];
if(vis[ver]) continue;
dp(ver);
ans = max(ans, dis[ver] + dis[x] + w[i]);
dis[x] = max(dis[x], dis[ver] + w[i]);
}
}
char s[5];
int main() {
scanf("%d %d", &n, &m);
int u, v, w;
for(int i = 1; i <= m; i ++) {
scanf("%d %d %d %s", &u, &v, &w, s);
add(u, v, w);
}
dp(1);
printf("%d", ans);
return 0;
}
=========================================================================
巡逻
题意:在一个地区有 n 个村庄,编号为 1,2,…,n。
有 n−1 条道路连接着这些村庄,每条道路刚好连接两个村庄,从任何一个村庄,都可以通过这些道路到达其他任一个村庄。
每条道路的长度均为 1 个单位。
为保证该地区的安全,巡警车每天都要到所有的道路上巡逻。
警察局设在编号为 1 的村庄里,每天巡警车总是从警局出发,最终又回到警局。
为了减少总的巡逻距离,该地区准备在这些村庄之间建立 K 条新的道路,每条新道路可以连接任意两个村庄。
两条新道路可以在同一个村庄会合或结束,甚至新道路可以是一个环。
因为资金有限,所以 K 只能为 1 或 2。
同时,为了不浪费资金,每天巡警车必须经过新建的道路正好一次。
编写一个程序,在给定村庄间道路信息和需要新建的道路数的情况下,计算出最佳的新建道路的方案,使得总的巡逻距离最小。
思路: \(k = 1\) 的情况很简单,直接在直径上新建道路即可。而 \(k = 2\) 的情况,则要考虑在直径上修建了道路之后该怎么去建另一条路。考虑先将直径上的边全部赋为 \(-1\),因为根据题意,所建道路要求恰好走一次,如果多走就要加上 1. 而 \(dis\) 之间的量如果有重复,就需要重新加上路径值。而对于新建的第二条路径来说,就是在修改后的图上再找一条直径。
点击查看代码
#include <cstdio>
#include <map>
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 1e5 + 5;
map<int, bool> maap;
int n, k, tot, id, maxn, start, ed, R, r;
int nex[MAXN * 2], to[MAXN * 2], value[MAXN * 2], head[MAXN], d[MAXN], dis[MAXN];
bool flag;
void add(int x, int y) {
to[++tot] = y;
nex[tot] = head[x];
value[tot] = 1;
head[x] = tot;
}
void dfs(int x, int f, int v) {
if (v >= maxn) {
maxn = v;
id = x;
}
for (int i = head[x]; i; i = nex[i]) {
if (to[i] != f)
dfs(to[i], x, v + value[i]);
}
}
void dfs2(int now, int f, int t) {
if (flag)
return;
for (int i = head[now]; i; i = nex[i]) {
if (flag)
return;
if (to[i] == f)
continue;
if (to[i] == t) {
d[now] = to[i];
flag = 1;
return;
}
d[now] = to[i];
dfs2(to[i], now, t);
if (flag)
return;
}
}
void find(int now, int f) {
for (int i = head[now]; i; i = nex[i]) {
if (to[i] == f)
continue;
find(to[i], now);
r = max(r, dis[now] + dis[to[i]] + value[i]);
dis[now] = max(dis[now], dis[to[i]] + value[i]);
}
}
int main() {
scanf("%d %d", &n, &k);
for (int i = 1; i < n; i++) {
int a, b;
scanf("%d %d", &a, &b);
add(a, b);
add(b, a);
}
dfs(1, 0, 0);
start = id;
maxn = 0;
dfs(start, 0, 0);
R = maxn;
ed = id;
if (k == 1) {
printf("%d", 2 * n - 1 - R);
return 0;
}
dfs2(start, 0, ed);
maap[start] = 1;
maap[ed] = 1;
for (int i = start; i != ed; i = d[i]) maap[i] = 1;
for (int i = 1; i <= n; i++) {
if (maap.count(i)) {
for (int j = head[i]; j; j = nex[j]) {
if (maap.count(to[j]))
value[j] = -1;
}
}
}
find(1, 0);
printf("%d", 2 * n - R - r);
return 0;
}
=========================================================================
树网的核
题目:设 \(T=(V,E,W)\) 是一个无圈且连通的无向图(也称为无根树),每条边都有正整数的权,我们称 \(T\) 为树网(treenetwork
),其中 \(V\),\(E\) 分别表示结点与边的集合,\(W\) 表示各边长度的集合,并设 \(T\) 有 \(n\) 个结点。
路径:树网中任何两结点 \(a\),\(b\) 都存在唯一的一条简单路径,用 \(d(a, b)\) 表示以 \(a, b\) 为端点的路径的长度,它是该路径上各边长度之和。我们称
\(d(a, b)\) 为 \(a, b\) 两结点间的距离。
\(D(v, P)=\min\{d(v, u)\}\), \(u\) 为路径 \(P\) 上的结点。
树网的直径:树网中最长的路径成为树网的直径。对于给定的树网 \(T\),直径不一定是唯一的,但可以证明:各直径的中点(不一定恰好是某个结点,可能在某条边的内部)是唯一的,我们称该点为树网的中心。
偏心距 \(\mathrm{ECC}(F)\):树网 \(T\) 中距路径 \(F\) 最远的结点到路径 \(F\) 的距离,即
\(\mathrm{ECC}(F)=\max\{D(v, F),v \in V\}\)
任务:对于给定的树网 \(T=(V, E, W)\) 和非负整数 \(s\),求一个路径 \(F\),他是某直径上的一段路径(该路径两端均为树网中的结点),其长度不超过 \(s\)(可以等于 \(s\)),使偏心距 \(\mathrm{ECC}(F)\) 最小。我们称这个路径为树网 \(T=(V, E, W)\) 的核(Core
)。必要时,\(F\) 可以退化为某个结点。一般来说,在上述定义下,核不一定只有一个,但最小偏心距是唯一的。
下面的图给出了树网的一个实例。图中,\(A-B\) 与 \(A-C\) 是两条直径,长度均为 \(20\)。点 \(W\) 是树网的中心,\(EF\) 边的长度为 \(5\)。如果指定 \(s=11\),则树网的核为路径DEFG
(也可以取为路径DEF
),偏心距为 \(8\)。如果指定 \(s=0\)(或 \(s=1\)、\(s=2\)),则树网的核为结点 \(F\),偏心距为 \(12\)。
思路:依旧是很恶心的题面
不愧是 CCF 语言((
直接根据树的直径的性质来搞就好了。
可以肯定的是,非直径上的点到直径的距离的最大值一定是最小的
所以先跑两遍dfs求出直径,在跑一遍非直径,取最大值就好了。
点击查看代码
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 5e5 + 5;
int n, m, tot, ans = 1e9, head[N], nex[N << 1], to[N << 1], w[N << 1];
void add(int x, int y, int z) {
to[++tot] = y, nex[tot] = head[x], head[x] = tot, w[tot] = z;
}
bool tag[N];
int fa[N], dis[N], mx;
void dfs(int x, int f) {
fa[x] = f; int ver;
if(dis[x] > dis[mx]) mx = x;
for(int i = head[x]; i; i = nex[i]) {
ver = to[i];
if(tag[ver] || ver == f) continue;
dis[ver] = dis[x] + w[i];
dfs(ver, x);
}
}
int main() {
scanf("%d %d", &n, &m);
int u, v, ww;
for(int i = 1; i < n; i ++) {
scanf("%d %d %d", &u, &v, &ww);
add(u, v, ww), add(v, u, ww);
}
dis[1] = 1, dfs(1, 0), dis[mx] = 0, dfs(mx, 0);
u = mx;
for(int i = u, j = u; i; i = fa[i]) {
while(dis[j] - dis[i] > m) j = fa[j];
ans = min(ans, max(dis[u] - dis[j], dis[i]));
}
for(int i = u; i; i = fa[i]) tag[i] = 1;
for(int i = u; i; i = fa[i]) {
mx = i, dis[i] = 0;
dfs(i, fa[i]);
}
for(int i = 1; i <= n; i ++) ans = max(ans, dis[i]);
printf("%d\n", ans);
return 0;
}
=========================================================================
LCA
模板题
思路:就,妹啥好说的,直接上模板就可以了。。
点击查看代码
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
#define ll long long
using namespace std;
const int N = 5e5 + 5;
int n,m,s,tot,head[N],dep[N],fa[N][25],lg[N];
struct node {
int to,nex;
}e[N * 2];
void add(int u,int v) {
e[++tot] = (node) {v,head[u]};
head[u] = tot;
}
void dfs(int x,int f) {
dep[x] = dep[f] + 1;
fa[x][0] = f;
for(int i = 1; i < 20; i ++) fa[x][i] = fa[fa[x][i - 1]][i - 1];
for(int i= head[x]; i; i = e[i].nex) {
int k = e[i].to;
if(k != f) dfs(k,x);
}
}
int lca(int x,int y) {
if(dep[x] < dep[y]) swap(x,y);
for(int i = 20; i >= 0; i --) {
if(dep[fa[x][i]] >= dep[y]) 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];
}
int main() {
scanf("%d %d %d",&n,&m,&s);
for(int i = 1; i < n; i ++) {
int u,v;
scanf("%d %d",&u,&v);
add(u,v);
add(v,u);
}
dfs(s,0);
for(int i = 1; i <= m; i ++) {
int u,v;
scanf("%d %d",&u,&v);
printf("%d\n",lca(u,v));
}
return 0;
}
=========================================================================
闇の連鎖
题意:传说中的暗之连锁被人们称为 Dark。
Dark 是人类内心的黑暗的产物,古今中外的勇者们都试图打倒它。
经过研究,你发现 Dark 呈现无向图的结构,图中有 N 个节点和两类边,一类边被称为主要边,而另一类被称为附加边。
Dark 有 N–1 条主要边,并且 Dark 的任意两个节点之间都存在一条只由主要边构成的路径。
另外,Dark 还有 M 条附加边。
你的任务是把 Dark 斩为不连通的两部分。
一开始 Dark 的附加边都处于无敌状态,你只能选择一条主要边切断。
一旦你切断了一条主要边,Dark 就会进入防御模式,主要边会变为无敌的而附加边可以被切断。
但是你的能力只能再切断 Dark 的一条附加边。
现在你想要知道,一共有多少种方案可以击败 Dark。
注意,就算你第一步切断主要边之后就已经把 Dark 斩为两截,你也需要切断一条附加边才算击败了 Dark。
思路:因为主要边构成的是一棵树,所以先不考虑附加边。
点击查看代码
#include<cstdio>
#include<cstring>
#include<queue>
#include<algorithm>
#include<iostream>
using namespace std;
const int N = 5e5 + 5;
int n, m, ans, dep[N], dp[N], fa[N][25];
int tot, head[N], nex[N << 1], to[N << 1];
void add(int x, int y) {
to[++tot] = y, nex[tot] = head[x], head[x] = tot;
}
void bfs(int x) {
queue<int> q;
q.push(1);
dep[1]=1;
while(q.size()) {
int x = q.front();
q.pop();
for(int i = head[x]; i; i = nex[i]) {
int y = to[i];
if(dep[y]) continue;
dep[y]=dep[x]+1;
fa[y][0]=x;
for(int j=1;j<=23;j++){
fa[y][j]=fa[fa[y][j-1]][j-1];
}
q.push(y);
}
}
}
int lca(int x, int y) {
if(dep[x] < dep[y]) swap(x, y);
for(int i = 23; i >= 0; i --) {
if(dep[fa[x][i]] >= dep[y]) x = fa[x][i];
}
if(x == y) return x;
for(int i = 23; i >= 0; i --) {
if(fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
}
return fa[x][0];
}
bool vis[N];
void solve(int x) {
int ver; vis[x] = 1;
for(int i = head[x]; i; i = nex[i]) {
ver = to[i];
if(vis[ver]) continue;
solve(ver);
dp[x] += dp[ver];
}
}
int main() {
scanf("%d %d", &n, &m);
int u, v;
for(int i = 1; i < n; i ++) {
scanf("%d %d", &u, &v);
add(u, v), add(v, u);
}
bfs(1);
for(int i = 1; i <= m; i ++) {
scanf("%d %d", &u, &v);
dp[u] ++, dp[v] ++, dp[lca(u, v)] -= 2;
}
solve(1);
for(int i = 2; i <= n; i ++) {
if(!dp[i]) ans += m;
if(dp[i] == 1) ans ++;
}
printf("%d", ans);
return 0;
}