算法设计与分析


迭代改进

计算机学院    张腾

tengzhang@hust.edu.cn

课程大纲

算法设计与分析分治法动态规划贪心法回溯法分支限界法迭代改进最大流 最小切割最大流算法的应用线性规划 单纯形法

最大流 问题背景

上世纪 50 年代中期,美国空军研究员 Theodore E. Harris 和退伍少将 Frank S. Ross 联合写了一份研究苏联到其东欧卫星国铁路网的报告,该报告于 1999 年解密

最大流 问题背景

铁路网可以看作加权有向图

  • 44 个点为重要地区
  • 105 条边为连接这些地区的铁路
  • 边上权重代表单位时间运输量,可以想象成平行的铁路条数

两个问题

  • 从苏联到东欧卫星国的单位时间最大运输量,最大流 (maximum flow)
  • 炸毁最少的铁路切断运输,最小切割 (minimum cut)

Ford 和 Fulkerson 将最大流问题的形式化归功于 Harris 和 Ross,但 Harris 和 Ross 又致谢了 George Dantzig 协助形式化了该问题


给定流网络$\Gcal = (\Vcal, \Ecal)$,源点$s$,汇点$t$

$f: \Ecal \mapsto \Rbb$是实值函数且满足流量守恒:对$\forall u \in \Vcal \setminus \{ s,t \}$

$$ \begin{align*} \quad \sum_{v \in \Vcal} f(v,u) = \sum_{v \in \Vcal} f(u,v) \end{align*} $$

流值$|f| \triangleq \sum_{v \in \Vcal} f(s,v) - \sum_{v \in \Vcal} f(v,s)$,即源点$s$的净流出流量

容量

容量$c: \Ecal \mapsto \Rbb^+$是非负实值函数

容量限制:对$\forall (u,v) \in \Ecal$$0 \le f(u,v) \le c(u,v)$

  • $f(u,v) = c(u,v)$,流$f$充斥 (saturate) 于边$(u,v)$
  • $f(u,v) = 0$,流$f$回避 (avoid) 了边$(u,v)$

满足容量限制的流称为可行的 (feasible)

最大流问题

$$ \begin{align*} \quad & \begin{cases} f(s,v_1) = 11 \\ f(s,v_2) = 8 \\ f(v_1,v_3) = 12 \\ f(v_2,v_1) = 1 \\ f(v_2,v_4) = 11 \\ f(v_3,v_2) = 4 \\ f(v_4,v_3) = 7 \\ f(v_3,t) = 15 \\ f(v_4,t) = 4 \end{cases} \\[10px] & |f| = \sum_{v \in \Vcal} f(s,v) - \sum_{v \in \Vcal} f(v,s) = 11 + 8 = 19 \end{align*} $$

最大流问题:在给定的流网络中找一个流值最大的流

两个约定

流网络中无反向平行边

对反向平行边$(v_1,v_2)$$(v_2,v_1)$,选择其中一条,比如$(v_1,v_2)$,加入新结点$v$,将其分为两条边$(v_1,v)$$(v,v_2)$,并将两条边的容量设为被替代掉的边的容量,即$c(v_1,v) = c(v,v_2) = c(v_1,v_2)$

可以证明,转换后的网络与原网络等价

两个约定

流网络中只有单一的源点和汇点

加入超级源点$s$和超级汇点$t$$c(s,s_i) = \infty$$c(t_j,t) = \infty$

可以证明,转换后的网络与原网络等价

切割

给定流网络$\Gcal = (\Vcal, \Ecal)$,源点$s$,汇点$t$

切割$(\Scal, \Tcal)$$\Vcal$分成$\Scal$$\Tcal = \Vcal \setminus \Scal$使得$s \in \Scal$$t \in \Tcal$

切割$(\Scal,\Tcal)$的容量为$c(\Scal, \Tcal) = \sum_{u \in \Scal} \sum_{v \in \Tcal} c(u,v)$

最小切割问题:在给定的流网络中找一个容量最小的切割

切割

切割$(\Scal, \Tcal)$$\Scal = \{ s, v_1, v_2 \}$$\Tcal = \{ t, v_3, v_4 \}$

切割$(\Scal,\Tcal)$的容量$c(\Scal, \Tcal) = 12 + 14 = 26$

该切割不是最小切割,$(\{s,v_1,v_2,v_4\},\{v_3,t\})$是更小的切割

流与切割的关系

引理:任意流值不超过任意切割的容量

$$ \begin{align*} \quad |f| & = \sum_{v \in \Vcal} f(s,v) - \sum_{v \in \Vcal} f(v,s) = \sum_{u \in \Scal} \left( \sum_{v \in \Vcal} f(u,v) - \sum_{v \in \Vcal} f(v,u) \right) \\ & = \sum_{u \in \Scal} \left( \sum_{v \in \Scal} f(u,v) + \sum_{v \in \Tcal} f(u,v) - \sum_{v \in \Scal} f(v,u) - \sum_{v \in \Tcal} f(v,u) \right) \\ & = \sum_{u \in \Scal} \sum_{v \in \Tcal} f(u,v) - \sum_{u \in \Scal} \sum_{v \in \Tcal} f(v,u) \\ & \le \sum_{u \in \Scal} \sum_{v \in \Tcal} f(u,v) \le \sum_{u \in \Scal} \sum_{v \in \Tcal} c(u,v) = c(\Scal, \Tcal) \end{align*} $$

  • 第四个等号表明流值等于横跨切割的净流量
  • $|f| = c(\Scal, \Tcal)$,则$f$为最大流、$(\Scal,\Tcal)$为最小切割
  • $f$为最大流时,所有不等式取等号,因此最大流充斥横跨切割的正向边、回避横跨切割的反向边
最大流最小切割定理

对任意流网络,存在最大流等于最小切割

  • 1954 年 Lester Randolph Ford Jr.、Delbert Ray Fulkerson 证明
  • 1956 年 Peter Elias、Amiel Feinstein、Claude Shannon 证明

最大流最小切割定理是线性规划中强对偶成立的一个特例

证明 残存网络

给定流网络$\Gcal = (\Vcal, \Ecal)$和流$f$,定义残存网络$\Gcal_f = (\Vcal, \Ecal_f)$记录每条边上的流量可以修改的极限

$0 < f(u,v) < c(u, v)$

  • 边上的流量最多可再增加$c(u,v) - f(u,v)$,将$(u,v)$加入$\Gcal_f$,并设其残存容量$c_f (u,v) = c(u,v) - f(u,v)$
  • 边上的流量最多可减少$f(u,v)$,将反向边$(v,u)$加入$\Gcal_f$,并设其残存容量$c_f (v,u) = f(u,v)$

