zoukankan      html  css  js  c++  java
  • CF1137C Museums Tour(Tarjan,强连通分量)

    好题,神题。

    题目链接:CF原网 洛谷

    题目大意:

    一个国家有 $n$ 个城市,$m$ 条有向道路组成。在这个国家一个星期有 $d$ 天,每个城市有一个博物馆。

    有个旅行团在城市 $1$ 出发,当天是星期一。每天早上,如果这个城市的博物馆开了,那么可以去这个博物馆参观。每天晚上,旅行团可以选择沿一条出边前往下一个城市,或者结束旅行。一个城市可以经过多次。

    请问旅行团最多能参观多少个博物馆。一个博物馆参观了多次,只计算一次。

    $1le n,mle 10^5,1le dle 50$。


    根据题解,先说做法:

    首先拆成 $n imes d$ 个点,点 $(i,j)$ 表示当前在城市 $i$,是星期 $j$。对于原图的边 $u ightarrow v$,连边 $(u,i) ightarrow (v,imod d+1)$。

    对这个图求强连通分量,统计每个强连通分量里面有多少个不同的博物馆。

    在缩点后的图中,找一条从包含 $1$ 的点开始的最长链(点权是不同博物馆个数),即为答案。

    为什么呢?


    首先当我们进入一个强连通分量时,肯定可以把里面所有博物馆游览完。然后也一定可以走到下一个强连通分量。

    然后为什么答案不会被重复计算呢?不会出现博物馆 $u$ 在这条链里出现多次的情况吗?

    我们发现,如果从 $(u,i)$ 能走到 $(u,j)$,那么从 $(u,j)$ 也能走到 $(u,i)$。

    因为这代表在原图上可以走一条长 $(j-i)mod d$ 的链从 $u$ 走回到 $u$。现在是星期 $j$ 时,只需要走这条链 $d-1$ 次就能到星期 $i$。

    那么如果从 $(u,i)$ 能走到 $(u,j)$,两点一定属于一个强连通分量。在统计强连通分量内的答案时可以判掉。于是便不可能有重复统计的情况。

    那么这题就做完了……吗?


    如果用DFS式的tarjan,那么递归深度可以到 $n imes d$ 即 $5 imes 10^6$,爆栈无疑。

    虽然可以用pragma指令或者各种其他方式开栈内存,但……假如这是OI考场上呢?

    那么就需要用手写栈模拟tarjan过程。具体写着会比较难受,我在代码里写好了注释。

    当然也可能是我写复杂了,我的代码真的巨慢无比

    时间复杂度 $O((n+m) imes d)$。

    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int maxn=5000500;
    #define FOR(i,a,b) for(int i=(a);i<=(b);i++)
    #define ROF(i,a,b) for(int i=(a);i>=(b);i--)
    #define MEM(x,v) memset(x,v,sizeof(x))
    inline int read(){
        char ch=getchar();int x=0,f=0;
        while(ch<'0' || ch>'9') f|=ch=='-',ch=getchar();
        while(ch>='0' && ch<='9') x=x*10+ch-'0',ch=getchar();
        return f?-x:x;
    }
    inline ll getc(){
        char ch=getchar();
        while(ch<'0' || ch>'1') ch=getchar();
        return ch-'0';
    }
    int n,m,d,head[maxn],el,to[maxn],nxt[maxn],head2[maxn],el2,to2[maxn],nxt2[maxn];
    int dfn[maxn],low[maxn],dfs_clock,stk[maxn],tp,id[maxn],scc,cnt[maxn],tmp[maxn],tl,dp[maxn];
    int recstk[maxn],rectp,cur[maxn];
    ll vld[100010];
    bool vis[maxn],ins[maxn],fst[maxn],was[maxn];
    inline void add(int u,int v){
        to[++el]=v;nxt[el]=head[u];head[u]=el;
    } 
    inline void add2(int u,int v){
        to2[++el2]=v;nxt2[el2]=head2[u];head2[u]=el2;
    }
    void dfs(int s){
        recstk[rectp=1]=s;    //recstk模拟系统函数栈 
        cur[s]=head[s];    //cur[u]表示当前u的边遍历到哪一条 
        while(rectp){
            int u=recstk[rectp];
            if(fst[u]){    //fst[u]表示不是第一次入栈(那么就遍历过边) 
                if(was[u]) low[u]=min(low[u],low[to[cur[u]]]),was[u]=false;
                //was[u]表示u上一条遍历到的边是否满足!dfn[v]
                //此时就要low[u]=min(low[u],low[v])
                //不过由于模拟栈中不能方便回溯,就这么写了 
                cur[u]=nxt[cur[u]];    //向后推一条边 
            }
            else{    //u第一次进栈 
                fst[u]=true;
                dfn[u]=low[u]=++dfs_clock;
                ins[u]=true;
                stk[++tp]=u;
            }
            if(!cur[u]){    //边遍历完了 
                if(low[u]==dfn[u]){
                    scc++;tl=0;    //强连通编号++ 
                    do{
                        ins[stk[tp]]=false;
                        id[stk[tp]]=scc;
                        int x=(stk[tp]+d-1)/d,y=stk[tp]%d?stk[tp]%d:d;
                        //这个点是(x,y) 
                        if(!((vld[x]>>y)&1)) continue;    //(x,y)博物馆不开,不管 
                        tmp[++tl]=x;
                        if(!vis[x]) vis[x]=true,cnt[scc]++;    //如果博物馆x之前没访问过,那么新计入答案 
                    }while(stk[tp--]!=u);
                    FOR(i,1,tl) vis[tmp[i]]=0;    //vis清空 
                }
                rectp--;    //u退栈(相当于回溯) 
            }
            else{
                int i=cur[u],v=to[i];
                if(!dfn[v]){
                    recstk[++rectp]=v;    //v进栈(相当于递归) 
                    cur[v]=head[v];    //v当前遍历的边是head[v] 
                    was[u]=true;    //u这条边满足!dfn[v] 
                }
                else if(ins[v]) low[u]=min(low[u],dfn[v]);
            }
        }
    }
    int main(){
        n=read();m=read();d=read();
        FOR(i,1,m){
            int u=read(),v=read();
            FOR(j,1,d) add((u-1)*d+j,(v-1)*d+j%d+1);
        }
        FOR(i,1,n) FOR(j,1,d) vld[i]|=getc()<<j;    //干脆压下空间……(一开始以为会MLE) 
        FOR(i,1,n*d) if(!dfn[i]) dfs(i);
        FOR(u,1,n*d) for(int i=head[u];i;i=nxt[i]){
            int v=to[i];
            if(id[u]!=id[v]) add2(id[u],id[v]);    //新图连边 
        }
        //根据tarjan的性质, 强连通编号为反向的拓扑序,所以可以从1到n直接遍历一遍 
        FOR(u,1,scc){
            for(int i=head2[u];i;i=nxt2[i]) dp[u]=max(dp[u],dp[to2[i]]);
            dp[u]+=cnt[u];
        }
        printf("%d
    ",dp[id[1]]); 
    }
    View Code

     upd:貌似可以把tarjan变成inline就没有栈的问题了……

    (代码好写N倍……)

    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int maxn=5000500;
    #define FOR(i,a,b) for(int i=(a);i<=(b);i++)
    #define ROF(i,a,b) for(int i=(a);i>=(b);i--)
    #define MEM(x,v) memset(x,v,sizeof(x))
    inline int read(){
        char ch=getchar();int x=0,f=0;
        while(ch<'0' || ch>'9') f|=ch=='-',ch=getchar();
        while(ch>='0' && ch<='9') x=x*10+ch-'0',ch=getchar();
        return f?-x:x;
    }
    inline ll getc(){
        char ch=getchar();
        while(ch<'0' || ch>'1') ch=getchar();
        return ch-'0';
    }
    int n,m,d,head[maxn],el,to[maxn],nxt[maxn],head2[maxn],el2,to2[maxn],nxt2[maxn];
    int dfn[maxn],low[maxn],dfs_clock,stk[maxn],tp,id[maxn],scc,cnt[maxn],tmp[maxn],tl,dp[maxn];
    ll vld[100010];
    bool vis[maxn],ins[maxn];
    inline void add(int u,int v){
        to[++el]=v;nxt[el]=head[u];head[u]=el;
    } 
    inline void add2(int u,int v){
        to2[++el2]=v;nxt2[el2]=head2[u];head2[u]=el2;
    }
    inline void dfs(int u){
        dfn[u]=low[u]=++dfs_clock;
        ins[stk[++tp]=u]=true;
        for(int i=head[u];i;i=nxt[i]){
            int v=to[i];
            if(!dfn[v]) dfs(v),low[u]=min(low[u],low[v]);
            else if(ins[v]) low[u]=min(low[u],dfn[v]);
        }
        if(dfn[u]==low[u]){
            scc++;tl=0;    //强连通编号++ 
            do{
                ins[stk[tp]]=false;
                id[stk[tp]]=scc;
                int x=(stk[tp]+d-1)/d,y=stk[tp]%d?stk[tp]%d:d;
                //这个点是(x,y) 
                if(!((vld[x]>>y)&1)) continue;    //(x,y)博物馆不开,不管 
                tmp[++tl]=x;
                if(!vis[x]) vis[x]=true,cnt[scc]++;    //如果博物馆x之前没访问过,那么新计入答案 
            }while(stk[tp--]!=u);
            FOR(i,1,tl) vis[tmp[i]]=0;    //vis清空 
        }
    }
    int main(){
        n=read();m=read();d=read();
        FOR(i,1,m){
            int u=read(),v=read();
            FOR(j,1,d) add((u-1)*d+j,(v-1)*d+j%d+1);
        }
        FOR(i,1,n) FOR(j,1,d) vld[i]|=getc()<<j;    //干脆压下空间……(一开始以为会MLE) 
        FOR(i,1,n*d) if(!dfn[i]) dfs(i);
        FOR(u,1,n*d) for(int i=head[u];i;i=nxt[i]){
            int v=to[i];
            if(id[u]!=id[v]) add2(id[u],id[v]);    //新图连边 
        }
        //根据tarjan的性质, 强连通编号为反向的拓扑序,所以可以从1到n直接遍历一遍 
        FOR(u,1,scc){
            for(int i=head2[u];i;i=nxt2[i]) dp[u]=max(dp[u],dp[to2[i]]);
            dp[u]+=cnt[u];
        }
        printf("%d
    ",dp[id[1]]); 
    }
    View Code
  • 相关阅读:
    Win10系列:VC++绘制几何图形3
    Win10系列:VC++绘制几何图形2
    二进制的编码
    win10下更新anaconda和pip源
    在win10上安装FFmpeg
    git设置代理模式,仅为github设置代理
    python3的pip3安装
    texlive2019安装
    ubuntu中安装python3和pip
    macbook安装win10
  • 原文地址:https://www.cnblogs.com/1000Suns/p/10566669.html
Copyright © 2011-2022 走看看