【图论】上下界网络流算法

上下界网络流算法详解与应用

1. 基础概念

1.1 传统网络流 vs 上下界网络流

传统网络流(最大流问题):

  • 每条边 (u, v) 只有容量上限 c(u, v)
  • 流量约束:0 ≤ f(u, v) ≤ c(u, v)
  • 满足流量守恒:对除源点和汇点外的任意点 u,有 ∑f(·, u) = ∑f(u, ·)

上下界网络流:

  • 每条边 (u, v) 有下界 l(u, v) 和上界 c(u, v)
  • 流量约束:l(u, v) ≤ f(u, v) ≤ c(u, v)
  • 同样满足流量守恒

1.2 为什么需要上下界?

在实际应用中,很多场景不仅有容量限制,还有最低要求:

  • 生产系统:每条生产线有最低产量要求
  • 交通网络:某些道路必须保持最低车流量
  • 电力系统:输电线路有最小功率传输要求
  • 资源分配:每个任务需要的资源有最低保障

2. 无源汇上下界可行流

这是最基础的上下界网络流问题,也是解决其他问题的基础。

2.1 问题描述

给定一个有向图,每条边有流量下界和上界,问是否存在一个循环流(即每个点都满足流量守恒)满足所有约束。

2.2 核心思想:强制流量分离

我们将每条边的流量分解为两部分:

  • 强制流量:等于下界 l(u, v),这部分必须存在
  • 自由流量:在 [0, c(u, v) - l(u, v)] 范围内可变

2.3 详细算法步骤

步骤1:计算每个点的"不平衡量"

对每个点 u,计算:

d[u] = ∑l(·, u) - ∑l(u, ·)

其中:

  • ∑l(·, u):所有指向 u 的边的下界之和
  • ∑l(u, ·):所有从 u 出发的边的下界之和

d[u] 的含义:

  • d[u] > 0:点 u 的流入强制流量大于流出强制流量,需要额外 d[u] 的流入来平衡
  • d[u] < 0:点 u 的流出强制流量大于流入强制流量,需要额外 |d[u]| 的流出
  • d[u] = 0:强制流量已平衡

步骤2:构建新网络

创建超级源点 S 和超级汇点 T

  1. 对每个满足 d[u] > 0 的点 u,添加边 (S, u),容量为 d[u]
  2. 对每个满足 d[u] < 0 的点 u,添加边 (u, T),容量为 -d[u]
  3. 原图中每条边 (u, v) 的容量变为 c(u, v) - l(u, v)(去掉下界部分)

步骤3:求解最大流

在新网络上求 ST 的最大流。

步骤4:判断可行性

如果最大流等于 ∑d[u](对所有 d[u] > 0 的点求和),则存在可行流。

2.4 为什么这个方法正确?

必要性证明:
如果存在可行流,那么:

  • 所有 d[u] > 0 的点需要的额外流入总量必须等于所有 d[u] < 0 的点需要的额外流出总量
  • 这个总量就是 ∑d[u] (d[u] > 0)
  • 因此从 S 流出的流量必须等于这个值

充分性证明:
如果最大流等于 ∑d[u] (d[u] > 0),说明:

  • 所有点的不平衡量都被补偿
  • 将最大流的流量加上强制流量,就得到原图的可行流

2.5 示例分析

考虑以下网络(边标注为 l/c,表示下界/上界):

    1/3     1/4
1 ------> 2 ------> 3
|         ^         |
| 1/2     | 1/2     | 1/3
|         |         |
V         |         V
4 ------> 5 ------> 6
    1/3     1/4

步骤1:计算不平衡量

∑l(·,u)∑l(u,·)d[u]
101+1=2-2
21+1=21=11
3101
4110
51+1=21+1=20
6101

步骤2:构建新网络

  • 添加边 (S, 2),容量 1
  • 添加边 (S, 3),容量 1
  • 添加边 (S, 6),容量 1
  • 添加边 (1, T),容量 2
  • 修改原边容量:所有边容量减1

步骤3:求最大流

在新网络上求 ST 的最大流,如果等于 3(1+1+1),则存在可行流。


3. 有源汇上下界可行流

3.1 问题描述

给定源点 s 和汇点 t,问是否存在从 st 的可行流。

3.2 转化方法

核心思想: 将有源汇问题转化为无源汇问题。

步骤:

  1. 添加一条从汇点 t 到源点 s 的边,下界为 0,上界为无穷大
  2. 转化为无源汇问题求解
  3. 如果存在可行流,则原问题存在可行流

3.3 为什么这样转化?

添加 t → s 的边后:

  • 整个网络变成了循环流
  • st 的流量等于从 ts 的流量
  • 这正是我们想要的流

