进阶图论

进阶图论

I. 割点与桥

首先,我们得了解割点的含义

割点

对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)。

通俗点说,就是连接两个或多个连通分量的公共点。

如何求割点呢,这里引用一个算法:Ttarjan

定义 dfn[i] 为 i 点DFS访问的顺序 , low[i] 为 i 点不经过其父亲能到达的最小的DFS序。

接着,我们开始DFS。

怎样判断割点呢, 对于一个顶点u,如果存在至少一个顶点 v (u的儿子),使得 low[v] > low[u] ,即不能回到祖先,那么u点为割点。(为什么自己想)

当然,此根据不适用搜索的起始点。(也是自己想)

更新low的代码:

if(!dfn[v])
    low[u] = min(low[u], low[v]);
else if(instack[i])
    low[u] = min(low[u], dfn[v]);

instack[i] 表示 i 点有没有被处理。u 是当前节点, v 是这个节点的所有邻居。

最后就是整体代码。

/*
洛谷 P3388 【模板】割点(割顶)
*/
#include <iostream>
#include <vector>
using namespace std;
int n, m;  // n:点数 m:边数
int dfn[100001], low[100001], idx, res;
// dfn:记录每个点的时间戳
// low:能不经过父亲到达最小的编号,idx:时间戳,res:答案数量
bool vis[100001], flag[100001];  // flag: 答案 vis:标记是否重复
vector<int> edge[100001];        // 存图用的

void Tarjan(int u, int fa) {  // u 当前点的编号,fa 自己爸爸的编号
  vis[u] = true;              // 标记
  low[u] = dfn[u] = ++idx;    // 打上时间戳
  int child = 0;              // 每一个点儿子数量
  for (const auto &v : edge[u]) {  // 访问这个点的所有邻居 (C++11)
    if (!vis[v]) {
      child++;                       // 多了一个儿子
      Tarjan(v, u);                  // 继续
      low[u] = min(low[u], low[v]);  // 更新能到的最小节点编号
      if (fa != u && low[v] >= dfn[u] && !flag[u]) {  // 主要代码
        // 如果不是自己,且不通过父亲返回的最小点符合割点的要求,并且没有被标记过
        // 要求即为:删了父亲连不上去了,即为最多连到父亲
        flag[u] = true;
        res++;  // 记录答案
      }
    } else if (v != fa) {
      // 如果这个点不是自己的父亲,更新能到的最小节点编号
      low[u] = min(low[u], dfn[v]);
    }
  }
  // 主要代码,自己的话需要 2 个儿子才可以
  if (fa == u && child >= 2 && !flag[u]) {
    flag[u] = true;
    res++;  // 记录答案
  }
}

int main() {
  cin >> n >> m;                  // 读入数据
  for (int i = 1; i <= m; i++) {  // 注意点是从 1 开始的
    int x, y;
    cin >> x >> y;
    edge[x].push_back(y);
    edge[y].push_back(x);
  }  // 使用 vector 存图
  for (int i = 1; i <= n; i++)  // 因为 Tarjan 图不一定连通
    if (!vis[i]) {
      idx = 0;       // 时间戳初始为 0
      Tarjan(i, i);  // 从第 i 个点开始,父亲为自己
    }
  cout << res << endl;
  for (int i = 1; i <= n; i++)
    if (flag[i]) cout << i << " ";  // 输出结果
  return 0;
}

割边(无重边)

和割点差不多,叫做桥(点换成边嘛)。不做解释。

更新的方法和割点差不多, 只需改一处low[v] > dfn[u],也就是不用判断根节点了。

下面代码实现了对 无重边 的无向图求割边,其中,当 isbridge[x] 为真时,(father[x],x) 为一条割边。

int low[MAXN], dfn[MAXN], idx;
bool isbridge[MAXN];
vector<int> G[MAXN];
int cnt_bridge;
int father[MAXN];

void tarjan(int u, int fa) {
  father[u] = fa;
  low[u] = dfn[u] = ++idx;
  for (const auto &v : G[u]) {
    if (!dfn[v]) {
      tarjan(v, u);
      low[u] = min(low[u], low[v]);
      if (low[v] > dfn[u]) {
        isbridge[v] = true;
        ++cnt_bridge;
      }
    } else if (v != fa) {
      low[u] = min(low[u], dfn[v]);
    }
  }
}

en就这么多。

圆方树

点双联通分量不是很好缩点,但是我们可以建立圆方树:每个点双建一个点,称之为方点,原图中每个点也建一个新点,称之为原点。每个方点与属于他的圆点连边,就构成了一棵树,称为圆方树。

图例

下面的图显示了一张图对应的点双和圆方树形态

imgimgimg

因为圆方树是基于割点的,所以只需要用类似求割点的方法构建圆方树。

void Tarjan(int u) {
  low[u] = dfn[u] = ++dfc;                // low 初始化为当前节点 dfn
  for (int v : G[u]) {                    // 遍历 u 的相邻节点
    if (!dfn[v]) {                        // 如果未访问过
      Tarjan(v);                          // 递归
      low[u] = min(low[u], low[v])	  // 未访问的和 low 取 min
      if (low[v] == dfn[u])  			 // 标志着找到一个以 u 为根的点双连通分量
        ++cnt;                 			 // 增加方点个数
         // 将点双中除了 u 的点退栈,并在圆方树中连边
      for (int x = 0; x != v; --tp) 
      {
         x = stk[tp];
         T[cnt].push_back(x);
         T[x].push_back(cnt);
     }
     // 注意 u 自身也要连边(但不退栈)
    T[cnt].push_back(u);
    T[u].push_back(cnt);
    else
      low[u] = min(low[u], dfn[v]);  // 已访问的和 dfn 取 min
  }
}

II.网络流

网络流是指一个特殊的有向图。

这个图的特殊点在于有两个特殊的点,源点和汇点,还有一个类似边权的容量

网络流满足以下限制

1.容量限制:对于每条边,流经该边的流量不得超过该边的容量

2.流守恒性:除源汇点外,任意结点 u的净流量为0.(也就是流入和流出的流量一样)

最大流

定义:

给定网络G及G上的流 f.

对于边(u, v),我们将其容量与流量之差称为剩余容量c(u,v)

我们将G中所有结点和剩余容量大于0的边构成的子图称为残量网络.

我们将G上一条从源点s到

Edmonds–Karp算法(简称EK)

对于每一个容量

posted @ 2025-04-01 20:50  HEGVDV  阅读(57)  评论(0)    收藏  举报