「Cable TV Network」 有线电视网络

GitHub跳转原题关系图返回列表

求无向图的点连通度:拆点把点权 1 变成边容量 1、原图的边给无穷容量,枚举所有点对跑最大流取最小割。

OJ: roj

题目 ID: 3207

难度:提高

标签:图论网络流最小割最大流python

创建: 2026-10-02 01:46

更新: 2026-10-10 13:00

形式化题目

给定无向图 G=(V,E)G=(V,E),∣V∣=n≤50|V| = n \le 50。求

κ(G)=min⁡{∣S∣:S⊆V, G−S 不连通}, \kappa(G)=\min\{|S|:S\subseteq V,\ G-S \text{ 不连通}\},

若对任何 S⊆VS \subseteq V 都做不到,则输出 nn。

这里 G−SG-S 表示"删掉 SS 中所有顶点及其关联边"后剩下的图,"不连通"指剩下至少两个连通块; 删到一个点都不剩、或只剩一个点,都不算不连通。

记 λ(s,t)\lambda(s,t) 为只要求把 s,ts,t 分到不同连通块的点最小割(s,ts,t 本身不许删),则本 题答案是

min⁡(min⁡s≠tλ(s,t), n). \min\Bigl(\min_{s\ne t}\lambda(s,t),\ n\Bigr).

边界情形直接照定义读:

输入 图的样子 答案 理由
0 0 空图 0 无点可言,谈不上连通
1 0 单点 1 =n=n 没有点对,删不到"不连通"
2 0 两个孤立点 0 已经是两块
3 3 三角形 K3K_3 3 =n=n 删 1 个点剩 1 个点,仍算连通
5 7(样例第 5 组) 两个三角形 0−1−20{-}1{-}2 与 1−3−41{-}3{-}4,再加边 2−32{-}3 2 删 {1,2}\{1,2\} 后剩下 {0}\{0\} 与 {3,4}\{3,4\}

正解

思路

1. 朴素做法与瓶颈

最老实的方法是枚举删除集合,从小到大试:

text
for k = 0, 1, 2, ..., n:
    for 每个 |S| = k 的 S:
        if G - S 不连通: return k
return n

n=50n=50 时 ∑k(50k)≈1015\sum_k \binom{50}{k} \approx 10^{15} 个子集,哪怕判连通只要 O(n+m)O(n+m) 也不可能。 瓶颈不在判连通的常数,而在把答案当成对子集空间的搜索。 换一个角度:连通性只取决于"有没有 s→ts \to t 的路径", 如果能对每对 (s,t)(s,t) 快速回答"切断所有 s→ts\to t 路径最少要删几个点", 再对所有点对取最小就够了。("不连通"要求剩下的图至少有两个连通块,所以 SS 删完至少要剩 2 个点; 下面会把 s,ts,t 排除在 SS 之外,正好满足这一点。)而"切断所需的最小代价"就是最小割。

2. 从删点到最小割:拆点

最小割的经典形态割的是边,本题删的是点。标准转换叫拆点:把每个顶点 vv 拆成 vinv_{in} 与 voutv_{out},中间连一条 vin→voutv_{in} \to v_{out} 的弧,容量为 1。 原来经过 vv 的任何通路都必须走这条弧:某条 s→ts \to t 的通路要么经过 vv(那就必然穿过这条拆点弧), 要么完全不经过 vv(那它就不受这条弧的影响)。所以"割断这条弧"和"删掉 vv"代价相同。

固定 s,ts,t 后按下面的方式建网络:

  • 拆点弧 vin→voutv_{in} \to v_{out}:普通点容量 1;枚举到点对 (s,t)(s,t) 时,把 s,ts,t 这两条弧临时改成 BIG(表示 s,ts,t 不允许出现在割里,这正是 λ(s,t)\lambda(s,t) 的定义);
  • 原图每条无向边 {u,v}\{u,v\}:两条各自独立的有向弧 uout→vinu_{out} \to v_{in} 与 vout→uinv_{out} \to u_{in}, 容量都是 BIG。

这张网络上 sin→touts_{in} \to t_{out} 的最大流就是 λ(s,t)\lambda(s,t)(最大流最小割定理 + 拆点等价性, 证明见下)。取 BIG = 1 << 20:一次最多删 n≤50n \le 50 个点,所以 BIG > n 保证最小割 绝不会去割原图的边;同时它又能塞进 32 位整数。

