Loading

【题解】P4103 - [HEOI2014] 大工程

题目大意

题目链接

给定一个大小为 \(n\) 的无根树以及 \(q\) 个询问,第 \(i\) 次询问给出 \(k_i\) 个结点。处理第 \(i\) 次询问时在给出的 \(k_i\) 个结点间两两连边,共连出 \(C_k^2\) 条边。已知每次询问互不影响,在结点 \(u, v\) 之间连边的代价为原树上 \(u, v\) 间的路径长度。对于每次询问,试求出代价和、最小代价、最大代价。

\(1 \leq n \leq 1000000, q \leq 50000, \sum\limits_{i = 1}^q k_i \leq 2 \times n\)

解题思路

观察数据范围发现给出的询问点数量较少,首先考虑 虚树。确定是虚树后,我们考虑先做出普通的树形 \(dp\) 做法,再推广到虚树上。

题面提到要连出 \(C_k^2\) 条边,如果我们暴力枚举这些边的边权再维护信息是行不通的。如果我们考虑建出一定包含结点 \(1\) 的虚树后先处理结点 \(1\) 的信息,再递推出其他结点的信息也是行不通的,这样做很难转移最值。因此,为了保证复杂度,我们一定仅维护每条边一次,通过维护这条边的出现次数来统计答案。

我们发现代价和可以通过 路径长度 \(\times\) 路径被统计的次数 来维护。考虑对于一条树边 \((u, v)\),设 \(size_i\) 为结点 \(i\) 的子树内含有的询问点数量,\(dep_i\) 为以结点 \(1\) 为根结点时结点 \(i\) 的深度。显然从 \(u\)\(v\) 的路径长度为 \(dep_v - dep_u\)。假设某条路径起点和终点都是询问点,若需要经过边 \((u, v)\),那么这条路径的起点一定在 \(v\) 的子树内,终点一定在 \(v\) 的子树外,所以经过树边 \((u, v)\) 的询问点对路径数量为 \(size_v \times (k_i - size_v)\)。这条树边对于答案的贡献即为 \((dep_v - dep_u) \times size_v \times (k_i - size_v)\)

因为上面我们计算了树边在路径贡献中的出现次数,所以如果它没有贡献,则出现为 \(0\),一定不会更新答案。否则加上的权值就是它对答案的所有贡献。我们这样做相当于找出所有询问点之间的路径,将它们拆分成若干条树边以后再合并相同的树边,将相同树边的贡献一起统计。维护且仅维护每条边一次,因此复杂度是正确的。

接下来考虑维护路径长度最值。我们发现如果要统计出任意结点 \(u\) 所贡献的路径长度最值很难维护,因此我们 不能维护包含结点 \(u\) 的整条路径,而是用类似于 \(LCA\) 的思想将一条完整的树上路径拆分成从起点到 \(LCA\)、从 \(LCA\) 到终点两部分处理,最后再对于每一个 \(LCA\) 合并它们的后代贡献的两部分答案。

\(f_{u, 0}\) 表示结点 \(u\)子树内\(u\) 距离 最短 的询问点到 \(u\) 的距离,\(f_{u, 1}\) 表示结点 \(u\)子树内\(u\) 距离 最长 的询问点到 \(u\) 的距离。

对于 \(f_{u, 0}\),如果结点 \(u\) 本身为询问点,那么显然自己到自己的距离最短,为 \(0\),所以 \(f_{u, 0} = 0\)。反之,我们需要用 \(u\) 的子树信息来更新 \(f_{u, 0}\),所以 \(f_{u, 0} = \infty\)。显然 \(f_{u, 0} = \min(f_{v, 0} + 1), (u, v) \in E\)

\(f_{u, 1}\) 同理。如果结点 \(u\) 本身为询问点,那么初始只有自己时自己到自己的距离最长,为 \(0\),所以 \(f_{u, 1} = 0\)。反之为了维护最值,\(f_{u, 1} = -\infty\)。类似地 \(f_{u, 1} = \max(f_{v, 0} + 1), (u, v) \in E\)

考虑如何维护答案。显然,假如一条以结点 \(u\) 的后代为端点的边可以对路径长度最小值产生贡献,这条边的另一个端点必须在满足限制的前提下深度最小。对路径长度最大值产生贡献同理,这条边的另一个端点必须在满足限制的前提下深度最大。因此,对于一个结点 \(u\),它最有可能贡献最小值答案的边长度为 \(f_{v, 0} + (dep_v - dep_u) + f_{u, 0}\),最有可能贡献最大值答案的边长度为 \(f_{v, 1} + (dep_v - dep_u) + f_{u, 1}\)。用这两个值分别更新最大值和最小值即可。

上式的正确性在于代码细节的问题。代码执行的顺序应该是先统计答案、再更新信息,换言之,上式中 \(f_{v, 0}\) 还没有对 \(f_{u, 0}\) 产生贡献,也就是没有用 \(f_{v, 0}\) 更新过 \(f_{u, 0}\)。也就是说 \(f_{u, 0}\) 表示的实际是 \(u\) 的子树中 \(v\) 的子树外 距离 \(u\) 最近的询问点到 \(u\) 的距离,\(f_{u, 1}\) 同理。显然假如结点 \(u\) 有多棵子树内存在询问点,那么最有可能对答案产生贡献的边,其两端点一定位于 \(u\) 不同的子树,并且深度在可能更新最大值时最大,在可能更新最小值时最小。如果结点 \(u\) 仅有一棵子树,此时若 \(u\) 为询问点,那么贡献路径一端点为 \(u\),一端点为 \(u\) 的子树内深度最大的结点。反之,我们在找到下一个询问点的时候再更新答案。显然建完虚树以后会遍历所有的询问点,所以延迟更新答案也是正确的。

