费用流,EK算法,Primal Dual 算法详解,OJ练习

文章目录

    • 一、费用流
      • [1.1 概念](#1.1 概念)
      • [1.2 Edmonds-Karps 增广路算法](#1.2 Edmonds-Karps 增广路算法)
      • [1.3 算法流程](#1.3 算法流程)
      • [1.4 算法实现/模板练习](#1.4 算法实现/模板练习)
    • 二、OJ练习
      • [2.1 运输问题](#2.1 运输问题)
      • [2.2 负载平衡问题](#2.2 负载平衡问题)
      • [2.3 分配问题](#2.3 分配问题)
      • [2.4 数字梯形问题](#2.4 数字梯形问题)
      • [2.5 K取方格数](#2.5 K取方格数)
      • [2.6 P4012 深海机器人问题](#2.6 P4012 深海机器人问题)
      • [2.7 餐巾计划问题](#2.7 餐巾计划问题)
      • [2.8 [NOI2008\] 志愿者招募](#2.8 [NOI2008] 志愿者招募)
    • [三、(补档)Primal Dual 算法](#三、(补档)Primal Dual 算法)
      • [3.1 定义](#3.1 定义)
        • [3.1.1 势](#3.1.1 势)
      • [3.2 实现](#3.2 实现)
        • [3.2.1 势的求解](#3.2.1 势的求解)
        • [3.2.2 代码实现](#3.2.2 代码实现)
        • [3.2.3 时间复杂度](#3.2.3 时间复杂度)

一、费用流

1.1 概念

给定一个网络 G = (V, E),每条边(u, v)除了有容量限制 c(u, v),还有一个给定的"单位费用"w(u, v)。当边(u, v)的流量为 f(u, v)时,就要花费 f(u, v) * w(u, v)

该网络中总花费最小的最大流被称为**"最小费用最大流",总花费最大的最大流被称为"最大费用最大流",二者合称为 "费用流"**模型。注意:费用流的前提是最大流,然后才考虑费用的最值。

类似于"二分图最大匹配"与最大流的关系,"二分图带权最大匹配"可直接用最大费用最大流求解,每条边的权值就是它的单位费用。

PS:由于"最大费用最大流"只需把最短路改为最长路,所以下面只讨论"最小费用最大流"

1.2 Edmonds-Karps 增广路算法

Edmonds-Karps 求解最大流的基础上, 把"通过 bfs 寻找增广路" 改为 "用spfa寻找一条单位费用之和最小的增广路"(即w(u, v)当作边权, 在残留网络上求最短路 ), 即可求出最小费用最大流.

证明:

数学归纳法:

对于当前流量 |f1|下的我们找到一个最小费用流f1,然后在f1的残留网络上通过spfa()增广出一条最小费用增广路, 该流为f2

那么叠加后有新流f = f1 + f2

只需证明f流量|f|下的最小费用流

假设f费用不最小,那么存在流f', |f'| = |f|

我们令f2' = f' - f1, 那么有|f2'| = |f2|, dst(f2') < dst(f2)

这与f2f1的残留网络上的最小费用增广路, 相矛盾, 得证

1.3 算法流程

  • 将EK算法的bfs改为spfa
  • 在EK算法的更新流量的同时更新费用
    • 为了退流,反向边的初始容量为0,反向边的容量每次 +f
    • 为了退费,反向边的初始费用为-w,走反向边的花费 + f * (-w)(和退流不同, 退流还要更改反向边剩余容量, 但是退费反向边的费用是固定不变的)

1.4 算法实现/模板练习

原题链接

P3381 【模板】最小费用最大流 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

AC代码

c++ 复制代码
#include <iostream>
#include <cstring>
#include <algorithm>
using PII = std::pair<int, int>;
const int N = 5005, M = 1e5 + 10, inf = 1e9;
int n, m, s, t, idx;
int head[N], pre[N], incf[N], dst[N], q[N];
bool vis[N];
struct edge{
    int v, c, w, nxt;
} edges[M];
void addedge(int u, int v, int c, int w) {
    edges[idx] = { v, c, w, head[u] }, head[u] = idx ++;
}
void add(int u, int v, int c, int w) {
    addedge(u, v, c, w), addedge(v, u, 0, -w);
}
void update(int& f, int& c) {
    for(int v = t; v != s; ) {
        int i = pre[v];
        edges[i].c -= incf[t];
        edges[i ^ 1].c += incf[t];
        v = edges[i ^ 1].v;
    }
    f += incf[t], c += incf[t] * dst[t];
}

bool spfa() {
    memset(incf, 0, sizeof incf);
    memset(dst, 0x3f, sizeof dst);
    int f = 0, b = 0;
    dst[q[b ++] = s] = 0, vis[s] = true, incf[s] = inf;
    while(b - f) {
        int u = q[f ++];
        f %= N;
        vis[u] = false;
        for(int i = head[u]; ~i; i = edges[i].nxt) {
            int v = edges[i].v;
            if (edges[i].c && dst[v] > dst[u] + edges[i].w) {
                dst[v] = dst[u] + edges[i].w;
                incf[v] = std::min(incf[u], edges[i].c);
                pre[v] = i;
                if(vis[v]) continue;
                vis[q[b ++] = v] = true;
                b %= N;
            }
        }
    }
    return incf[t];
}

PII EK() {
    int f = 0, c = 0;
    while(spfa())
        update(f, c);
    return std::make_pair(f, c);
}
int main() {
    memset(head, -1, sizeof head);
    std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
    std::cin >> n >> m >> s >> t;
    for(int i = 0, a, b, c, d; i < m; i ++) 
        std::cin >> a >> b >> c >> d, add(a, b, c, d);
    auto [f, c] = EK();
    std::cout << f << ' ' << c;
    return 0;
}

二、OJ练习

2.1 运输问题

原题链接

P4015 运输问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路分析

就是一个模板题

仓库和商店构成二分图

考虑这样建模:

边<x, y>代表仓库x到商店y的边,边的费用就是题目定义的费用,流量为正无穷

建立源点向每个左部点(仓库)连费用为0,流量为储货量的边

建立汇点每个右部点(商店)向汇点连费用为0,流量为需货量的边

我们跑最小费用最大流和最大费用最大流的板子

这样我们跑出来的最大流就是左部点能给右部点提供的最大货量,由于题目保证了储货量等于需货量,所以此时跑出来是满流也符合题目要求

最小费用和最大费用就是题目所求

我们没有必要写两边板子,求最大费用,把边权取反,然后输出相反数就行了

AC代码

c++ 复制代码
#include <bits/stdc++.h>
const int N = 205, M = 20105, inf = 1e9;

struct edge {
    int v, c, w, nxt;
} edges[M];

int n, m, s, t;
int head[N], idx;
int q[N], dst[N], incf[N], pre[N];
bool vis[N];

void addedge(int u, int v, int c, int w) {
    edges[idx] = { v, c, w, head[u] }, head[u] = idx ++;
}

void add(int u, int v, int c, int w) {
    addedge(u, v, c, w), addedge(v, u, 0, -w);
}

bool spfa() {
    memset(incf, 0, sizeof incf);
    memset(dst, 0x3f, sizeof dst);
    int f = 0, b = 0;
    dst[q[b ++] = s] = 0, vis[s] = true, incf[s] = inf;
    while (b - f) {
        int u = q[f ++];
        f %= N;
        vis[u] = false;
        for (int i = head[u]; ~i; i = edges[i].nxt) {
            int v = edges[i].v;
            if (edges[i].c && dst[v] > dst[u] + edges[i].w) {
                dst[v] = dst[u] + edges[i].w;
                incf[v] = std::min(incf[u], edges[i].c);
                pre[v] = i;
                if (vis[v]) continue;
                vis[q[b ++] = v] = true;
                b %= N;
            }
        }
    }
    return incf[t];
}

void update(int& c) {
    for (int v = t; v != s; ) {
        int i = pre[v];
        edges[i].c -= incf[t];
        edges[i ^ 1].c += incf[t];
        v = edges[i ^ 1].v;
    }
    c += dst[t] * incf[t];
}
int EK() {
    int c = 0;
    while (spfa())
        update(c);
    return c;
}


int main() {
    memset(head, -1, sizeof head);
    std::cin >> m >> n;
    s = 0, t = n + m + 1;
    for (int i = 1, a; i <= m; i ++)
        std::cin >> a, add(s, i, a, 0);
    for (int i = 1, a; i <= n; i ++)
        std::cin >> a, add(i + m, t, a, 0);
    for (int i = 1; i <= m; i ++)
        for (int j = 1, a; j <= n; j ++) {
            std::cin >> a;
            add(i, m + j, inf, a);
        }


    std::cout << EK() << '\n';
    for (int i = 0; i < idx; i += 2) {
        std::swap(edges[i].w, edges[i ^ 1].w);
        edges[i].c += edges[i ^ 1].c, edges[i ^ 1].c = 0;
    }
    std::cout << -EK() << '\n';
    return 0;
}

2.2 负载平衡问题

原题链接

P4016 负载平衡问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路分析

我们考虑最终每个仓库的货物数目都是平均数avg。

那么初始的仓库可以分为:货物数目大于avg的,货物数目小于等于avg,我们不妨称为左部点,右部点

我们考虑这样建模,边的流量即二者之间流通的货物量,平均费用自然为1

然后对于初始货物量大于平均数的仓库,从源点向其连容量为a[i] - avg的边,平均费用为0

对于初始货物量小于等于平均数的仓库,向汇点连容量为avg - a[i]的边,平均费用为0

然后我们在流网络中可以求出一个最小费用最大流 ,下面只需证明最小费用最大流的费用就是原问题的解

由于原问题的可行性,所以我们的最大流一定是满流,所以只需证明最小费用就是最小运输量

每一点流量都代表一次交换中的货物数目,再乘上平均费用就对应了此次交换的运输量,所以我们的可行流一定对应一个方案中的运输量

而最小费用就是所有方案中的最小运输量,因为原问题的解是可行流的子集,而整数可行流的解在此子集中。

AC代码

c++ 复制代码
#include <bits/stdc++.h>
const int N = 105, M = 610, inf  =1e9;
int n, s, t, avg, idx;
int head[N], dst[N], pre[N], q[N], a[N], incf[N];
bool vis[N];

struct edge{
    int v, c, w, nxt;
} edges[M];


inline void addedge(int u, int v, int c, int w) {
    edges[idx] = { v, c, w, head[u] }, head[u] = idx ++ ;
}


inline void add(int u, int v, int c, int w) {
    addedge(u, v, c, w), addedge(v, u, 0, -w);
}



bool spfa() {
    int f = 0, b = 0;
    memset(dst, 0x3f, sizeof dst), memset(incf, 0, sizeof incf);
    dst[q[b ++] = s] = 0, vis[s] = true, incf[s] = inf;
    while (b - f) {
        int u = q[f ++];
        f %= N;
        vis[u] = false;
        for (int i = head[u]; ~i; i = edges[i].nxt) {
            int v = edges[i].v;
            if (edges[i].c && dst[u] + edges[i].w < dst[v]) {
                pre[v] = i;
                incf[v] = std::min(incf[u], edges[i].c);
                dst[v] = dst[u] + edges[i].w;
                if (vis[v]) continue;
                vis[q[b ++] = v] = true;
                b %= N;
            }
        }
    }
    return incf[t];
}

void update(int& f, int& c) {
    for (int v = t; v != s; ) {
        int i = pre[v];
        edges[i].c -= incf[t], edges[i ^ 1].c += incf[t];
        v = edges[i ^ 1].v;
    }
    f += incf[t], c += dst[t] * incf[t];
}

int EK() {
    int f = 0, c = 0;
    while (spfa()) 
        update(f, c);
    return c;
}


int main () {
    memset(head, -1, sizeof head);
    std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
    std::cin >> n, s = 0, t = n + 1;
    for (int i = 1; i <= n; i ++ ) {
        std::cin >> a[i], avg += a[i];
        add(i, i < n ? i + 1 : 1, inf, 1);
        add(i, i > 1 ? i - 1 : n, inf, 1);
    }
    avg /= n;
    for (int i = 1; i <= n; i ++ ) 
        if (a[i] > avg) add(s, i, a[i] - avg, 0);
        else add(i, t, avg - a[i], 0);
    std::cout << EK();
    return 0;
}

2.3 分配问题

原题链接

P4014 分配问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路分析

原问题就是一个二分图模型

左部点为员工,右部点为任务

我们可以求出若干组最大匹配并且最大匹配为完备匹配,而要求的是所有完备匹配中花费最小和最大的方案

考虑建立虚拟源点s,汇点t,s向左部点连容量为1,费用为0,右边点向汇点连容量为1,费用为0的边

原图边容量也为1,费用为题目给的费用

我们可以求出最大流且最大流是满流,只要求出最大流中的最小费用流,那么就是原问题的花费最小方案

如果求最大,我们将边的费用取反,然后再求最小费用流,费用取反就是答案

AC代码

c++ 复制代码
#include <bits/stdc++.h>
const int N = 105, M = 20205, inf = 1e9;
struct edge{
    int v, c, w, nxt;
} edges[M];
int n, s, t;
int head[N], dst[N], q[N], incf[N], pre[N], idx;
bool vis[N];

void addedge(int u, int v, int c, int w) {
    edges[idx] = { v, c, w, head[u] }, head[u] = idx ++ ;
}

void add(int u, int v, int c, int w) {
    addedge(u, v, c, w), addedge(v, u, 0, -w);
}

bool spfa() {
    memset(dst, 0x3f, sizeof dst);
    memset(incf, 0, sizeof incf);
    int b = 0, f = 0;
    dst[q[b ++] = s] = 0, incf[s] = 1e9, vis[s] = true;
    
    while (b - f) {
        int u = q[f ++];
        f %= N;
        vis[u] = false;
        for (int i = head[u]; ~i; i = edges[i].nxt) {
            int v = edges[i].v;
            if (edges[i].c && dst[u] + edges[i].w < dst[v]) {
                dst[v] = edges[i].w + dst[u];
                //std::cout << u << " " << v << " " << dst[v] << '\n';
                incf[v] = std::min(edges[i].c, incf[u]);
                pre[v] = i;
                if (vis[v]) continue;
                vis[q[b ++] = v] = true;
                b %= N;
            }
        }
    }
    //std::cout << incf[t] << " " << dst[t]; exit(0);
    return incf[t];
}

void update(int& f, int& c) {
    for (int u = t; u != s; ) {
        int i = pre[u];
        edges[i].c -= incf[t], edges[i ^ 1].c += incf[t];
        u = edges[i ^ 1].v;
    }
    f += incf[t], c += dst[t] * incf[t];
}

int EK() {
    int f = 0, c = 0;
    while (spfa())
        update(f, c);
    return c;
}

int main () {
    memset(head, -1, sizeof head);
    std::cin >> n;
    s = 0, t = n << 1 | 1;
    for (int i = 1, m; i <= n; i ++ ) {
        add(s, i, 1, 0), add(i + n, t, 1, 0);
        for (int j = 1; j <= n; j ++ ) {
            std::cin >> m;
            add(i, j + n, 1, m);
        }
    }  
    std::cout << EK() << '\n';
    for (int i = 0; i < idx; i += 2 ) 
        std::swap(edges[i].w, edges[i ^ 1].w), edges[i].c -= edges[i ^ 1].c, edges[i ^ 1].c = 0;
    std::cout << EK() << '\n';
    return 0;
}

2.4 数字梯形问题

原题链接

P4013 数字梯形问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路分析

这题也挺简单,建图逻辑很清晰

我们建立虚拟源汇点s、t,s向第一层连容量为1费用为0的边

最后一层向汇点连边,费用为0,容量根据三种规则而定

由于要考虑每个点用几次,所以要拆点

规则1:点和边都只能用一次------入点和出点容量为1,出点和下一层入点间容量为1,最后一层到汇点容量是1

规则2:点用多次,边只能用一次------入点和出点容量为无穷,出点和下一层入点间容量为1,最后一层到汇点容量是无穷

规则3:点用多次,边用多次------入点和出点容量为无穷,出点和下一层入点间容量为无穷,最后一层到汇点容量是无穷

然后三个规则都建图跑费用流即可

AC代码

c++ 复制代码
#include <bits/stdc++.h>
const int N = 1205, M = 5005, inf = 1e9;
struct edge{
    int v, c, w, nxt;
} edges[M];

int n, m, s, t;
int head[N], q[N], dst[N], incf[N], pre[N], idx;
bool vis[N];

void addedge (int u, int v, int c, int w) {
    edges[idx] = { v, c, w, head[u] }, head[u] = idx ++ ;
}

void add(int u, int v, int c, int w) {
    addedge(u, v, c, w), addedge(v, u, 0, -w);
}

bool spfa() {
    memset(incf, 0, sizeof incf), memset(dst, -0x3f, sizeof dst);
    int f = 0, b = 0;
    dst[q[b ++] = s] = 0, incf[s] = inf, vis[s] = true;
    while (b - f) {
        int u = q[f ++ ];
        vis[u] = false;
        f %= N;
        for (int i = head[u]; ~i; i = edges[i].nxt) {
            int v = edges[i].v;
            if (edges[i].c && dst[v] < dst[u] + edges[i].w) {
                dst[v] = dst[u] + edges[i].w;
                incf[v] = std::min(incf[u], edges[i].c);
                pre[v] = i;
                if (vis[v]) continue;
                vis[q[b ++] = v] = true;
                b %= N;
            }
        }
    }
    return incf[t];
}

void update(int& f, int& c) {
    for (int v = t; v != s; ) {
        int i = pre[v];
        edges[i].c -= incf[t], edges[i ^ 1].c += incf[t];
        v = edges[i ^ 1].v;
    } 
    f += incf[t], c += dst[t] * incf[t];
}

int EK() {
    int f = 0, c = 0;
    while (spfa())
        update(f, c);
    return c;
}

int main () {
    std::cin >> m >> n;
    int tot = 0;
    std::vector<std::vector<int>> nums(n, std::vector<int>(m + n));
    std::vector<std::vector<int>> id(n, std::vector<int>(m + n));
    for (int i = 0; i < n; i ++) 
        for (int j = 0; j < i + m; j ++ ) 
            std::cin >> nums[i][j], id[i][j] = ++ tot;
    s = 0, t = tot << 1 | 1; 

    //solve1
    memset(head, -1, sizeof head), idx = 0;
    for (int i = 0; i < m; i ++) add(s, id[0][i], 1, 0);
    for (int i = 0; i < m + n - 1; i ++) add(id[n - 1][i] + tot, t, 1, 0);
    for (int i = 0; i < n; i ++) 
        for (int j = 0; j < m + i; j ++) {
            add(id[i][j], id[i][j] + tot, 1, nums[i][j]);
            if (i < n - 1)
                add(id[i][j] + tot, id[i + 1][j], 1, 0), 
                add(id[i][j] + tot, id[i + 1][j + 1], 1, 0);
        }

    std::cout << EK() << '\n';

    //solve2
    memset(head, -1, sizeof head), idx = 0;
    for (int i = 0; i < m; i ++) add(s, id[0][i], 1, 0);
    for (int i = 0; i < m + n - 1; i ++) add(id[n - 1][i] + tot, t, 1e9, 0);
    for (int i = 0; i < n; i ++) 
        for (int j = 0; j < m + i; j ++) {
            add(id[i][j], id[i][j] + tot, 1e9, nums[i][j]);
            if (i < n - 1)
                add(id[i][j] + tot, id[i + 1][j], 1, 0), 
                add(id[i][j] + tot, id[i + 1][j + 1], 1, 0);
        }

    std::cout << EK() << '\n';
    //solve3
    memset(head, -1, sizeof head), idx = 0;
    for (int i = 0; i < m; i ++) add(s, id[0][i], 1, 0);
    for (int i = 0; i < m + n - 1; i ++) add(id[n - 1][i] + tot, t, 1e9, 0);
    for (int i = 0; i < n; i ++) 
        for (int j = 0; j < m + i; j ++) {
            add(id[i][j], id[i][j] + tot, 1e9, nums[i][j]);
            if (i < n - 1)
                add(id[i][j] + tot, id[i + 1][j], 1e9, 0), 
                add(id[i][j] + tot, id[i + 1][j + 1], 1e9, 0);
        }

    std::cout << EK() << '\n';
    return 0;
}

2.5 K取方格数

原题链接

382. K取方格数 - AcWing题库

思路分析

考虑最大费用最大流 + 拆点

由于每个格子走一次,我们将格子拆为入点和出点

左和上向入点连容量无穷费用0的边

入点向出点连一条容量为1,费用为grid[i][j] 的边,再连一条容量为无穷,费用为0的边

源点向(0, 0) 连 容量无穷,费用0的边

(n - 1, n - 1) 向汇点连 容量无穷,费用0的边

AC代码

c++ 复制代码
#include <bits/stdc++.h>
// #include <ranges>
// #define DEBUG
using i64 = long long;
using u32 = unsigned;
using u64 = unsigned long long;
constexpr int inf32 = 1E9 + 7;
constexpr i64 inf64 = 1E18 + 7;
constexpr double eps = 1E-9;

using i64 = long long;

const int N = 5005, M = 25050;
int s, t;

int head[N], idx;

auto clear = [] {
    memset(head, -1, sizeof head);
    return 0;
}();


struct edge {
    int v, c, w, nxt;
}edges[M];

void addedge(int u, int v, int c, int w) {
    edges[idx] = { v, c, w, head[u] }, head[u] = idx ++;
}

void add(int u, int v, int c, int w) {
    addedge(u, v, c, w);
    addedge(v, u, 0, -w);
}

int dist[N], pre[N], q[N], incf[N];
bool vis[N];

bool spfa() {
    memset(dist, -0x3f, sizeof dist);
    memset(incf, 0, sizeof incf);

    int f = 0, b = 0;

    dist[q[b ++] = s] = 0;
    incf[s] = inf32;

    while (b - f) {
        int u = q[f ++];
        f %= N;

        vis[u] = false;

        for (int i = head[u]; ~i; i = edges[i].nxt) {
            int v = edges[i].v;
            if (edges[i].c > 0 && dist[u] + edges[i].w > dist[v]) {
                dist[v] = dist[u] + edges[i].w;
                incf[v] = std::min(edges[i].c, incf[u]);
                pre[v] = i;
                if (!vis[v]) {
                    vis[q[b ++] = v] = true;
                    b %= N;
                }
            }
        }
    }
    return incf[t] > 0;
}

void update(int &f, int &c) {
    for (int v = t; v != s; v = edges[pre[v] ^ 1].v) {
        edges[pre[v]].c -= incf[t];
        edges[pre[v] ^ 1].c += incf[t];
    }
    f += incf[t];
    c += dist[t] * incf[t];
}

std::pair<int, int> EK() {
    int f = 0, c = 0;

    while (spfa()) {
        update(f, c);
    }

    return std::pair(f, c);    
}

void solve() {
    int n, k;
    std::cin >> n >> k;
    std::vector<std::vector<int>> g(n, std::vector<int>(n));

    s = n * n * 2, t = n * n * 2 + 1;

    for (int i = 0; i < n; ++ i) 
        for (int j = 0; j < n; ++ j) {
            std::cin >> g[i][j];
            int u = i * n + j, v = u + n;
            if (i) {
                add((i - 1) * n + j + n * n, u, inf32, 0);
            }
            if (j) {
                add(i * n + j - 1 + n * n, u, inf32, 0);
            }
            add(i * n + j, i * n + j + n * n, inf32, 0);
            add(i * n + j, i * n + j + n * n, 1, g[i][j]);
        }

    add(s, 0, k, 0);
    add(t - 2, t, k, 0);

    std::cout << EK().second;
}

auto FIO = []{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    return 0;
} ();

int main() {
    #ifdef DEBUG
        freopen("in.txt", "r", stdin);
        freopen("out.txt", "w", stdout);
    #endif     

    int t = 1;
    // std::cin >> t;
    while (t --)
        solve();

    return 0;
}

2.6 P4012 深海机器人问题

原题链接

P4012 深海机器人问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路分析

和上一道题一样,这个题思路也是拆点,但是实际建图的时候不用拆点,因为这个题的图比较特殊,输入也比较逆天

AC代码

c++ 复制代码
#include <bits/stdc++.h>
// #include <ranges>
// #define DEBUG
using i64 = long long;
using u32 = unsigned;
using u64 = unsigned long long;
constexpr int inf32 = 1E9 + 7;
constexpr i64 inf64 = 1E18 + 7;
constexpr double eps = 1E-9;

const int N = 500, M = 2005;
int s, t;

int head[N], idx;

auto clear = [] {
    memset(head, -1, sizeof head);
    return 0;
}();


struct edge {
    int v, c, w, nxt;
}edges[M << 1];

void addedge(int u, int v, int c, int w) {
    edges[idx] = { v, c, w, head[u] }, head[u] = idx ++;
}

void add(int u, int v, int c, int w) {
    addedge(u, v, c, w);
    addedge(v, u, 0, -w);
}

int dist[N], pre[N], q[N], incf[N];
bool vis[N];

bool spfa() {
    memset(dist, -0x3f, sizeof dist);
    memset(incf, 0, sizeof incf);

    int f = 0, b = 0;

    dist[q[b ++] = s] = 0;
    incf[s] = inf32;

    while (b - f) {
        int u = q[f ++];
        f %= N;

        vis[u] = false;

        for (int i = head[u]; ~i; i = edges[i].nxt) {
            int v = edges[i].v;
            if (edges[i].c > 0 && dist[u] + edges[i].w > dist[v]) {
                dist[v] = dist[u] + edges[i].w;
                incf[v] = std::min(edges[i].c, incf[u]);
                pre[v] = i;
                if (!vis[v]) {
                    vis[q[b ++] = v] = true;
                    b %= N;
                }
            }
        }
    }
    return incf[t] > 0;
}

void update(int &f, int &c) {
    for (int v = t; v != s; v = edges[pre[v] ^ 1].v) {
        edges[pre[v]].c -= incf[t];
        edges[pre[v] ^ 1].c += incf[t];
    }
    f += incf[t];
    c += dist[t] * incf[t];
}

std::pair<int, int> EK() {
    int f = 0, c = 0;

    while (spfa()) {
        update(f, c);
    }

    return std::pair(f, c);    
}


void solve() {
    int a, b;
    std::cin >> a >> b;
    int P, Q;
    std::cin >> P >> Q;
    s = (P + 1) * (Q + 1), t = s + 1;
    for (int i = 0, x; i <= P; ++ i)
        for (int j = 0; j < Q; ++ j) {
            std::cin >> x;
            add(i * (Q + 1) + j, i * (Q + 1) + j + 1, 1, x);
            add(i * (Q + 1) + j, i * (Q + 1) + j + 1, inf32, 0);
        }

    for (int j = 0, x; j <= Q; ++ j) 
        for (int i = 0; i < P; ++ i) {
            std::cin >> x;
            add(i * (Q + 1) + j, (i + 1) * (Q + 1) + j, 1, x);
            add(i * (Q + 1) + j, (i + 1) * (Q + 1) + j, inf32, 0);
    }


    for (int i = 0, k, x, y; i < a; ++ i) {
        std::cin >> k >> x >> y;
        add(s, x * (Q + 1) + y, k, 0);
    }

    for (int i = 0, k, x, y; i < b; ++ i) {
        std::cin >> k >> x >> y;
        add(x * (Q + 1) + y, t, k, 0);
    }

    std::cout << EK().second;
}

auto FIO = []{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    return 0;
} ();

int main() {
    #ifdef DEBUG
        freopen("in.txt", "r", stdin);
        freopen("out.txt", "w", stdout);
    #endif     

    int t = 1;
    // std::cin >> t;
    while (t --)
        solve();

    return 0;
}

2.7 餐巾计划问题

原题链接

P1251 餐巾计划问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路分析

考虑每天拆成两个点:使用的r[i] 条 干净毛巾(可能是新的可能是旧的)记为入点,当天结束后产生的 r[i] 条旧毛巾,记为出点

  • 出点向汇点连容量为r[i],费用为0的边
  • 汇点向入点连容量为r[i],费用为0的边,即每天一定会产生的旧毛巾
  • 汇点向出点连容量为r[i],费用为p的边,即每天可以买新毛巾
  • 入点向m天后的出点连容量为无穷,费用为 f 的边,即第m + 1天就可以用快洗的毛巾
  • 入点向n天后的出点连容量为无穷,费用为 s 的边,即第m + 1天就可以用慢洗的毛巾
  • 入点向下一天的入点连容量为无穷,费用为0的边,即可以存到下一天

建模后一定存在满流,一点流量用一次,不存在毛巾被非法使用,所以一定对应原题一个可行,合法的解

由于是最小费用流,所以是最优解

AC代码

c++ 复制代码
#include <bits/stdc++.h>
// #include <ranges>
// #define DEBUG
using i64 = long long;
using u32 = unsigned;
using u64 = unsigned long long;
constexpr int inf32 = 1E9 + 7;
constexpr i64 inf64 = 1E18 + 7;
constexpr double eps = 1E-9;

constexpr int N = 4002, M = 25005;

int head[N], idx;

auto clear = []{
    memset(head, -1, sizeof head);
    return 0;
}();

struct edge{
    int v, c, w, nxt;
} edges[M];

void addedge(int u, int v, int c, int w) {
    edges[idx] = { v, c, w, head[u] }, head[u] = idx ++;
    assert(idx < M);
}

void add(int u, int v, int c, int w) {
    addedge(u, v, c, w);
    addedge(v, u, 0, -w);
}

int s, t;
int pre[N], incf[N], q[N];
i64 dist[N];
bool vis[N];

bool spfa() {
    memset(dist, 0x3f, sizeof dist);
    memset(incf, 0, sizeof incf);
    int b = 0, f = 0;
    dist[q[b ++] = s] = 0;
    incf[s] = inf32;

    while (b - f) {
        int u = q[f ++];
        f %= N;
        vis[u] = false;

        for (int i = head[u]; ~i; i = edges[i].nxt) {
            int v = edges[i].v, c = edges[i].c, w = edges[i].w;
            if (c > 0 && dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                incf[v] = std::min(incf[u], c);
                pre[v] = i;
                if (!vis[v]) {
                    vis[q[b ++] = v] = true;
                    b %= N;
                }
            }
        }
    }
    return incf[t] > 0;
}

void update(int &f, i64 &c) {
    for (int v = t; v != s; v = edges[pre[v] ^ 1].v) {
        edges[pre[v]].c -= incf[t];
        edges[pre[v] ^ 1].c += incf[t];
    }

    f += incf[t];
    c += incf[t] * dist[t];
}

std::pair<int, i64> EK() {
    int f = 0;
    i64 c = 0;

    while (spfa())
        update(f, c);

    return std::make_pair(f, c);
}

void solve() {
    int n;
    std::cin >> n;
    s = 2 * n, t = 2 * n + 1;
    
    std::vector<int> r(n);

    for (int i = 0; i < n; ++ i) {
        std::cin >> r[i];
        add(i + n, t, r[i], 0);
        add(s, i, r[i], 0);
        if (i + 1 < n)
            add(i, i + 1, inf32, 0);
    }

    int a, b, c, d, e;  // p m f n s
    std::cin >> a >> b >> c >> d >> e;

    for (int i = 0; i < n; ++ i) {
        if (i + b < n) // 快洗
            add(i, i + b + n, inf32, c);
        
        if (i + d < n) // 慢洗
            add(i, i + d + n, inf32, e);

        add(s, i + n, inf32, a);
    }

    std::cout << EK().second;
}

auto FIO = []{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    return 0;
} ();

int main() {
    #ifdef DEBUG
        freopen("in.txt", "r", stdin);
        freopen("out.txt", "w", stdout);
    #endif     

    int t = 1;
    // std::cin >> t;
    while (t --)
        solve();

    return 0;
}

2.8 [NOI2008] 志愿者招募

原题链接

[P3980 NOI2008] 志愿者招募 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路分析

洛谷上费用流的题解感觉很多都没说到点上

首先:所有种类志愿者的服务区间并集是[1, n],保证一定有解

我们网络流建模一定要保证 建模后的可行流/满流 能对应原问题的一个可行解

我也不会正面解释建模方式正确性,我是将原问题转换了一下:

转换为初始有 inf 个人工作,保证每一天都至少有 a[i] 个人休息,现在有若干休息方案:在[s[i], t[i]] 休息,一个人的花费为c[i]

问满足限制的最小花费

这样建模就好解释了

有 inf 个人要从 虚拟源点 s 到 虚拟汇点 t,中间有[0, n] 这 n + 1个点,从点i 到 i + 1,代表在第 i 天工作

为了保证每天都至少有a[i]个人休息 ,我们让工作的边,即 <i, i + 1> 边的容量为 inf - a[i],费用为0即当天最多有这么多人工作

然后对于 休息方案 :[s[i], t[i]],我们连接<s[i], t[i] + 1> ,边容量为inf,费用为c[i],即想休息多少休息多少,但是休息一个人费用为c[i]

我们跑最小费用最大流就是答案

证明:

  • 一定满流,如果不满流说明有员工消失了(
    • 因为区间并集为[0, n],所以一定有0开始的工作区间[s[0], t[0]]
    • 从 0 开始,源点向0连了inf容量的边,工作边能流inf - a[0],休息边能流a[0],所以此处满流,只需证明这inf流量能顺利流到汇点t
    • 工作边的a0 点流量流到t[0] + 1后,因为区间并集为[0, n],所以t[0] + 1以及其左边一定有休息区间起点,那么这a[0] 点流量可以兵分两路,一部分往右边走工作边和休息边,一部分退流到某个休息区间起点,接着走休息边
    • 如此下去......当然可以更严谨地证明每次兵分多路都有足够的容量流a0
    • 故我们一定能够得到满流
  • 因而满流对应原问题一个可行解,休息边代表区间内休息的人数对应原问题雇佣的人数
  • 由于是最小费用流,所以是最优解

AC代码

c++ 复制代码
#include <bits/stdc++.h>
// #include <ranges>
// #define DEBUG
using i64 = long long;
using u32 = unsigned;
using u64 = unsigned long long;
constexpr int inf32 = 1E9 + 7;
constexpr i64 inf64 = 1E18 + 7;
constexpr double eps = 1E-9;

constexpr int N = 1003, M = 22010;

struct edge{
    int v, c, w, nxt;
} edges[M];

int n, m, s, t;
int head[N], q[N], dst[N], incf[N], pre[N], idx;
bool vis[N];

void addedge (int u, int v, int c, int w) {
    edges[idx] = { v, c, w, head[u] }, head[u] = idx ++ ;
}

void add(int u, int v, int c, int w) {
    addedge(u, v, c, w), addedge(v, u, 0, -w);
}

bool spfa() {
    memset(incf, 0, sizeof incf), memset(dst, 0x3f, sizeof dst);
    int f = 0, b = 0;
    dst[q[b ++] = s] = 0, incf[s] = inf32, vis[s] = true;
    while (b - f) {
        int u = q[f ++ ];
        vis[u] = false;
        f %= N;
        for (int i = head[u]; ~i; i = edges[i].nxt) {
            int v = edges[i].v;
            if (edges[i].c && dst[v] > dst[u] + edges[i].w) {
                dst[v] = dst[u] + edges[i].w;
                incf[v] = std::min(incf[u], edges[i].c);
                pre[v] = i;
                if (vis[v]) continue;
                vis[q[b ++] = v] = true;
                b %= N;
            }
        }
    }
    return incf[t];
}

void update(int& f, int& c) {
    for (int v = t; v != s; ) {
        int i = pre[v];
        edges[i].c -= incf[t], edges[i ^ 1].c += incf[t];
        v = edges[i ^ 1].v;
    } 
    f += incf[t], c += dst[t] * incf[t];
}

int EK() {
    int f = 0, c = 0;
    while (spfa())
        update(f, c);
    return c;
}

void solve() {
    int n, m;
    std::cin >> n >> m;

    memset(head, -1, sizeof head);

    s = n + 1, t = n + 2;
    for (int i = 0, x; i < n; ++ i) {
        std::cin >> x;
        add(i, i + 1, inf32 - x, 0);
    }

    for (int i = 0, a, b, c; i < m; ++ i) {
        std::cin >> a >> b >> c;
        add(a - 1, b, inf32, c);
    }

    add(s, 0, inf32, 0);
    add(n, t, inf32, 0);

    std::cout << EK();
}

auto FIO = []{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    return 0;
} ();

int main() {
    #ifdef DEBUG
        freopen("in.txt", "r", stdin);
        freopen("out.txt", "w", stdout);
    #endif     

    int t = 1;
    // std::cin >> t;
    while (t --)
        solve();

    return 0;
}

三、(补档)Primal Dual 算法

2024-08-09 16:20:21,现在很少见到网络流的题目其实,今天偶然间发现原来费用流除了spfa增广外和消圈法外,还有很多方法,这里更新一下Primal Dual 算法,多的也不想学了,学习算法毕竟还是为了训练思维。


3.1 定义

3.1.1 势

指的是给每个顶点赋予一个新的标号 h(v)

其作用为 :对于原图的边<u, v, c, w>(c为容量,w为花费),将 h[u] - h[v] 累加到 w 上后,保证 w' = w + h[u] - h[v] >= 0 ,这样,我们的spfa 就可以用 堆优化 djkstra替代,在大多数图中,有着比spfa更为稳定的效率

缺陷:仍未解决负环问题, 但是我也没精力学习消圈法了

3.2 实现

3.2.1 势的求解

对于所有边都 >=0的图,我们直接令 h[] = 0即可

对于存在负权边,但是不存在负环的图,我们跑 spfa算法 求解从源点 s 到所有点的最短距离

令h[u] = mindist(s, u)

则 对于边 <u, v, c, w>,h[u] + w >= h[v] => w + h[u] - h[v] >=0 ,就完成了构造

3.2.2 代码实现

因为第三部分和前两部分时间差了3、4个月,所以码风有些改变()

c++ 复制代码
using i64 = long long;

struct MCFGraph{
    struct Edge{
        int v, cap, w;
        Edge(int _v, int _cap, int _w) : v(_v), cap(_cap), w(_w) {}
    };

    const int n;
    std::vector<Edge> e;
    std::vector<std::vector<int>> g;
    std::vector<i64> h, dis;    // h 为 势 数组
    std::vector<int> pre;
    std::vector<bool> vis;


    void spfa(const int s, const int t) {
        static std::queue<int> q;
        h.assign(n, std::numeric_limits<i64>::max());
        q.push(s);
        h[s] = 0;
        while (q.size()) {
            int u = q.front();
            q.pop();
            vis[u] = false;
            for (int i : g[u]) {
                const auto& [v, cap, w] = e[i];
                if (cap > 0 && h[v] > h[u] + w) {
                    h[v] = h[u] + w;
                    if (!vis[v])
                        q.push(v), vis[v] = true;
                }
            }
        }
    }

    bool dijkstra(const int s, const int t){
        static std::priority_queue<std::pair<i64, int>, std::vector<std::pair<i64, int>>, std::greater<std::pair<i64, int>>> pq;

        dis.assign(n, std::numeric_limits<i64>::max());
        pre.assign(n, -1);

        dis[s] = 0;
        pq.emplace(0, s);

        while (pq.size()) {
            auto [d, u] = pq.top();
            pq.pop();

            if (dis[u] < d) continue;

            for (int i : g[u]) {
                const auto& [v, cap, w] = e[i];
                if (cap > 0 && dis[v] > w + d + h[u] - h[v]) {
                    dis[v] = w + d + h[u] - h[v];
                    pre[v] = i;
                    pq.emplace(dis[v], v);
                }
            }
        }

        return dis[t] < std::numeric_limits<i64>::max();
    }

    MCFGraph(int _n) : n(_n), g(n), vis(n)
    {}

    void addEdge(int u, int v, int c, int f) { // 最大流
        g[u].push_back(e.size());
        e.emplace_back(v, c, f);
        g[v].push_back(e.size());
        e.emplace_back(u, 0, -f);
    }

    std::pair<int, i64> flow(const int s, const int t) {
        int flow = 0;
        i64 cost = 0;
        h.assign(n, 0);
        spfa(s, t);

        while (dijkstra(s, t)) {
            // 更新h 为实际dis
            for (int i = 0; i < n; ++ i)    h[i] += dis[i];
            int aug = std::numeric_limits<int>::max();
            for (int i = t; i != s; i = e[pre[i] ^ 1].v)
                aug = std::min(aug, e[pre[i]].cap);
            for (int i = t; i != s; i = e[pre[i] ^ 1].v)
                e[pre[i]].cap -= aug, e[pre[i] ^ 1].cap += aug;
            flow += aug;
            cost += (i64)aug * h[t];
        }
        return std::make_pair(flow, cost);
    }
};
3.2.3 时间复杂度

O(NM + (N + M) logN)

相关推荐
泉崎11 分钟前
11.7比赛总结
数据结构·算法
你好helloworld12 分钟前
滑动窗口最大值
数据结构·算法·leetcode
AI街潜水的八角1 小时前
基于C++的决策树C4.5机器学习算法(不调包)
c++·算法·决策树·机器学习
白榆maple1 小时前
(蓝桥杯C/C++)——基础算法(下)
算法
JSU_曾是此间年少1 小时前
数据结构——线性表与链表
数据结构·c++·算法
此生只爱蛋2 小时前
【手撕排序2】快速排序
c语言·c++·算法·排序算法
咕咕吖3 小时前
对称二叉树(力扣101)
算法·leetcode·职场和发展
九圣残炎3 小时前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
lulu_gh_yu3 小时前
数据结构之排序补充
c语言·开发语言·数据结构·c++·学习·算法·排序算法
丫头,冲鸭!!!4 小时前
B树(B-Tree)和B+树(B+ Tree)
笔记·算法