Tarjan相关最全(附训练题和答案)

算法思想

本篇主要介绍Tarjan算法,其中求强连通分量的其他算法也一并介绍,但是不为重点,学习这个知识点的起因是上一次队内训练赛没能做出来模板题,感觉需要学习这个知识点(虽然还有很多都要学习)

Tarjan

Tarjan的核心算法是DFS,用于求解强连通分量,割点割边等关于图的连通性问题,相关概念参考下面的部分
Tarjan主要利用的是dfs序,也叫时间戳,是图中每个节点在进行DFS时被访问的时间顺序,一般使用数组来存储,Tarjan还需要利用搜索树

搜索树:在无向图中,以某一节点x出发dfs,每个节点只访问一次,所有被访问过的节点与边构成一棵树,将这棵树成为无向连通图的搜索树

追溯值:该值表示从当前节点x作为搜索树的根节点除非,能够访问到的所有节点中,时间戳最小的值,一般用low[x]表示,在这里,能够访问到的所有节点,需要满足以下两个条件之一(推导一下其实两个条件差不多):以x为根的搜索树的所有节点;通过一条非搜索树上的边连接到了搜索树,在有向图中,这个概念为x通过有向边可以回溯到的最早的时间戳

结合有向图与无向图对追溯值的定义,可以总结出这样的结论:追溯值为以x为根的搜索树中经过非搜索树边能够到达的节点中最早的时间戳

简单证明:在回溯过程中,low[x]会因邻接点的low[x]值而被更改,当时间戳=low[x]时,代表x为一个强连通分量的根,如果x不是强连通分量的根,那么一定属于另一个强连通分量,而且x的根为其父辈节点,那么对于包含当前顶点的到其祖宗的回路,可知low[x]一定会被更改成一个比时间戳更小的值。

至此,Tarjan算法的铺垫到此结束

强连通分量

强连通分量的相关定义:

定义1: 如果有向图G的任何两顶点都互相可达,则称图G为强联通图,否则为非强联通图

定义2: 如果有向图G不是强联通图,子图G’为强联通图,点v属于G’,任意包含v的强联通子图为G’子图,则称G’为G的极大强连通子图,也称强连通分量

Tarjan求强连通分量的思路较为简单:如果当前节点为一个强连通分量的根,那么其强连通分量一定是以该根为节点的搜索树子树,那么关键是求出哪些节点是属于搜索树子树的,这里使用堆栈来存储这个强连通分量。有一个很重要的一点,当一个节点,在当前节点压入堆栈后压入并且还存在,同时不属于该强连通分量,那么一定属于另一个强连通分量,但当前节点是其根的祖宗,那么这个强连通分量应该在此之前已经取出,具体分析如下

在这里插入图片描述

如图,这个是一个有向图,时间戳为节点编号,现在进行一次tarjan算法的流程
从1号节点一直遍历到3号节点,此时1~3节点的时间戳为{1,2,3},low值也为{1,2,3},堆栈的内容为 {1,2,3}
当3再继续往下遍历的时候,它有两种选择,如果选择4节点,结果和时间戳一样,没有参考价值,这里假设选择了1号节点
那么,栈中已经有了1号节点,1号节点搜索完之后又回到了自己,这代表这个过程中栈中的节点是彼此可达的,那么代表有一个以1号为根节点的强连通分量,此时更新1,2,3号节点的low值为{1,1,1},将栈弹出直到遇到1,将1弹出,有些类似括号匹配
接下来3继续遍历4号,4号遍历5号,到达5号时,栈中内容为 {3,4,5},假设5号选择了去遍历3号,此时又和之前的状况一样了,但是注意,此时3,4,5号节点更新的值为3的low值,也就是1,栈依然清空
接下来遍历6,然后无法进行下去,6自身即为一个强连通分量

代码(不默认连通,有向图与无向图区别只在构造上)