$f(u,v) = c(u, v)$,则正向边$(u,v)$不加入,没有可增加的余地

$f(u,v) = 0$,则反向边$(v,u)$不加入,没有可减少的余地

证明 残存网络

残存网络$\Gcal_f = (\Vcal, \Ecal_f)$中边$(u,v)$的残存容量$c_f(u,v)$定义为

$$ \begin{align*} \quad c_f(u,v) = \begin{cases} c(u,v) - f(u,v), & (u,v) \in \Ecal \\ f(v,u), & (v,u) \in \Ecal \\ 0, & \text{其它} \end{cases} \end{align*} $$

残存容量定义中的前两种情形是互斥的,因为我们约定了给定的流网络中不存在反向平行边

证明 增广路径

给定流网络$\Gcal = (\Vcal, \Ecal)$和流$f$,设$p$是残存网络$\Gcal_f$中一条从源点$s$到汇点$t$的路径,称为增广路径 (augmenting path)

增广路径的残存容量是所有边上残存容量的最小值

$$ \begin{align*} \quad c_f (p) = \min \{ c_f (u,v): (u,v) \in p \} \end{align*} $$

下图蓝色边构成一条增广路径,$c_f (p) = \min \{ 5,4,5 \} = 4$

证明 增广路径

若残存网络$\Gcal_f$包含增广路径,则可根据残存容量增加流

$$ \begin{align*} \quad f'(u,v) = \begin{cases} f(u,v) + c_f(p), & (u,v) \in p \\ f(u,v) - c_f(p), & (v,u) \in p \\ f(u,v), & \text{其它} \end{cases} \end{align*} $$

增广路径的残存容量为$4$,原流网络的流值可以增大$4$

证明 流的递增

可以证明$f'$是原网络中的一个流,流值$|f'| = |f| + c_f (p) > |f|$

流量守恒:对任意点$v \in \Vcal \setminus \{ s,t \}$

$v \not \in p$,进入、离开点$v$的流量不变,显然满足流量守恒

$v \in p$,设$p = \langle s, \ldots, u, v, w, \ldots, t \rangle$,分四种情况:

  • $(u,v), (v,w) \in \Ecal$ => $(u,v), (v,w)$的流量都增加$c_f(p)$
  • $(v,u), (w,v) \in \Ecal$ => $(v,u), (w,v)$的流量都减少$c_f(p)$
  • $(v,u), (v,w) \in \Ecal$ => $(v,u)$的流量减少$c_f(p)$$(v,w)$的流量增加$c_f(p)$
  • $(u,v), (w,v) \in \Ecal$ => $(u,v)$的流量增加$c_f(p)$$(w,v)$的流量减少$c_f(p)$

流值为源点$s$的净流出流量,源点$s \in p$,故流值增加$c_f(p)$

证明 流的递增

容量限制:对任意边$(u,v) \in \Ecal$

$(u,v), (v,u) \not \in p$,则$(u,v)$的流量不变

$(u,v) \in p$,则$(u,v)$的流量增加

$$ \begin{align*} \quad f' (u,v) & = f(u,v) + c_f(p) \\ & \le f(u,v) + c_f(u,v) = f(u,v) + c(u,v) - f(u,v) = c(u,v) \end{align*} $$

$(v,u) \in p$,则$(u,v)$的流量减少

$$ \begin{align*} \quad f' (u,v) & = f(u,v) - c_f(p) \\ & \ge f(u,v) - c_f(v,u) = f(u,v) - f(u,v) = 0 \end{align*} $$

证明 最后一步

若残存网络$\Gcal_f$不包含任何增广路径,即$s$$t$不连通,构造切割$(\Scal, \Tcal)$,其中$\Scal$为与$s$连通的所有结点,$\Tcal = \Vcal \setminus \Scal$

$\forall u \in \Scal$$\forall v \in \Tcal$,边$(u,v) \not \in \Gcal_f$,否则$v$也与$s$连通

  • $(u,v) \in \Ecal$,则$f(u,v) = c(u,v)$$f$充斥横跨切割的正向边
  • $(v,u) \in \Ecal$,则$f(v,u) = 0$$f$回避横跨切割的反向边

由引理知此时$f$为最大流、$(\Scal, \Tcal)$为最小切割

Ford-Fulkerson 算法

最大流最小切割定理直接给出了 Ford-Fulkerson 算法:

  • 初始化流为零
  • 不断在残存网络中寻找增广路径,根据残存容量增加流网络的流值,直至不再存在增广路径

算法没有明确指定如何寻找增广路径

  • 从源点开始做 DFS,搜到汇点就返回
  • 从源点开始做 BFS,搜到汇点就返回,最少边数的增广路径
  • 最多边数的增广路径
  • 残存容量最大的增广路径
算法实现

def initialize_flow(c):  # 零初始化
    f = {}
    for u in c:
        f[u] = {}
    for u in c:
        for v in c[u]:
            f[u][v] = 0
    return f


def res_net(c, f):                             # 计算残存网络
    c_f = {}
    for u in c:
        c_f[u] = {}
    for u in c:
        for v in c[u]:
            if f[u][v] == c[u][v]:             # 满流量
                c_f[v][u] = c[u][v]
            elif f[u][v] == 0:                 # 无流量
                c_f[u][v] = c[u][v]
            else:
                c_f[u][v] = c[u][v] - f[u][v]  # 正向边容量等于剩余容量
                c_f[v][u] = f[u][v]            # 反向边容量等于当前流量
    return c_f


def dfs_rec(c_f, pre, visited, u):  # 深度优先 递归
    if u == "t":
        return
    for v in c_f[u].keys():         # 残存网络中 u -> v
        if v not in visited:
            visited.append(v)
            pre[v] = u
            dfs_rec(c_f, pre, visited, v)


def xxfs_iter(c_f, n):
    pre, visited, l = {}, ["s"], ["s"]  # 前驱表 访问标记 用于做搜索的数据结构
    while l:
        u = l.pop(n)                    # 弹出第n个元素
        for v in c_f[u].keys():         # 残存网络中 u -> v
            if v not in visited:
                visited.append(v)
                l.append(v)
                pre[v] = u
                if v == "t":
                    return pre
    return pre


