Wannafly挑战赛1 MMset2(虚树+树的直径)

题目链接:点击打开链接

思路:虚树直径除以2,向上取整。不会证明的话,仔细思考、举几个sample可以YY出来。

通过这个题学了几个新东西:

1.虚树

所谓虚树,其实就是把询问中需要用到的点建到另一棵树上,对于一些问题可以降低复杂度。比如我们询问一条链上的两个端点,直接做dfs的复杂度是O(N)的,但是对于虚树,这两个端点可以直接相连,它们之间的边记录了原本整条链上的信息,于是复杂度变成了O(1)。

那么怎么建虚树呢?除了用到给出的询问点,我们还会用到询问点之间的lca,但是求出所有点对之间的lca是O(N^2)级别的,较好的方式是,把询问点按dfs序排序,有三个点x、y、z,,他们按dfs序排序,lca(x,z)一定在lca(x,y)或者lca(y,z)中,以此类推,k个询问点只需要求k - 1个点对间的lca,然后,我们再次把所有点按dfs序排序、去重,准备工作完成后,就开始建树了,方法是,我们维护一个栈,如果栈为空,直接点入栈;否则,我们看栈顶元素是不是当前点的祖先,如果不是,那么弹出栈顶继续判断,否则就可以从栈顶元素向这个点连一条边再让当前点进栈了。具体判断栈顶元素是不是当前点的祖先的方式是dfs序,但光用一个数组(设为in数组)存储各节点的dfs序还不够,还要用另一个数组(设为out数组)存储各节点的子节点中最大的dfs序为多少,只有in[u] < in[v] <= out[u],v才为u的子孙节点。第一个点就直接作为虚树的树根。

2.树的直径

树的直径指的是树上的最长简单路。

直径的求法:

最简单暴力的方法是,求出每个点的最远距离后取最大值,然后暴力求解一定适用性小......

最常用的方法是两遍BFS或者DFS。

任选一点u为起点,对树进行BFS/DFS遍历,找出离u最远的点v,以v为起点,再进行BFS/DFS遍历,找出离v最远的点w,则v到w的路径长度即为树的直径。

原理:距某个点最远的叶子节点一定是树的某一条直径的端点。
证明:关键在于证明第一次遍历的正确性,也就是对于任意点u,距离它最远的点v一定是最长路的一端。
如果u在最长路上,那么v一定是最长路的一端。可以用反证法:假设v不是最长路的一端,则存在另一点v’使得(u→v’)是最长路的一部分,于是len(u→v’) > len(u→v),但这与条件“v是距u最远的点”矛盾。

如果u不在最长路上,则u到其距最远点v的路与最长路一定有一交点c,且(c→v)与最长路的后半段重合(也能反证),即v一定是最长路的一端。

注:树还有一个性质,可以用于求直径。树的直径的长度一定会是某个点的最长距离与次长距离之和,所以求出max{最长距离 + 次长距离}就可以了,一个dfs就行。

其实可以再YY一下,其实一个点能够到达的最远节点一定是某条直径的端点,那么反过来可以利用直径求每个点到达其他节点的最远距离:先取任意一点,利用两遍BFS/DFS求直径的方法得到树的某条直径的两个端点,以这两个端点开始去遍历整棵树,两个端点到每个点的距离较大值就是这个点在树上能够到达的最远距离。

此题代码:

