图论(Tarjan算法与无向图)

一、基础

1、搜索树:在无向图中,我们以某一个节点 x 出发进行深度优先搜索,每一个节点只访问一次,所有被访问过的节点与边构成一棵树,称为无向连通图的搜索树

1、割点:若从图中删除节点x(以及所有与x关联的边之后),图将被分成不相连的子图,那么称 x 为图的割点

2、割边(桥):若从图中删除边e之后,图将分裂成两个不相连的子图,那么称e为图的割边或桥

3、时间戳:​用来标记图中每个节点在进行深度优先搜索时被访问的时间顺序,用 dfn[x] 来表示

4、追溯值:表示从当前节点x作为根节点出发,能够访问到的所有节点中,时间戳最小的值,用 low[x]来表示

计算追溯值:

(1)、先令low[x] = dfn[x]

(2)、若搜索树上x是y的父节点,则令low[x] = min(low[x], low[y])

(3)、若无向边(x,y)不是搜索树上的边,则令low[x] = min(low[x], dfn[y])

5、双连通分量:若一张无向连通图不存在割点,则称它为点双联通图,记为v-DCC;若一张无向连通图不存在桥,则称它为边双连通图,记为e-DCC

6、强连通分量:对于图中任意两个结点x、y,既存在从x到y的路径,也存在从y到x的路径,则称该图为强连通图;有向图的极大连通子图被称为强连通分量

7、Tarjan算法是基于深度优先搜索的算法,用于求解图的连通性问题。Tarjan算法可以在线性时间内求出无向图的割点与桥,进一步地可以求解无向图的双连通分量;同时,也可以求解有向图的强连通分量、必经点与必经边

二、求割边

一条边(u,v)是割边,当且仅当(u,v)为树枝边(即非负边),且满足:dfn(u)<low(v)(没有重边)

公式说明:从v的子树出发,在不经过(u, v)的前提下,不管走哪条路,都无法到达u或比u更早访问的结点。也就是说,u的儿子v之间只有一条边(无重边),且v点只能到u点到不了u点前

int dfn[maxn], low[maxn], bridge[maxn];
int to[maxn << 1], nex[maxn << 1], head[maxn];
int root, total, cnt;		//邻接表从2开始
void add(int u, int v)
{
	to[++cnt] = v;
	nex[cnt] = head[u];
	head[u] = cnt;
}
void Tarjan(int u, int father)
{
	dfn[u] = low[u] = ++total;
	for (int i = head[u]; i; i = nex[i])
	{
		int v = to[i];
		if (!dfn[v])
		{
			Tarjan(v, i);
			low[u] = min(low[u], low[v]);
			if (low[v] > dfn[u])
				bridge[i] = bridge[i ^ 1] = 1;	//记录路径
		}
		else if (i != (father ^ 1))			//判断重边
		{
			low[u] = min(low[u], dfn[v]);
		}
	}
}
void solve()
{
	int x, y, n, m;
	cin >> n >> m;
    cnt = 1;
	for (int i = 0; i < m; i++)
	{
		cin >> x >> y;
		add(x, y);
		add(y, x);
	}
	for (int i = 1; i <= n; i++)
	{
		if (!dfn[i])
			Tarjan(i, 0);
	}
	int ans = 0;
	for (int i = 2; i < cnt; i += 2) //邻接表存边从2开始
	{
		if (bridge[i])
			ans++;
	}
	cout << ans << '\n';
	for (int i = 2; i < cnt; i += 2)
	{
		if (bridge[i])
			cout << to[i ^ 1] << "->" << to[i];
	}
}

三、求边双连通分量

在求出所有桥的基础上,把桥都删去,就可以得到边双连通分量

实现:先用Tarjan算法标记所有桥边,然后对整个无向图进行dfs,历遍过程不访问桥边,由此划分出每个连通块

//在求割边的基础上加上以下代码
int c[maxn], dcc;		//存储每个点的归属,和dcc的数量
void dfs(int x)
{
	c[x] = dcc;
	for (int i = head[x]; i; i = nex[i])
	{
		int y =to[i];
		if (c[y] || bridge[i])
			continue;
		dfs(y);
	}
}

//下面加在main
for (int i = 1; i <= n; i++)
{
	if (!c[i])
	{
		dcc++;
		dfs(i);
	}
}
cout << dcc << '\n';
for (int i = 1; i <= n; i++)
{
	cout << i << " belongs to " << c[i] << '\n';
}

四、e-DCC缩点

在求出e-DCC的基础上,把每个e-DCC缩成一个点构成一棵新的树,存在邻接表中

int eto[maxn << 1], enex[maxn << 1], ehead[maxn], ecnt;
//在求出e-DCC的基础上加入以下代码
void add_dcc(int u, int v)
{
	eto[++ecnt] = v;
	enex[ecnt] = ehead[u];
	ehead[u] = ecnt;
}

//下面代码加到main
ecnt = 1;
for (int i = 2; i <= cnt; i++)
{
	int x = to[i ^ 1], y = to[i];
	if (c[x] == c[y])
		continue;
	add_dcc(c[x], c[y]);
}
for (int i = 2; i < ecnt; i+=2)
{
	cout << eto[i ^ 1] << eto[i];    //输出边
}

