网络流/费用流学习总结

模板

网络流模板
大概原理是建边时建一条流量为0的反向边,可以用来反悔,选择一条路径后,路径上原来的边减去流量,反向边加上流量,不断找有没有更大的答案即可,dinic用dfs优化了一下,一般不会被卡。
最小割,最少去掉的边的值使得源点汇点不相连,最小割 = = = 最大流

//点从1-n 包括源点汇点 边从2开始编号 方便异或
//一次建好两条边
const int INF = 0x3f3f3f3f;
const int N = 1e5+7;
const int MAXN = 1e5+7;
int cnt = 1, n, st, ed, d[N], cur[N], head[N];
int T, r, c;
struct Edge
{
    int to, w, nxt;
}e[MAXN<<1];
void AddEdge(int f, int t, int w)
{
    e[++cnt].nxt = head[f]; e[cnt].to = t; e[cnt].w = w; head[f] = cnt;
    e[++cnt].nxt = head[t]; e[cnt].to = f; e[cnt].w = 0; head[t] = cnt;
}
bool BFS()
{
    rep(i, 1, n) d[i] = 0;
    queue <int> q;
    q.push(st); d[st] = 1;
    while(!q.empty())
    {
        int u = q.front(); q.pop();
        for(int i = head[u]; i; i = e[i].nxt)
        {
            int v = e[i].to;
            if(d[v] || !e[i].w) continue;
            q.push(v); d[v] = d[u] + 1;
        }
    }
    rep(i, 1, n) cur[i] = head[i];
    return d[ed] != 0;
}
int DFS(int u, int flow)
{
    if(u == ed) return flow;
    for(int i = cur[u]; i; i = e[i].nxt)
    {
        cur[u] = i;
        int v = e[i].to;
        if(d[v] != d[u]+1 || !e[i].w) continue;
        int det = DFS(v, min(flow, e[i].w));
        if(!det) continue;
        e[i].w -= det; e[i^1].w += det;
        return det;
    }
    return 0;
}
LL Dinic()
{
    LL maxflow = 0, det;
    while(BFS())
        while(det = DFS(st, INF))
            maxflow += det;
    return maxflow;
}
void init()
{
    cnt = 1;
    rep(i, 1, n)
        head[i] = 0;
}

费用流模板
最小费用最大流,每条边有费用 c c c,当流过 w w w时,花费 c × w c\times w c×w 的费用,求最大流的基础上,最少的费用。
把DFS换成了SPFA,因为有负边要用SPFA。
最大费用最大流就是把边建成负权,输出 ( − 1 ) × a n s (-1) \times ans (1)×ans
最小费用流,不考虑流的多少,那直接每次SPFA若发现 d i s [ t ] ≥ 0 dis[t] \geq 0 dis[t]0 直接结束就行。
最大费用流,同上。

//最小费用最大流 如果求最大费用,建边cost赋值为负数,输出费用*(-1)
const int INF = 0x3f3f3f3f;
const int N = 1e4+7;
const int MAXN = 1e5+7;

int T, n, head[N], cnt = 1, dis[N], pre[N], preve[N];
struct Edge
{
    int f, to, w, c, nxt;
}e[MAXN];
//一次建两条边 cnt从1开始
void AddEdge(int f, int t, int w, int c)
{
    e[++cnt].to = t; e[cnt].w = w; e[cnt].c = c; e[cnt].nxt = head[f]; head[f] = cnt;
    e[++cnt].to = f, e[cnt].w = 0; e[cnt].c =-c; e[cnt].nxt = head[t]; head[t] = cnt;
}

bool SPFA(int s, int t)
{
    int inq[N];
    rep(i, 1, n)
        dis[i] = INF, inq[i] = 0, pre[i] = -1;
    dis[s] = 0;
    queue <int> q;
    q.push(s); inq[s] = 1;
    while(!q.empty())
    {
        int u = q.front(); q.pop(); inq[u] = 0;
        for(int i = head[u]; i; i = e[i].nxt)
        {
            if(e[i].w > 0)
            {
                int v = e[i].to, cost = e[i].c;
                if(dis[u] + cost < dis[v])
                {
                    dis[v] = dis[u] + cost;
                    pre[v] = u; preve[v] = i;
                    if(!inq[v])
                    {
                        q.push(v); inq[v] = 1;
                    }
                }
            }
        }
    }
    return dis[t] != INF;
}

LL MinCost(int s, int t)
{
    LL cost = 0; //maxflow = 0;
    while(SPFA(s, t))
    {
    	/* 最小费用流
    	if(dis[t] >= 0)
    		break;
		*/
        int v = t, flow = INF;
        while(pre[v] != -1)
        {
            int u = pre[v], i = preve[v];
            flow = min(flow, e[i].w);
            v = u;
        }
        v = t;
        while(pre[v] != -1)
        {
            int u = pre[v], i = preve[v];
            e[i].w -= flow;
            e[i^1].w += flow;
            v = u;
        }
        cost += (LL)dis[t] * flow; //maxflow += flow;
    }
    return cost;
}

