割边 + 缩点(得到边连通分量) + 朴素LCA
Posted daybreaking
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了割边 + 缩点(得到边连通分量) + 朴素LCA相关的知识,希望对你有一定的参考价值。
用到的算法
割边 + 缩点(得到边连通分量) + 朴素LCA
算法解析
-
无向图区分重边与同一条边的反方向: 对每一条边都用一个变量id来标识,一条无向边的两个方向用同一个id表示。
-
割边:
c++ if(low[v] > dfn[u])
,即以点v为根的子树不能到达点u及以上,所以边uv为一条割边。 -
缩点(得到边双连通分量): 去掉桥后,图就变成了若干个隔离的边双连通分量,因为将这些分量缩点。
- 实现方法:直接对整个图进行dfs,如果有些点属于同一个连通分量,则可以被相同的变量值标记,代表同一个点。这样以每一个没有被标记过的点为起点进行dfs,则完成缩点。
-
朴素LCA:离线查询,时间复杂度是两点距离最近公共祖先的距离和,
- 思想:若两点的深度不一样,则深度大的结点向上跳;若两点多的深度一样但不是同一个结点,则两点同时向上跳。
-
其他注意的点
-
求得割边后,如何找到它同一条边反方向的边?利用疑惑操作的性质,若a是奇数,则 a ^ 1 = a - 1; 若a是偶数,则 a ^ 1 = a + 1.
-
由于LCA是对树的操作, 所以新图必须要找到一个根节点,来确定其它结点的深度。可以让1号结点作为根节点,因为缩点时,新形成点的序号是从1开始的。
-
添加边后,需要对途径的边进行标记,防止下次再减一遍,但LCA是对点的操作,如何标记边?判断边的下端点,若点需要向上跳且该点还未标记过,则割边数--。
-
//poj 3694
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <queue>
#include <stack>
#include <vector>
#include <deque>
#include <map>
#include <iostream>
using namespace std;
typedef long long LL;
const double pi = acos(-1.0);
const double e = exp(1);
//const int MAXN =2e5+10;
const LL N = 1000000007;
struct edge
{
int id;
int flag;
int from;
int to;
int next;
} edge[200009], edge3[200009];
int head[200009];
int head3[200009]; //描述新图中结点之间的关系
int dfn[100009];
int low[100009];
int cnt = 1;
int cnt3 = 0; //用于构建缩点后新图的链式前向星
int New[100009]; //第i个双联通分量所含有的结点个数
int captain[100009]; //点i所在的边双连通分量
int father[100009];
int vis[100009];
int deep[100009];
int sum;
void tarjan(int u, int id) //需要考虑去重边的问题
{
int i;
low[u] = dfn[u] = cnt++;
for (i = head[u]; i != -1; i = edge[i].next)
{
int v = edge[i].to;
if(id == edge[i].id) //判断是不是同一条边,若id相同则为同一条
continue;
if (!dfn[v])
{
tarjan(v, edge[i].id);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u])
{
edge[i].flag = edge[i ^ 1].flag = 1; // 标记割边,在寻找边双连通分量时忽略掉该边
// printf("%d ---> %d
", edge[i].from, edge[i].to);
}
}
else
{
low[u] = min(low[u], dfn[v]);
}
}
}
void seek_doubleEdge(int u, int cnt2) //缩点
{
int i;
dfn[u] = cnt2;
New[cnt2]++;
captain[u] = cnt2;
for (i = head[u]; i != -1; i = edge[i].next)
{
int v = edge[i].to;
if (edge[i].flag) //连接新形成的点
{
if (captain[v]) //如果所连接的点还没有缩点,则跳过。由于是条无向边且当前端点已经缩点,当对无向边的另一个端点缩点时可以建立一条边。
{
int a = captain[u];
int b = captain[v];
edge3[cnt3].to = b;
edge3[cnt3].next = head3[a];
head3[a] = cnt3++;
edge3[cnt3].to = a;
edge3[cnt3].next = head3[b];
head3[b] = cnt3++;
// cout << u << " " << v << " " << a << " "<< b << " ** " << endl;
}
continue;
}
else
{
if (!dfn[v])
{
seek_doubleEdge(v, cnt2);
}
}
}
}
void init_lca(int u, int fa, int d)
{
int i;
deep[u] = d;
father[u] = fa;
// printf("%d %d ??
", u, deep[u]);
for (i = head3[u]; i != -1; i = edge3[i].next)
{
int v = edge3[i].to;
if (!deep[v])
{
//printf("%d %d %d (^_^)
", v, u, d);
init_lca(v, u, d + 1);
}
}
}
int lca(int a, int b)
{
while (deep[a] < deep[b])
{
if (vis[b] == 0 && b != 1)
{
sum--;
vis[b] = 1;
}
b = father[b];
}
while (deep[a] > deep[b])
{
if (vis[a] == 0 && a != 1)
{
sum--;
vis[a] = 1;
}
a = father[a];
}
while (deep[a] == deep[b] && a != b)
{
if (vis[a] == 0 && a != 1)
{
sum--;
vis[a] = 1;
}
if (vis[b] == 0 && b != 1)
{
sum--;
vis[b] = 1;
}
a = father[a];
b = father[b];
}
return sum;
}
int main()
{
int n, m, i;
int cnt1, a, b, q, nn = 0;
while (scanf("%d%d", &n, &m) != EOF)
{
if (n == 0 && m == 0)
break;
nn++;
cnt1 = 0;
cnt = 1;
memset(head, -1, sizeof(head));
memset(dfn, 0, sizeof(dfn));
memset(low, 0, sizeof(low));
memset(captain, 0, sizeof(captain));
while (m--)
{
scanf("%d%d", &a, &b);
edge[cnt1].id = cnt1; //[1]
edge[cnt1].flag = 0;
edge[cnt1].from = a;
edge[cnt1].to = b;
edge[cnt1].next = head[a];
head[a] = cnt1++;
edge[cnt1].id = cnt1 - 1; //[2],同[1]处注释一起为一条无向边的来回方向都标记同一个id.
edge[cnt1].flag = 0;
edge[cnt1].from = b;
edge[cnt1].to = a;
edge[cnt1].next = head[b];
head[b] = cnt1++;
}
for (i = 1; i <= n; i++) //双连通分量去割边
{
if (!dfn[i])
{
tarjan(i, -1);
}
}
memset(dfn, 0, sizeof(dfn));
memset(head3, -1, sizeof(head3));
memset(vis, 0, sizeof(vis));
memset(deep, 0, sizeof(deep));
int cnt2 = 0;
for (int i = 1; i <= n; i++) //标记双连通分量(缩点)
{
if (!dfn[i])
{
cnt2++;
seek_doubleEdge(i, cnt2);
}
}
/*
cout << " ** " << cnt2 << endl; // 边双连通分量的个数
for (int i = 1; i <= n; i++)
{
printf("* %d %d
", i, captain[i]);
}
for (int i = 1; i <= cnt2; i++) //输出缩点后的新图
{
for (int j = head3[i]; j != -1; j = edge3[j].next)
{
cout << i << " " << edge3[j].to << endl;
}
}
*/
sum = cnt2 - 1;
init_lca(1, -1, 1);
/* for (int i = 1; i <= cnt2; i++)
{
cout << i << " " << deep[i] << " ?? " << endl;
}
*/
printf("Case %d:
", nn);
scanf("%d", &q);
while (q--)
{
scanf("%d%d", &a, &b);
if (captain[a] != captain[b])
{
lca(captain[a], captain[b]);
}
printf("%d
", sum);
}
printf("
");
}
return 0;
}
以上是关于割边 + 缩点(得到边连通分量) + 朴素LCA的主要内容,如果未能解决你的问题,请参考以下文章
tarjan算法(强连通分量 + 强连通分量缩点 + 桥 + 割点 + LCA)
Tarjan应用:求割点/桥/缩点/强连通分量/双连通分量/LCA(最近公共祖先)