五、求割点

如果x不是搜索树的根节点,则x是割点当且仅当搜索树上存在x的一个子节点y,满足:dfn(x)<=low(y)

int  dfn[maxn], low[maxn], point[maxn];
int head[maxn], to[maxn << 1], nex[maxn << 1];
int cnt, total, root;
void add(int u, int v)
{
	to[++cnt] = v;
	nex[cnt] = head[u];
	head[u] = cnt;
}
void Tarjan(int u)
{
	dfn[u] = low[u] = ++total;
	int child = 0;
	for (int i = head[u]; i; i = nex[i])
	{
		int v = to[i];
		if (!dfn[v])
		{
			Tarjan(v);
			low[u] = min(low[u], low[v]);
			if (low[v] >= dfn[u] && u != root)
				point[u] = 1;
			if (u == root && ++child >= 2)
				point[u] = 1;
		}
		else
			low[u] = min(low[u], dfn[v]);
	}
}
void solve()
{
	int x, y, n, m;
	cin >> n >> m;
	for (int i = 1; i <= m; i++)
	{
		cin >> x >> y;
		add(x, y);
		add(y, x);
	}
	for (int i = 1; i <= n; i++)
	{
		if (!dfn[i])
		{
			root = i;
			Tarjan(i);
		}
	}
	int ans = 0;
	for (int i = 1; i <= n; i++)
	{
		if (point[i])
			ans++;
	}
	cout << ans << '\n';
	for (int i = 1; i <= n; i++)
	{
		if (point[i])
			cout << i << ' ';
	}
}

六、求点双连通分量

点双连通分量不是将割点删去后的剩余连通块,而是剩余连通块加上割点

我们将深搜时遇到的所有边加入到栈里面(第一次访问一个结点时入栈),当找到一个割点的时候(dfn(x)<=low(y)),就将这个割点往下走到的所有边弹出(从x开始向外弹出,直到y被弹出),而这些点构成的就是一个点双连通分量

int  dfn[maxn], low[maxn], point[maxn];
int head[maxn], to[maxn << 1], nex[maxn << 1];
int cnt, total, root;
stack<int>stk;
vector<int>dcc[maxn];
int vcnt;
void add(int u, int v)
{
	to[++cnt] = v;
	nex[cnt] = head[u];
	head[u] = cnt;
}
void Tarjan(int u)
{
	dfn[u] = low[u] = ++total;
	stk.push(u);
	if (u == root && head[u] == 0)
	{
		dcc[++vcnt].push_back(u);
		return;
	}
	int child = 0;
	for (int i = head[u]; i; i = nex[i])
	{
		int v = to[i];
		if (!dfn[v])
		{
			Tarjan(v);
			low[u] = min(low[u], low[v]);
			if (low[v] >= dfn[u])
			{
				if (u != root)
					point[u] = 1;
				vcnt++;
				int temp;
				do
				{
					temp = stk.top();
					stk.pop();
					dcc[vcnt].push_back(temp);
				} while (temp != v);
				dcc[vcnt].push_back(u);
			}
			if (u == root && ++child >= 2)
				point[u] = 1;
		}
		else
			low[u] = min(low[u], dfn[v]);
	}
}
void solve()
{
	int x, y, n, m;
	cin >> n >> m;
	for (int i = 1; i <= m; i++)
	{
		cin >> x >> y;
		add(x, y);
		add(y, x);
	}
	for (int i = 1; i <= n; i++)
	{
		if (!dfn[i])
		{
			root = i;
			Tarjan(i);
		}
	}
	cout << vcnt << '\n';
	for (int i = 1; i <= vcnt; i++)
	{
		cout << dcc[i].size() << ' ';
		for (int j = 0; j < dcc[i].size(); j++)
		{
			cout << dcc[i][j] << ' ';
		}
		cout << '\n';
	}
}

七、v-DCC缩点

由于一个割点可能属于多个点双连通分量,因此我们建一个包含p(割点数量)+t(v-DCC数量)个结点的新图,由每个割点连接v-DCC

//在求出v-DCC的基础上
int vto[maxn << 1], vnex[maxn << 1], vhead[maxn], vvcnt;
int newid[maxn],c[maxn];			//给割点的new id
void add_dcc(int u, int v)
{
	vto[++vvcnt] = v;
	vnex[vvcnt] = vhead[u];
	vhead[u] = vvcnt;
}

//在main中加入以下代码
int num = vcnt;
for (int i = 1; i <= n; i++)
{
	if (point[i])
		newid[i] = ++num;
}
vvcnt = 1;
for (int i = 1; i <= vcnt; i++)
{
	for (int j = 0; j < dcc[i].size(); j++)
	{
		int x = dcc[i][j];
		if (point[x])
		{
			add_dcc(i, newid[x]);
			add_dcc(newid[x], i);
		}
		else
			c[x] = i;
	}
}
for (int i = 2; i < vvcnt; i += 2)
{
	cout << vto[i ^ 1] << ' ' << vto[i] << '\n';
}

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值