def longest_path(c_f):  # 穷举检查 带环有向图中寻找最长简单路径是NP难的
    nodes = []          # 存储全部中间结点
    for u in c_f:
        if u != "s" and u != "t":
            nodes.append(u)
    pre = {}
    for i in range(len(nodes), 0, -1):
        combination = list(itertools.combinations(nodes, i))  # 按结点数从多到少遍历幂集
        for comb in combination:
            permutation = list(itertools.permutations(comb))  # 对于某个结点子集 生成全排列
            for perm in permutation:
                find_path = True
                if perm[0] not in c_f["s"].keys():   # 如果没有 s -> 第一个点
                    find_path = False
                if "t" not in c_f[perm[-1]].keys():  # 如果没有 最后一个点 -> t
                    find_path = False
                for j in range(1, len(perm)):
                    if perm[j] not in c_f[perm[j - 1]].keys():
                        find_path = False
                        break
                if find_path:
                    pre["t"] = perm[-1]
                    for j in range(len(perm) - 1, 0, -1):
                        pre[perm[j]] = perm[j - 1]
                    pre[perm[0]] = "s"
                    return pre
    return pre


def max_res_cap(c_f):    # 类似于最小生成树的Prim算法
    pre, weight, in_tree, pq = {}, {}, [], PriorityQueue()
    for v in c_f:
        weight[v] = -1   # 记录还没加入树中的点与树中结点的边的最大容量

    pq.put([None, "s"])
    while not pq.empty():
        _, u = pq.get()  # 获取队首元素点u
        if u not in in_tree:
            in_tree.append(u)
            for v in c_f[u].keys():          # 残存网络中 u -> v
                if v not in in_tree and weight[v] < c_f[u][v]:
                    weight[v] = c_f[u][v]
                    pq.put([-weight[v], v])  # 最小优先队列找最大容量 故取反加入
                    pre[v] = u
    return pre


def increase_flow(c_f, f, pre):
    v, path = "t", []
    while True:                      # 根据前驱表生成增广路径
        path.append((pre[v], v))
        v = pre[v]
        if v == "s":
            break
    path.reverse()
    print("增广路径:", path)

    res_cap_edge = []                # 存储增广路径上边的容量
    for u, v in path:
        res_cap_edge.append(c_f[u][v])
        res_cap = min(res_cap_edge)  # 确定残存容量
    print("残存容量:", res_cap)

    for u, v in path:
        if v in f[u].keys():
            f[u][v] += res_cap       # 正向边流量增大
        else:
            f[v][u] -= res_cap       # 反向边流量减小


def calculate_flow(f):
    value = 0
    for v in f["s"].keys():
        value += f["s"][v]
    return value


def ford_fulkerson(c, mode):

    f, i = initialize_flow(c), 1  # 初始化流值为零

    while True:
        print("第%d轮" % i)
        print("当前的流:", f)

        c_f = res_net(c, f)  # 计算残存网络
        print("残存网络:", c_f)

        match mode:
            case 1:  # 深度优先 递归
                pre = {}
                dfs_rec(c_f, pre, ["s"], "s")
            case 2:  # 深度优先 循环 栈
                pre = xxfs_iter(c_f, -1)
            case 3:  # 广度优先 循环 队列 or 最少边数
                pre = xxfs_iter(c_f, 0)
            case 4:  # 最多边数
                pre = longest_path(c_f)
            case 5:  # 最大残存容量
                pre = max_res_cap(c_f)
            case _:
                print("mode取值必须是{1,2,3,4,5}!")
                return

        if "t" not in pre.keys():   # 若汇点、源点已不再连通
            print("最大流值为%d" % calculate_flow(f))
            return
        else:
            increase_flow(c_f, f, pre)

        i += 1


c = {                           # 容量
    "s": {"v1": 16, "v2": 13},  # c(s,v1) = 16, c(s,v2) = 13        v1 -- v3
    "v1": {"v3": 12},           # c(v1,v3) = 12                   / |   / | \
    "v2": {"v1": 4, "v4": 14},  # c(v2,v1) = 4, c(v2,v4) = 14   s   |  /  |  t
    "v3": {"v2": 9, "t": 20},   # c(v3,v2) = 9, c(v3,t) = 20      \ | /   | /
    "v4": {"v3": 7, "t": 4},    # c(v4,v3) = 7, c(v4,t) = 4        v2 -- v4
    "t": {}
}
ford_fulkerson(c, mode=5)
# -----------------------------------------------------
# 第1轮
# 当前的流: {'s': {'v1': 0, 'v2': 0}, 'v1': {'v3': 0}, 'v2': {'v1': 0, 'v4': 0}, 'v3': {'v2': 0, 't': 0}, 'v4': {'v3': 0, 't': 0}, 't': {}}
# 残存网络: {'s': {'v1': 16, 'v2': 13}, 'v1': {'v3': 12}, 'v2': {'v1': 4, 'v4': 14}, 'v3': {'v2': 9, 't': 20}, 'v4': {'v3': 7, 't': 4}, 't': {}}
# 增广路径: [('s', 'v1'), ('v1', 'v3'), ('v3', 't')]
# 残存容量: 12
# 第2轮
# 当前的流: {'s': {'v1': 12, 'v2': 0}, 'v1': {'v3': 12}, 'v2': {'v1': 0, 'v4': 0}, 'v3': {'v2': 0, 't': 12}, 'v4': {'v3': 0, 't': 0}, 't': {}}
# 残存网络: {'s': {'v1': 4, 'v2': 13}, 'v1': {'s': 12}, 'v2': {'v1': 4, 'v4': 14}, 'v3': {'v1': 12, 'v2': 9, 't': 8}, 'v4': {'v3': 7, 't': 4}, 't': {'v3': 12}}
# 增广路径: [('s', 'v2'), ('v2', 'v4'), ('v4', 'v3'), ('v3', 't')]
# 残存容量: 7
# 第3轮
# 当前的流: {'s': {'v1': 12, 'v2': 7}, 'v1': {'v3': 12}, 'v2': {'v1': 0, 'v4': 7}, 'v3': {'v2': 0, 't': 19}, 'v4': {'v3': 7, 't': 0}, 't': {}}
# 残存网络: {'s': {'v1': 4, 'v2': 6}, 'v1': {'s': 12}, 'v2': {'s': 7, 'v1': 4, 'v4': 7}, 'v3': {'v1': 12, 'v2': 9, 't': 1, 'v4': 7}, 'v4': {'v2': 7, 't': 4}, 't': {'v3': 19}}
# 增广路径: [('s', 'v2'), ('v2', 'v4'), ('v4', 't')]
# 残存容量: 4
# 第4轮
# 当前的流: {'s': {'v1': 12, 'v2': 11}, 'v1': {'v3': 12}, 'v2': {'v1': 0, 'v4': 11}, 'v3': {'v2': 0, 't': 19}, 'v4': {'v3': 7, 't': 4}, 't': {}}
# 残存网络: {'s': {'v1': 4, 'v2': 2}, 'v1': {'s': 12}, 'v2': {'s': 11, 'v1': 4, 'v4': 3}, 'v3': {'v1': 12, 'v2': 9, 't': 1, 'v4': 7}, 'v4': {'v2': 11}, 't': {'v3': 19, 'v4': 4}}
# 最大流值为23
例子 深度优先 递归

