P2661 信息传递Exp.

· · 题解

已更新:update\:on\:2019.9.19

这篇文章说明了tarjan在这道题里的局限性,希望能让更多同学理解普适的最小环求法.

P2661 信息传递

最近刚学完tarjan算法,所以看到这道题的第一想法就是能不能用tarjan来做。试着画了一下,发现可行,

最小环的大小=最小强联通分量的边数=最小强联通分量的点数

然后花了三分钟就AC了。

#include<bits/stdc++.h>
#define maxn 200010
using namespace std;
int n,ans=0x7f7f7f;
vector<int >g[maxn];
int deep,top,dfn[maxn],low[maxn],color[maxn],stac[maxn],sum;
bool vis[maxn];
void tarjan(int u){
    deep+=1;
    dfn[u]=deep;
    low[u]=deep;
    top+=1;
    stac[top]=u;
    vis[u]=true;
    for(int i=0;i<g[u].size();i++){
        int v=g[u][i];
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[v],low[u]);
        }
        else{
            if(vis[v]){
                low[u]=min(low[v],low[u]);
            }
        }
    }
    if(dfn[u]==low[u]){
        sum+=1;
        vis[u]=false;
        color[sum]++;
        while(stac[top]!=u){
            top--;
            vis[stac[top]]=false;
            color[sum]++;
        }
        top--;
    }
}
int main(){
    scanf("%d",&n);
    int x;
    for(int i=1;i<=n;i++){
        scanf("%d",&x);
        g[i].push_back(x);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])tarjan(i);
    for(int i=1;i<=sum;i++)
        if(color[i]>1)ans=min(ans,color[i]);//除自环或单个元素 
    printf("%d\n",ans);
    return 0;
}

但是!

如此做完之后,我仔细一想,又觉得不是很妙,在纸上比划几下,竟然把这种算法hack掉了!

如图,这是一个强联通分量,但是最小环是3而不是tarjan的6.

不由得惊出了一身冷汗,难道题目如此之水,连这种错误的算法都不能查出来吗?

赶忙回去看题,发现了这样一行:

注意:可能有人可以从若干人那里获取信息, 但是每人只会把信息告诉一个人,即自己的信息传递对象

怪不得我的垃圾tarjan能过呢……

那么,既然不是所有的最小环都能够用tarjan算法,并查集怎么样呢?

我们先按上图构造一组数据:

6 7
1 2
2 3
3 4
4 1
2 6
6 5
5 2

然后对代码的读入稍作修改,使其可以读入这组数据。

运行tarjan,得到了如下结果:

显然不正确。

我在这里借用前面一位大佬的代码(对不起!)来运行,结果如下:

正确!

为什么呢?我们研究一下,这两份代码有什么不同。

#include<bits/stdc++.h>
#define maxn 200010
using namespace std;
int n,m,fa[maxn],ans=0x3f3f3f3f;
int get(int x,int &cnt){//由于cnt要修改,所以得传参 
    cnt++;
    if(fa[x]==x)return x;
    else return get(fa[x],cnt); 
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)fa[i]=i;
    for(int i=1;i<=m;i++){
        int cnt=0,f,e;
        scanf("%d%d",&f,&e);
        if(get(e,cnt)==f){//这里最后也没有连成环
            ans=min(ans,cnt);
        }else fa[f]=e;//f的父亲节点前进一格,
    }
    printf("%d\n",ans);
    return 0;
}

实际上duoluoluo巨佬已经说得很清楚了,

我们在连接一个点到另一个点之前,先用并查集判断是否构成一个环,如果是的话,我们就可以记录下这个答案,然后维护最小的答案。

那么,如果构成一个环的话,怎么记录它的长度呢?

我们可以先定义一个变量cnt,在并查集获取祖先的函数中使cnt的值加1,最后函数结束时就能得到这个环的长度了qwq!

同时,如果构成了一个环,就不需要把这个环的结尾接上,否则会陷入死循环!!

由此可见,并查集算法是动态更新的,而tarjan是静态更新的,这就是为什么tarjan在执行时会出错了。

综上所述,虽然在这道题里tarjan算法能够AC,但是它并不能普遍适用。学会并查集的求最小环方法才是硬道理。

最后贴一下测试以上样例的代码

update\:on\:2019.9.19

后来发现实际上并查集的做法也不能普遍适用.

注意到这里的所有边权都为1,所以并查集算法才成立.

如果是带权图,并查集和tarjan算法都不适用,这时候就要用到另外的算法.

无向图上据我所知比较方便的是floyd求最小环(同样在有向图上适用,但是需要修改一下转移方程,因为无向图最小环至少经过三个节点).

有向图上求最小环可以使用dijkstra算法的变体. 操作如下:

枚举每一个点作为起点,在松弛完起点所有的出边后把其dis重置为infvis重置为false,之后再扫到该起点时该起点的dis为最小环. 可行性由dijkstra的贪心易证.

堆优化后时间复杂度为O(n(n+m)\:log\:n),比朴素的想法枚举边然后断边求最短路O(m(n+m)\:log\:n)要优.