Tarjan 总结

Tarjan 基础

  dfn[i]: 在dfs中该节点被搜索的次序(时间戳)。

  low[i]: 为i或i的子树能够追溯到的最早的栈中节点的次序号。

   dfn[i] == low[i] 时,为i或i的子树可以构成一个强连通分量。

void tarjan(int x)
{
    id++;
    dfn[x] = id;
    low[x] = id;
    vis[x] = 1;//是否在栈中
    stk[++top] = x;//入栈
    for(int i = head[x]; i != 0; i = edge[i].nxt){
        int temp = edge[i].to;
        if(!dfn[temp]){
            tarjan(temp);
            low[x] = min(low[x],low[temp]);
        }
        else if(vis[temp]){
            low[x] = min(low[x],dfn[temp]);
        }
    }
    if(dfn[x] == low[x]){//构成强连通分量,进行染色
        vis[x] = 0;
        color[x] = ++col;
        while(stk[top] != x){
            color[stk[top]] = col;
            vis[stk[top--]] = 0;
        }
        top--;
    }
}
View Code

 割边、割点

一、基本概念

    桥:无向连通图中,如果删除某边后,图变成不连通,则称该边为桥。

    割点:无向连通图中,如果删除某点后,图变成不连通,则称该点为割点。

二、Tarjan算法求解桥和割点

    1.割点:1)当前节点为树根的时候,条件是要有至少两颗子树

            2)当前节点u不是树根的时候,条件是存在u的一个子节点v使得 low[v]>=dfn[u]

    2.桥:当且仅当无向边(u,v)是树枝边的时候,条件是 dfn[u]<low[v]

#include<bits/stdc++.h>

using namespace std;
const int N = 201;
vector<int>G[N];
int n,m,low[N],dfn[N];
bool is_cut[N];
int father[N],tim;
void input()
{
    scanf("%d%d",&n,&m);
    int a,b;
    for(int i=1;i<=m;++i)
    {
        scanf("%d%d",&a,&b);
        G[a].push_back(b);
        G[b].push_back(a);
    }
}
void Tarjan(int i,int Father)
{
    father[i]=Father;
    dfn[i]=low[i]=tim++;
    for(int j=0;j<G[i].size();++j)
    {
        int k=G[i][j];
        if(dfn[k]==-1)
        {
            Tarjan(k,i);
            low[i]=min(low[i],low[k]);
        }
        else if(Father!=k)/*假如k是i的父亲的话,那么这就是无向边中的重边,有重边那么一定不是桥*/
            low[i]=min(low[i],dfn[k]);
    }
}
void _count()
{
    int rootson=0;
    Tarjan(1,0);
    for(int i=2;i<=n;++i)
    {
        int v=father[i];
        if(v==1)
        rootson++;
        else{
            if(low[i]>=dfn[v])/*割点的条件*/
            is_cut[v]=true;
        }
    }
    if(rootson>1)
    is_cut[1]=true;
    for(int i=1;i<=n;++i)
    if(is_cut[i])
    printf("%d\n",i);
    for(int i=1;i<=n;++i)
    {
        int v=father[i];
        if(v>0&&low[i]>dfn[v])/*桥的条件*/
        printf("%d,%d\n",v,i);
    }

}
int main()
{
    input();
    memset(dfn,-1,sizeof(dfn));
    memset(father,0,sizeof(father));
    memset(low,-1,sizeof(low));
    memset(is_cut,false,sizeof(is_cut));
    _count();
    return 0;
}
View Code

 有向图缩点

思想:将一个有向图强连通分量缩点为一个点去代替一堆点,要修改两个属性,一个是边,一个是点。

方法:运用Tarjan算法找出一个强连通分量,每次找出一个强连通分量,我们就用其中的一个点去代表这一堆点。记这个点为代表点( ̄□ ̄||只是自己这么叫),那么这堆点的contract[x] = 代表点。

    对于非代表点:

   ①、将它的出边全部复制给代表点。

   ②、将它的点权加给代表点。

   ③、所有指向它的点在使用时用指向 contract[x] 代替即可,不需要做修改。

#include<bits/stdc++.h>

using namespace std;
typedef long long ll;
const int maxn = 200005;
vector<int> e[maxn];
int ins[maxn], dfn[maxn], low[maxn], contract[maxn];
ll w[maxn];
int ind;
stack<int> s;
void tarjan(int u)
{
    dfn[u] = low[u] = ++ind;
    ins[u] = 1;
    s.push(u);
    for(int i = 0; i < e[u].size(); i++) {
        int v = e[u][i];
        if(!dfn[v])
        {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        }
        else if(ins[v]) low[u] = min(low[u], dfn[v]);
    }
    if(dfn[u] == low[u]) {
        int v;
        do {
            v = s.top();
            s.pop();
            ins[v] = 0;
            contract[v] = u;
            if(u != v) {
                w[u] += w[v];
                while(!e[v].empty()) {
                    e[u].push_back(e[v].back());
                    e[v].pop_back();
                }
            }
        } while(u != v);
    }
}

