题目大意:给你一个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;
}