void init()
{
    cnt = 1;
    rep(i, 1, n)
        head[i] = pre[i] = preve[i] = dis[i] = 0;
}

习题

网络流费用流题目难在转化问题,一般都很难直接看出来。

偷宝石 (最小割)

n n n个宝石, m m m个保安,每个宝石可能被若干个保安监视着,每个宝石有 a i a_i ai的价值,贿赂一个保安要 b i b_i bi,求可以获得的最大价值。

思考
如果想要么拿要么不拿,就不对了,因为这样走拿宝石和贿赂保安是对立的,我们考虑,假如全部都要拿,那么我们可以选择放弃一些宝石,或者贿赂保安,这样选择都是失去的价值。

建图
源点连宝石,保安连汇点。宝石和对应的保安相连。
考虑最小割,因为要么不拿这个宝石,要么把保安都贿赂了,要么割源点到宝石的边,权值就是宝石的价值,要么割保安到汇点的边,权值就是贿赂的钱,保安和宝石的关系不能破坏,权值为无穷大。
割掉若干边,使得源点到汇点没有路径就满足条件了,那么求出来最小割就是最少要放弃的值。

K取方格数 (费用流+拆点)

从左上角走到右下角,每次走右或者下,走到一个位置获得当然位置的值,但是只能获得一次,求最多能获得的值。
思考
费用流问题,重点在费用和建图,最大流就是走的K趟。
考虑源点到起点建容量为K费用为0的边,终点到汇点建容量为K,费用为0的边。
那么中间走的过程怎么建边,只能选一个怎么建边。
考虑拆点,把一个点拆为两个,第一个到第二个连两条边,一条容量为1,费用为点权,代表只能选一次,得到这么大价值,再连一条容量无穷大,费用为0,代表再走就没有价值了。再把所有能走的点之间连上容量无穷价值为0的边,跑最大费用最大流即可。
代码

//inexorable and inevitable
#include <bits/stdc++.h>
using namespace std;
#pragma GCC optimize (Ofast)
#define fastio ios_base::sync_with_stdio(0); cin.tie(NULL);
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
#define per(i, a, b) for(int i = (a); i >= (b); i--)
#define LL long long
#define mp make_pair
#define pb push_back
#define fr first
#define se second
#define endl "\n"
#define debug1 cout << "???" << endl;
#define debug2(x) cout << #x << ": " << x << endl;
const int INF = 0x3f3f3f3f;
const int N = 1e4+7;
const int MAXN = 1e5+7;

int T, n, head[N], cnt = 1, dis[N], pre[N], preve[N], a[101][101], k;
struct Edge
{
    int f, to, w, c, nxt;
}e[MAXN];
void AddEdge(int f, int t, int w, int c)
{
    e[++cnt].to = t; e[cnt].w = w; e[cnt].c = c; e[cnt].nxt = head[f]; head[f] = cnt;
    e[++cnt].to = f, e[cnt].w = 0; e[cnt].c =-c; e[cnt].nxt = head[t]; head[t] = cnt;
}

bool SPFA(int s, int t)
{
    int inq[N];
    rep(i, 1, n)
        dis[i] = INF, inq[i] = 0, pre[i] = -1;
    dis[s] = 0;
    queue <int> q;
    q.push(s); inq[s] = 1;
    while(!q.empty())
    {
        int u = q.front(); q.pop(); inq[u] = 0;
        for(int i = head[u]; i; i = e[i].nxt)
        {
            if(e[i].w > 0)
            {
                int v = e[i].to, cost = e[i].c;
                if(dis[u] + cost < dis[v])
                {
                    dis[v] = dis[u] + cost;
                    pre[v] = u; preve[v] = i;
                    if(!inq[v])
                    {
                        q.push(v); inq[v] = 1;
                    }
                }
            }
        }
    }
    return dis[t] != INF;
}

LL MinCost(int s, int t)
{
    LL cost = 0; //maxflow = 0;
    while(SPFA(s, t))
    {
        int v = t, flow = INF;
        while(pre[v] != -1)
        {
            int u = pre[v], i = preve[v];
            flow = min(flow, e[i].w);
            v = u;
        }
        v = t;
        while(pre[v] != -1)
        {
            int u = pre[v], i = preve[v];
            e[i].w -= flow;
            e[i^1].w += flow;
            v = u;
        }
        cost += (LL)dis[t] * flow; //maxflow += flow;
    }
    return cost;
}

void init()
{
    cnt = 1;
    rep(i, 1, n)
        head[i] = pre[i] = preve[i] = dis[i] = 0;
}