4. 有源汇上下界最大流

4.1 问题描述

在满足上下界约束的前提下,求从源点到汇点的最大流量。

4.2 两步法

步骤1:求可行流

使用上述方法求一个可行流。

步骤2:在残余网络中求最大流

  1. 保持可行流的状态
  2. 在残余网络中,从源点 s 到汇点 t 求最大流
  3. 总流量 = 可行流中的流量 + 第二步求得的最大流

4.3 详细解释

为什么可以在残余网络中继续增广?

  • 可行流已经满足所有上下界约束
  • 在残余网络中增广不会破坏下界约束
  • 因为增广只会增加流量,而不会减少到下界以下

5. 有源汇上下界最小流

5.1 问题描述

在满足上下界约束的前提下,求从源点到汇点的最小流量。

5.2 两步法

步骤1:求可行流

同最大流的第一步。

步骤2:尝试减少流量

  1. 在残余网络中,从汇点 t 到源点 s 求最大流(反向增广)
  2. 最小流量 = 可行流中的流量 - 第二步求得的最大流

5.3 为什么可以反向增广?

反向增广相当于:

  • 减少从 st 的流量
  • 只要不减少到下界以下,就是合法的
  • 残余网络中的反向边容量保证了不会违反下界约束

6. C++ 实现(超详细注释版)

#include <iostream>
#include <vector>
#include <queue>
#include <climits>
#include <algorithm>
#include <cstring>
using namespace std;

const int MAXN = 210;  // 最大点数(考虑超级源汇)
const int INF = 0x3f3f3f3f;  // 无穷大,使用0x3f3f3f3f避免溢出

// 边的结构体
struct Edge {
    int to;           // 目标节点
    int rev;          // 反向边在邻接表中的索引
    int cap;          // 容量
    int flow;         // 当前流量
    int lower;        // 下界(原始下界,用于恢复实际流量)
    
    // 构造函数
    Edge(int t, int r, int c, int l = 0) : to(t), rev(r), cap(c), flow(0), lower(l) {}
};

class LowerBoundFlow {
private:
    vector<Edge> graph[MAXN];  // 邻接表表示图
    int level[MAXN];           // BFS分层
    int iter[MAXN];            // 当前弧优化
    int n, s, t;               // 原图的点数、源点、汇点
    int S, T;                  // 超级源点、超级汇点
    
    // 内部添加边函数(不包含下界信息)
    void add_edge_internal(int from, int to, int cap, int lower = 0) {
        graph[from].push_back(Edge(to, graph[to].size(), cap, lower));
        // 添加反向边,容量为0
        graph[to].push_back(Edge(from, graph[from].size() - 1, 0, 0));
    }
    
    // BFS构建分层图
    void bfs(int s) {
        // 初始化所有点的层级为-1(未访问)
        memset(level, -1, sizeof(level));
        queue<int> q;
        level[s] = 0;
        q.push(s);
        
        while (!q.empty()) {
            int u = q.front(); q.pop();
            // 遍历u的所有出边
            for (const Edge& e : graph[u]) {
                // 如果边还有剩余容量且目标点未访问
                if (e.cap > e.flow && level[e.to] < 0) {
                    level[e.to] = level[u] + 1;
                    q.push(e.to);
                }
            }
        }
    }
    
    // DFS寻找增广路
    int dfs(int u, int t, int f) {
        if (u == t) return f;  // 到达汇点,返回可增广流量
        
        // 当前弧优化:从上次中断的位置开始
        for (int& i = iter[u]; i < graph[u].size(); i++) {
            Edge& e = graph[u][i];
            // 如果边有剩余容量且目标点在下一层
            if (e.cap > e.flow && level[u] < level[e.to]) {
                // 递归寻找增广路
                int d = dfs(e.to, t, min(f, e.cap - e.flow));
                if (d > 0) {
                    // 更新正向边流量
                    e.flow += d;
                    // 更新反向边流量(反向边流量减少)
                    graph[e.to][e.rev].flow -= d;
                    return d;
                }
            }
        }
        return 0;  // 未找到增广路
    }
    
    // Dinic算法求最大流
    int max_flow(int s, int t) {
        int total_flow = 0;
        while (true) {
            // 构建分层图
            bfs(s);
            // 如果无法到达汇点,结束
            if (level[t] < 0) return total_flow;
            
            // 当前弧优化初始化
            memset(iter, 0, sizeof(iter));
            
            int f;
            // 不断寻找增广路
            while ((f = dfs(s, t, INF)) > 0) {
                total_flow += f;
            }
        }
    }
    
