tarjan好题

本文深入探讨了边双连通分量算法在解决特定类型问题中的应用,包括求解必经边、必经点及冗余路径等问题。通过实例分析,如洛谷上的CF652E题目和POI2008 BLO-Blockade题目,详细解释了算法原理、实现步骤及调试技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

CF652E 洛谷上的
这道题 草从早上调到晚上,最后由帅气的gigo_64同学 调出了我那个无比蒟蒻的垃圾渣渣nmsl草我无语了fuck idiot的问题
首先来分析这道题的做法
当我们看到这种类型的题 我们首先是不会想到边双连通分量的
所以只能多做 然后就熟练了
(说了和没说一样)
但是可以记住的是 边双联通分量可以解决的是缩点之快速求必经边,必经点之类的问题
在这道题中我们可以用边双连通分量缩点,缩完点之后就会是一棵树,然后直接从起点dfs到终点为止
我们可以在处理边双连通分量的时候就处理在该边双连通分量中是否包含魔法石
然后dfs的时候判断经过的边双连通分量或者是边是否包含魔法石
包含输出1 否则输出0
需要注意的一点是,多组数据在clear的时候 如果first数组赋值为-1 需要在遍历的时候 f o r ( i n t i = f i r s t [ x ] ; i ! = − 1 ; i = n x t [ i ] ) for(int i=first[x];i!=-1;i=nxt[i]) for(inti=first[x];i!=1;i=nxt[i]) emmm 我tm就是因为之前不知道-1是true的 然后判定条件只打了i 然后就凉凉 关键是凉凉成40分?错的数据贼大完全调试不出来
再次感谢gigo_64!!

#include <iostream>
#include <algorithm>
#include <cstring>
//记住了!-1也是true  只有0是false  以后把first数组赋值为-1  在for循环里面一定是  	for(int i=first[x];i!=-1;i=nxt[i])
//啊啊啊啊啊啊(土拨鼠狂怒)
//无能狂怒! 
using namespace std;
const int maxn=600010;
int first[maxn<<1],nxt[maxn<<1],to[maxn<<1],money[maxn<<1];
int dfn[maxn<<1],low[maxn<<1],times,tot;
int n,m,test;
int dcc,s,t;
int flag=0;
int jew[maxn<<1];
bool bridge[maxn<<1],visit[maxn<<1];
struct node{
	int a,b,stone;
}e[maxn<<1];
int c[maxn<<1];
inline int read()
{
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='0')	f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		x=x*10+ch-'0';
		ch=getchar();
	}
	return x*f;
}

inline void add(int x,int y,int z)
{
	tot++;
	nxt[tot]=first[x];
	first[x]=tot;
	to[tot]=y;
	money[tot]=z;
}

void tarjan(int x,int fa)
{
	dfn[x]=low[x]=++times;
	for(int i=first[x];i!=-1;i=nxt[i])
	{
		int y=to[i];
		if(!dfn[y])
		{
			tarjan(y,i);
			low[x]=min(low[x],low[y]);
			if(low[y]>dfn[x])
				bridge[i]=bridge[i^1]=true;
		}
		else if(i!=(fa^1))
			low[x]=min(low[x],dfn[y]);
	}
}

void dfs(int x)
{
	c[x]=dcc;
	for(int i=first[x];i!=-1;i=nxt[i])
	{
		int y=to[i];
		if(c[y] || bridge[i]) continue;
		dfs(y);
	}
}

void dfs1(int x,int fa,int have)
{
	if(x==c[t])
	{
		flag|=have;
		return ;
	}
	visit[x]=true;
	for(int i=first[x];i!=-1;i=nxt[i])
	{
		int y=to[i];
		if(visit[y] || y==fa)	continue;
		dfs1(y,x,money[i]|jew[y]|have);
	}
	visit[x]=false;
	return ;
}

int main()
{
	test=read();
	while(test--)
	{
		n=read();m=read();tot=1;
		memset(c,0,sizeof(c));
		memset(jew,0,sizeof(jew));
		memset(dfn,0,sizeof(dfn));
		memset(low,0,sizeof(low));
		memset(first,-1,sizeof(first));
		memset(bridge,false,sizeof(bridge));
		memset(visit,false,sizeof(visit));
		for(int i=1;i<=m;i++)
		{
			int x,y,z;
			x=read();y=read();z=read();
			add(x,y,z);add(y,x,z);
			e[i].a=x;e[i].b=y;e[i].stone=z;
		}
		for(int i=1;i<=n;i++)
			if(!dfn[i])
				tarjan(i,0);
		dcc=0;
		for(int i=1;i<=n;i++)
			if(!c[i])
			{
				++dcc;
				dfs(i);
			}
		tot=1;
		memset(first,-1,sizeof(first));
		for(int i=1;i<=m;i++)
		{
			int x=e[i].a,y=e[i].b;
			x=c[x],y=c[y];
			if(x==y)
			{
				jew[x]|=e[i].stone;
			} 
			else{
				add(x,y,e[i].stone);add(y,x,e[i].stone);
			}
		} 
		s=read();t=read();
		flag=0;
		if(c[s]==c[t] && jew[c[s]])	printf("YES\n");
		else if(c[s]==c[t] && !jew[c[s]])	printf("NO\n");
		else{
			dfs1(c[s],0,jew[c[s]]);//从s所在的边双向t所在的边双dfs 看经过的所有边双或桥是否权值为1	 
			if(flag>=1)	printf("YES\n");
			else printf("NO\n");
		}	
	}
	return 0;
}

