首先,(tarjan)是干什么用的?在学之前,我就知道一个名为“缩点”的模板题要用(tarjan)算法来解决,所以我对这个算法是这样理解的。把一堆点在不影响题目的情况下缩成一个点,以转化为(DAG)(有向无环图)快速求解。其实我觉得模板题正大大体现了(tarjan)的优势,就拿模板题来讲一讲这个算法吧。
思路
这题非常好的体现了(tarjan)的优势和用武之处。如果一条边和一个点能无数次重复经过,那么也就是说只要两个点能相互到达,我们就可以把它看做一个点。由于每个点的点权都是正的,那么缩成一个点绝对不会影响结果。正确性很显然,如果你能两个点一起拿再回来,那为什么不拿呢?所以说只要把所有的强联通分量缩成一个点,然后进行拓扑排序(dp)即可。
代码
说起来简单,但是代码如何实现呢?以下参考了《算法竞赛进阶指南》:首先从任意一个点开始深搜,搜到出度为(0)的节点停止,不重复经过同一个点,这样搜完之后将形成的树称为“搜索树”。还有一个“强联通分量”的定义,按我自己的理解,就是一堆点都能通过它们之间的路径相互到达,那么这些点就构成一个强联通分量,也就是我们要执行缩点的目标。开一个栈,来记录节点之间的父子关系。(dfn)表示时间戳,第(i)个被访问的点时间戳就是(i),(low)表示以这个点为子树的根结点,能访问到的最小编号的节点的编号。由于我们是按搜索树编号的,所以很显然,搜索树上一条边的终点时间戳肯定比起点大。所以说,如果两点能相互到达,那么肯定会更新这个点的(low),所以说就是一个强联通分量,而且两点中间中间的所有具有父子关系的点都可以相互到达。所以说如果一个点的(low)和(dfn)相等的话,那么它就已经不能和后面的点构成强联通分量了,就只能往前找,去靠拢前面的点。
void tarjan(int x){
low[x]=dfn[x]=++num;
sta[++top]=x;
ins[x]=1;
for(int i=head[x];i;i=Nxt[i]){
int y=ver[i];
if(!dfn[y]){//没有访问过就继续搜
tarjan(y);
low[x]=min(low[x],low[y]);//更新low
}
else{
if(ins[y]){//如果有夫子关系,那么也更新
low[x]=min(low[x],dfn[y]);
}
}
}
if(low[x]==dfn[x]){//这个点已经到头了,那么往回找
while(1){
int y=sta[top];
ins[y]=0;//一定要出栈,因为它已经跟后面的点构不成强联通分量了
c[y]=x;
top--;
if(x!=y){
p[x]+=p[y];
}
else{
break;
}
}
}
}
这样(tarjan)就差不多了(主要是写给自己看的)
然后再拓扑排序一下,简单(dp)就搞定了
完整代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<vector>
#include<queue>
#define ll long long
using namespace std;
const int N=200005,M=400005;
int n,m,tot,top,tc,num,cnt;
ll c[N],head[N],edge[M],Nxt[M],ver[M],low[N],dfn[N],sta[N],p[N],w[N],Nc[M],vc[M],hc[N],f[N],in[N],ans;
bool ins[N];
queue<int> q;
void add(int x,int y){
ver[++tot]=y;
Nxt[tot]=head[x];
head[x]=tot;
}
void add_c(int x,int y){
vc[++tc]=y;
Nc[tc]=hc[x];
hc[x]=tc;
}
void tarjan(int x){
low[x]=dfn[x]=++num;
sta[++top]=x;
ins[x]=1;
for(int i=head[x];i;i=Nxt[i]){
int y=ver[i];
if(!dfn[y]){
tarjan(y);
low[x]=min(low[x],low[y]);
}
else{
if(ins[y]){
low[x]=min(low[x],dfn[y]);
}
}
}
if(low[x]==dfn[x]){
while(1){
int y=sta[top];
ins[y]=0;
c[y]=x;
top--;
if(x!=y){
p[x]+=p[y];
}
else{
break;
}
}
}
}
void topo(){
for(int i=1;i<=n;i++){
if(!in[i]&&c[i]==i){
q.push(i);
f[i]=p[i];
// printf("%d
",f[i]);
}
}
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=hc[x];i;i=Nc[i]){
int y=vc[i];
in[y]--;
f[y]=max(f[y],f[x]+p[y]);
if(in[y]==0){
q.push(y);
}
}
}
for(int i=1;i<=n;i++){
ans=max(ans,f[i]);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%lld",&p[i]);
}
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
}
for(int i=1;i<=n;i++){
if(!dfn[i]) tarjan(i);
}
for(int x=1;x<=n;x++){
for(int i=head[x];i;i=Nxt[i]){
int y=ver[i];
if(c[x]==c[y]) continue;
add_c(c[x],c[y]);
in[c[y]]++;
}
}
topo();
printf("%lld
",ans);
return 0;
}
例题:洛谷P2002 消息扩散
思路
发现这个题貌似比板子题还简单,都不用缩完点之后拓扑排序,缩完点直接看有几个点入度为(0)即可,因为入度为(0)的点不可能通过别的节点到达。反之,如果入度不为(0),那么肯定能到达。
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<vector>
#include<queue>
#define ll long long
using namespace std;
const int N=100005,M=500005;
int n,m,tot,tc,num,top,ans;
int head[N],ver[M],Nxt[M],c[N],sta[N],dfn[N],low[N],in[N];
bool ins[N];
void add(int x,int y){
ver[++tot]=y;
Nxt[tot]=head[x];
head[x]=tot;
}
void tarjan(int x){
dfn[x]=low[x]=++num;
sta[++top]=x;
ins[x]=1;
for(int i=head[x];i;i=Nxt[i]){
int y=ver[i];
if(!dfn[y]){
tarjan(y);
low[x]=min(low[x],low[y]);
}
else if(ins[y]){
low[x]=min(low[x],low[y]);
}
}
if(low[x]==dfn[x]){
while(1){
int y=sta[top];
top--;
c[y]=x;
ins[y]=0;
if(x==y){
break;
}
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1,x,y;i<=m;i++){
scanf("%d%d",&x,&y);
add(x,y);
}
for(int i=1;i<=n;i++){
if(!dfn[i]) tarjan(i);
}
for(int x=1;x<=n;x++){
for(int i=head[x];i;i=Nxt[i]){
int y=ver[i];
if(c[x]==c[y]) continue;
in[c[y]]++;
}
}
for(int i=1;i<=n;i++){
if(!in[i]&&c[i]==i){
ans++;
}
}
printf("%d
",ans);
return 0;
}
例题:洛谷P2341 [USACO03FALL][HAOI2006]受欢迎的牛 G
思路
这个题比上个题稍微多想那么一点点点点。首先如果缩点之后有两个点的出度都为(0),那么其中一个点必然不会得到另一个点的“支持”,所以就有(0)只受欢迎的牛。反之,只需要看那唯一一个出度为(0)的点缩点前内含几个点就行了
代码
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long int ll;
const int N=10005,M=50005;
int n,m,tot,top,num,ans;
int head[N],ver[M],Nxt[M],sta[N],dfn[N],low[N],p[N],c[N],f[N],out[N];
bool ins[N];
queue<int> q;
void add(int x,int y){
ver[++tot]=y;
Nxt[tot]=head[x];
head[x]=tot;
}
void tarjan(int x){
low[x]=dfn[x]=++num;
ins[x]=1;
sta[++top]=x;
for(int i=head[x];i;i=Nxt[i]){
int y=ver[i];
if(!dfn[y]){
tarjan(y);
low[x]=min(low[x],low[y]);
}
else if(ins[y]){
low[x]=min(low[x],low[y]);
}
}
if(dfn[x]==low[x]){
while(1){
int y=sta[top];
ins[y]=0;
top--;
c[y]=x;
p[x]++;
if(x==y) break;
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1,x,y;i<=m;i++){
scanf("%d%d",&x,&y);
add(x,y);
}
for(int i=1;i<=n;i++){
if(!dfn[i]) tarjan(i);
}
for(int i=1;i<=n;i++){
if(i==c[i]){
f[i]=p[i];
}
}
for(int x=1;x<=n;x++){
for(int i=head[x];i;i=Nxt[i]){
int y=ver[i];
if(c[x]==c[y]) continue;
out[c[x]]++;
}
}
int sum=0;
for(int i=1;i<=n;i++){
if(i==c[i]&&out[i]==0){
sum++;
ans=p[i];
}
}
if(sum>1){
printf("0
");
return 0;
}
printf("%d
",ans);
return 0;
}