下面的网络图就是样例第 5 组、取 s=0,t=3s=0,t=3 时的拆点结果:实线是拆点弧(容量 1,源汇为 INF), 虚线是原图的边(容量 INF)。

digraph {
  rankdir=LR; node [fontname="Helvetica", fontsize=10];
  edge [fontsize=9];
  in0 [label="0_in"]; out0 [label="0_out"];
  in0 -> out0 [label="INF", color="#c0392b", penwidth=2];
  in1 [label="1_in"]; out1 [label="1_out"];
  in1 -> out1 [label="1"];
  in2 [label="2_in"]; out2 [label="2_out"];
  in2 -> out2 [label="1"];
  in3 [label="3_in"]; out3 [label="3_out"];
  in3 -> out3 [label="INF", color="#c0392b", penwidth=2];
  in4 [label="4_in"]; out4 [label="4_out"];
  in4 -> out4 [label="1"];
  out0 -> in1 [label="INF", style=dashed];
  out1 -> in0 [label="INF", style=dashed];
  out0 -> in2 [label="INF", style=dashed];
  out2 -> in0 [label="INF", style=dashed];
  out1 -> in3 [label="INF", style=dashed];
  out3 -> in1 [label="INF", style=dashed];
  out1 -> in2 [label="INF", style=dashed];
  out2 -> in1 [label="INF", style=dashed];
  out1 -> in4 [label="INF", style=dashed];
  out4 -> in1 [label="INF", style=dashed];
  out2 -> in3 [label="INF", style=dashed];
  out3 -> in2 [label="INF", style=dashed];
  out3 -> in4 [label="INF", style=dashed];
  out4 -> in3 [label="INF", style=dashed];
}

读图时注意三件事:每个点被一条实线拆点弧穿过,它是这个点的"必经之路";所有虚线弧的容量 都是 INF,所以最小割不可能落在它们身上,只能落在实线上;00 和 33 的拆点弧被标红并写成 INF, 表示源点汇点不许删。任何一条有限割都对应一组被割断的实线,也就是一个可以删掉、且删了就能把 ss 和 tt 分开的顶点集合;本例里最小的那组就是 {1,2}\{1,2\}。

3. 外层必须枚举点对

一个很容易踩的坑是"随便挑一对点跑一次最大流就当答案"。λ(s,t)\lambda(s,t) 依赖点对,取错了会漏掉 真正的答案。样例第 5 组的完整割值表如下(不可切断 表示割值达到 INF):

ss tt 只要求分开 s,ts,t 的点最小割 备注
0 1 不可切断 看不到答案
0 2 不可切断 看不到答案
0 3 2 全局最优就出在这一对
0 4 2 全局最优就出在这一对
1 2 不可切断 看不到答案
1 3 不可切断 看不到答案
1 4 不可切断 看不到答案
2 3 不可切断 看不到答案
2 4 2 全局最优就出在这一对
3 4 不可切断 看不到答案

表的行是点对,第三列是这一对的点最小割。只有 (0,3),(0,4),(2,4)(0,3),(0,4),(2,4) 三对给出有限值 2, 其余点对之间有多条互不相交的通路,割不断。所以外层必须枚举全部 (n2)\binom{n}{2} 对点对, 把所有 λ(s,t)\lambda(s,t) 取最小。

样例第 5 组删掉 {1,2}\{1,2\} 后的样子(红点是删掉的点,红边是随它一起消失的邻接边):

graph {
  layout=neato; node [shape=circle, style=filled, fillcolor="#e8f0fe", fontname="Helvetica"];
  0 [pos="0,0!"];
  1 [pos="1.2,1.0!", fillcolor="#f8d7da", color="#c0392b"];
  2 [pos="1.2,-1.0!", fillcolor="#f8d7da", color="#c0392b"];
  3 [pos="2.6,0.6!"];
  4 [pos="2.6,-1.2!"];
  0 -- 1 [color="#c0392b", penwidth=2];
  0 -- 2 [color="#c0392b", penwidth=2];
  1 -- 3 [color="#c0392b", penwidth=2];
  1 -- 2 [color="#c0392b", penwidth=2];
  1 -- 4 [color="#c0392b", penwidth=2];
  2 -- 3 [color="#c0392b", penwidth=2];
  3 -- 4;
}