流值变化$0 \overset{+4}{\longrightarrow} 4 \overset{+8}{\longrightarrow} 12 \overset{+7}{\longrightarrow} 19 \overset{+4}{\longrightarrow} 23$

例子 深度优先 递归

流值变化$0 \overset{+4}{\longrightarrow} 4 \overset{+8}{\longrightarrow} 12 \overset{+7}{\longrightarrow} 19 \overset{+4}{\longrightarrow} 23$

例子 深度优先 栈

流值变化$0 \overset{+4}{\longrightarrow} 4 \overset{+7}{\longrightarrow} 11 \overset{+12}{\longrightarrow} 23$

例子 深度优先 栈

流值变化$0 \overset{+4}{\longrightarrow} 4 \overset{+7}{\longrightarrow} 11 \overset{+12}{\longrightarrow} 23$

例子 广度优先 队列

流值变化$0 \overset{+12}{\longrightarrow} 12 \overset{+4}{\longrightarrow} 16 \overset{+7}{\longrightarrow} 23$

例子 广度优先 队列

流值变化$0 \overset{+12}{\longrightarrow} 12 \overset{+4}{\longrightarrow} 16 \overset{+7}{\longrightarrow} 23$

例子 边数最多

流值变化$0 \overset{+4}{\longrightarrow} 4 \overset{+4}{\longrightarrow} 8 \overset{+4}{\longrightarrow} 12 \overset{+4}{\longrightarrow} 16 \overset{+3}{\longrightarrow} 19 \overset{+1}{\longrightarrow} 20 \overset{+3}{\longrightarrow} 23$

例子 边数最多

流值变化$0 \overset{+4}{\longrightarrow} 4 \overset{+4}{\longrightarrow} 8 \overset{+4}{\longrightarrow} 12 \overset{+4}{\longrightarrow} 16 \overset{+3}{\longrightarrow} 19 \overset{+1}{\longrightarrow} 20 \overset{+3}{\longrightarrow} 23$

例子 边数最多

流值变化$0 \overset{+4}{\longrightarrow} 4 \overset{+4}{\longrightarrow} 8 \overset{+4}{\longrightarrow} 12 \overset{+4}{\longrightarrow} 16 \overset{+3}{\longrightarrow} 19 \overset{+1}{\longrightarrow} 20 \overset{+3}{\longrightarrow} 23$

例子 边数最多

流值变化$0 \overset{+4}{\longrightarrow} 4 \overset{+4}{\longrightarrow} 8 \overset{+4}{\longrightarrow} 12 \overset{+4}{\longrightarrow} 16 \overset{+3}{\longrightarrow} 19 \overset{+1}{\longrightarrow} 20 \overset{+3}{\longrightarrow} 23$

例子 残存容量最大

流值变化$0 \overset{+12}{\longrightarrow} 12 \overset{+7}{\longrightarrow} 19 \overset{+4}{\longrightarrow} 23$

例子 残存容量最大

流值变化$0 \overset{+12}{\longrightarrow} 12 \overset{+7}{\longrightarrow} 19 \overset{+4}{\longrightarrow} 23$

算法分析

若边的容量均为整数,则最大流$f^\star$在各个边上的流量也是整数

  • 初始流为零,是整数
  • 每轮迭代开始,流值为整数、容量为整数 => 残存网络中的容量都为整数 => 增广路径的残存容量为整数 => 流值增量为整数

每轮迭代流值至少增加$1$,故最多迭代$O(|f^\star|)$

  • 计算残存网络需计算每条边的残存容量,运行时间$O(|\Ecal|)$
  • 利用 XX 优先搜索寻找增广路径,运行时间$O(|\Vcal| + |\Ecal|) = O(|\Ecal|)$
  • 更新每条边的流值,运行时间$O(|\Ecal|)$

Ford-Fulkerson 算法的总时间复杂度为$O(|\Ecal| |f^\star|)$

算法分析

Jack Edmonds、Richard Karp 发现时间复杂度$O(|\Ecal| |f^\star|)$是紧的

  • 最多边数的增广路径,每轮流值加$1$$|f^\star| = 2 X$,迭代$\Theta(|f^\star|)$
  • 表示$X$只需$\Theta(\lg X)$比特,因此算法最坏情况下时间复杂度是指数级
算法分析

c = {                           # 容量                             v
    "s": {"u": 100, "v": 100},  # c(s,u) = 100, c(s,v) = 100    / | \
    "u": {"v": 1, "t": 100},    # c(u,v) = 1, c(u,t) = 100     s  |  t
    "v": {"t": 100},            # c(v,t) = 100                  \ | /
    "t": {}                     # #                               u
}
ford_fulkerson(c, mode=4)
# ------------------------------------
# 第1轮
# 当前的流: {'s': {'u': 0, 'v': 0}, 'u': {'v': 0, 't': 0}, 'v': {'t': 0}, 't': {}}
# 残存网络: {'s': {'u': 100, 'v': 100}, 'u': {'v': 1, 't': 100}, 'v': {'t': 100}, 't': {}}
# 增广路径: [('s', 'u'), ('u', 'v'), ('v', 't')]
# 残存容量: 1
# 第2轮
# 当前的流: {'s': {'u': 1, 'v': 0}, 'u': {'v': 1, 't': 0}, 'v': {'t': 1}, 't': {}}
# 残存网络: {'s': {'u': 99, 'v': 100}, 'u': {'s': 1, 't': 100}, 'v': {'u': 1, 't': 99}, 't': {'v': 1}}
# 增广路径: [('s', 'v'), ('v', 'u'), ('u', 't')]
# 残存容量: 1
# 第3轮
# 当前的流: {'s': {'u': 1, 'v': 1}, 'u': {'v': 0, 't': 1}, 'v': {'t': 1}, 't': {}}
# 残存网络: {'s': {'u': 99, 'v': 99}, 'u': {'s': 1, 'v': 1, 't': 99}, 'v': {'s': 1, 't': 99}, 't': {'u': 1, 'v': 1}}
# 增广路径: [('s', 'u'), ('u', 'v'), ('v', 't')]
# 残存容量: 1
# 第4轮
# 当前的流: {'s': {'u': 2, 'v': 1}, 'u': {'v': 1, 't': 1}, 'v': {'t': 2}, 't': {}}
# 残存网络: {'s': {'u': 98, 'v': 99}, 'u': {'s': 2, 't': 99}, 'v': {'s': 1, 'u': 1, 't': 98}, 't': {'u': 1, 'v': 2}}
# 增广路径: [('s', 'v'), ('v', 'u'), ('u', 't')]
# 残存容量: 1
# 第5轮
# 当前的流: {'s': {'u': 2, 'v': 2}, 'u': {'v': 0, 't': 2}, 'v': {'t': 2}, 't': {}}
# 残存网络: {'s': {'u': 98, 'v': 98}, 'u': {'s': 2, 'v': 1, 't': 98}, 'v': {'s': 2, 't': 98}, 't': {'u': 2, 'v': 2}}
# 增广路径: [('s', 'u'), ('u', 'v'), ('v', 't')]
# 残存容量: 1
# ...
# 第201轮
# 当前的流: {'s': {'u': 100, 'v': 100}, 'u': {'v': 0, 't': 100}, 'v': {'t': 100}, 't': {}}
# 残存网络: {'s': {}, 'u': {'s': 100, 'v': 1}, 'v': {'s': 100}, 't': {'u': 100, 'v': 100}}
# 最大流值为200
非整数容量

