999E. Reachability from the Capital
time limit per test: 2 seconds
memory limit per test: 256 megabytes
input: standard input
output: standard output
URL
http://codeforces.com/contest/999/problem/E
Introduction
基本上裸的 Tarjan 缩点题。有向图的顶点从 1 开始连续编号。题目大意是在原图中选定一顶点 v0 ,缩点后记其位于 v1 点,求除 v1 外其他缩点中入度为 0 的个数。
Input
Codeforces老规矩,一组输入。第一行第一个数是顶点数 V,第二个数是边数 E,第三个数是选定的顶点 v0,接下来 E 行每行两个数 u v,给出有向边 u->v。
Output
记 v0 缩点后位于 v1 点,输出除 v1 外其他缩点中入度为 0 的个数。
Sample Input
9 9 1
1 2
1 3
2 3
1 5
5 6
6 1
1 8
9 8
7 1
Sample Output
3
Analysis & AC Code
本题大致可以按以下步骤来写:
-
1. Tarjan,求出各个强连通分量
-
2. 处理强连通分量
-
3. 在除 v1 外的缩点中寻找入度为 0 的点的个数
下面按步骤分析:
1. Tarjan,求出各个强连通分量
首先给出 Tarjan 模板:
int belong[MV], low[MV], dfn[MV], dnt;
int sta[MV], top;
void tarjan(const int u)
{
// 注意,栈中可能同时存在多个SCC,但每个SCC必然是连续的。如果不连续,说明SCC中的某个点连向了SCC外的一个点w,然后再连连连又连回了SCC,说明w必可达SCC中任何一点,且SCC中任何一点也可达w,那么w必定也在SCC里,推出矛盾,故栈中SCC结点是连续分布的
sta[top++] = u;
dfn[u] = low[u] = ++dnt;
FED(u)
{
const int v = p->v;
if (!dfn[v]) // v第一次访问,所以需要求出它的low然后再讨论。若low[v]>=low[u],则他们不可能在同一个SCC(否则v可以走到u甚至u的祖先,这样low[v]就<low[u]了),就不可更新low[u]因为low数组的定义就是同一个SCC中dfn最小值;
{ // 若low[v]<low[u],则他们一定在同一个SCC且这个SCC中最小的dfn就是low[v],所以就得把low[v]直接赋给low[u]
tarjan(v);
if (low[u] > low[v])
low[u] = low[v];
}
// v不是第一次访问(v访问比u早),那么v是别的SCC中的点(这种情况就不用管v了),或是v的返祖出点(等价于 v是u的祖先且v又是u的出点)(等价于 u->v这条边是返祖边)
// 若此时有belong[v]==-1,则说明v不是别的SCC中的点,所以它是v的返祖出点,所以它们必在同一个SCC中
else if (belong[v]==-1) // v和u在同一个SCC中且先访问到的v,所以拿v的dfs序号更新low[u]。注意不能直接赋值,因为当前的v可能只是u的某一个返祖出点,而low[u]应该是所有返祖出点中的dfn最小者,所以要动态更新这个域而不是遇到一个返祖出点就直接赋值。
{
if (low[u] > dfn[v])
low[u] = dfn[v];
}
// 注意到有重边时,如果v是其他SCC中的点,那访问两次完全没影响;如果是和u在同一个SCC,那弹栈必然在u或者u的祖先才弹,不会在v弹(因为v的dfn不可能等于low,否则v就是SCC中的最高点了)所以不会出现弹栈错乱;同时由于递归v时没弹栈所以belong[v]仍然是-1,所以还是会进这个if,不过重复更新也不会有不好的后果,所以无所谓
// 所以tarjan面对重边没有任何问题
// 而如果有自环,则会尝试拿自己的dfn更新自己的low(由于第一次访问后dfn就有值了,所以一定不会进第一个if),这也是完全没问题的。
}
// u的所有后继结点全部访问完毕,如果dfn[u] == low[u]说明u就是它所在SCC中最先访问的那个,就该pop了;否则不用管,直接返回上一层
// 又因为栈中结点是按SCC连续排布的(同一个SCC的点在栈中紧挨),所以对于不和u在同一个SCC的那些点,他们已经在之前pop了,所以现在从栈顶一直到u结点的位置都是u所在SCC中的点,所以不断pop即可
if (dfn[u] == low[u])
{
while (top--)
{
belong[sta[top]] = u;
// do something
if (sta[top] == u)
break;
}
}
}
对于本题,我们要对所有点都做 Tarjan 。在 Tarjan 每次进入后面那个 while(1) 的时候就是在处理强连通分量了。那么 do something 我们要做什么呢?肯定是要缩点的。
2. 处理强连通分量
由于我们后续只需要求缩点后入度不为 0 的点的个数,因此我们这里不必把缩点后的图真的建出来,只需要用一种方法让我们在后面的计算中知道这个顶点在缩点后缩到哪去了就行。考虑到这一点,不妨用并查集来处理。
这是并查集的模板:
int uf[M];
void initUf(void)
{
memset(uf, -1, sizeof(uf));
}
int find(int x)
{
if (uf[x]>=0)
return uf[x] = find(uf[x]);
return x;
}
void merge(int x, int y)
{
int r1 = find(x);
int r2 = find(y);
if (r1<r2)
{
uf[r1] += uf[r2];
uf[r2] = r1;
}
else if(r2<r1)
{
uf[r2] += uf[r1];
uf[r1] = r2;
}
}
这样的话,我们就可以把第一步 Tarjan 里面的 do something 写成
merge(now, u);
这样的话我们在之后的计算中调用 UF::find(v) 就可以知道缩点后 v 缩到哪去了。并且对于之前的每条边,只要它的起点和终点不在同一个并查集集合中,这条边就是连接两个缩点的有向边。这样就不用真正建出缩点后的图。
3. 在除 v1 外的缩点中寻找入度为 0 的点的个数
要求入度为 0 的缩点的个数,我们需要两步:
-
1. 遍历边求入度
我们需要遍历所有的边来求一下每个缩点的入度。如果原图中某条边的起点和终点不在同一个强连通分量中(在并查集的不同集合中),那么这条边在缩点后就仍然存在。每遍历到一条缩点后仍然存在的边,它的终点的入度就++。注意它的终点是缩点后的点。下面是遍历边的代码。
for (e=0; e<E; ++e)
{
tp = find(Es[e].v);
if (find(Es[e].u) != tp)
indegree[tp]++;
}
-
2. 遍历缩点求入度为 0 的个数
最后后我们需要遍历所有的缩点,然后记录那些入度为 0 的即可。
如何得知缩点编号呢?并查集中的根结点也就是缩点。因此 uf[v] < 0 时表明这个 v 是缩点后留下的点
最后别忘了那个选定点!!!!如果那个选定点所在的缩点的入度为 0 ,是不要计入最终答案的 !!
for (u=1; u<=V; ++u)
if (uf[u]<0 && !indegree[u])
ccnt++;
if (!indegree[find(src)])
ccnt--;
至此,本题就解决了。我们再回顾一下本题的解题过程:
-
1. Tarjan,求出各个强连通分量
-
2. 处理强连通分量
-
①. 运用并查集,将每个强连通分量缩为一点。
-
②. 并没有真正求出的缩点后的图 ,而是得到一些集合(每个强连通分量都是一个独立的集合)
-
-
2. 在除 v1 外的缩点中寻找入度为 0 的点的个数
-
①. 遍历边求缩点入度。
-
②. 遍历缩点求入度为 0 的个数(别忘了那个选定点)
-
最后贴出完整 AC 代码:
/** CodeForces 999E Tarjan + 并查集
* by Kevin.
*/
#include <iostream>
#include <cstring>
#define M 5003
int V, E, g[M][M];
int cnt, stack[M], top;
char vis[M], ins[M];
int indegree[M];
int dfn[M], low[M];
int uf[M];
struct Edge
{
int u, v;
Edge(void) { }
Edge(int uu, int vv) : u(uu), v(vv) { }
} Es[M];
void initUf(void)
{
memset(uf, -1, sizeof(uf));
}
int find(int x)
{
if (uf[x]>=0)
return uf[x] = find(uf[x]);
return x;
}
void merge(int x, int y)
{
int r1 = find(x);
int r2 = find(y);
if (r1<r2)
{
uf[r1] += uf[r2];
uf[r2] = r1;
}
else if(r2<r1)
{
uf[r2] += uf[r1];
uf[r1] = r2;
}
}
void Tarjan(int u)
{
vis[u] = ins[u] = 1;
dfn[u] = low[u] = ++cnt;
stack[top++] = u;
for (int v=1; v<=V; ++v)
{
if (g[u][v])
{
if (!vis[v])
{
Tarjan(v);
if (low[u] > low[v])
low[u] = low[v];
}
else if (ins[v])
{
if (low[u] > dfn[v])
low[u] = dfn[v];
}
}
}
if (dfn[u] == low[u])
{
int now;
while (1)
{
now = stack[--top];
ins[now] = 0;
if (now==u) break;
merge(now, u);
}
}
}
int main()
{
int u, v, e, ccnt, tp, src;
scanf("%d %d %d", &V, &E, &src);
for (e=0; e<E; ++e)
{
scanf("%d %d", &u, &v);
g[u][v] = 1;
Es[e] = Edge(u, v);
}
initUf();
ccnt = 0;
for (u=1; u<=V; ++u)
if (!vis[u])
Tarjan(u);
for (e=0; e<E; ++e)
{
tp = find(Es[e].v);
if (find(Es[e].u) != tp)
indegree[tp]++;
}
for (u=1; u<=V; ++u)
if (uf[u]<0 && !indegree[u])
ccnt++;
if (!indegree[find(src)])
ccnt--;
printf("%d\n", ccnt);
}
(ps : 以上 Tarjan 和并查集的模板都是博主自己手敲的,确保没有 bug ,有需要的小伙伴可以自行拿走 ~)