int n,m,head[maxn],cnt,low[maxn],dfn[maxn],vis[maxn],ans,acc,belong[maxn];
stack<int>S;
//节点数,边数,链式前向星头,边计数,追溯值,时间戳
//访问标记,tarjan计数,分量计数,索引,栈
struct node {
    int to,next;
} e[maxn];
void Add(int from,int to) {
    e[++cnt].next=head[from];
    e[cnt].to=to;
    head[from]=cnt;
}
void tarjan(int id) {
    vis[id]=1;//标记访问
    dfn[id]=++ans;//时间戳
    low[id]=ans;//初始化
    S.push(id);//压入栈中
    for(int i=head[id]; i; i=e[i].next) {
        int v=e[i].to;//dfs下一个节点
        if(!dfn[v]) {//如果没访问过{
            tarjan(v);//下一节点
            low[id]=min(low[id],low[v]);//必须在这里也加一个
        }
        if(vis[v])
            low[id]=min(low[id],low[v]);
        //如果已经dfs过了,而且还在栈里,代表节点v的子节点也已经dfs过了
        //更新链接关系
    }
    if(low[id]==dfn[id]) {//找到一个根
        belong[id]=++acc;//建立强连通分量索引
        vis[id]=0;
        while(1) {//清除栈中的强连通分量
            int t=S.top();
            belong[t]=acc;
            vis[t]=0;
            S.pop();
            if(t==id)
                break;
        }
    }
}

割点

割点定义:无向连通图中,该点和连接该点的边全部去掉后,图不再连通

根据割点的定义,我们可以考虑一下一个什么样的点是割点,假设这个点为x,那么就有以下情况

  1. x为非搜索树根节点,并且x在搜索树中无子节点
  2. x为非搜索树根节点,并且x在搜索树中有子节点
  3. x为搜索树根节点,并且x在搜索树中无子节点或只有一个子节点
  4. x为搜索树根节点,并且x在搜素树中有多个子节点

那么来分类讨论一下:

对于第一种情况,x为非搜索树根节点,那么x必然有祖先节点,但是无子节点,显然消去x后不影响其祖先节点的连通性

对于第二种情况,就有如图所示的三种状态
在这里插入图片描述
第一种,如果low[x的子代]大于dfn[x],这代表在x的子代中,已经有至少一个强连通分量,那么消去x自然会增加子代的强连通分量,也就是增加连通块,x是割点

第二种,如果low[x的子代]=dfn[x],代表x为一个强连通分量的根,消去x后自然也能增加连通块的个数,x是割点

第三种,如果low[x的子代]小于dfn[x],代表x与其子代都属于一个强连通分量,x只是强连通分量里的一个节点,去掉x后不影响连通块数量

接下来探讨x为搜索树根节点的情况,如图

在这里插入图片描述

第一种情况,由于x为根节点,无子节点代表图只有一个点,只有一个子节点,去掉根节点后,新的根节点变成子节点,不会增加连通块

第二种情况,去掉x后,x的多个子节点便无法连通,会产生连通块,x为割点

综上所述,x为割点的条件为:x非根节点&&x有子节点&&low[x子节点]>=dfn[x]||x为根节点&&x子节点个数≥2

当判断条件给出了,那么代码也就很好写出来了

代码

void tarjan(int u,int r) {//当前节点
    dfn[id]=++ans;//时间戳
    low[id]=ans;//初始化
    int c=0;
    for(int i=head[u]; i; i=e[i].next) {//遍历
        int v=e[i].to;//dfs下一个节点
        if(!dfn[v]) { //如果没访问过
            tarjan(v,r);//下一节点
            low[u]=min(low[u],low[v]);//回溯更新
            if(low[v]>=dfn[u]&&u!=r)part[u]=1;//如果非根
            if(r==u)c++;//如果为根
        }
        low[u]=min(low[u],dfn[v]);//回溯更新
    }
    if(c>=2&&u==r)part[r]=1;//为根情况判断
}

这里根据后面的参考文献解释一下
low[u]=min(low[u],dfn[v]); 这行代码:
在强连通分量的求解中,如果v已经在栈中,说明u和v一定在一个强连通分量中,最后一定有low[u]=low[v],所以更新不会有问题,但是求割点时,由于是无向图,low的定义应该变成了最早能通过未经过边回溯到的割点,如果将dfn[v]改成low[v],可能会回溯过度,直接进入了另一个更大的强连通分量当中,总的来说,我的理解是这样的:割点可以产生多个强连通分量,但是割点也可能属于更大的强连通分量,如果只考虑产生产生而不考虑属于就会出错,具体例子参考tarjan算法总结 (强连通分量+缩点+割点),看这一篇就够了~

割桥

桥定义:无向连通图中,该边去掉后,图不再连通

与割点类似,桥也有多种情况需要讨论,如图

在这里插入图片描述

  1. 第一种情况,low[y]>dfn[x],由图可知,y无法通过其他未经过的边回溯到x,也就是x与y与y的子树联系只有一条边,那么删去x,y这条边就可以产生多个连通块,x-y为桥
  2. 第二种情况,low[y]=dfn[x],由图可知,y可以通过其他未经过的边回溯到x,换句话来说,y与y的子树节点有不止一条路到x,那么删去x-y显然不会产生新的连通块
  3. 第三种情况,low[y]<dfn[x],由图可知,y及其子树有不止一条路到x的父节点,那么删去x-y也不会产生新的连通块