删完之后剩下的图分成两块:{0}\{0\} 和 {3,4}\{3,4\},中间再没有任何边相连,所以图不连通, 删 2 个点是够的。另一方面 1,21,2 之外的点都不是割点,删 1 个点图仍然连通,所以 2 就是最优。

4. 正确性

引理(拆点等价). 对固定 s≠ts \ne t,上述网络中 sin→touts_{in} \to t_{out} 的最小割容量等于 λ(s,t)\lambda(s,t)。

证明. 先看一个方向:给定删点集 SS(S∩{s,t}=∅S\cap\{s,t\}=\varnothing)使 G−SG-S 中 s,ts,t 不连通, 取 AA 为 G−SG-S 中含 ss 的那个连通块(于是 t∉At\notin A,且 A∩S=∅A\cap S=\varnothing)。 把网络的顶点分成两侧:

  • v∈Av \in A:vinv_{in} 与 voutv_{out} 都在含 sins_{in} 的一侧;
  • v∈Sv \in S:vinv_{in} 在含 sins_{in} 的一侧,voutv_{out} 放到另一侧;
  • v∉A∪Sv \notin A \cup S:vinv_{in} 与 voutv_{out} 都放到另一侧。

看哪些弧跨越这个划分:v∈Sv\in S 的拆点弧被切开,共 ∣S∣|S| 条,每条容量 1; 若某条原图边弧 uout→winu_{out}\to w_{in} 被切开,则必须 u∈Au\in A 而 w∉A∪Sw\notin A\cup S, 也就是说 G−SG-S 中存在一条从 AA 连向外面的边,与 AA 是连通块矛盾; s,ts,t 这两条容量 BIG 的拆点弧也都在同一侧(s∈As\in A,t∉A∪St\notin A\cup S)。 所以这个割的容量恰好是 ∣S∣|S|,最小割 ≤λ(s,t)\le \lambda(s,t)。

再看另一个方向:任取一个容量有限的割 (P,Q)(P,Q)(sin∈Ps_{in}\in P,tout∈Qt_{out}\in Q)。因为它有限, 容量 BIG 的弧都不可能从 PP 侧指向 QQ 侧,于是 P→QP\to Q 的跨越弧只能是容量 1 的 普通点拆点弧(容量 0 的反向残量弧也不贡献容量);把这些拆点弧对应的顶点收集起来记作 SS,则割容量恰为 ∣S∣|S|。再令 A={v∉S:vin∈P}A=\{v\notin S: v_{in}\in P\},于是 s∈As\in A,A∩S=∅A\cap S=\varnothing, 且 t∉At\notin A。 若 G−SG-S 中存在一条从 AA 连到 V∖A∖SV\setminus A\setminus S 的边 {u,w}\{u,w\},注意到 u∈Au\in A 给出 uin∈Pu_{in}\in P,而 u∉Su\notin S 说明拆点弧 uin→uoutu_{in}\to u_{out} 不是 P→Q 的跨越弧, 所以 uout∈Pu_{out}\in P;另一方面 w∉A∪Sw\notin A\cup S 给出 win∈Qw_{in}\in Q。 于是 uout→winu_{out}\to w_{in} 是一条 P→Q 的、容量 BIG 的弧,与割有限矛盾。 所以 G−SG-S 中 AA 与其余顶点之间没有边,s,ts,t 被分开,即 ∣S∣≥λ(s,t)|S|\ge \lambda(s,t)。

两个方向合起来,最小割 = λ(s,t)\lambda(s,t)。□\square

由最大流最小割定理,代码里 max_flow 算出的就是 λ(s,t)\lambda(s,t); 由"图不连通   ⟺  \iff 存在两个点分属不同连通块",

min⁡S∣S∣=min⁡s≠tλ(s,t), \min_S |S| = \min_{s \ne t} \lambda(s,t),

外层枚举全部点对取最小即得答案。代码里还有一步剪枝 if best <= 1: return best: 答案下界是 0,一旦某对点对给出 0 或 1,就不可能更小了,可以直接返回。

