Problem:CODEVS 2822
爱在心中 ID:WZH
总耗时:4ms 总内存损耗:364KB
算法:tarjan缩点Floyd传递闭包;
CODEVS:http://codevs.cn/problem/2822/
题目描述 Description
“每个人都拥有一个梦,即使彼此不相同,能够与你分享,无论失败成功都会感动。爱因为在心中,平凡而不平庸,世界就像迷宫,却又让我们此刻相逢Our Home。”
在爱的国度里有N个人,在他们的心中都有着一个爱的名单,上面记载着他所爱的人(不会出现自爱的情况)。爱是具有传递性的,即如果A爱B,B爱C,则A也爱C。
如果有这样一部分人,他们彼此都相爱,则他们就超越了一切的限制,用集体的爱化身成为一个爱心天使。
现在,我们想知道在这个爱的国度里会出现多少爱心天使。而且,如果某个爱心天使被其他所有人或爱心天使所爱则请输出这个爱心天使是由哪些人构成的,否则输出-1。
输入描述 Input Description
第1行,两个数N、M,代表爱的国度里有N个人,爱的关系有M条。
第2到第M+1行,每行两个数A、B,代表A爱B。
输出描述 Output Description
第1行,一个数,代表爱的国度里有多少爱心天使。
第2行,如果某个爱心天使被其他所有人和爱心天使所爱则请输出这个爱心天使是由哪些人构成的(从小到大排序),否则输出-1。
样例输入1:
6 7
1 2
2 3
3 2
4 2
4 5
5 6
6 4
样例输出1:
2
2 3
样例输入2:
3 3
1 2
2 1
2 3
样例输出2:
1
-1
数据范围:点大约有10000,边大约有100000
解答:
第一次看见这个题目被分类在spfa中,我不知道是谁分的。如果是spfa的话,那么我们的思路很简单:从每个点出发走一遍图的便利,时间复杂度大概是O(n2),绝对超时,反而不如用Floyd传递闭包。因为我要从每一个点出发走向每一个点。
Floyd算法在《算法竞赛入门经典第二版》P364的Floyd算法中有解释;
具体的思路参考Floyd的证明过程,不难发现因为本题中并没有权值,所以我们只用把点与点之间的联通性传递就可以了。时间复杂度O(n3)。
但是这个数据用Floyd依然会超时,具体会有多少分我没有尝试,因为太复杂了(懒)。
那么我们再来看一下题目。爱心天使,用专业的话来说就是强联通分量(SCC),那么我们就可以很自然地想到用Tarjan找出这个带环的森林中的环,然后把环缩成点,最后用floyd传递闭包,这样我们就可以优化程序中的一大半时间。因为一个SCC中向外的联通性是相同的,所以我们把一个SCC当作一个点来处理就可以了。这里不懂的同学可以画个图来模拟一下。
Tarjan的具体原理请看网上讲解的代码,说明一下我上面数组名的含义
vis:记录我这个点有没有被访问过,flag:就是记录的我两个点的联通性。
dfn:记录的是这个点在被前序遍历的过程中的编号
low:记录的是当前这个点所在SCC中的最小的dfn;
那么我们很容易得出当dfn等于low的时候这个点就是该SCC中最早出现的点。
这儿可能有两种情况:第一,当前点自己本身就是一个SCC,第二,当前是SCC中最早出现的点(祖宗)。
sccno记录的是当前点的scc编号(对应的是scc_cnt),(可能很多人觉得这一步没有必要,直接记录为low的值不就可以了吗?)(在缩点中有奇效)
num:记录的是当前scc中有多少个点。
tarjan的大致思路如下:这一段中首先标记当前这个点已经被遍历了,然后dfs_clock++的含义就是给每一个点编号,同时把这个点压到栈。现在栈中的所有的点都是当前这个强连通分量中的点。
然后对这个点的子节点进行遍历,如果子节点没有被访问过,那么就访问子节点。同时在DFS结束之后对当前节点【不是子节点】的low值进行更新。【对应第一个IF语句】
如果这个子节点已经被访问过,但是并没有被划入别的强连通分量中,就代表这个点和当前节点在同一个强连通分量中。这个时候就对我这个节点的low的值进行更新,注意是把我当前节点的的low值和子节点的dfn值进行比较然后取较小的值进行更新。【对应第二个IF语句】
然后这个DFS之后所有的强连通分量中的点就已经全部在栈中间了,并且low的值都是当前强连通分量中最小的dfs_clock的值。
我们就要进行编号工作了。
当我当前节点的的dfn等于low值的时候,这个点已经是强连通分量中的最早点了。所以我对在栈中处于该点一上的点进行编号【并且划分进同一个强连通分量】。这个时候应该注意这个循环当该强联通分量的点全部弹出结束,而不是把栈中的元素全部弹出来。
这样我们整个Tarjan函数就结束了。下面我来贴代码
void tarjan(int u)
{
vis[u]=true;
dfn[u]=low[u]=++dfs_clock;
s.push(u);
for(node *p=head[u];p!=NULL;p=p->next)
{
if(!vis[p->v])
{
tarjan(p->v);
low[u]=min(low[p->v],low[u]);
}
else if(!sccno[p->v])
{
low[u]=min(low[u],dfn[p->v]);
}
}
if(low[u]==dfn[u])
{
scc_cnt++;
for(;;)
{
int x=s.top();s.pop();
sccno[x]=scc_cnt;
num[scc_cnt]++;
if(x==u) break;
}
if(num[scc_cnt]==1) k++;
}
}
然后我们就要开始缩点啦啦啦啦。
for(int i=1;i<=n;i++)
for(node *p=head[i];p!=NULL;p=p->next)
flag[sccno[p->u]][sccno[p->v]]=true;
我们重新的在一个新的邻接矩阵中间再一次建立一个图,这里选用邻接矩阵有两个原因第一:我们这个时候的数据范围已经很小了。邻接矩阵完全可以存下去。第二:邻接矩阵才可以Floyd传递闭包。
缩完点之后我们应该传递闭包来证明联通性,输出满足的那些点。
for(int k=1;k<=scc_cnt;k++)
for(int i=1;i<=scc_cnt;i++)
for(int j=1;j<=scc_cnt;j++)
if(flag[i][k]&&flag[k][j])
flag[i][j]=true;