代码

void Addb(int from,int to) {
    brige[++ans].next=h[from];
    brige[ans].to=to;
    h[from]=ans;
}
void tarjan(int u,int fa) {//当前节点
    dfn[u]=++ans;//时间戳
    low[u]=ans;//初始化
    for(int i=head[u]; i; i=e[i].next) {//遍历
        int v=e[i].to;//dfs下一个节点
        if(v==fa)continue;
        if(!dfn[v]) { //如果没访问过
            tarjan(v,u);//下一节点
            low[u]=min(low[u],low[v]);//回溯更新
            if(low[v]>dfn[u])Addb(u,v);
        }
        low[u]=min(low[u],dfn[v]);//回溯更新
    }
}

缩点

缩点的定义与使用是建立在强连通分量的基础之上的,一般用来求解图上的集合问题与点权&边权问题,基本思路为,因为强连通分量中任意两个点都是连通的,所以可以将一个强连通分量视为一个更大的点,更准确的来说是一个点集,而该集合的权重一般为点集中各个点权之和,如图

在这里插入图片描述具体使用的时候先进行一遍tarjan,获得各个点属于的强连通分量编号,然后重新遍历一遍建图即可

缩点的相关问题主要涉及到出度与出度,代码较简单,所以在此略过,详细可以看题

Kosaraju

Kosaraju算法有两个理论基础:

  1. 反图与原图的强连通分量相同
  2. 原图能从分量I走到分量II,则反图不能从II到I

归根结底,还是强连通分量内部的点可彼此到达,强连通分量外部的点与强连通内部的点不能彼此可达

Kosaraju算法的基本步骤如下

  1. 对原图DFS,记录搜索序的逆序(入栈)
  2. 出栈顶点,以顶点反向搜索,标记能够到达的顶点,这些顶点可以构成一个强连通分量
  3. 如果还有顶点未标记,继续步骤2

代码

void DFS(int u) {//后序遍历
    vis[u]=1;
    for(int i=h[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(!vis[v])
            DFS(v);
    }
    S.push(u);
}
void kosaraju() {
    for(int i=1; i<=n; i++)
        if(!vis[i])DFS(i);
    queue<int>Q;
    while(!S.empty()) {//反向遍历
        int u=S.top();
        S.pop();
        if(bel[u])continue;
        Q.push(u);
        ans++;
        bel[u]=ans;//第一个点特殊处理
        sum[ans]++;
        while(!Q.empty()) {//BFS
            u=Q.front();
            Q.pop();
            for(int i=nh[u]; i; i=ne[i].next) {//遍历反向边
                int v=ne[i].to;
                if(!bel[v]) {
                    bel[v]=ans;
                    sum[ans]++;
                    Q.push(v);
                }
            }
        }
    }
}

Garbow

在Tarjan中,dfn与low是求出强连通分量的根的主要根据,而Garbow主要是用一个栈来实现这两者的功能的。在Tarjan中,每次low[v]的修改都是因为环的出现,每次出现环,只有一个节点low[v]不变(回溯值)或者全部改变(归属到更大的强连通分量)。Garbow算法利用了这一不变特性,栈变化就是删除成环节点,只保留不变的那个唯一的节点或者全部删除,用出栈实现,因为深度最低的节点一定比前面的先访问,那么只要一直出战到栈顶顶点访问时间不大于深度最低的那个顶点,中间弹出的节点就是构成环的节点,即这些节点属于同一个强连通分量

代码

void DFS(int u){
	s1.push(u);s2.push(u);//s2为辅助栈
	low[u]=++dfn;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(!low[v]) DFS(v);//未访问过
		else if(!bel[v]) while(low[s2.top()]>low[v])s2.pop();
		/*访问过但未弹出,栈中访问时间的点在v之后一定属于同一个强连通分量,弹出的这些节点必然属于v的子树,这些节点能
		回溯的节点的low必然等于或小于low[v],如果大于low[v]的话(即在v的子树中就有强连通分量),那么这些节点必然会
		更早的弹出,而不是在现在*/
	}
	if(s2.top()==u){//与Tarjan相同
		s2.pop();
		ans++;
		int v;
		do{
			v=s1.top();
			s1.pop();
			bel[v]=ans;
		}while(v!=u);
	}
}

训练

POJ2186