若边的容量均为有理数,可将有理数表示成既约分数的形式,设所有分母的最小公倍数为$d$,将所有边的容量乘以$d$可全部变成整数,根据 Ford-Fulkerson 算法求得最大流后再除以$d$就是原流网络的最大流

若存在边的容量为无理数,则 Ford-Fulkerson 算法可能无法停止,甚至极限流值不等于正确流值

无理数容量

1993 年,Uri Zwick 提出如下 6 个点的流网络,其中$\phi$为黄金分割比,满足$\phi^2 + \phi = 1$

假设第$1$轮的增广路径为$s \rightarrow b \rightarrow c \rightarrow t$,之后中间三条边的残存容量分别为$1$$0$$\phi$

无理数容量

假设某轮后中间三条边的残存容量分别为$\phi^{k-1}$$0$$\phi^k$

轮次 增广路径 流值增量 残存容量
$1$ $(B)$ $\phi^k$ $\phi^{k-1} - \phi^k = \phi^{k+1}$$\phi^k$$0$
$2$ $(C)$ $\phi^k$ $\phi^{k+1}$$0$$\phi^k$
$3$ $(B)$ $\phi^{k+1}$ $0$$\phi^{k+1}$$\phi^k - \phi^{k+1} = \phi^{k+2}$
$4$ $(A)$ $\phi^{k+1}$ $\phi^{k+1}$$0$$\phi^{k+2}$

$4$轮一个周期,残存容量从$\phi^{k-1}$$0$$\phi^k$变为$\phi^{k+1}$$0$$\phi^{k+2}$

无理数容量

$4$轮一个周期,残存容量从$\phi^{k-1}$$0$$\phi^k$变为$\phi^{k+1}$$0$$\phi^{k+2}$

经过$4n+1$轮后

$$ \begin{align*} \quad |f| & = 1 + 2 \sum_{i=1}^{2n} \phi^i = 1 + 2 \frac{\phi(1 - \phi^{2n})}{1 - \phi} \\ & \longrightarrow 1 + 2 \frac{\phi}{1 - \phi} = 2 + \sqrt{5} \ll 2 X +1 \end{align*} $$

计算机内部无法精确表示无理数,研究无理数容量有何意义?

  • 限制容量为有理数是计算设备的局限性,不是计算问题本身带来的
  • 实际问题中会有浮点数容量,如果算法实现时不注意,可能会因为舍入误差导致例子中的情形
算法下界

Ford-Fulkerson 算法的时间复杂度上界为$O(|\Ecal| |f^\star|)$

Ford-Fulkerson 算法的时间复杂度下界?

流分解定理:对任意流$f$,可将其分解为$s$到$t$的路径和有向环的正线性组合,$f(u,v) > 0$当且仅当$(u,v)$在路径或环中至少出现一次,路径和环的总数不超过$|\Ecal|$

流分解定理 证明

在流网络中添加边$(t,s)$并令$f(t,s) = |f|$,于是流网络流值为零且所有点流量守恒,这样的流称为环流 (circulation)