ll dfs(int u, ll cnt)
{
    cnt += w[u];
    ll ret = cnt;
    for(int i = 0; i < e[u].size(); i++) {
        int v = contract[e[u][i]];
        if(v != u) ret = max(ret, dfs(v, cnt));
    }
    return ret;
}
int main()
{
    int n, m;
    scanf("%d%d",&n,&m);
    for(int i = 1; i <= n; i++) {
        scanf("%d",&w[i]);
    }
    for(int i = 1; i <= m; i++) {
        int u, v;
        scanf("%d%d",&u,&v);
        e[u].push_back(v);
    }
    tarjan(1);
    ll ans = dfs(1,0);
    printf("%lld\n",ans);
    return 0;
}
View Code

向图的双连通分量

一、点双连通分量

  定义:对于一个连通图,如果任意两点至少存在两条点不重复路径,则称这个图为点双连通的(简称双连通)。点双连通图的定义等价于任意两条边都同在一个简单环中。对一个无向图,点双连通的极大子图称为点双连通分量(简称双连通分量)。

#include<bits/stdc++.h>

using namespace std;
const int maxn = 1000;

struct Edge {
    int u,v;
    Edge(int uu,int vv)
    {
        u = uu;
        v = vv;
    }
};
stack<Edge> s;

struct edge  //链式前向星建图的边结构
{
    int v,next;
}edges[maxn];

int n,m;        //节点的数目,无向边的数目
int e,head[maxn];
int dfn[maxn];           //第一次访问的时间戳
int dfs_clock;           //时间戳
int iscut[maxn];         //标记节点是否为割点
int bcc_cnt;             //点_双连通分量的数目
int bccno[maxn];         //节点属于的点_双连通分量的编号
vector<int> bcc[maxn];   //点_双连通分量

void addedges(int u,int v)  //加边
{
    edges[e].v = v;
    edges[e].next = head[u];
    head[u] = e++;
    edges[e].v = u;
    edges[e].next = head[v];
    head[v] = e++;
}

int dfs(int u,int fa)
{
    int low = dfn[u] = ++dfs_clock;
    int child = 0;
    for(int i=head[u];i!=-1;i=edges[i].next)
    {
        int v = edges[i].v;
        Edge e = (Edge){u,v};
        if(!dfn[v])
        {
            s.push(e);
            child++;
            int lowv = dfs(v,u);
            low = min(low,lowv); //用后代更新low
            if(lowv >= dfn[u])     //找到了一个子树满足割顶的条件
            {
                iscut[u] = 1;
                bcc_cnt++;
                bcc[bcc_cnt].clear();
                for(;;)            //保存bcc信息
                {
                    Edge x = s.top(); s.pop();
                    if(bccno[x.u] != bcc_cnt) {bcc[bcc_cnt].push_back(x.u); bccno[x.u] = bcc_cnt;}
                    if(bccno[x.v] != bcc_cnt) {bcc[bcc_cnt].push_back(x.v); bccno[x.v] = bcc_cnt;}
                    if(x.u == u && x.v == v) break;
                }
            }
        }
        else if(dfn[v] < dfn[u] && v != fa)   //用反向边更新low
        {
            s.push(e);
            low = min(low,dfn[v]);
        }
    }
    if(fa < 0 && child == 1) iscut[u] = 0;    //对于根节点若只有一个子树则不是割顶
    return low;
}

void init()
{
    memset(dfn,0,sizeof(dfn));
    memset(iscut,0,sizeof(iscut));
    memset(head,-1,sizeof(head));
    memset(bccno,0,sizeof(bccno));
    e = 0; dfs_clock = 0; bcc_cnt = 0;
}

int main()
{
    int u,v;
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        init();
        for(int i=0;i<m;i++)
        {
            scanf("%d%d",&u,&v);
            addedges(u,v);
        }
        dfs(1,-1);
        for(int i=1;i<=bcc_cnt;i++)
        {
            for(int j=0;j<bcc[i].size();j++)
                cout<<bcc[i][j]<<" ";
            cout<<endl;
        }

    }
    return 0;
}
View Code

二、边双连通分量

  定义:对于一个连通图,如果任意两点至少存在两条边不重复路径,则称该图为边双连通的。边双连通图的定义等价于任意一条边至少在一个简单环中。对一个无向图,边双连通的极大子图称为边双连通分量。

#include<bits/stdc++.h>

using namespace std;

const int maxn = 1005;
struct Edge
{
    int no,v,next;      //no:边的编号
}edges[maxn];

int n,m,ebcnum;         //节点数目,无向边的数目,边_双连通分量的数目
int e,head[maxn];
int dfn[maxn];          //第一次访问的时间戳
int dfs_clock;          //时间戳
int isbridge[maxn];     //标记边是否为桥
vector<int> ebc[maxn];  //边_双连通分量

void addedges(int num,int u,int v)    //加边
{
    edges[e].no = num;
    edges[e].v = v;
    edges[e].next = head[u];
    head[u] = e++;
    edges[e].no = num++;
    edges[e].v = u;
    edges[e].next = head[v];
    head[v] = e++;
}

