【知识点复习】树的直径与最近公共祖先

前言

快一年没写过正经博客了……已经不会排版辽。。(虽然以前也不太会就是说

参考链接: 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;
}  

posted @ 2022-07-10 15:31  Spring-Araki  阅读(28)  评论(0编辑  收藏  举报