关于tarjan算法的各种题型

9 篇文章 0 订阅
2 篇文章 0 订阅

求强连通分量

强连通的定义是:有向图 G 强连通是指,G 中任意两个结点连通。

强连通分量(Strongly Connected Components,SCC)的定义是:极大的强连通子图。

题目链接:https://www.luogu.com.cn/problem/P2863

在这里插入图片描述题解:

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int n,m;
int cnt;//记录强联通分量的个数
int visitnum;//遍历的步数
int dfn[100010];//记录元素第一次被访问的步数
int low[100010];//包含i的强联通分量最早被访问的步数
int num[100010];//记录强联通分量里的点的个数
int belong[100010];//i从属的强联通分量的序号
int top;//栈中元素的个数
int stack[100010];//手打栈
int instack[100010];//判断元素是否在栈中
int head[100010];
struct node{
int to,next;
}edge[100010];//链式前向星存边

int read()//读入优化
{
    int x=0,w=1;char ch=getchar();
    while(ch>'9'||ch<'0') {if(ch=='-')w=-1;ch=getchar();}
    while(ch<='9'&&ch>='0') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return x*w;
}

void tarjan(int u)
{
    int v;
    visitnum++;
    dfn[u]=low[u]=visitnum;
    stack[++top]=u;//入栈
    instack[u]=1;//入栈
    for(int i=head[u];i;i=edge[i].next)
    {
        v=edge[i].to;
        if(!dfn[v])//还没被访问过
        {
            tarjan(v);
            low[u]=min(low[u],low[v]);//判断u是否为v的子节点
        }
        else if(instack[v])
        {
            low[u]=min(low[u],dfn[v]);
                       //其实这里的dfn[v]也能换成low[v] 但最好写dfn
        }
    }
    if(dfn[u]==low[u])//u为强联通分量的根
    {
        cnt++;
        do//退栈
        {
            num[cnt]++;
            v=stack[top--];
            belong[v]=cnt;
            instack[v]=0;
        }while(u!=v);
    }
}
int main()
{
    int ans=0;
    int p,q;
    n=read();m=read();
    for(int i=1;i<=m;i++)
    {
        p=read();q=read();
        edge[i].to=q;
        edge[i].next=head[p];
        head[p]=i;
    }
    for(int i=1;i<=n;i++)
    {
        if(!dfn[i])//i没被访问过了
        {
            tarjan(i);
        }
    }
    for(int i=1;i<=cnt;i++)
    {
        if(num[i]>1)
        ans++;
    }
    printf("%d",ans);
}

求双连通分量

在这里插入图片描述
①点连通分量(BCC)

在一个无向图中,若任意两点间至少存在两条“点不重复”的路径,则说这个图是点双连通的(简称双连通,biconnected)

在一个无向图中,点双连通的极大子图称为点双连通分量(简称双连通分量,Biconnected Component,BCC)

模板

#include<cstdio>
#include<cctype>
#include<vector>
using namespace std;
struct edge
{
    int to,pre;
}edges[1000001];
int head[1000001],dfn[1000001],dfs_clock,tot;
int num;//BCC数量 
int stack[1000001],top;//栈 
vector<int>bcc[1000001];
int tarjan(int u,int fa)
{
    int lowu=dfn[u]=++dfs_clock;
    for(int i=head[u];i;i=edges[i].pre)
        if(!dfn[edges[i].to])
        {
            stack[++top]=edges[i].to;//搜索到的点入栈 
            int lowv=tarjan(edges[i].to,u);
            lowu=min(lowu,lowv);
            if(lowv>=dfn[u])//是割点或根 
            {
                num++;
                while(stack[top]!=edges[i].to)//将点出栈直到目标点 
                    bcc[num].push_back(stack[top--]);
                bcc[num].push_back(stack[top--]);//目标点出栈 
                bcc[num].push_back(u);//不要忘了将当前点存入bcc 
            }
        }
        else if(edges[i].to!=fa)
            lowu=min(lowu,dfn[edges[i].to]);
    return lowu;
}
void add(int x,int y)//邻接表存边 
{
    edges[++tot].to=y;
    edges[tot].pre=head[x];
    head[x]=tot;
}

int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y),add(y,x);
    }
    for(int i=1;i<=n;i++)//遍历n个点tarjan 
        if(!dfn[i])
        {
            stack[top=1]=i;
            tarjan(i,i);
        }
    for(int i=1;i<=num;i++)
    {
        printf("BCC#%d: ",i);
        for(int j=0;j<bcc[i].size();j++)
            printf("%d ",bcc[i][j]);
        printf("\n");
    }
    return 0;
 }

②边强连通分量

与求割边的过程相同,详细代码见下面的求割边代码。

求割点

例题:https://www.luogu.com.cn/problem/P3388

在这里插入图片描述题解

#include<bits/stdc++.h>
using namespace std;
struct edge{
    int nxt,mark;
}pre[200010];
int n,m,idx,cnt,tot;
int head[100010],DFN[100010],LOW[100010];
bool cut[100010];
void add (int x,int y)
{
    pre[++cnt].nxt=y;
    pre[cnt].mark=head[x];
    head[x]=cnt;
}
void tarjan (int u,int fa)
{
    DFN[u]=LOW[u]=++idx;
    int child=0;
    for (int i=head[u];i!=0;i=pre[i].mark)
    {
        int nx=pre[i].nxt;
        if (!DFN[nx])
        {
            tarjan (nx,fa);
            LOW[u]=min (LOW[u],LOW[nx]);
            if (LOW[nx]>=DFN[u]&&u!=fa)
                cut[u]=1;
            if(u==fa)
                child++;
        }
        LOW[u]=min (LOW[u],DFN[nx]);
    }
    if (child>=2&&u==fa)
        cut[u]=1;
}
int main()
{
    memset (DFN,0,sizeof (DFN));
    memset (head,0,sizeof (head));
    scanf ("%d%d",&n,&m);
    for (int i=1;i<=m;i++)
    {
        int a,b;
        scanf ("%d%d",&a,&b);
        add (a,b);
        add (b,a);
    }
    for (int i=1;i<=n;i++)
        if (DFN[i]==0)
            tarjan (i,i);
    for (int i=1;i<=n;i++)
        if (cut[i])
            tot++;
    printf ("%d\n",tot);
    for (int i=1;i<=n;i++)
        if (cut[i])
            printf ("%d ",i);
    return 0;
}

