Tarjan

Tarjan

割点

low[x]:x能到达的cnt最小的节点(包括x自身节点)
对于x节点,存在dfs树上x的子节点y,low[y]>=low[x],即y能到达的最小编号节点是以x为根节点的子树,删除x之后,y与x的父节点不连通(特判根节点)

void tarjan(int x){
    low[x]=dfn[x]=++tarcnt;
    int flag=0;
    for(int i=0;i<mp[x].size();++i){
        int y=mp[x][i];
        if(!dfn[y]){
            tarjan(y);
            low[x]=min(low[x],low[y]);
            if(low[y]>=dfn[x]){
                flag++;
                if(x^root||flag>1)cut[x]=1;
            }
        }else low[x]=min(low[x],dfn[y]);
    }
}

对于一个点x,删除x和与x相连的边后,所有满足low[y]>=dfn[x]的点的子树构成连通块,剩下的节点共同构成一个连通块。

割边

low[x]:x能到达的cnt最小的节点(不包括x自身节点)
对于x节点,存在dfs树上x的子节点y,low[y]>low[x],即y能到达的最小编号节点是以x为根节点的子树,删除x之后,y与x的父节点不连通(不用特判根节点)

void tarjan(int x,int f){
    low[x]=dfn[x]=++tarcnt;
    for(int i=0;i<mp[x].size();++i){
        int y=mp[x][i];
        if(!dfn[y]){
            tarjan(y);
            low[x]=min(low[x],low[y]);
            if(low[y]>dfn[x]){
                bridge[i]=1;
            }//x-y的边就是桥,可以把标记放在节点y上。
        }else if(y^f)low[x]=min(low[x],dfn[y]);
    }
}

Network

给定无向连通图,q次操作,每次加一条边,求前i次操作后桥的数量。

把桥标记放在子节点上,(x,y)之间加边,就是把dfs树上x与y路径上的所有桥删除。
可以暴力,每次询问O(N),优化:将删除后的图缩成一个点。

struct Greap{
    struct E{int y,nt;}e[MAXN<<2];
    int head[MAXN],cnt;
    void add(int x,int y){
        e[++cnt].y=y;
        e[cnt].nt=head[x];
        head[x]=cnt;
    }
    void init(){
        memset(head,0,sizeof(head));
        cnt=0;
    }
}g;
int n,m;
struct Tarjan{
    int dfn[MAXN],cnt,low[MAXN],fat[MAXN],ans;
    bool isb[MAXN];
    void tarjan(int x,int f){
        low[x]=dfn[x]=++cnt;
        fat[x]=f;
        for(int i=g.head[x];i;i=g.e[i].nt){
            int y=g.e[i].y;
            if(!dfn[y]){
                tarjan(y,x);
                low[x]=min(low[x],low[y]);
                if(low[y]>dfn[x])isb[y]=1,ans++;
            }else if(y^f)low[x]=min(low[x],dfn[y]);
        }
    }
    int tp;
    void lca(int x,int y){
        tp=0;
        while(x^y){
        //类似树剖,x,y往上跳的时候记录路径
        //把路径上的所有点的fat数组直接指向最后的一个节点,即指向dfs树上的lca(x,y)
            if(dfn[x]<dfn[y])swap(x,y);
            low[tp++]=x;
            ans-=isb[x];
            isb[x]=0,x=fat[x];
        }
        if(!tp)return;
        int ff=low[--tp];
        for(int i=0;i<tp;++i)fat[low[i]]=ff;
    }
    void init(){
        cnt=ans=0;
        memset(dfn,0,sizeof(dfn));
        memset(isb,0,sizeof(isb));
    }
}tj;
int main() {
    int cas=1;
    while(1){
        read(n,m);
        if(n==0&&m==0)return 0;
        if(cas^1)putchar('\n');
        printf("Case %d:\n",cas++);
        g.init();
        for(int i=0,x,y;i<m;++i){
            read(x,y);
            g.add(x,y),g.add(y,x);
        }
        tj.init();
        tj.tarjan(1,0);
        int q;read(q);
        for(int i=0,x,y;i<q;++i){
            read(x,y);
            tj.lca(x,y);
            printf("%d\n",tj.ans);
        }
    }
    return 0;
}
连通分量
有向图强联通分量(SCC)

有向图 G强连通是指,G 中任意两个结点相互可达。
在dfs树中,如果遍历到某个强联通分量的某个节点x,该强连通分量所有节点都在x的dfs子树中。
dfs到x节点时,x入栈,如果回溯时low[x]==dfn[x]说明栈x后的节点构成一个强联通分量

