HDU6604系列题解

本文介绍了如何利用支配树和最近公共祖先的概念解决一道有向无环图(DAG)的题目。在给定的DAG中,需要找到在每次询问时删除一个点,使得特定的两点与所有出度为0的点不连通的方案数。通过构造新的支配树,计算单个点不连通的方案,并用容斥原理处理两种不连通情况的交集,最终实现O((m + q) logn)的时间复杂度解题方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目链接

题目大意:给你一个n个点,m条边的有向无环图,q次询问。每次询问给出两个点x和y,要求在图上删掉一个点及这个点连的所有边,使得x和y中至少有一个点和所有出度为0的点不连通,问有多少种删法。n ≤ \le 1e5,m ≤ \le 2e5,q ≤ \le 1e5。

难度:Ag

分析:

看到出度为0、不连通这种关键字,就应该想到支配树。

先说一下什么是支配树,一个有向图的支配树,首先是这个图的树形图,然后满足一个性质:如果在图上删去一个点及这个点连的所有边,那么这个点在支配树上的所有后代,都和树形图的根节点在原图中不连通。

假设我们已经会求一个图的支配树,考虑如何解这道题。题目要求x和y与所有出度为0的点不连通,所以支配树的根节点应该和这些点有关。因为x和y每次询问都会变,所以支配树的根节点不会是x或y。考虑出度为0的点,由于题目要求与所有出度为0的点不连通,所以支配树的根节点也不能是某个出度为0的点,我们需要一种方法,使得所有出度为0的点都近似成为支配树的根节点。

为了达到上述目的,我们选择新建一个点p,然后所有出度为0的点向p连一条边,这样p就可以作为支配树的根节点,和p不连通,就意味着和原图中所有出度为0的点不连通。最后,由于支配树必须是一个树形图,我们发现还需要使原图中所有边方向变反,才能使得树形图存在。

建立支配树后,先要会计算使x不连通的方案数。删去图上某个点之后,这个点在支配树上的所有后代都和支配树根节点p不连通。所以需要计算有多少个点在支配树中的后代节点包括x,显然这个问题的答案是x在支配树中的祖先个数,也就等于x在支配树中的深度。

既然会计算单独使x不连通的方案数和单独使y不连通的方案数,计算x或y不连通的方案数,只需要容斥一下即可。考虑单独使x不连通的方案和单独使y不连通的方案中,有哪些是重复的?显然,x和y的所有公共祖先被重复计算了,只要减掉x和y的公共祖先数量即可,这个数量等于x和y的最近公共祖先的深度。

最后,只剩下一个问题,如何求一个图的支配树?由于这道题保证给出的图为DAG,所以只需要会求DAG的支配树即可,方法如下:首先把根节点加入支配树(根节点的入度一定为0,否则不可能产生树形图),然后按照拓扑顺序依次将DAG上每个点加入支配树,某个点x在支配树上的父亲,就是原图上连了至少一条指向x的边的所有点,它们的最近公共祖先。至此,问题解决,时间复杂度O((m + q) logn),n为点数,m为边数,q为询问数。

代码:

# include <bits/stdc++.h>
# define MAXN 100005
# define MAXM 200005

using namespace std;

struct Tree
{
	int n, m;

	int to[MAXN << 1];	//链式前向星存树
	int next[MAXN << 1];
	int head[MAXN];

	int depth[MAXN];	//记录每个点的深度
	int fa[MAXN][20];	//倍增求lca需要预处理的信息
	int lg2[MAXN];

	void build(int size)
	{
		n = size;
		m = 0;
		memset(head, 0, sizeof(int) * (n + 1));
		depth[n + 1] = 1;	//将n+1号点视作支配树的根节点
		for (int i = 1; i <= n; ++i)
			memset(fa[i], 0, sizeof(fa[i]));
		for (int i = 2; i <= n; ++i)
			lg2[i] = (1 << (lg2[i - 1] + 1)) == i ? lg2[i - 1] + 1 : lg2[i - 1];
	}

	void _add(int u, int v)
	{
		to[++m] = v;
		next[m] = head[u];
		head[u] = m;
	}

	void add(int u, int v)
	{
		_add(u, v);
		_add(v, u);
		depth[v] = depth[u] + 1;
		fa[v][0] = u;
		for (int i = 1; (1 << i) <= depth[v]; ++i)
			fa[v][i] = fa[fa[v][i - 1]][i - 1];
	}

	int lca(int x, int y)
	{
		if (depth[x] < depth[y])
			swap(x, y);
		while (depth[x] > depth[y])
			x = fa[x][lg2[depth[x] - depth[y]]];
		if (x == y)
			return x;
		for (int i = lg2[depth[x]]; i >= 0; --i)
			if (fa[x][i] != fa[y][i])
			{
				x = fa[x][i];
				y = fa[y][i];
			}
		return fa[x][0];
	}

	int query(int x, int y)
	{
		int z = lca(x, y);
		return depth[x] + depth[y] - depth[z] - 1;	//注意这里要-1,原因是支配树上有一个虚拟的根节点,每次求深度都多算了虚拟的根节点
	}
};

struct Graph
{
	int n, m;

	int to[2][MAXM];	//两个链式前向星,一个存图,一个存该图的反图
	int next[2][MAXM];
	int head[2][MAXN];
	int indeg[MAXN];	//记录每个点的入度

	Tree tr;	//该图对应的支配树
	queue<int> q;	//拓扑排序用

	void build(int size)
	{
		n = size;
		m = 0;
		for (int i = 0; i < 2; ++i)
			memset(head[i], 0, sizeof(int) * (n + 1));
		tr.build(n);
	}

	void add(int u, int v)
	{
		to[0][++m] = v;
		next[0][m] = head[0][u];
		head[0][u] = m;
		to[1][m] = u;
		next[1][m] = head[1][v];
		head[1][v] = m;
		++indeg[v];
	}

	void insert(int x)
	{
		int lca = to[1][head[1][x]];
		for (int i = head[1][x]; i; i = next[1][i])	//计算至少连了一条指向x的边的所有点,它们的lca
			lca = tr.lca(to[1][i], lca);
		tr.add(lca, x);
	}

	void topo_sort()
	{
		for (int i = 1; i <= n; ++i)
			if (indeg[i] == 0)
			{
				tr.add(n + 1, i);	//入度为0的点的父亲是虚拟的根节点
				q.push(i);
			}
		while (!q.empty())
		{
			int cur = q.front();
			q.pop();
			for (int i = head[0][cur]; i; i = next[0][i])
				if (--indeg[to[0][i]] == 0)
				{
					insert(to[0][i]);
					q.push(to[0][i]);
				}
		}
	}

	int query(int x, int y)
	{
		return tr.query(x, y);
	}
};

Graph g;

int main()
{
	int t;
	scanf("%d", &t);
	int n, m, q;
	while (t--)
	{
		scanf("%d %d", &n, &m);
		g.build(n);
		int u, v;
		for (int i = 0; i < m; ++i)
		{
			scanf("%d %d", &u, &v);
			g.add(v, u);
		}
		g.topo_sort();
		scanf("%d", &q);
		while (q--)
		{
			scanf("%d %d", &u, &v);
			printf("%d\n", g.query(u, v));
		}
	}

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值