从上面的结论推广一下,\(u\) 是询问点的情况也显然合法。\(f_{u, 0}\)\(f_{u, 1}\) 的初值均为 \(0\),此时如果 \(u\) 对答案可能产生的贡献来自于不同的子树,那么无论先更新哪一棵子树,在更新另外一棵子树的时候也一定会统计对答案的贡献,换句话说就是 更新子树的顺序 以及 是否统计剩余所有子树 对于维护答案没有影响。如果 \(u\) 对答案可能产生的贡献来自同一棵子树,说明 \(u\) 一定只有一棵子树包含询问点,此时答案肯定在访问可能产生贡献的询问点端点时维护过,所以同样合法。其余的情况正确性显然,模拟若干数据即可。

最后把上面的树形 \(dp\) 做法在虚树上执行一遍,注意状态转移方程中的边权不再是 \(1\),而是虚树中父子结点在原树上的路径长度。

参考代码

#include <cstdio>
#include <algorithm>
using namespace std;

const int maxn = 1e6 + 5;
const int maxm = 2e6 + 5;
const int inf = 0x3f3f3f3f;

struct node
{
	int to, nxt;
} edge[maxm], Edge[maxm];

int n, q;
int cnt, top;
int mind, maxd;
int head[maxn], Head[maxn];
int minv[maxn], maxv[maxn];
int k[maxn], st[maxn], dfn[maxn];
int f[maxn][20], dep[maxn], size[maxn];
bool vis[maxn];
long long ans;

bool cmp(int a, int b)
{
	return dfn[a] < dfn[b];
}

void add_edge(int u, int v)
{
	cnt++;
	edge[cnt].to = v;
	edge[cnt].nxt = head[u];
	head[u] = cnt;
}

void add_Edge(int u, int v)
{
	cnt++;
	Edge[cnt].to = v;
	Edge[cnt].nxt = Head[u];
	Head[u] = cnt;
}

void dfs1(int u, int fa)
{
	dep[u] = dep[fa] + 1;
	dfn[u] = ++top;
	for (int i = head[u]; i; i = edge[i].nxt)
	{
		int v = edge[i].to;
		if (v != fa)
		{
			f[v][0] = u;
			dfs1(v, u);
		}
	}
}

void dfs2(int u, int fa, int x)
{
	size[u] = vis[u];
	if (vis[u])
		minv[u] = maxv[u] = 0;
	else
	{
		minv[u] = inf;
		maxv[u] = -inf;
	}
	for (int i = Head[u]; i; i = Edge[i].nxt)
	{
		int v = Edge[i].to, w = dep[v] - dep[u];
		if (v != fa)
		{
			dfs2(v, u, x);
			ans += 1LL * w * size[v] * (x - size[v]);
			if (size[u] > 0)
			{
				mind = min(mind, minv[v] + w + minv[u]);
				maxd = max(maxd, maxv[v] + w + maxv[u]);
			}
			minv[u] = min(minv[u], minv[v] + w);
			maxv[u] = max(maxv[u], maxv[v] + w);
			size[u] += size[v];
		}
	}
	Head[u] = 0;
	vis[u] = false;
}

int lca(int u, int v)
{
	int k = 0;
	if (dep[u] < dep[v])
		swap(u, v);
	while ((1 << (k + 1)) <= dep[u])
		k++;
	for (int i = k; i >= 0; i--)
		if (dep[f[u][i]] >= dep[v])
			u = f[u][i];
	if (u == v)
		return u;
	for (int i = k; i >= 0; i--)
	{
		if (f[u][i] != f[v][i])
		{
			u = f[u][i];
			v = f[v][i];
		}
	}
	return f[u][0];
}

int main()
{
	int u, v, x;
	scanf("%d", &n);
	for (int i = 1; i <= n - 1; i++)
	{
		scanf("%d%d", &u, &v);
		add_edge(u, v);
		add_edge(v, u);
	}
	dfs1(1, 0);
	for (int j = 1; (1 << j) <= n; j++)
		for (int i = 1; i <= n; i++)
			f[i][j] = f[f[i][j - 1]][j - 1];
	scanf("%d", &q);
	while (q--)
	{
		cnt = top = ans = 0;
		mind = inf, maxd = -inf;
		scanf("%d", &x);
		for (int i = 1; i <= x; i++)
		{
			scanf("%d", &k[i]);
			vis[k[i]] = true;
		}
		sort(k + 1, k + x + 1, cmp);
		st[++top] = k[1];
		for (int i = 2; i <= x; i++)
		{
			while (true)
			{
				u = lca(st[top], k[i]);
				if (dep[u] >= dep[st[top - 1]])
				{
					if (u != st[top])
					{
						add_Edge(u, st[top]);
						if (u != st[top - 1])
							st[top] = u;
						else
							top--;
					}
					break;
				}
				else
				{
					add_Edge(st[top - 1], st[top]);
					top--;
				}
			}
			st[++top] = k[i];
		}
		while (--top)
			add_Edge(st[top], st[top + 1]);
		dfs2(st[1], 0, x);
		printf("%lld %d %d\n", ans, mind, maxd);
	}
	return 0;
}
posted @ 2021-07-27 19:51  kymru  阅读(63)  评论(0编辑  收藏  举报