引理:任意环流$f$可分解为不超过$\max\{ 0, \# f - 1 \}$个有向环的正线性组合,其中$\# f = \{ |(u,v)| \mid f(u,v) > 0 \}$为有非零流量的边数

引理 证明

分三种情况:

若流值为零,则$\# f = 0$,引理显然成立

若流值非零,且$f$只在单个有向环上有非零流量,则$\# f \ge 2$,引理显然成立

若流值非零,且$f$不只在单个有向环上有非零流量,对$\# f$的大小进行归纳

假设引理对非零流量边数$< \# f$的环流都成立

引理 证明

考虑从$(u,v)$开始、所有边都有非零流量的路径$u \rightarrow v \rightarrow \cdots$,由于所有点流量守恒,因此该路径可不断延长直至某个点第二次出现,于是出现环$C$

$F = \min_{(u,v) \in C} f(u,v)$,考虑新的流

$$ \begin{align*} \quad f'(u,v) = \begin{cases} f(u,v) - F, & (u,v) \in C \\ f(u,v), & \text{其它} \end{cases} \end{align*} $$

$e \in C$$f(e) = F$,于是$f'(e) = 0$$\# f' \le \# f - 1$,由归纳假设,$f'$可分解为不超过$\max\{ 0, \# f' - 1 \}$个有向环的正线性组合,于是$f$可分解为不超过$\max\{ 0, \# f' \} \le \max\{ 0, \# f - 1 \}$个有向环的正线性组合

流分解定理 证明

对任意流$f$,添加边$(t,s)$构造环流$f'$,则$\# f' = \# f + 1$

由引理,$f'$可分解为不超过$\max\{ 0, \# f' - 1 = \# f \}$个有向环的正线性组合,对包含$(t,s)$的环,删掉$(t,s)$变成$s$$t$的路径,于是$f$分解成不超过$\max\{ 0, \# f \} \le |\Ecal|$$s$$t$的路径或有向环的正线性组合

流分解算法

流分解定理可导出流分解算法

  1. 每轮从当前剩余流中寻找一条$s$$t$的路径或有向环
  2. 找出路径或环上的关键边,最大限度减少路径或环上的流量,直至流为零

寻找$s$$t$的路径或有向环需$O(|\Vcal|)$时间

  • $s$的流出为正,从$s$出发沿有向边游走,直至到达$t$得到一条路径,或某个点第二次出现得到一个有向环
  • $s$的流出为零,对任意有正流出的点,沿有向边游走,直至某个点第二次出现得到一个有向环

流分解算法每轮至少可以让一条边的流量变为零,因此最多迭代$O(|\Ecal|)$轮,总的时间复杂度为$O(|\Vcal||\Ecal|)$

算法下界

流分解算法每轮至少可以让一条边的流量变为零,因此最多迭代$O(|\Ecal|)$轮,总的时间复杂度为$O(|\Vcal||\Ecal|)$

存在流网络其中的环流可被分解成$|\Ecal| - |\Vcal| + 1 = O(|\Ecal|)$个有向环,且无法被分解成更少个有向环

类 Ford-Fulkerson 算法:每轮迭代寻找一条路径或是有向环,增加其上流值的算法,时间复杂度下界为$\Omega(|\Vcal| |\Ecal|)$

Edmonds-Karp 算法

Ford-Fulkerson 算法的低效源自于不合适的增广路径寻找方法

70 年代初期,Jack Edmonds、Richard Karp 提出两个准则

  • 寻找残存容量最大的增广路径,让剩余流值尽量小
  • 寻找边数最少的增广路径,增广路径越短,参与求残存容量的边越少,残存容量越大,流值增量越大

Ford、Fulkerson 原始的最大流论文中也提到了准则二,作为寻找增广路径的启发式方法;1970年,苏联数学家 Yefim Dinitz 提出了准则二的一个变种,当时他还是 Georgy Adelson-Velsky 算法课上的学生,他的算法多保存了一些中间结果,将时间复杂度从$O(|\Vcal| |\Ecal|^2)$降到$O(|\Vcal|^2 |\Ecal|)$

Edmonds-Karp 算法

准则 Ⅰ:寻找残存容量最大的增广路径

每轮迭代:以$s$为根结点生长树$\Tcal$,不断向$\Tcal$中加入残存容量最大的边 (类似最小生成树的 Prim 算法),直至$\Tcal$中包含从$s$$t$的路径,时间复杂度为$O(|\Ecal|\lg|\Ecal|) = O(|\Ecal|\lg|\Vcal|)$

迭代轮数:设$\Gcal_f$中的最大流为$f'$,根据流分解定理,$f'$最多可分解到$|\Ecal|$条路径上,因此至少有一条路径的流量为$|f'|/|\Ecal|$,选其为增广路径,剩余流量$\le f' - |f'|/|\Ecal| = |f'| (1 - 1/|\Ecal|)$

若存在边的容量为无理数,Edmonds-Karp算法Ⅰ可能无法停止

Edmonds-Karp 算法

初始流网络即为残存网络,最大流为$f^\star$,每轮迭代后至少衰减到原来$1 - 1/|\Ecal|$,经过$|\Ecal| \ln |f^\star|$轮迭代后,流值小于$1$

$$ \begin{align*} \quad |f^\star| (1 - 1/|\Ecal|)^{|\Ecal| \ln |f^\star|} < |f^\star| \exp (- \ln |f^\star|) = 1 \end{align*} $$

若边的容量均为整数,则流值亦为整数,小于$1$就意味着等于零,即$f$是最大流,总时间复杂度为$O(|\Ecal|^2 \lg|\Vcal| \ln |f^\star|)$

相较之前的$O(|\Ecal| |f^\star|)$$|f^\star|$移到了$\ln()$里面,因此时间复杂度是多项式级

这里用到了不等式$1 - x < \exp(-x)$

Edmonds-Karp 算法

准则 Ⅱ:寻找边数最少的增广路径

每轮迭代:残存网络中边的数目不超过$2 |\Ecal|$,采用 BFS 找一条从$s$$t$的最短路径的时间是$O(|\Ecal|)$

迭代轮数:依赖下面两个引理

  • $\delta_f (v)$$\Gcal_f$$s$$v$的最短路径长度,则$\delta_f (v)$随着迭代单调递增
  • 每条边成为增广路径上的关键边的次数不超过$|\Vcal|/2$

每轮选增广路径至少产生一条关键边,总迭代轮数$O(|\Vcal||\Ecal|)$,总时间复杂度$O(|\Vcal| |\Ecal|^2)$,与最大流值无关

Edmonds-Karp 算法

引理:对$\forall v \in \Vcal \setminus \{ s, t \}$$\delta_f (v)$随着迭代单调递增

直觉上,随着迭代进行,残存网络中的正向边越来越少,反向边越来越多,从$s$到其它点的路径越来越曲折

证明:假设存在结点$v$在某轮迭代后,$\delta_f (s,v)$减少

$f \rightarrow f'$$\delta_f (s, v)$首次变小的轮次,$\delta_f (s, v) > \delta_{f'} (s, v)$,且$v$是最短路径上离$s$最近的点