题目大意:奶牛之间的崇拜为有向关系,A->B,B->C可以推得A->C,判断有多少头奶牛被所有奶牛崇拜

思路:找出图中所有的强连通分量,然后缩点,统计出度为0的强连通分量,因为出度为0的强连通分量可能为满足条件的集合,如果存在两个及以上的出度为0的点,代表有互相无法崇拜的奶牛集合,无解,如果只有1个,直接输出集合的大小即可
这里说一下为什么不能用入度,自己在尝试的时候,发现入度无法确定一个强连通分量是否满足条件,如图

在这里插入图片描述
缩点之后,看图中的蓝点统计出来的度为3,但如果直接判断度为强连通分量总数-1,显然得到的是错误答案

代码

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <queue>
//#include <unordered_map>
#include <map>
#include <set>
#include <numeric>
#include <stack>
#include <sstream>
#include <cmath>
#include <bitset>
//#include <unordered_set>
#include <functional>
#include <list>
#include <vector>
#include <iterator>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
//typedef __int128 Bint;
const int maxn=2e7+10;
int head[maxn],cnt,n,m,dfn[maxn],low[maxn],sum;
int acc,ans,belong[maxn],degree[maxn],num[maxn];
stack<int>S;
bool vis[maxn];
struct node {
    int to,next,u;
} e[maxn];
void Add(int from,int to) {
    e[++cnt].to=to;
    e[cnt].next=head[from];
    head[from]=cnt;
    e[cnt].u=from;//记录起始点
}
void tarjan(int u) {
    dfn[u]=low[u]=++ans;
    vis[u]=1;
    S.push(u);
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(!dfn[v]) {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        if(vis[v])
            low[u]=min(low[u],low[v]);
    }
    if(low[u]==dfn[u]) {
        acc++;
        int v;
        do {
            v=S.top();
            S.pop();
            num[acc]++;
            belong[v]=acc;
            vis[v]=0;
        } while(u!=v);
    }
}
int main() {
    scanf("%d%d",&n,&m);
    while(m--) {
        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<=cnt; i++) {
        int u=belong[e[i].u],v=belong[e[i].to];
        if(u!=v)//统计出度
            degree[u]++;
    }
    for(int i=1; i<=acc; i++)
        if(degree[i]==0) {//找到一个
            if(sum==0)
                sum=num[i];
            else {//找到两个,代表不能连通
                sum=0;
                break;
            }
        }
    printf("%d",sum);
    return 0;
}

Kosaraju解法代码