求割边

例题https://www.luogu.com.cn/problem/P1656

在这里插入图片描述
题解:

#include <bits/stdc++.h>
using namespace std;
int maps[151][151];//邻接矩阵
struct Edge {
    int x,y;
} E[5001];//这是存答案的,用邻接表存,应该不用解释
int dfn[151],low[151],n,m,id,cnt,f[151];/*这些数组的含义:
dfn:
{
下标:点编号
内存的值:深度优先搜索时第几个遍历
}
low:
{
下标:点编号
内存的值:这个点能通过它的子孙到达的dfn值最小的点的dfn
}
f:
{
下标:点标号
内存的值:它遍历的上一个点
}
变量的含义:
n:结点个数
m:边个数
id:用于dfn标记
cnt:用于邻接表存图
*/
bool cmp(struct Edge a,struct Edge b) {
    if(a.x==b.x)return a.y<b.y;
    return a.x<b.x;
}//因题目要求,边要排序,要做这道题的人应该都知道cmp
void addEdge(int x,int y) {
    E[++cnt].x=x;
    E[cnt].y=y;
}//addedge函数,存入邻接表
void tarjan(int x) {
    int c=0,y;
    dfn[x]=low[x]=++id;
    for(register int i=1; i<=n; i++) {
        if(!maps[x][i])continue;//首先要有边
        y=i;//处理对象
        if(dfn[y]&&y!=f[x])low[x]=min(low[x],dfn[y]);//如果是它爸爸,割边就没有用了,好好理解
        if(!dfn[y]) {//如果找到祖先还有什么用呢
            f[y]=x;//不是祖先就认爸爸
            tarjan(y);//dfs过程
            low[x]=min(low[x],low[y]);//回溯时带着爸爸更新low
            if(low[y]>dfn[x])addEdge(x,y);//是割边,就加入吧
        }
    }
}//tarjan部分,证明在下面
int main() {
    int x,y;
    cin>>n>>m;
    for(register int i=1; i<=m; i++) {
        cin>>x>>y;
        maps[x][y]=maps[y][x]=1;//存边
    }
    for(register int i=1; i<=n; i++) {
        if(!dfn[i])tarjan(i);//tarjan
    }
    sort(E+1,E+cnt+1,cmp);//sort大法好
    for(register int i=1; i<=cnt; i++) {
        cout<<min(E[i].x,E[i].y)<<' '<<max(E[i].x,E[i].y)<<endl;//输出
    }
    return 0;//程序结束了
}

求缩点

缩点:顾名思义,就是将一些点缩成一个点的算法。

题目链接:https://www.luogu.com.cn/problem/P3387

在这里插入图片描述
题解:

#include<bits/stdc++.h>
using namespace std;

const int maxn=10000+15;
int n,m,sum,tim,top,s;
int p[maxn],head[maxn],sd[maxn],dfn[maxn],low[maxn];//DFN(u)为节点u搜索被搜索到时的次序编号(时间戳),Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序号 
int stac[maxn],vis[maxn];//栈只为了表示此时是否有父子关系 
int h[maxn],in[maxn],dist[maxn];
struct EDGE
{
	int to;int next;int from;
}edge[maxn*10],ed[maxn*10];
void add(int x,int y)
{
	edge[++sum].next=head[x];
	edge[sum].from=x;
	edge[sum].to=y;
	head[x]=sum;
}
void tarjan(int x)
{
	low[x]=dfn[x]=++tim;
	stac[++top]=x;vis[x]=1;
	for (int i=head[x];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if (!dfn[v]) {
		tarjan(v);
		low[x]=min(low[x],low[v]);
	}
	    else if (vis[v])
	    {
	    	low[x]=min(low[x],low[v]);
		}
	}
	if (dfn[x]==low[x])
	{
		int y;
		while (y=stac[top--])
		{
			sd[y]=x;
			vis[y]=0;
			if (x==y) break;
			p[x]+=p[y];
		}
	}
}
int topo()
{
	queue <int> q;
	int tot=0;
	for (int i=1;i<=n;i++)
	if (sd[i]==i&&!in[i])
	{
		q.push(i);
        dist[i]=p[i];
	 } 
	while (!q.empty())
	{
		int k=q.front();q.pop();
		for (int i=h[k];i;i=ed[i].next)
		{
			int v=ed[i].to;
			dist[v]=max(dist[v],dist[k]+p[v]);
			in[v]--;
			if (in[v]==0) q.push(v);
		}
	}
    int ans=0;
    for (int i=1;i<=n;i++)
    ans=max(ans,dist[i]);
    return ans;
}
int main()
{
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++)
	scanf("%d",&p[i]);
	for (int i=1;i<=m;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		add(u,v);
	}
	for (int i=1;i<=n;i++)
	if (!dfn[i]) tarjan(i);
	for (int i=1;i<=m;i++)
	{
		int x=sd[edge[i].from],y=sd[edge[i].to];
		if (x!=y)
		{
			ed[++s].next=h[x];
			ed[s].to=y;
			ed[s].from=x;
			h[x]=s;
			in[y]++;
		}
	}
	printf("%d",topo());
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值