int main()
{
    fastio

//    freopen("in.txt", "r", stdin);
    cin >> T;
    while(T--)
    {
        int r, c, s, t;
        cin >> r >> c >> k;
        n = r*c*2+2, s = r*c*2+1, t = r*c*2+2;
        init();
        rep(i, 1, r)
        {
            rep(j, 1, c)
            {
                cin >> a[i][j];
                int u = (i-1) * c + j;
                AddEdge(u*2-1, u*2, INF, 0);
                AddEdge(u*2-1, u*2, 1, -a[i][j]);
                if(j < c)
                    AddEdge(u*2, u*2+1, INF, 0);
                if(i < r)
                    AddEdge(u*2, (u+c)*2-1, INF, 0);
            }
        }
        AddEdge(s, 1, k, 0);
        AddEdge(r*c*2, t, k, 0);
        cout << MinCost(s, t) * (-1) << endl;
    }
    return 0;
}

越流越贵(拆边)

最小费用最大流,但是一条边流过花费 w i 2 w_i^2 wi2的价值。
思考
考虑拆边,因为边最大流量较小,拆为 w i w_i wi条边,边权为 n 2 n^2 n2的差分数列,所有类似单调的费用流都可以拆边做。一般最大流量都比较小。
代码

const int INF = 0x3f3f3f3f;
const int N = 1e3+7;
const int MAXN = 1e5+7;

int m, T, n, head[N], cnt = 1, dis[N], pre[N], preve[N], sq[101], t[101];
struct Edge
{
    int f, to, w, c, nxt;
}e[MAXN];
//一次建两条边 cnt从1开始
void AddEdge(int f, int t, int w, int c)
{
    e[++cnt].to = t; e[cnt].w = w; e[cnt].c = c; e[cnt].nxt = head[f]; head[f] = cnt;
    e[++cnt].to = f, e[cnt].w = 0; e[cnt].c =-c; e[cnt].nxt = head[t]; head[t] = cnt;
}

bool SPFA(int s, int t)
{
    int inq[N];
    rep(i, 1, n)
        dis[i] = INF, inq[i] = 0, pre[i] = -1;
    dis[s] = 0;
    queue <int> q;
    q.push(s); inq[s] = 1;
    while(!q.empty())
    {
        int u = q.front(); q.pop(); inq[u] = 0;
        for(int i = head[u]; i; i = e[i].nxt)
        {
            if(e[i].w > 0)
            {
                int v = e[i].to, cost = e[i].c;
                if(dis[u] + cost < dis[v])
                {
                    dis[v] = dis[u] + cost;
                    pre[v] = u; preve[v] = i;
                    if(!inq[v])
                    {
                        q.push(v); inq[v] = 1;
                    }
                }
            }
        }
    }
    return dis[t] != INF;
}

pair<LL, LL> MinCost(int s, int t)
{
    LL cost = 0, maxflow = 0;
    while(SPFA(s, t))
    {
        int v = t, flow = INF;
        while(pre[v] != -1)
        {
            int u = pre[v], i = preve[v];
            flow = min(flow, e[i].w);
            v = u;
        }
        v = t;
        while(pre[v] != -1)
        {
            int u = pre[v], i = preve[v];
            e[i].w -= flow;
            e[i^1].w += flow;
            v = u;
        }
        cost += (LL)dis[t] * flow; maxflow += flow;
    }
    return mp(cost, maxflow);
}

void init()
{
    cnt = 1;
    rep(i, 1, n)
        head[i] = pre[i] = preve[i] = dis[i] = 0;
}


int main()
{
    fastio

//    freopen("in.txt", "r", stdin);
    rep(i, 1, 100)
        sq[i] = i * i, t[i] = sq[i] - sq[i-1];
    cin >> T;
    while(T--)
    {
        cin >> n >> m;
        init();
        int u, v, w;
        rep(i, 1, m)
        {
            cin >> u >> v >> w;
            rep(j, 1, w) AddEdge(u, v, 1, t[j]);
        }
        pair <LL, LL> ans = MinCost(1, n);
        cout << ans.se << ' ' << ans.fr << endl;
    }
    return 0;
}

序列选数(网络流思想,贪心+可反悔)

给一个序列,选 k k k个数,若选了一个数就不能选它两边的了,求最大和。
思考
显然嗯贪心是不行的,我们考虑可以反悔,因为若选了一个数,如果之后不选它,说明一定是选了它两边的数,那么我们每选一个数,把这三个数更新为一个数,值是 a i − 1 + a i + 1 − a i a_{i-1} + a_{i+1} - a_{i} ai1+ai+1ai,每次选最大的即可。
注意!
如果选了当前最左边或者最右边的位置(有一边没数了),这个时候就不能像之前一样,因为如果这样更新当前这个数,再选它不是多选了,而是没变,因为是一个数变成一个数。
这个时候如果选了端点的位置,说明端点位置大于旁边有数那个,一定不会再选有数的那个了,把两个都设置为不会再选。
两种做法
Ⅰ用优先队列,优先队列存位置,cmp是对应的数组的值比较。
Ⅱ用树状数组维护区间最大值,有坑,初始化为-INF,因为可能会有负数,两边的数的和小于中间的时候。

struct cmp
{
    bool operator()(const int aa, const int bb)
    {
        return a[aa] < a[bb];
    }
};
priority_queue <int, vector <int>, cmp> pq;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值