    // 清除指定点的流量(用于最小流计算)
    void clear_flow(int u) {
        for (Edge& e : graph[u]) {
            e.flow = 0;
        }
    }

public:
    // 构造函数
    LowerBoundFlow(int nodes, int source, int sink) : n(nodes), s(source), t(sink) {
        S = n + 1;  // 超级源点编号
        T = n + 2;  // 超级汇点编号
        
        // 清空图
        for (int i = 0; i <= n + 2; i++) {
            graph[i].clear();
        }
    }
    
    // 添加带上下界的边
    void add_edge(int from, int to, int lower, int upper) {
        // 实际添加的边容量为 upper - lower
        add_edge_internal(from, to, upper - lower, lower);
    }
    
    // 求无源汇上下界可行流
    bool feasible_flow() {
        vector<int> demand(n + 1, 0);  // 每个点的需求量
        
        // 计算每个点的净需求
        for (int u = 1; u <= n; u++) {
            for (const Edge& e : graph[u]) {
                if (e.lower > 0) {  // 只考虑原始的带下界边
                    // 流出u的强制流量
                    demand[u] -= e.lower;
                    // 流入v的强制流量
                    demand[e.to] += e.lower;
                }
            }
        }
        
        // 统计总需求
        int total_demand = 0;
        
        // 添加超级源点和超级汇点的边
        for (int u = 1; u <= n; u++) {
            if (demand[u] > 0) {
                // 需要额外流入,从超级源点连入
                add_edge_internal(S, u, demand[u]);
                total_demand += demand[u];
            } else if (demand[u] < 0) {
                // 需要额外流出,连向超级汇点
                add_edge_internal(u, T, -demand[u]);
            }
        }
        
        // 如果总需求为0,直接存在可行流
        if (total_demand == 0) return true;
        
        // 求超级源点到超级汇点的最大流
        int flow = max_flow(S, T);
        
        // 如果最大流等于总需求,说明可以满足所有点的需求
        return flow == total_demand;
    }
    
    // 求有源汇上下界可行流
    bool feasible_flow_st() {
        // 添加t->s的无穷边,将有源汇转化为无源汇
        add_edge_internal(t, s, INF);
        
        // 求解无源汇可行流
        return feasible_flow();
    }
    
    // 求有源汇上下界最大流
    int max_flow_st() {
        // 先求可行流
        if (!feasible_flow_st()) {
            return -1;  // 不存在可行流
        }
        
        // 保存可行流中t->s边的流量(即基础流量)
        int base_flow = 0;
        for (const Edge& e : graph[t]) {
            if (e.to == s) {
                base_flow = e.flow;
                break;
            }
        }
        
        // 重置流量,准备在残余网络中求最大流
        // 注意:只重置原图中的边,保留t->s边的流量
        for (int u = 1; u <= n; u++) {
            for (Edge& e : graph[u]) {
                if (!(u == t && e.to == s)) {  // 不是t->s边
                    e.flow = 0;
                }
            }
        }
        
        // 在残余网络中求s->t的最大流
        int additional_flow = max_flow(s, t);
        
        // 总流量 = 基础流量 + 额外流量
        return base_flow + additional_flow;
    }
    
    // 求有源汇上下界最小流
    int min_flow_st() {
        // 先求可行流
        if (!feasible_flow_st()) {
            return -1;  // 不存在可行流
        }
        
        // 保存可行流中t->s边的流量(即基础流量)
        int base_flow = 0;
        for (const Edge& e : graph[t]) {
            if (e.to == s) {
                base_flow = e.flow;
                break;
            }
        }
        
        // 重置流量
        for (int u = 1; u <= n; u++) {
            for (Edge& e : graph[u]) {
                if (!(u == t && e.to == s)) {
                    e.flow = 0;
                }
            }
        }
        
        // 在残余网络中求t->s的最大流(反向增广)
        // 这相当于减少从s到t的流量
        int reduce_flow = max_flow(t, s);
        
        // 最小流量 = 基础流量 - 可减少的流量
        return base_flow - reduce_flow;
    }
    
    // 获取实际流量(用于调试和验证)
    void print_actual_flow() {
        cout << "实际流量分布:" << endl;
        for (int u = 1; u <= n; u++) {
            for (const Edge& e : graph[u]) {
                if (e.lower > 0) {  // 原始的带下界边
                    int actual_flow = e.flow + e.lower;
                    cout << "边 " << u << " -> " << e.to 
                         << ": 流量 = " << actual_flow 
                         << " (下界=" << e.lower 
                         << ", 上界=" << (e.lower + e.cap) << ")" << endl;
                }
            }
        }
    }
    