// MMSet2 运行/限制:1944ms/3000ms
#include <cstdio>
#include <cstring>
#include <cmath>
#include <iostream>
#include <algorithm>
#include <stack>
using namespace std;
const int N = 3e5 + 10;
struct e1 {
	int v, next;
}edge1[N<<1];
struct e2 {
	int v, w, next;
}edge2[N<<1];
int cnt1, cnt2;
int head1[N], head2[N];
int n;//树节点数量
int depth[N], parent[N][20];//节点深度,节点父亲;用于lca  使用parent[20][N],超时了,原因未知......
int tot,in[N],out[N];//dfs序
int t[N];//用于建虚树
int pos, ans;//求树的直径,第一遍遍历到的最远叶子节点;树的直径
void add1(int x, int y) {//原树加边
	cnt1++;
	edge1[cnt1].v = y;
	edge1[cnt1].next = head1[x];
	head1[x] = cnt1;
}
void add2(int x,int y,int dis){//虚树加边
	cnt2++;
	edge2[cnt2].v = y;
	edge2[cnt2].w = dis;
	edge2[cnt2].next = head2[x];
	head2[x] = cnt2;
}
void dfs(int x, int fa) {
	tot++;
	in[x] = tot;
	for (int i = head1[x]; i; i = edge1[i].next) {
		int y = edge1[i].v;
		if (y != fa) {
			depth[y] = depth[x] + 1;
			parent[y][0] = x;
			dfs(y, x);
		}
	}
	out[x] = tot;
}
void init() {//lca算法预处理
	tot = 0;//lca预处理同时标记dfs序
	memset(depth, 0, sizeof(depth));
	memset(parent, 0, sizeof(parent));
	dfs(1, 0);
	int k = (int)floor(log(n) / log(2.0));
	for (int i = 0; i + 1 <= k; i++) {
		for (int j = 1; j <= n; j++) {
			if (parent[j][i] == 0) parent[j][i + 1] = 0;
			else  parent[j][i + 1] = parent[parent[j][i]][i];
		}
	}
}
int lca(int x, int y) {//lca算法
	if (depth[x] < depth[y]) {
		swap(x, y);
	}
	int k = (int)floor(log(n) / log(2.0));
	for (int i = 0; i <= k; i++) {
		if ((depth[x] - depth[y]) >> i & 1) {
			x = parent[x][i];
		}
	}
	if (x == y) {
		return x;
	}
	for (int i = k; i >= 0; i--) {
		if (parent[x][i] != parent[y][i]) {
			x = parent[x][i];
			y = parent[y][i];
		}
	}
	return parent[x][0];
}
bool cmp(int x, int y) {
	return in[x] < in[y];
}
int build(int s) {//创建虚树
	cnt2 = 0;
	sort(t, t + s, cmp);
	int upp = s;
	for (int i = 1; i < upp; i++) {
		t[s++] = lca(t[i], t[i - 1]);
	}
	sort(t, t + s, cmp);
	s = unique(t, t + s) - t;//STL去重函数,去除相邻相同的元素只保留一个,所以要先保证序列有序
	stack<int> st;
	st.push(t[0]);
	for (int i = 1; i < s; i++) {
		int u = t[i];
		while (!st.empty() && !(in[st.top()] < in[u] && out[st.top()] >= in[u])) {//条件说明st.top()必须为u的父亲节点(虚树而言),否则出栈
			st.pop();
		}
		int v = st.top();
		add2(u, v, depth[u] - depth[v]);
		add2(v, u, depth[u] - depth[v]);
		st.push(u);
	}
	return s;
}
void dfs1(int x, int fa, int val) {//求树的直径第一遍遍历,求出距离出发点的最远叶子节点
	if (val > ans) {
		ans = val;
		pos = x;
	}
	for (int i = head2[x]; i; i = edge2[i].next) {
		int y = edge2[i].v;
		if (y != fa) {
			dfs1(y, x, val + edge2[i].w);
		}
	}
}
void dfs2(int x, int fa, int val) {//求树的直径第二遍遍历,从找到的叶子节点出发,搜索距离其最远的叶子节点
	ans = max(ans, val);
	for (int i = head2[x]; i; i = edge2[i].next) {
		int y = edge2[i].v;
		if (y != fa) {
			dfs2(y, x, val + edge2[i].w);
		}
	}
}
int main(){
	int q, s;
	int a, b;
	scanf("%d", &n);
	cnt1 = 0;
	memset(head1, 0, sizeof(head1));
	memset(head2, 0, sizeof(head2));
	for (int i = 1; i < n; i++) {
		scanf("%d%d", &a, &b);
		add1(a, b);
		add1(b, a);
	}
	init();//lca算法预处理
	scanf("%d", &q);
	while (q--) {
		scanf("%d", &s);
		for (int i = 0; i < s; i++) {
			scanf("%d", &t[i]);
		}
		s = build(s);//创建虚树
		//求树的直径
		ans = 0;
		dfs1(t[0], 0, 0);
		ans = 0;
		dfs2(pos, 0, 0);
		printf("%d\n", (ans + 1) / 2);
		for (int i = 0; i < s; i++) {//如果每次都memset(head2,0,sizeof(head2))一次,超时
			head2[t[i]] = 0;
		}
	}
    return 0;
}

遇到的问题:

lca算法,之前一直写为fa[k][N]形式(此题为fa[20][N]),没出现啥问题,而且个人感觉除了相应代码书写不同,效率差异不大,但此题会导致一直超时,改为fa[N][20]写法才过,具体啥原因还没想到......




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值