5. 必须注意的细节

  • 无向边要连两条独立的有向弧,各带容量 BIG,不能写成一条"无向弧"。 写成单向会漏掉一半通路:实测随机图中有相当比例会算错。
  • 自环与重边要先清掉。自环对连通性毫无贡献,还会在拆点网络里制造 vin→voutv_{in} \to v_{out} 的 平行弧;重边会让一对点之间出现多条容量 BIG 的弧,破坏"边弧不会被割"的前提。
  • ∞\infty 要显著大于 nn。这里取 2202^{20},既能保证最小割不碰边弧, 又保证残量数组里的任何值(最大就是 BIG 本身,回退后的反向弧也不会超过它)都能留在 32 位有符号整数里。
  • n=1n=1 没有点对。内层循环一次都不进,答案取初值 n=1n = 1,正好符合题面。

6. 实现要点

四点实现上的选择,正文里用到的名字都能在代码里找到:

  • 残量网络用链式前向星存:to/nxt/head 三个数组,一次 link(u, v) 压入正向弧与 它的一条残量 0 反向弧,两者边号为 e 与 e ^ 1,回退时直接异或取反边。
  • 容量用 array('i') 存一个「模板」base(普通点 1、边弧 BIG、反向弧 0), 每个点对只需 array('i', base) 拷贝一份,再把 s,ts,t 的拆点弧改成 BIG,比每次重建图便宜。
  • max_flow 是迭代版 Dinic:BFS 分层后用一个显式栈 DFS,一次找到瓶颈后回退到第一条被榨干 的边,而不是每单位流量都从源点重新开始;死路点直接把 level 置 -1(当前弧剪枝)。
  • 读入用 re.findall(rb"\d+", ...) 把 n、m 与所有 (x,y) 按出现顺序抽成整数流, 正好匹配「数对内不含空格、其余用空格分隔」的格式;^Z 先替换成空格。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-10 13:00
 * update_at: 2026-10-10 13:00
 */

// 电话网络:点连通度,拆点建图后对每个点对求最大流(Dinic)
#include <cstdio>
#include <vector>
using namespace std;

const int BIG = 1 << 20; // “不可割断”的容量
const int INF = 1 << 30;

int node_cnt;
vector<int> to, cap, nxt_e, head;
vector<int> level, it;

// 连一条有向弧 u->v,紧跟一条残量 0 的反向弧,返回正向弧的边号
int link(int u, int v, int c) {
    to.push_back(v);
    cap.push_back(c);
    nxt_e.push_back(head[u]);
    head[u] = (int)to.size() - 1;
    to.push_back(u);
    cap.push_back(0);
    nxt_e.push_back(head[v]);
    head[v] = (int)to.size() - 1;
    return (int)to.size() - 2;
}

// BFS 分层,汇点不可达时返回 false
bool bfs_level(int s, int t) {
    level.assign(node_cnt, -1);
    vector<int> q;
    level[s] = 0;
    q.push_back(s);
    for (int h = 0; h < (int)q.size(); h++) {
        int u = q[h];
        for (int e = head[u]; e != -1; e = nxt_e[e]) {
            int v = to[e];
            if (cap[e] > 0 && level[v] < 0) {
                level[v] = level[u] + 1;
                q.push_back(v);
            }
        }
    }
    return level[t] >= 0;
}

// 沿分层图找一条增广路并推送流量
int dfs_flow(int u, int t, int limit) {
    if (u == t) {
        return limit;
    }
    for (; it[u] != -1; it[u] = nxt_e[it[u]]) {
        int e = it[u];
        int v = to[e];
        if (cap[e] > 0 && level[v] == level[u] + 1) {
            int got = dfs_flow(v, t, cap[e] < limit ? cap[e] : limit);
            if (got > 0) {
                cap[e] -= got;
                cap[e ^ 1] += got;
                return got;
            }
        }
    }
    return 0;
}

int dinic(int s, int t) {
    int total = 0;
    while (bfs_level(s, t)) {
        it = head;
        while (true) {
            int f = dfs_flow(s, t, INF);
            if (f == 0) {
                break;
            }
            total += f;
        }
    }
    return total;
}