int dfs_findbridge(int u,int fa)    //找出所有的桥
{
    int lowu = dfn[u] = ++dfs_clock;
    for(int i=head[u];i!=-1;i=edges[i].next)
    {
        int v = edges[i].v;
        if(!dfn[v])
        {
            int lowv = dfs_findbridge(v,u);
            lowu = min(lowu,lowv);
            if(lowv > dfn[u])
            {
                isbridge[edges[i].no] = 1; //
            }
        }
        else if(dfn[v] < dfn[u] && v != fa)
        {
            lowu = min(lowu,dfn[v]);
        }
    }
    return lowu;
}

void dfs_coutbridge(int u,int fa)     //保存边_双连通分量的信息
{
    ebc[ebcnum].push_back(u);
    dfn[u] = ++dfs_clock;
    for(int i=head[u];i!=-1;i=edges[i].next)
    {
        int v = edges[i].v;
        if(!isbridge[edges[i].no] && !dfn[v]) dfs_coutbridge(v,u);
    }
}

void init()
{
    memset(dfn,0,sizeof(dfn));
    memset(isbridge,0,sizeof(isbridge));
    memset(head,-1,sizeof(head));
    e = 0; ebcnum = 0;
}

int main()
{
    int u,v;
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        init();
        for(int i=0;i<m;i++)
        {
            scanf("%d%d",&u,&v);
            addedges(i,u,v);
        }
        dfs_findbridge(1,-1);
        memset(dfn,0,sizeof(dfn));
        for(int i=1;i<=n;i++)
        {
            if(!dfn[i])
            {
                ebc[ebcnum].clear();
                dfs_coutbridge(i,-1);
                ebcnum++;
            }
        }
        for(int i=0;i<ebcnum;i++)
        {
            for(int j=0;j<ebc[i].size();j++)
                cout<<ebc[i][j]<<" ";
            cout<<endl;
        }
    }
    return 0;
}
View Code

三、点双连通分量和边双连通分量的区别和联系

  ①、二者都是基于无向图。

  ②、边双连通分量是删边后还连通,而后者是删点。

  ③、点双连通分量一定是边双连通分量(除两点一线的特殊情况),反之不一定。

  ④、点双连通分量可以有公共点,而边双连通分量不能有公共边。

 Tarjan离线算法求LCA

思路dfs... 

#include<bits/stdc++.h>

using namespace std;
const int N = 500005;
struct EDGE{
    int next;
    int to;
    int lca;
};
EDGE edge[N];//树的链表
EDGE qedge[N];//需要查询LCA的两节点的链表
int n,m,p,x,y;
int num_edge,num_qedge,head[N],qhead[N];
int father[N];
int visit[N];//判断是否被找过
void add_edge(int from,int to){//建立树的链表
    edge[++num_edge].next=head[from];
    edge[num_edge].to=to;
    head[from]=num_edge;
}
void add_qedge(int from,int to){//建立需要查询LCA的两节点的链表
    qedge[++num_qedge].next=qhead[from];
    qedge[num_qedge].to=to;
    qhead[from]=num_qedge;
}
int fin(int z){//找爹函数
    if(father[z]!=z)
        father[z]=fin(father[z]);
    return father[z];
}
int dfs(int x){//把整棵树的一部分看作以节点x为根节点的小树
    father[x]=x;//由于节点x被看作是根节点,所以把x的father设为它自己
    visit[x]=1;//标记为已被搜索过
    for(int k=head[x];k;k=edge[k].next)//遍历所有与x相连的节点
        if(!visit[edge[k].to]){//若未被搜索
            dfs(edge[k].to);//以该节点为根节点搞小树
            father[edge[k].to]=x;//把x的孩子节点的father重新设为x
        }
    for(int k=qhead[x];k;k=qedge[k].next)//搜索包含节点x的所有询问
        if(visit[qedge[k].to]){//如果另一节点已被搜索过
            qedge[k].lca=fin(qedge[k].to);//把另一节点的祖先设为这两个节点的最近公共祖先
            if(k%2)//由于将每一组查询变为两组,所以2n-1和2n的结果是一样的
                qedge[k+1].lca=qedge[k].lca;
            else
                qedge[k-1].lca=qedge[k].lca;
        }
}
int main(){
    scanf("%d%d%d",&n,&m,&p);//输入节点数,查询数和根节点
    for(int i=1;i<n;++i){
        scanf("%d%d",&x,&y);//输入每条边
        add_edge(x,y);
        add_edge(y,x);
    }
    for(int i=1;i<=m;++i){
        scanf("%d%d",&x,&y);//输入每次查询,考虑(u,v)时若查找到u但v未被查找,所以将(u,v)(v,u)全部记录
        add_qedge(x,y);
        add_qedge(y,x);
    }
    dfs(p);//进入以p为根节点的树的深搜
    for(int i=1;i<=m;i++)
        printf("%d ",qedge[i*2].lca);//两者结果一样,只输出一组即可
    return 0;
}
View Code

 

posted on 2018-09-12 21:13  solvit  阅读(300)  评论(0编辑  收藏  举报

导航