//#include <bits/stdc++.h>
#include <iostream>
#include <stack>
#include <queue>
#include <cstdlib>
#include <cstdio>
using namespace std;
const int maxn=1e6+10;
stack<int>S;
int n,m,h[maxn],nh[maxn],cnt,bel[maxn],out[maxn],sum[maxn],res,ans;
bool vis[maxn];
struct node {
    int next,to,f;
} e[maxn],ne[maxn];
void Add(int from,int to) {
    e[++cnt].next=h[from];
    e[cnt].to=to;
    e[cnt].f=from;
    h[from]=cnt;
    ne[++cnt].next=nh[to];
    ne[cnt].to=from;
    ne[cnt].f=to;
    nh[to]=cnt;
}
void DFS(int u) {
    vis[u]=1;
    for(int i=h[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(!vis[v])
            DFS(v);
    }
    S.push(u);
}
void kosaraju() {
    for(int i=1; i<=n; i++)
        if(!vis[i])DFS(i);
    queue<int>Q;
    while(!S.empty()) {
        int u=S.top();
        S.pop();
        if(bel[u])continue;
        Q.push(u);
        ans++;
        bel[u]=ans;
        sum[ans]++;
        while(!Q.empty()) {
            u=Q.front();
            Q.pop();
            for(int i=nh[u]; i; i=ne[i].next) {
                int v=ne[i].to;
                if(!bel[v]) {
                    bel[v]=ans;
                    sum[ans]++;
                    Q.push(v);
                }
            }
        }
    }
}
int main() {
    scanf("%d%d",&n,&m);
    while(m--) {
        int u,v;
        scanf("%d%d",&u,&v);
        Add(u,v);
    }
    kosaraju();
    for(int i=1; i<=cnt; i++) {
        int u=bel[e[i].f],v=bel[e[i].to];
        if(u!=v)
            out[u]++;
    }
    for(int i=1; i<=ans; i++)
        if(out[i]==0) {
            if(res==0)
                res=sum[i];
            else {
                res=0;
                break;
            }
        }
    printf("%d",res);
    return 0;
}

POJ1236

题目大意:给出一个不保证连通有向图,A节点向B节点发送文件,但是B不一定能发给A,求出向几个节点投放文件可以到达全图,并且求出加上几条单向边可以使整个有向图为强连通分量

思路:第一个问题的思路与上一个题目相似,直接统计入度为0的节点的数量即可。

对于第二个问题,统计入度为0的节点数量为f,出度为0的节点为g,最后答案为max(f,g),证明参考了POJ 1236(强连通分量+DAG性质),写的很通俗易懂,感谢

代码

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <queue>
//#include <unordered_map>
#include <map>
#include <set>
#include <numeric>
#include <stack>
#include <sstream>
#include <cmath>
#include <bitset>
//#include <unordered_set>
#include <functional>
#include <list>
#include <vector>
#include <iterator>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
//typedef __int128 Bint;
const int maxn=1e4+10;
int head[maxn],cnt,N,t;
int dfn[maxn],low[maxn],belong[maxn],in[maxn],out[maxn],ans,acc,point,edge;
bool vis[maxn];
stack<int>S;
struct node {
    int to,next,f;
} e[maxn];
void Add(int from,int to) {
    e[++cnt].to=to;
    e[cnt].f=from;
    e[cnt].next=head[from];
    head[from]=cnt;
}
void tarjan(int u) {
    dfn[u]=low[u]=++ans;
    vis[u]=1;
    S.push(u);
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(!dfn[v]) {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        if(vis[v])
            low[u]=min(low[u],low[v]);
    }
    if(low[u]==dfn[u]) {
        int v;
        acc++;
        do {
            v=S.top();
            S.pop();
            vis[v]=0;
            belong[v]=acc;
        } while(u!=v);
    }
}
int main() {
    while(~scanf("%d",&N)) {
        for(int i=1; i<=N; i++)
            while(scanf("%d",&t)&&t)
                Add(i,t);
        for(int i=1; i<=N; i++)
            if(!dfn[i])
                tarjan(i);
        for(int i=1; i<=cnt; i++) {
            int u=belong[e[i].f],v=belong[e[i].to];
            if(u!=v)
                in[v]++,out[u]++;//记录出度入度
        }
        for(int i=1; i<=acc; i++)//入度为0节点
            if(in[i]==0)
                point++;
        printf("%d\n",point);
        if(acc>1) {//直接缩成一点就不需要加边了
            for(int i=1; i<=acc; i++)//出度为0节点
                if(out[i]==0)
                    edge++;
            printf("%d\n",max(point,edge));
        } else
            printf("0\n");
        ans=acc=cnt=point=edge=0;
        memset(vis,0,sizeof(vis));
        memset(in,0,sizeof(in));
        memset(out,0,sizeof(out));
        memset(low,0,sizeof(low));
        memset(dfn,0,sizeof(dfn));
        memset(head,0,sizeof(head));
    }
    return 0;
}

POJ2375

题目大意:一个矩阵,每个坐标有值,代表高度,只能从一个点向他相邻的并且高度不大于它的点运动,现在想在某些点对之间加上缆车使得较低可以到较高,问最少需要多少缆车

思路:本题可以用tarjan,但没必要,可以较容易的看出,该题本质上是求让所有强连通分量连通需要加几条边

直接用BFS/DFS找到连通块并缩点,然后从高到低构建边,最后用POJ1236的结论直接输出结果即可

代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
const int maxn=1e3+9;
int a[maxn][maxn];
int con;
int ss[maxn*maxn],in[maxn*maxn],out[maxn*maxn];
int n,m;
struct {
    int t,s;
} que[maxn*maxn];
bool vis[maxn*maxn];
void bfs() {
    con=0;
    memset(vis,0,sizeof(vis));
    for(int i=1; i<=n; i++)
        for(int j=1; j<=m; j++)
            if(!vis[i*m+j]) {
                int front=1,end=0;
                que[++end].t=i;
                que[end].s=j;
                vis[i*m+j]=1;
                while(front<=end) {
                    int t=que[front].t,s=que[front++].s;
                    for(int p=-1; p<=1; p++)
                        for(int q=-1; q<=1; q++)
                            if(fabs(p)+fabs(q)==1&&t+p>=1&&t+p<=n&&s+q>=1&&s+q<=m)//没出边界且走1步
                                if(a[t][s]==a[t+p][s+q])//高度相等
                                    if(!vis[(t+p)*m+s+q]) {//未访问过
                                        vis[(t+p)*m+s+q]=1;
                                        que[++end].t=t+p;
                                        que[end].s=s+q;
                                    }
                }
                con++;
                for(int i=1; i<=end; i++)//缩点
                    ss[que[i].t*m+que[i].s]=con;
            }
}
int main() {
    while(~scanf("%d%d",&m,&n)) {
        for(int i=1; i<=n; i++)
            for(int j=1; j<=m; j++)
                scanf("%d",&a[i][j]);
        bfs();//缩点
        memset(in,0,sizeof(in));
        memset(out,0,sizeof(out));
        for(int i=1; i<=n; i++)
            for(int j=1; j<=m; j++)
                for(int p=-1; p<=1; p++)
                    for(int q=-1; q<=1; q++)
                        if(fabs(p)+fabs(q)==1&&i+p>=1&&i+p<=n&&j+q>=1&&j+q<=m)//未出界
                            if(a[i][j]>=a[i+p][j+q])//判断边的方向
                                if(ss[(i+p)*m+j+q]!=ss[i*m+j]) {//判断是否同一分量
                                    in[ss[(i+p)*m+j+q]]++;
                                    out[ss[i*m+j]]++;
                                }
        int ansin=0,ansout=0,ans;
        for(int i=1; i<=con; i++) {
            if(in[i]==0)
                ansin++;
            if(out[i]==0)
                ansout++;
        }
        ans=max(ansin,ansout);//确保连通,直接使用结论
        if(con<=1)
            printf("0\n");
        else
            printf("%d\n",ans);
    }
    return 0;
}

Luogu P3387

题目大意:略

思路:缩点+拓扑排序+DP

首先给出的为DAG,经过一次的点只算一次,题目要求最大解,那么可以直接经过一个环里的所有节点,自然而然就可以想到缩点,将图中所有强连通分量缩成一点,并将点权集中,动态规划的转移方程为dp[v]=max(dp[v],dp[u]+val[v]),用拓扑序以度为0的节点更新相邻节点的dp值

代码

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <queue>
#include <unordered_map>
#include <map>
#include <set>
#include <numeric>
#include <stack>
#include <sstream>
#include <cmath>
#include <bitset>
#include <unordered_set>
#include <functional>
#include <list>
#include <vector>
#include <iterator>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef __int128 Bint;
const int maxn=1e4+10;
int head[maxn],cnt,n,m,ans,acc,dfn[maxn],low[maxn],belong[maxn],dp[maxn];
int val[maxn],in[maxn],h[maxn],res;
bool vis[maxn];
stack<int>S;
queue<int>Q;
struct node {
    int to,next,f;
} e[maxn*10],ne[maxn*10];
void Add(int from,int to) {//初始建图
    e[++cnt].f=from;
    e[cnt].next=head[from];
    e[cnt].to=to;
    head[from]=cnt;
}
void Adde(int from,int to) {//缩点后建图
    ne[++cnt].f=from;
    ne[cnt].next=h[from];
    ne[cnt].to=to;
    h[from]=cnt;
    in[to]++;
}
void tarjan(int u) {
    low[u]=dfn[u]=++ans;
    vis[u]=1;
    S.push(u);
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(!dfn[v]) {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        if(vis[v])
            low[u]=min(low[u],low[v]);
    }
    if(low[u]==dfn[u]) {
        int v;
        do {
            v=S.top();
            S.pop();
            belong[v]=u;
            vis[v]=0;
            if(u==v)break;
            val[u]+=val[v];
        } while(1);
    }
}
int solve() {
    for(int i=1; i<=n; i++)
        if(belong[i]==i&&!in[i]) {
            Q.push(i);
            dp[i]=val[i];
        }
    while(!Q.empty()) {//拓扑序操作+更新
        int u=Q.front();
        Q.pop();
        for(int i=h[u]; i; i=ne[i].next) {
            int v=ne[i].to;
            dp[v]=max(dp[v],dp[u]+val[v]);
            in[v]--;
            if(in[v]==0)Q.push(v);
        }
    }
    int res=0;
    for(int i=1; i<=n; i++)
        res=max(res,dp[i]);
    return res;
}
int main() {
    scanf("%d%d",&n,&m);
    for(int i=1; i<=n; i++)
        scanf("%d",&val[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);
    cnt=0;
    for(int i=1; i<=m; i++) {
        int u=belong[e[i].f],v=belong[e[i].to];
        if(u!=v)Adde(u,v);
    }
    printf("%d",solve());
    return 0;
}

2020ICPC·小米 网络选拔赛第一场 D

题目大意:给出一个无向图,不保证连通,求分别把每个点删除后连通块的个数

思路:这道题是学习这个知识点的起因。

对每个节点进行一次tarjan判断:1,是否为割点,2,是几个连通块公用的割点,在tarjan过程中对每次遍历的新子节点判断去掉父节点后是否会生成新的连通块

代码

#include <bits/stdc++.h>
using namespace std;
typedef __int128 ll;
const int maxn=5e6+10;
int n,m,head[maxn],cnt,low[maxn],dfn[maxn],acc[maxn],f[maxn],sum,ans;
struct node {
    int to,next;
} e[maxn];
void Add(int from,int to) {
    e[++cnt].next=head[from];
    e[cnt].to=to;
    head[from]=cnt;
}
void tarjan(int u) {//当前节点
    dfn[u]=++ans;//时间戳
    low[u]=ans;//初始化
    int c=0;
    for(int i=head[u]; i; i=e[i].next) {//遍历
        int v=e[i].to;//dfs下一个节点
        if(!dfn[v]) { //如果没访问过
            f[v]=u;
            c++;
            tarjan(v);//下一节点
            low[u]=min(low[u],low[v]);//回溯更新
            if(f[u]&&low[v]>=dfn[u])acc[u]++;
            else if(f[u]==0&&c>=2)acc[u]++;
        }
        if(v==f[u])continue;
        low[u]=min(low[u],dfn[v]);//回溯更新
    }
}
int main() {
    cin >>n>>m;
    while(m--) {
        int u,v;
        cin >>u>>v;
        Add(u,v);
        Add(v,u);
    }
    for(int i=1; i<=n; i++)
        if(!dfn[i]) {
            ++sum;
            tarjan(i);
        }
    for(int i=1; i<=n; i++) {
        if(head[i]==0)printf("%d",sum-1);
        else printf("%d",sum+acc[i]);
        printf("%c",i!=n?' ':'\n');
    }
    return 0;
}

NC14411

题目大意:略

思路:一个基本的思路就是把有向图中的彼此相连的强连通分量找到,然后在强连通分量中判断有几个字符串是同构的,这里处理字符串有一个技巧,具体代码如下

void getmin(string& t) {//获得最小同构串
    int i=0,j=1,k=0,len=t.size();
    while(i<len&&j<len&&k<len) {
        char a=t[(i+k)%len],b=t[(j+k)%len];
        if(a==b)k++;
        else if(a>b)i=i+k+1,k=0;
        else if(a<b)j=j+k+1,k=0;
        if(i==j)j++;
    }
    t=(t+t).substr(min(i,j),len);
}

将所有输入的字符串都转换成其字典序最小/最大的形式,所有的形式必须相同(最大/最小),具体可以查看团结就是力量题解,这里只补充一点,关于min(i,j),是因为当确定了i/j之后,另一个必定增加直到最后一个元素,显然,增加的i/j必定不是解,此时标志着先前一个i/j为字典序最小的位置,(t+t).substr()是为了消除循环带来的位置不一,并且使得min(i,j) – len长度的字符串可取

代码

#include <bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;//开1e5过不了
int n,m,head[maxn],cnt,belong[maxn],dfn[maxn],low[maxn],ans,res;
bool vis[maxn];
string s[maxn];
unordered_map<string,int>mp;
stack<int>S;
struct node {
    int next,to;
} e[maxn];
void Add(int from,int to) {
    e[++cnt].to=to;
    e[cnt].next=head[from];
    head[from]=cnt;
}
void getmin(string& t) {//获得最小同构串
    int i=0,j=1,k=0,len=t.size();
    while(i<len&&j<len&&k<len) {
        char a=t[(i+k)%len],b=t[(j+k)%len];
        if(a==b)k++;
        else if(a>b)i=i+k+1,k=0;
        else if(a<b)j=j+k+1,k=0;
        if(i==j)j++;
    }
    t=(t+t).substr(min(i,j),len);
}
void init() {
    ans=cnt=res=0;
    memset(dfn,0,sizeof(int)*(n+1));
    memset(vis,0,sizeof(int)*(n+1));
    memset(low,0,sizeof(int)*(n+1));
    memset(belong,0,sizeof(int)*(n+1));
    memset(head,0,sizeof(int)*(n+1));
}
void tarjan(int u) {
    dfn[u]=low[u]=++ans;
    vis[u]=1;
    S.push(u);
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(!dfn[v]) {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        if(vis[v])
            low[u]=min(low[u],low[v]);
    }
    if(dfn[u]==low[u]) {
        int v;
        do {
            v=S.top();
            S.pop();
            belong[v]=u;
            mp[s[v]]++;//记录出现次数
            vis[v]=0;
        } while(v!=u);
    }
    for(auto p=mp.begin(); p!=mp.end(); p++)//判断整个强连通分量里面有几个同构
        res=max(res,p->second);
    mp.clear();//清空,mp只算一个强连通分量的
}
int main() {
    while(~scanf("%d%d",&n,&m)) {
        init();
        for(int i=1; i<=n; i++) {
            cin >>s[i];
            getmin(s[i]);
        }
        while(m--) {
            int x,y;
            scanf("%d%d",&x,&y);
            Add(x,y);
        }
        for(int i=1; i<=n; i++)
            if(!dfn[i])tarjan(i);
        printf("%d\n",res);
    }
    return 0;
}

NC15707

题目大意:略

思路:一开始自己想的是求出图的最大连通分量,然后其余的点都归在集合里,但这样做是不妥的,首先,图没有保证连通,其次,一个图中除了最大的强连通分量,还有其他规模较小的强连通分量,而这些较小的强连通分量实际上只取其中编号最小的即可,那么改进之后的思路如下:求出图中所有的强连通分量,选取各个强连通分量中编号最小的节点即可

代码

#include <bits/stdc++.h>
using namespace std;
const int maxn=1e6;
int n,m,cnt,ans,res,acc,head[maxn],dfn[maxn],low[maxn],pre[maxn],belong[maxn],in[maxn];
bool vis[maxn];
struct node {
    int to,next,f;
} e[maxn];
stack<int>S;
void Add(int from,int to) {
    e[++cnt].to=to;
    e[cnt].next=head[from];
    e[cnt].f=from;
    head[from]=cnt;
}
void tarjan(int u) {
    dfn[u]=low[u]=++ans;
    vis[u]=1;
    S.push(u);
    for(int i=head[u]; i; i=e[i].next) {
        int v=e[i].to;
        if(!dfn[v]) {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        if(vis[v])low[u]=min(low[u],low[v]);
    }
    if(dfn[u]==low[u]) {
        int v;
        ++acc;
        do {
            v=S.top();
            S.pop();
            belong[v]=acc;
            pre[acc]=min(pre[acc],v);//需要保留编号最小,因为是字典序
            vis[v]=0;
        } while(v!=u);
    }
}
int main() {
    scanf("%d%d",&n,&m);
    memset(pre,0x3f3f3f3f,sizeof(pre));//初始化编号为无穷大
    while(m--) {
        int u,v;
        scanf("%d%d",&u,&v);
        Add(u,v);
    }
    for(int i=1; i<=n; i++)if(!dfn[i])tarjan(i);
    if(acc==1) {
        printf("1\n");
        return 0;
    }
    for(int i=1; i<=cnt; i++) {//求入度
        int u=belong[e[i].f],v=belong[e[i].to];
        if(u!=v)in[v]++;
    }
    vector<int>t;
    for(int i=1; i<=acc; i++)if(!in[i])t.push_back(pre[i]);
    //统计入度为0的点集
    sort(t.begin(),t.end());//排序
    printf("%d\n",t.size());
    for(int i=0; i<t.size(); i++)//无控制格式
        printf("%d ",t[i]);
    printf("\n");
    return 0;
}

总结

这次有一个很大的收获——看视频比看文章更容易学会知识点,虽然也花了不少时间,但是视频里的讲解还是比较通俗易懂的。Tarjan算法的应用有很多,一般用于图&树上的连通性与祖先等相关问题,由Tarjan算法衍生出来的知识点有很多,强连通分量,缩点,割点割边,点/边双连通分量,最小点基等等,后期需要刻苦学习与训练,在遇到具体问题时不必生搬硬套固有算法,稍微的改变能够在不影响正确性的基础上提高效率

PS:本来计划中秋之前完成的,但是还是拖到了中秋第二天,此时窗外的月亮很亮,宿舍里很静,有一种别样的过节的气氛。祝所有看到此篇的读者中秋节快乐!

参考文献

  1. 60 分钟搞定图论中的 Tarjan 算法(一)
  2. tarjan算法总结 (强连通分量+缩点+割点),看这一篇就够了~
  3. ACM-ICPC设计系列 图论及应用
  4. noip讲堂tarjan求强连通
  5. [算法]轻松掌握tarjan割点&桥算法
  6. POJ 1236(强连通分量+DAG性质)
  7. poj 2375 Cow Ski Area bfs
  8. 团结就是力量题解
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值