// 最少删几个点能让图不连通;删不掉时按题面要求返回 n
int vertex_connectivity(int n, const vector<int>& a, const vector<int>& b) {
    if (n == 0) {
        return 0;
    }
    vector<vector<int> > neighbors(n);
    for (int i = 0; i < (int)a.size(); i++) {
        if (a[i] == b[i]) {
            continue; // 去掉自环与重边
        }
        bool dup = false;
        for (int t = 0; t < (int)neighbors[a[i]].size(); t++) {
            if (neighbors[a[i]][t] == b[i]) {
                dup = true;
            }
        }
        if (!dup) {
            neighbors[a[i]].push_back(b[i]);
            neighbors[b[i]].push_back(a[i]);
        }
    }

    // 先看原图是否已经断开(孤立点也算断开)
    vector<char> seen(n, 0);
    vector<int> stk;
    stk.push_back(0);
    seen[0] = 1;
    int cnt = 1;
    while (!stk.empty()) {
        int u = stk.back();
        stk.pop_back();
        for (int i = 0; i < (int)neighbors[u].size(); i++) {
            int v = neighbors[u][i];
            if (!seen[v]) {
                seen[v] = 1;
                cnt++;
                stk.push_back(v);
            }
        }
    }
    if (cnt != n) {
        return 0;
    }

    // 拆点建图:点 v 换成 2v -> 2v+1 的容量 1 弧,原图无向边换成容量 BIG 的双向弧
    int N = 2 * n;
    vector<int> split(n, -1), t2, c2, n2, h2;
    to.clear(); cap.clear(); nxt_e.clear();
    head.assign(N, -1);
    for (int v = 0; v < n; v++) {
        split[v] = link(2 * v, 2 * v + 1, 1);
    }
    for (int u = 0; u < n; u++) {
        for (int i = 0; i < (int)neighbors[u].size(); i++) {
            int v = neighbors[u][i];
            if (u < v) {
                link(2 * u + 1, 2 * v, BIG);
                link(2 * v + 1, 2 * u, BIG);
            }
        }
    }
    t2 = to; c2 = cap; n2 = nxt_e; h2 = head; // 残量模板
    node_cnt = N;

    int best = n;
    for (int s = 0; s < n; s++) {
        for (int t = s + 1; t < n; t++) {
            to = t2; cap = c2; nxt_e = n2; head = h2;
            cap[split[s]] = cap[split[t]] = BIG; // 源、汇不许被删
            int flow = dinic(2 * s, 2 * t + 1);
            if (flow < best) {
                best = flow;
                if (best <= 1) { // 已经断开 / 只有割点
                    return best;
                }
            }
        }
    }
    return best;
}

int main() {
    // 一次读入全部内容,再按“整数出现顺序”抽取(坐标写成 (x,y) 也能正确抽出来)
    vector<char> buf;
    int ch;
    while ((ch = getchar()) != EOF) {
        buf.push_back((char)ch);
    }
    vector<int> flat;
    long long cur = -1;
    for (int i = 0; i <= (int)buf.size(); i++) {
        char c = (i < (int)buf.size()) ? buf[i] : ' ';
        if (c >= '0' && c <= '9') {
            if (cur < 0) {
                cur = 0;
            }
            cur = cur * 10 + (c - '0');
        } else {
            if (cur >= 0) {
                flat.push_back((int)cur);
                cur = -1;
            }
        }
    }

    int printed = 0;
    int pos = 0;
    while (pos + 1 < (int)flat.size()) {
        int n = flat[pos], m = flat[pos + 1];
        vector<int> a(m), b(m);
        for (int i = 0; i < m; i++) {
            a[i] = flat[pos + 2 + 2 * i];
            b[i] = flat[pos + 3 + 2 * i];
        }
        pos += 2 + 2 * m;
        printf("%d\n", vertex_connectivity(n, a, b));
        printed++;
    }
    if (printed == 0) {
        printf("\n");
    }
    return 0;
}
python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 01:46
# update_at: 2026-10-02 01:51

import re
import sys
from array import array
from collections import deque

BIG = 1 << 20  # "不可割断"的容量:一次最多删 n <= 50 个点,远小于它
HEAD = -1      # 链式前向星的"没有下一条边"