    // 验证流量是否满足约束(调试用)
    bool verify_flow() {
        // 检查每条边的流量是否在上下界内
        for (int u = 1; u <= n; u++) {
            for (const Edge& e : graph[u]) {
                if (e.lower > 0) {  // 原始边
                    int actual_flow = e.flow + e.lower;
                    if (actual_flow < e.lower || actual_flow > e.lower + e.cap) {
                        return false;
                    }
                }
            }
        }
        return true;
    }
};

// 使用示例和测试
int main() {
    cout << "=== 上下界网络流算法演示 ===" << endl;
    
    // 示例1:有源汇上下界最大流
    cout << "\n示例1:有源汇上下界最大流" << endl;
    LowerBoundFlow flow1(4, 1, 4);  // 4个点,源点1,汇点4
    
    // 添加边:from, to, lower, upper
    flow1.add_edge(1, 2, 1, 3);
    flow1.add_edge(1, 3, 1, 2);
    flow1.add_edge(2, 3, 1, 2);
    flow1.add_edge(2, 4, 1, 3);
    flow1.add_edge(3, 4, 1, 2);
    
    int max_flow = flow1.max_flow_st();
    if (max_flow == -1) {
        cout << "不存在可行流!" << endl;
    } else {
        cout << "最大流: " << max_flow << endl;
        flow1.print_actual_flow();
        cout << "流量验证: " << (flow1.verify_flow() ? "通过" : "失败") << endl;
    }
    
    // 示例2:无源汇上下界可行流
    cout << "\n示例2:无源汇上下界可行流" << endl;
    LowerBoundFlow flow2(3, 1, 3);  // 用于无源汇问题,s,t参数不重要
    
    flow2.add_edge(1, 2, 1, 3);
    flow2.add_edge(2, 3, 1, 3);
    flow2.add_edge(3, 1, 1, 3);
    
    bool feasible = flow2.feasible_flow();
    cout << "是否存在可行流: " << (feasible ? "是" : "否") << endl;
    
    return 0;
}

7. 算法正确性深入分析

7.1 可行性判定的数学证明

设原图为 G = (V, E),其中每条边 e ∈ E 有下界 l(e) 和上界 c(e)

定义:

  • f: E → R 为流量函数
  • 对每个点 v ∈ V,定义净流量:div(f, v) = ∑f(·, v) - ∑f(v, ·)

对于可行流,要求:

  1. l(e) ≤ f(e) ≤ c(e),对所有 e ∈ E
  2. div(f, v) = 0,对所有 v ∈ V(无源汇情况)

引理1:如果存在可行流 f,则对任意点集 U ⊂ V,有:

∑_{e ∈ δ⁻(U)} l(e) ≤ ∑_{e ∈ δ⁺(U)} c(e)

其中 δ⁻(U) 是进入 U 的边集,δ⁺(U) 是离开 U 的边集。

这个引理说明了可行流存在的必要条件。

引理2:通过超级源汇构造的新网络中,最大流等于 ∑d[v]⁺ 当且仅当原网络存在可行流。

这个引理证明了算法的充分必要性。

7.2 复杂度优化技巧

  1. 当前弧优化:避免重复检查已饱和的边
  2. 多路增广:一次DFS中尽可能多地增广
  3. ISAP算法:可以进一步优化,避免重复BFS

8. 实际应用案例

8.1 生产调度问题

某工厂有3条生产线,生产2种产品:

  • 生产线1:产能 [10, 50],可生产产品A
  • 生产线2:产能 [15, 60],可生产产品B
  • 生产线3:产能 [20, 80],可生产产品A和B

产品需求:

  • 产品A:至少需要40单位
  • 产品B:至少需要35单位

建模:

  • 源点 → 生产线:下界=最小产能,上界=最大产能
  • 生产线 → 产品:无下界,上界=生产线产能
  • 产品 → 汇点:下界=需求,上界=无穷大

8.2 交通网络优化

城市道路网络中:

  • 主干道:车流量必须在 [1000, 5000] 辆/小时
  • 次干道:车流量必须在 [500, 3000] 辆/小时
  • 问:能否在满足这些约束的情况下,实现从A区到B区的最大交通流量?

9. 常见错误和陷阱

  1. 无穷大值选择不当:导致整数溢出
  2. 忘记重置流量:在求最大/最小流时未正确处理残余网络
  3. 下界为0的边处理:需要特殊判断
  4. 点编号冲突:超级源汇编号与原图冲突
  5. 流量恢复错误:实际流量 = 流量 + 下界

10. 总结

上下界网络流是网络流理论的重要扩展,其核心思想是:

  1. 分离强制流量和自由流量
  2. 通过超级源汇补偿不平衡
  3. 利用残余网络进行优化

掌握这一技术需要理解:

  • 流量守恒的本质
  • 网络流的线性规划本质
  • 转化思想的应用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值