冗余路径Redundant Paths
这道题也是先边双连通分量,首先在一个双连通分量中,任意两个点之间的路径肯定是有两条且互不相交的 所以我们需要处理的是加边后的所有原来的边连通分量变为一个边连通分量 求加边数量最少
那么怎么加边呢?这是个玄学的问题。我肯定是不会的这辈子都不会的。然后我看了看别人的代码,恍然大悟
每次在两个最近公共祖先最远的两个叶节点之间连接一条边 然后形成一个环 然后再次进行上述操作 总共加边 ( c n t + 1 ) / 2 (cnt+1)/2 (cnt+1)/2 ,其中cnt表示边连通分量的数量 然后这就是答案
总的这道题不难 要想到如何加边 当然如果实在想不到,可以看一下别人的思路,然后多做一做这种类型的题 这样你就可以在看到这种题的时候很快的想到思路

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#define int long long 
using namespace std;
const int maxn=10010;
int first[maxn<<1],next[maxn<<1],to[maxn<<1];
int dfn[maxn],low[maxn],deg[maxn],times,ans;
int dcc,c[maxn],tot,n,m;
bool bridge[maxn];
struct node{
	int a,b;
}e[maxn];
inline int read()
{
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-')
			f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		x=x*10+ch-'0';
		ch=getchar();
	}
	return x*f;
}

inline void add(int x,int y)
{
	tot++;
	next[tot]=first[x];
	first[x]=tot;
	to[tot]=y;
}

void tarjan(int x,int fa)
{
	dfn[x]=low[x]=++times;
	for(int i=first[x];i;i=next[i])
	{
		int y=to[i];
		if(!dfn[y])
		{
			tarjan(y,i);
			low[x]=min(low[x],low[y]);
			if(low[y]>dfn[x])
				bridge[i]=bridge[i^1]=true;
		}
		else if(i!=(fa^1))//求割边不能用这条边的反向边来更新 
			low[x]=min(low[x],dfn[y]);
	}
}

void dfs(int x)
{
	c[x]=dcc;
	for(int i=first[x];i;i=next[i])
	{
		int y=to[i];
		if(c[y] || bridge[i]) continue;
		dfs(y);
	}
}

signed main()
{
	n=read();m=read();tot=1;
	for(int i=1;i<=m;i++)
	{
		int x,y;x=read();y=read();
		e[i].a=x;
		e[i].b=y;
		add(x,y);add(y,x);
	}
	for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0);
	for(int i=1;i<=n;i++)
	{
		if(!c[i]){
			++dcc;dfs(i);
		}
	}
	for(int i=1;i<=m;i++)
	{
		if(c[e[i].a]!=c[e[i].b])
			deg[c[e[i].a]]++,deg[c[e[i].b]]++;
	}
	for(int i=1;i<=dcc;i++)
	{
		if(deg[i]==1)	ans++;
	}
	printf("%lld",(ans+1)/2);
	return 0;
}

[POI2008]BLO-Blockade
这道题我觉得很经典 首先给你一个无向连通图,求任何一个点被封锁后 有多少个有序点对 ( x , y ) (x,y) (x,y)满足 x x x到达不了 y y y
首先注意是有序点对。
然后我们分析一个点被封锁后会导致多少个有序点对无法到达
令被封锁的点为i
若i不是割点 那么只会有从i其他点 和其他点到i的有序点对无法到达 ans即为 2 ∗ ( n − 1 ) 2*(n-1) 2(n1)
若i是割点 那么就有一点恶心了 喵喵喵?
若i是割点,那么把i删除后就会形成很多个联通块,我们首先求出所有联通块的大小,两两相乘再相加。在搜索树中,节点i的所有子节点(注意是子节点,而不是子树节点)的集合中,有t个点满足割点法则 d f n [ i ] < = l o w [ s k ] dfn[i]<=low[sk] dfn[i]<=low[sk]。于是封锁节点后,无向图至多分成t+2个连通块 分别为:i本身;以搜索树上满足割点法则的t个节点为根的连通块;以及除了上述点以外的所有点构成的集合(即i的父亲所在的集合)。
然后就可以愉快的解决这道题了 看起来好棒的亚子

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#define int long long 
using namespace std;
const int maxn=100010,maxm=500010;
int first[maxm<<1],next[maxm<<1],to[maxm<<1];
bool cut[maxn];
int n,m,x,y,tot=1;
int ans[maxn],dfn[maxn],low[maxn],times,size[maxn];
inline int read()
{
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch=='-')	f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9')
	{
		x=x*10+ch-'0';
		ch=getchar();
	}
	return x*f;
}

inline void add(int u,int v)
{
	tot++;
	next[tot]=first[u];	
	first[u]=tot;
	to[tot]=v;
}

void tarjan(int x)
{
	dfn[x]=low[x]=++times;size[x]=1;
	int flag=0,sum=0;
	for(int i=first[x];i;i=next[i])
	{
		int y=to[i];
		if(!dfn[y])
		{
			tarjan(y);
			low[x]=min(low[x],low[y]);
			size[x]+=size[y];
			if(low[y]>=dfn[x])
			{
				flag++;
				ans[x]+=size[y]*(n-size[y]);
				sum+=size[y];
				if(x!=1 || flag>1) cut[x]=true;
			}	
		}
		else low[x]=min(low[x],dfn[y]);
	}
	if(cut[x])
		ans[x]+=(n-sum-1)*(sum+1)+(n-1);
	else ans[x]=2*(n-1);
}

signed main()
{
	n=read();m=read();
	for(int i=1;i<=m;i++)
	{
		x=read();y=read();
		if(x==y)	continue;
		add(x,y);add(y,x);
	}
	tarjan(1);
	for(int i=1;i<=n;i++)
		printf("%lld\n",ans[i]);
	return 0;
	
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值