def max_flow(to: list[int], cap: array, nxt: list[int], head: list[int],
             s: int, t: int) -> int:
    """Dinic 最大流:cap 是对称存边(边 e 的反向边是 e ^ 1),head 是出边链头。"""
    size = len(head)
    total = 0
    while True:
        level = [-1] * size                   # BFS 分层:-1 表示本次还没到过
        level[s] = 0
        queue = deque([s])
        while queue:
            u = queue.popleft()
            e = head[u]
            while e != HEAD:
                v = to[e]
                if cap[e] > 0 and level[v] < 0:
                    level[v] = level[u] + 1
                    queue.append(v)
                e = nxt[e]
        if level[t] < 0:                      # 汇点不可达,已无增广路
            return total

        path: list[tuple[int, int]] = []       # DFS 栈,元素是 (点, 从该点走出去的边号)
        u = s
        while True:
            if u == t:
                bottle = min(cap[e] for _, e in path)
                for _, e in path:
                    cap[e] -= bottle
                    cap[e ^ 1] += bottle
                total += bottle
                # 退回到第一条被榨干的边,从那里继续找下一条增广路
                first = next(i for i, (_, e) in enumerate(path) if cap[e] == 0)
                path = path[:first]
                u = to[path[-1][1]] if path else s
            else:
                e, found = head[u], HEAD
                while e != HEAD:
                    v = to[e]
                    if cap[e] > 0 and level[v] == level[u] + 1:
                        found = e
                        break
                    e = nxt[e]
                if found != HEAD:
                    path.append((u, found))
                    u = to[found]
                elif not path:                 # 从源点出发已无路可走
                    break
                else:                          # u 分层内废点:作废它的层号,避免重扫
                    level[u] = -1
                    u = path.pop()[0]


def build_network(n: int, neighbors: list[set[int]]) -> tuple[list[int], list[int],
                                                              list[int], list[int],
                                                              list[int]]:
    """拆点建图:点 v 换成 2v -> 2v+1 的容量 1 弧,原图无向边换成容量 BIG 的双向弧。"""
    to: list[int] = []
    nxt: list[int] = []
    head = [HEAD] * (2 * n)
    split: list[int] = []        # split[v] = 点 v 那条"拆点弧"的边号
    cross: list[int] = []        # cross = 所有表示原图边的弧的边号

    def link(u: int, v: int) -> int:
        """连一条有向弧 u->v,紧跟一条残量 0 的反向弧,返回正向弧的边号。"""
        to.append(v)
        nxt.append(head[u])
        head[u] = len(to) - 1
        to.append(u)
        nxt.append(head[v])
        head[v] = len(to) - 1
        return len(to) - 2

    for v in range(n):
        split.append(link(2 * v, 2 * v + 1))
    for a in range(n):
        for b in neighbors[a]:
            if a < b:                # 无向边只连一次,两个方向各一条弧
                cross += [link(2 * a + 1, 2 * b), link(2 * b + 1, 2 * a)]

    base = [0] * len(to)             # 残量模板:只与源汇无关的部分
    for v in range(n):
        base[split[v]] = 1           # 删一个普通点花 1 的代价
    for e in cross:
        base[e] = BIG                # 原图的边删不掉,等价于无穷容量
    return to, nxt, head, split, base


def vertex_connectivity(n: int, edges: list[tuple[int, int]]) -> int:
    """最少删几个点能让图不连通;删不掉时按题面要求返回 n。"""
    if n == 0:
        return 0
    neighbors = [set() for _ in range(n)]       # 去掉自环与重边,避免无向边容量被重复叠加
    for a, b in edges:
        if a != b:
            neighbors[a].add(b)
            neighbors[b].add(a)

    seen, stack = {0}, [0]                      # 先看原图是否已经断开(孤立点也算断开)
    while stack:
        fresh = neighbors[stack.pop()] - seen
        seen |= fresh
        stack += fresh
    if len(seen) != n:
        return 0

    to, nxt, head, split, base = build_network(n, neighbors)
    best = n      # 删光 n 个点一定不连通,所以答案的上界就是 n(n = 1 时无点对可枚举,正好返回 1)
    for s in range(n):
        for t in range(s + 1, n):
            cap = array("i", base)
            cap[split[s]] = cap[split[t]] = BIG  # 源、汇不许被删
            flow = max_flow(to, cap, nxt, head, 2 * s, 2 * t + 1)
            if flow < best:
                best = flow
                if best <= 1:                    # 已经断开 / 只有割点,不可能更小
                    return best
    return best