$\Gcal_{f'}$$s$$v$的最短路径为$s \rightsquigarrow u \rightarrow v$$\delta_{f'} (s, u) = \delta_{f'} (s, v) - 1$

Edmonds-Karp 算法

$\delta_f (s, v) > \delta_{f'} (s, v)$$\delta_{f'} (s, u) = \delta_{f'} (s, v) - 1$

根据$v$的选取,$\delta_f (s, u) \le \delta_{f'} (s, u)$

易知$(u,v) \not \in \Ecal_f$,否则

$$ \begin{align*} \quad \delta_{f'} (s, v) < \delta_f (s, v) \le \delta_f (s, u) + 1 \le \delta_{f'} (s, u) + 1 = \delta_{f'} (s, v) \end{align*} $$

  • $(u,v) \not \in \Ecal_f \Rightarrow f(u,v) = c(u,v)$
  • $(u,v) \in \Ecal_{f'} \Rightarrow f'(u,v) < c(u,v)$

$\Gcal_f$中得到的增广路径在边$(v,u)$上有流量,抵消了$(u,v)$上的部分流量,故$(v,u)$$\Gcal_f$$s$$u$的最短路径上的最后一条边

$$ \begin{align*} \quad \delta_f (s, v) = \delta_f (s, u) - 1 \le \delta_{f'} (s, u) - 1 = \delta_{f'} (s, v) - 2 \end{align*} $$

Edmonds-Karp 算法

引理:每条边成为关键边的次数不超过$|\Vcal|/2$

$(u,v)$第一次成为关键边时,$\delta_f (s, v) = \delta_f (s, u) + 1$

  • 流增加后,$f$充斥$(u,v)$$(u,v)$在残存网络中消失
  • 直到某一轮$(v,u)$进入增广路径,从而$(u,v)$上的流量减少,记此时流量为$f'$$\delta_{f'} (s, u) = \delta_{f'} (s, v) + 1$

根据前一个引理,$\delta_f (s, v) \le \delta_{f'} (s, v)$,于是

$$ \begin{align*} \quad \delta_{f'} (s, u) = \delta_{f'} (s, v) + 1 \ge \delta_f (s, v) + 1 = \delta_f (s, u) + 2 \end{align*} $$

$(u,v)$成为关键边后到再次成为关键边前$\delta_f (s, u)$至少增加$2$

最短路径长度不超过$|\Vcal|$,每条边成为关键边的次数不超过$|\Vcal|/2$

更多算法

最大流问题至今是一个活跃的研究领域,2012 年,James Orlin 首次提出$O(|\Vcal| |\Ecal|)$复杂度的算法,达到理论最优

技术 复杂度 复杂度 (用数据结构)
blocking flow $O(\shu \Vcal \shu^2 \shu \Ecal \shu)$ $O(\shu \Vcal \shu \shu \Ecal \shu \log \shu \Vcal \shu)$
network simplex $O(\shu \Vcal \shu^2 \shu \Ecal \shu)$ $O(\shu \Vcal \shu \shu \Ecal \shu \log \shu \Vcal \shu)$
push-relabel (generic) $O(\shu \Vcal \shu^2 \shu \Ecal \shu)$ -
push-relabel (FIFO) $O(\shu \Vcal \shu^3)$ $O(\shu \Vcal \shu \shu \Ecal \shu \log (\shu \Vcal \shu^2 / \shu \Ecal \shu))$
push-relabel (highest label) $O(\shu \Vcal \shu^2 \sqrt{\shu \Ecal \shu})$ -
push-relabel-add games - $O(\shu \Vcal \shu \shu \Ecal \shu \log_{\shu \Ecal \shu / (\shu \Vcal \shu \log \shu \Vcal \shu)} \shu \Vcal \shu)$
pseudoflow $O(\shu \Vcal \shu^2 \shu \Ecal \shu)$ $O(\shu \Vcal \shu \shu \Ecal \shu \log \shu \Vcal \shu)$
pseudoflow (highest label) $O(\shu \Vcal \shu^3)$ $O(\shu \Vcal \shu \shu \Ecal \shu \log (\shu \Vcal \shu^2 / \shu \Ecal \shu))$
incremental BFS $O(\shu \Vcal \shu^2 \shu \Ecal \shu)$ $O(\shu \Vcal \shu \shu \Ecal \shu \log (\shu \Vcal \shu^2 / \shu \Ecal \shu))$
compact networks - $O(\shu \Vcal \shu \shu \Ecal \shu )$
更多算法

对于单位容量的流网络,即每条边容量为$1$的流网络

1973 年,Alexander Karzanov 证明 Dinitz 的 blocking flow 算法的时间复杂度为$O(\min\{ |\Vcal|^{2/3}, |\Ecal|^{1/2} \} |\Ecal|)$

Alexander Karzanov 同时证明了单位容量流网络中的流分解时间复杂度是$O(\min\{ |\Vcal|^{2/3}, |\Ecal|^{1/2} \} |\Ecal|)$,因此并没有和前面的下界$\Omega(|\Vcal| |\Ecal|)$矛盾

2013 年,Aleksander Mądry 提出了一个$O(|\Ecal|^{10/7} \mathrm{poly} (\log |\Ecal|))$时间复杂度的算法,在$|\Ecal| = o(|\Vcal|^{14/9})$的情况下更优

最大边不相交路径数

任给有向图,求任意两点间的最大边不相交路径数

下图$s \rightsquigarrow t$有 3 条边不相交的路径,是否为最大?

归约为单位容量的流网络的最大流问题

最大边不相交路径数

一方面,对任意$k$条边不相交的路径,每条上面可运输单位流量,得到流值为$k$的流,因此最大边不相交路径数$\le$最大流值

另一方面,由于边的容量均为整数,因此最大流在各条边上的流值也为整数,非$0$$1$,每条边要么满流量、要么零流量;根据流分解定理,最大流可分解为若干条$s \rightsquigarrow t$的路径,每条路径上的流量为$1$,因此这些路径不会有公共边,故最大流值$\le$最大边不相交路径数

利用 James Orlin 算法,时间复杂度为$O(|\Vcal| |\Ecal|)$

事实上,切割$(\{s\}, \Vcal \setminus \{s\})$的容量最多为$|\Vcal| - 1$,因此最大流值不超过$|\Vcal| - 1$,朴素的 Ford - Fulkerson 算法也有$O(|\Vcal| |\Ecal|)$的时间复杂度

点容量限制

如果流网络$\Gcal$对任意点$v \in \Vcal \setminus \{ s, t \}$也有容量限制

$$ \begin{align*} \quad \sum_u f(u,v) \le c(v) \end{align*} $$

1962年,Ford、Fulkerson提出如下$\Gcal \rightarrow \Gcal'$的归约

  • 对任意点$v$,替换为一进一出两个点$v_i$、$v_o$
  • 添加边$(v_i, v_o)$,容量$c(v_i, v_o) = c(v)$
  • 替换边$(u,v)$为$(u_o, v_i)$,容量不变

对$\Gcal'$求$s_o$到$t_i$的最大流即可

点容量限制

归约遍历所有点和边,时间复杂度为$O(|\Vcal| + |\Ecal|) = O(|\Ecal|)$

最大点不相交路径数

给每个点的容量设为$1$,求最大流即可

匹配 最大匹配

匹配:无向图$\Gcal = (\Vcal, \Ecal)$的匹配是边的一个子集$\Mcal \subseteq \Ecal$,使得对$\forall v$,子集$\Mcal$中最多有一条边与$v$相连

最大匹配:若$\Mcal$是最大匹配,则对任意匹配$\Mcal'$$|\Mcal| \ge |\Mcal'|$

最大二分匹配

二分图:结点集合$\Vcal = \Lcal \uplus \Rcal$,边集$\Ecal$中所有边横跨$\Lcal$$\Rcal$

问题:在二分图中寻找最大匹配

应用

  • 求职平台:求职者、招聘公司
  • 相亲平台:👦、👧
最大二分匹配

归约为流网络$\Gcal' = \{ \Vcal', \Ecal' \}$中的最大流问题

  • $\Vcal' = \Vcal \cup \{ s, t \}$
  • $\Ecal' = \Ecal \cup \{ (s, u) \mid u \in \Lcal \} \cup \{ (v,t) \mid v \in \Rcal \}$
  • 每条边上的容量为单位容量,即对$\forall (u,v) \in \Ecal'$$c(u,v) = 1$
最大二分匹配

一方面,任意匹配$\Mcal$可以转化为一个可行的流$f_{\Mcal}$:对任意边$(u,v) \in \Mcal$,在路径$s \rightarrow u \rightarrow v \rightarrow t$上运输单位流,因此最大匹配$\le$最大流

另一方面,由于边的容量均为整数,因此最大流在各条边上的流值也为整数,非$0$$1$:对于$\Lcal$中的点$u$,只有一条入边$(s,u)$,若其有流量流入,则必然有一条边流出;对于$\Rcal$中的点$v$,只有一条出边$(v,t)$,若其有流量流出,则必然有一条边流入,故从$\Lcal$$\Rcal$的有流量的边是一个匹配,于是最大流$\le$最大匹配

综上,最大流等于最大匹配

时间复杂度

对于二分图,假设每个结点至少与一条边相连,即$|\Ecal| \ge |\Vcal| / 2$

$|\Ecal'| = |\Ecal| + |\Vcal| \le 3 |\Ecal| = \Theta(|\Ecal|)$,最大流值不超过$|\Vcal|$

Ford-Fulkerson 算法的时间复杂度为$O(|\Vcal| |\Ecal|)$,与 James Orlin 算法相当

交替路径

交替路径:从$\Lcal$中某个不属于匹配的点到$\Rcal$中某个不属于匹配的点的路径,其中的边交替属于或不属于匹配

Ford-Fulkerson 算法迭代寻找增广路径的过程就是在原二分图中寻找交替路径的过程,每找到一条交替路径,匹配数加$1$

1957年,Claude Berge研究了通过交替路径来寻找最大二分匹配,独立于最大流最小切割的研究;1973年,John Hopcroft、Richard Karp通过每轮寻找多个交替路径,将时间复杂度降到$O(\sqrt{|\Vcal|} |\Ecal|)$

考试安排

$n$个班级、$r$个教室、$t$个时间段、$p$位监考员,输入

  • $\text{class}[1, \ldots, n]$,其中$\text{class}[i]$表示第$i$个班级的人数
  • $\text{room}[1, \ldots, r]$,其中$\text{room}[j]$表示第$j$个教室的座位数
  • $\text{available}[1, \ldots, t][1, \ldots, p]$,其中$\text{available}[k][l] = \text{True}$表示第$l$个监考员在第$k$个时间段有空

要求

  • 每个教室每个时间段只能安排一个班级的考试
  • 一个班级的学生不能分到多个教室或多个时间段考试
  • 每场考试至少要有一位监考员
  • 每位监考员至多只能监考 5 场考试

输出:是否能安排全部班级的考试,若能,给出方案

考试安排

构建流网络,若最大流值为$n$,则可安排全部班级的考试

g cluster_1 班级 cluster_2 教室 cluster_3 时间 cluster_4 监考员 s 11 s->11 1 12 s->12 13 s->13 14 s->14 15 s->15 21 11->21 22 11->22 23 11->23 12->21 12->22 12->23 13->21 13->22 13->23 14->22 14->23 24 14->24 15->22 15->23 15->24 31 21->31 32 21->32 33 21->33 34 21->34 22->31 22->32 22->33 22->34 23->31 1 23->32 23->33 23->34 24->31 24->32 24->33 24->34 41 31->41 1 42 31->42 43 31->43 32->41 32->42 32->43 44 32->44 45 32->45 33->41 33->42 33->43 33->44 33->45 34->44 34->45 t 41->t 5 42->t 43->t 44->t 45->t

元组选择

假设有$n$个有限集$\Xcal_1, \ldots, \Xcal_n$,代表$n$种资源

从每个集合选一个元素构成$n$元组

  • 每个$x \in \Xcal_i$最多出现在$c(x)$个元组中
  • 每对$(x,y) \in \Xcal_i \times \Xcal_{i+1}$最多出现在$c(x,y)$个元组中

这是个一般性的问题

  • 最大二分匹配:$n = 2$$c(x)=1$$c(x,y) = 1$
  • 考试安排:$n = 4$、……
不相交路径覆盖数

给定有向图$\Gcal$,寻找若干条不相交的路径包含每个结点恰好一次

平凡解:每个结点单独一个路径

最少的不相交路径覆盖数?

最少的不相交路径覆盖数为 1 等价于图中存在哈密顿路径

这是一个 NP 难的问题

但当输入是无环图 (DAG) 时有基于最大流的高效解法

不相交路径覆盖数

根据$\Gcal$,构建二分图$\Gcal'$

  • 对任意点$v$,替换为两个点$v_l$$v_r$
  • 对任意有向边$(u,v)$,替换为无向边$(u_r, v_l)$

$\Gcal$可被$k$条不相交路径覆盖当且仅当$\Gcal'$中存在$|\Vcal| - k$的匹配

不相交路径覆盖数

不相交路径覆盖数

$\Gcal$可被$k$条不相交路径覆盖当且仅当$\Gcal'$中存在$|\Vcal| - k$的匹配

$\Gcal$可被$k$条不相交路径覆盖,记路径集合构成的子图为$\Pcal$,由于每条路径的点数比边数多$1$,因此$\Pcal$共有$|\Vcal| - k$条边,定义$\Gcal'$中的边集$\Mcal' = \{ (u_r, v_l) \mid (u, v) \in \Pcal \}$,显然$|\Mcal| = |\Vcal| - k$$\Mcal'$中每个点最多只与一条边相连,反设$(u_r, v_l), (u_r, w_l) \in \Mcal'$,则$(u, v), (u, w) \in \Pcal$,这是不可能的,因此$\Mcal'$是一个匹配

$\Gcal'$中存在$|\Vcal| - k$的匹配$\Mcal'$,定义$\Gcal$中的子图$\Pcal = (\Vcal, \Mcal)$,其中$\Mcal = \{ (u,v) \in \Ecal \mid (u_r, v_l) \in \Mcal' \}$,由于$\Mcal'$是一个匹配,因此$\Pcal$中每个点最多只有一条入边、一条出边,故$\Pcal$是一系列不相交的路径,又其包含所有结点,因此路径数为$k$

最少雇佣人数

$n$个课程、$m$个教室,输入

  • $C[1,\ldots, n]$,其中$C[i]$包含 3 个子域,分别为开始时间$C[i].\text{start}$、结束时间$C[i].\text{end}$、上课教室$C[i].\text{loc}$
  • $T[1,\ldots, m][1,\ldots, m]$,其中$T[j][k]$为从第$j$个教室步行到第$k$个教室的时间

假设

  • 每个老师都是全能,所有课都能教
  • 每个老师都只喜欢步行

输出:最少需雇佣多少名老师才能完成教学任务

最少雇佣人数

构建图$\Gcal$

  • 每个点对应一门课程
  • $(u,v)$存在当且仅当$C[v].\text{end} \ge C[u].\text{start} + T[C[u].\text{loc}][C[v].\text{loc}]$

之后在图中求最少的不相交路径覆盖数

作业

算法导论 3rd

26.1-1、26.2-3、26.3-1