int scc[MAXN];//scc[i]:节点i所在scc编号
int siz[MAXN];//siz[i]:强联通i的大小
void tarjan(int x){
    low[x]=dfn[x]=++cnt;
    sta[++tp]=x;//入栈
    for(int i=g.head[x];i;i=g.e[i].nt){
        int y=g.e[i].y;
        if(!dfn[y])tarjan(y,x),low[x]=min(low[x],low[y]);
        else if(!scc[y])low[x]=min(low[x],dfn[y]);
        //y还在栈中
    }
    if(low[x]==dfn[x]){
        sc++;int y;
        do{
            y=sta[tp--];
            siz[sc]++;
            scc[y]=sc;
        }while(y^x)
}

scc缩点
遍历每条边,如果x->y边x,y属于不同的scc

for(int x=1;x<=n;++x){
    for(int i=head[x];i;i=e[i].nt){
        int y=g.e[i].y;
        if(scc[x]^scc[y])add_c(scc[x],scc[y]);
    }
}

poi-1236-Network of Schools
给定n个学校,a支援b即a的软件b可以使用

  1. 求一个新软件至少要给多少个学校

  2. 至少添加几个支援关系使得软件给任意一个学校,其他所有学校都能获得

scc缩点后,每个scc内部节点都相互可达,所以,ans1就是缩点后零入度节点数量
添加若干有向边,使得一个有向图变成一个强联通图。
对任意节点v,如果v出度为零,则必须添加一条从v开始的有向边。
对任意节点v,如果v入度为零,则必须添加一条到达v的有向边。
若图中共有p个入度为零的节点,q个出度为零的节点,至少添加max(p,q)条有向边。

struct G{;
    struct E{int y,nt;}e[MAXN*100];
    int head[MAXN],cnt;
    void add(int x,int y){...}
}g,gg;
int n;
int dfn[MAXN],tarcnt,low[MAXN];
int scc[MAXN],scccnt,stk[MAXN],tp;
void tarjan(int x){
    dfn[x]=low[x]=++tarcnt;
    stk[++tp]=x;
    for(int i=g.head[x];i;i=g.e[i].nt){
        int y=g.e[i].y;
        if(!dfn[y]){
            tarjan(y);
            low[x]=min(low[x],low[y]);
        }else if(!scc[y])low[x]=min(low[x],dfn[y]);
    }
    if(low[x]==dfn[x]){
        int y;++scccnt;
        do y=stk[tp--],scc[y]=scccnt;while(y^x);
    }
}
int in[MAXN],out[MAXN];
int main() {
    ...
    for(int i=1;i<=n;++i)if(!dfn[i])tarjan(i);
    for(int x=1;x<=n;++x){
        for(int i=g.head[x];i;i=g.e[i].nt){
            int y=g.e[i].y;
            if(scc[x]^scc[y]){
                gg.add(scc[x],scc[y]);
                in[scc[y]]++,out[scc[x]]++;
            }
        }
    }
    int ans1=0,ans2=0;
    for(int i=1;i<=scccnt;++i)ans1+=(in[i]==0),ans2+=(out[i]==0);
    printf("%d\n%d",ans1,scccnt==1?0:max(ans1,ans2));
    return 0;
}
边双联通分量(e-dcc)

图g是边双联通图,当且仅当图中任意一条边都包含在至少一个简单环中,即不存在桥。
e-dcc求法:求出所有桥后,删除桥,原图分为若干个联通分量,每个联通分量都是一个边双联通分量。

//先tarjan求出桥
int dcc[MAXN],dc;//dcc[i]:节点i所属的dcc编号
void dfs(int x){
    dcc[x]=dc;
    for(int i=head[x];i;i=e[i].nt){
        int y=e[i].y;
        if(dcc[y]||bridge[i])continue;
        dfs(y);
    }
}
//在main里加入。
for(int i=1;i<=n;++i){
    if(dcc[i])continue;
    ++dc;
    dfs(i);
}

e-DCC缩点
将每个e-dcc缩成一个点,新点的编号是dcc编号(也可以是原图节点的编号)

//在求出dcc[]基础上
for(int x=1;x<=n;++x){
    for(int i=head[x];i;i=e[i].nt){
        int y=e[i].y;
        if(bridge[i])add(dcc[x],dcc[y]);
    }
}
点双联通分量(v-dcc)

图g是点双联通图,当且仅当图的顶点数为2或者图中任意两个顶点都包含在至少一个简单环中,即不存在割点。
注意,某个割点可以在多个v-dcc中
求v-dcc:

  1. 当一个节点第一次被访问时,入栈该节点。

  2. low[y]>=dfn[x]时,无论x是否为根,都要:

    1. 出栈,直至y出栈。

    2. 出栈的所有节点与x节点构成一个v-dcc。

vector<int>dcc[MAXN];//dcc[i]:保存第i个v-dcc的节点
int dc,stk[MAXN],tp;//栈
void tarjan(int x){
    low[x]=dfn[x]=++cnt;
    stk[++tp]=x;
    int flag=0;
    if(x==root&&head[x]==0){//特判孤立节点
        dcc[++dc].clear();
        dcc[dc].push_back(stk[tp--]);
        return;
    }
    for(int i=g.head[x];i;i=g.e[i].nt){
        int y=g.e[i].y;
        if(!dfn[y]){
            tarjan(y);
            low[x]=min(low[x],low[y]);
            if(low[y]>=dfn[x]){
                dcc[++dc].clear();int z;
                do dcc[dc].push_back(z=sta[tp--]);while(z^y);
                dcc[dc].push_back(x);
                flag++;
                if(x^root||flag>1)cut[x]=1;
            }
        }
        else low[x]=min(low[x],dfn[y]);
    }
}

v-DCC缩点
一个割点可能属于多个v-dcc,设图中共有p个割点,t个v-dcc,缩点后建立p+t 个节点的新图。

int belong[MAXN];//belong[i]:节点i所属的v-dcc编号
int main(){
    num=dc;//每个割点新编号,从dc+1开始
    for(int i=1;i<=n;++i)
        if(cut[i])new_id[i]=++num;
    for(int i=1;i<=dc;++i){
        for(int j=0;j<dcc[i].size();++j){
            int y=dcc[i][j];
            if(cut[y]){
                add_c(i,new_id[y]);
                add_c(new_id[y],i);
            }else c[x]=i;//除了割点,每个节点仅属于一个v-dcc
        }
    }
}

Knights of the Round Table
n个骑士,m个关系,a,b之间有有矛盾<=>a,b不能邻座,最终选择奇数(至少3个人)个满足条件的人,求不可能被选上的人数。
建立补图,只有a,b有边才能邻座。被选上的x个人在补图中组成奇环,即求补图中不经任意一个奇环的点数。
一个点双联通分量如果存在奇环,那么分量中的所有点都至少在一个奇环里。
对一个v-dcc进行染色,如果不是二分图,那么必然存在奇环。

#define Init(arr,val) memset(arr,val,sizeof(arr))
bool mp[MAXN][MAXN];
int dfn[MAXN],tarcnt,low[MAXN];
vector<int>dcc[MAXN];//储存e-dcc的点
int dc,stk[MAXN],tp,rt;
void tarjan(int x) {
    low[x]=dfn[x]=++tarcnt;
    stk[++tp]=x;
    if(x==rt&&head[x]==0) {//孤立节点。
        dcc[++dc].push_back(stk[tp--]);
        return;
    }
    for(int i=head[x]; i; i=e[i].nt) {
        int y=e[i].y;
        if(!dfn[y]) {
            tarjan(y);
            low[x]=min(low[x],low[y]);
            if(low[y]>=dfn[x]) {
                ++dc;int z;
                do dcc[dc].push_back(z=stk[tp--]);while(z^y);
                dcc[dc].push_back(x);
            }
        } else low[x]=min(low[x],dfn[y]);
    }
}
int V[MAXN],col[MAXN],ok[MAXN];//ok[i]:点i符合条件
bool dfs(int x,int color) {//dfs染色
    col[x]=color;
    for(int i=head[x]; i; i=e[i].nt) {
        int y=e[i].y;
        if(V[x]^V[y])continue;
        if(col[y]==color)return 0;
        if(!col[y]&&!dfs(y,3-color))return 0;
    }return 1;
}
int solve(int n) {
    int ans=n;
    for(int i=1; i<=n; ++i)if(!dfn[i])tarjan(rt=i);//补图可能不连通
    while(dc) {//遍历每个v-dcc
        int siz=dcc[dc].size();
        for(int i=0; i<siz; ++i)V[dcc[dc][i]]=dc;
        if(!dfs(dcc[dc][0],1))for(int i=0; i<siz; ++i)ok[dcc[dc][i]]=1;
        for(int i=0; i<siz; ++i)col[dcc[dc][i]]=0;
        dc--;
    }
    for(int i=1; i<=n; ++i)ans-=ok[i];
    return ans;
}
int main() {
    while(1) {
        read(n,m);
        if(!n)return 0;
        dc=tarcnt=e_cnt=0;
        Init(dfn,0);Init(col,0);Init(ok,0);Init(head,0),Init(V,0);Init(mp,0);
        for(int i=0;i<=n;++i)dcc[i].clear();
        for(int i=0,x,y; i<m; ++i) {
            read(x,y);
            mp[x][y]=1,mp[y][x]=1;
        }
        for(int i=1; i<=n; ++i)for(int j=i+1; j<=n; ++j)
            if(!mp[i][j])add(i,j),add(j,i);
        printf("%d\n",solve(n));
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值