def solve() -> None:
    # 抽掉数据文件末尾可能出现的 ^Z;\d+ 会把 (x,y) 与 n、m 一起按出现顺序抽成整数流
    flat = list(map(int, re.findall(rb"\d+", sys.stdin.buffer.read().replace(b"\x1a", b" "))))
    out: list[str] = []
    pos = 0
    while pos + 1 < len(flat):
        n, m = flat[pos], flat[pos + 1]
        edges = list(zip(flat[pos + 2:pos + 2 + 2 * m:2], flat[pos + 3:pos + 3 + 2 * m:2]))
        pos += 2 + 2 * m
        out.append(str(vertex_connectivity(n, edges)))
    print("\n".join(out))


if __name__ == "__main__":
    solve()

复杂度

  • 时间复杂度:(n2)\binom{n}{2} 次最大流,网络规模 2n2n 个节点、2n+4m2n+4m 条弧(含反向弧), 即 O(n2⋅maxflow(2n, 2n+4m))O\bigl(n^2 \cdot \mathrm{maxflow}(2n,\ 2n+4m)\bigr),与标准 C++ 正解同阶。 又因为最大流的值不超过 nn,每次 Dinic 的实际增广轮数是 O(n)O(n) 级别。
  • 空间复杂度:O(n+m)O(n+m),一次残量数组用 array('i') 存放,每组只用几 KB。

实测(Apple Silicon / CPython 3.14):真实数据 f.in(10 组,n≤40n \le 40)0.24 s; 单组 K50K_{50}(m=1225m=1225)0.37 s;4 组 n=50,m=900n=50,m=900 的稠密随机图(答案约 28, best <= 1 剪枝失效)合计 1.84 s;峰值内存实测不到 16 MB,远低于 128 MB。 算法本身与 C++ 同阶,最坏构造下接近本题 1000 ms 的限制, Python 侧的常数劣势是主要风险。

总结

  • 遇到"删最少的东西让图不连通",第一反应应该是最小割,而不是枚举删除集合。
  • 割的是点时用拆点:每个点拆成 in→outin \to out 的一条容量 1 弧, 原图的边给 INF 容量,于是"删点"变成"割边"。
  • 题目不指定 s,ts,t 时,答案是 min⁡s≠tλ(s,t)\min_{s\ne t}\lambda(s,t),必须枚举所有点对; 样例第 5 组里 (0,1)(0,1) 根本割不断、答案只出现在 (0,3)(0,3) 这类点对上。
  • 完全图、单点图这类"删不掉"的情形正好用 min⁡(⋅,n)\min(\cdot, n) 的上界处理: 内层循环无点对可枚举时返回初值 nn 即可。
  • 无穷大的取值要"大于 nn,但别太大":大于 nn 才能保证最小割不碰它, 够小才能留在 32 位整数里。

图示解析

这张图串起本题从建模到输出答案的主线:

text
输入:n 个点、m 条边的无向图
`- 目标:最少删几个点让图不连通(删不到就输出 n)
   |- 关键等价 1:删点集 S 让图断开 <=> 存在一对点 (s,t) 被 S 分开
   |  `- 于是答案 = min over s != t of lambda(s,t),再与 n 取小
   `- 关键等价 2:lambda(s,t) 难算 -> 用拆点把"点割"变成"边割"
      |- 建网络:v 拆成 v_in --1--> v_out;原图边 {u,v} 变成两条 INF 有向弧
      |- 源汇的拆点弧改成 INF(不许删)
      `- 最大流最小割定理:maxflow(s_in, t_out) = lambda(s,t)
         `- 外层枚举 s < t,取最小;best <= 1 时立刻返回
            `- 输出 best(没有更小割时就是 n)

读图时抓两个分叉的因果:第一个分叉把"图不连通"这个全局性质拆成了点对上的局部问题, 这解释了为什么会有外层 (n2)\binom{n}{2} 的枚举;第二个分叉解释了为什么不能直接对原图跑最大流 ——原图割的是边,而题目要割的是点,拆点的容量 1 弧只是把"删一个点的代价"从顶点搬到了边上。