ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

7.7 $\to$网络流初步$\to$

2022-07-07 20:32:46  阅读:141  来源: 互联网

标签:int text 餐巾 最小 网络 初步 7.7 incf def


\(\large \text{Date: 7.7}\)

扯点闲话:最近迷上了这样一种毒瘤的变量定义方法:
#define def register auto
管他定义什么变量(甚至是函数也可以),直接无脑 auto,加了 register 还跑得快...

所以,下边的某些代码可能到处都是 autodef(register auto)...

网络流初步

什么是网络流:

网络:有源点(\(s\))和汇点(\(t\)),每一条有向边都定义了一个“容量” \(c(u,v)\) 。从源点出发的流量经过若干条边全部汇聚到汇点,一条路径的流量不得超过组成它的每一条边的容量(就是这些边中容量最小的一条)。可以把源点理解为大水库,有多少流量就流多少流量,但肯定不是无限流的,因为不能超过边的容量。

最大流:

求网络的总流量最大是多少。

能解决这个的算法很多,这里先写一个 \(\rm Dinic\)。网络流算法的精髓在于,每遍历一条边,就建立一条和当前边\((u,v)\)流量(不是容量)相等,方向相反的边 \((v,u)\)。这样做是因为一旦以后寻找增广路就可以让这个点收回这些流量,分配给别的地方使用,接近最优解。所以我们要做的就是不断地寻找增广路。这样一直下去我们找不到增广路了就达到了网络最大流的状态(证明略)。同时,\(\rm Dinic\) 算法使用 \(bfs\) 分层,在之后 \(dfs\) 访问的时候就只访问深度比当前点大 \(1\) 的相邻点,提高搜索效率。

时间复杂度 \(\mathcal{O}(n^2m)\)

\(\rm Code:(Dinic)\)

#include <bits/stdc++.h>
#define LL long long
#define rg register
using namespace std;
const int N = 1e4 + 5, M = 2e5 + 5;

int n, m, s, t;
LL ans = 0, cnt = 1;
LL head[N], nxt[M], to[M], val[M];

inline void add(int u, int v, LL w) {
    to[++cnt] = v;
    val[cnt] = w;
    nxt[cnt] = head[u];
    head[u] = cnt;
}

int dep[N], q[N], l, r;

inline bool BFS() {
    memset(dep, 0, sizeof(dep));
    q[l = r = 1] = s;
    dep[s] = 1;
    while(l <= r) {
        rg int u = q[l++];
        for(rg int i = head[u]; i; i = nxt[i]) {
            rg int v = to[i];
            if(val[i] && !dep[v]) {
                dep[v] = dep[u] + 1;
                q[++r] = v;
            }
        }
    }
    return dep[t];
}

LL dfs(int u, LL in) {
    if(u == t) return in;
    rg LL out = 0;
    for(rg int i = head[u]; i && in; i = nxt[i]) {
        rg int v = to[i];
        if(val[i] && dep[v] == dep[u] + 1) {
            rg LL res = dfs(v, min(val[i], in));
            val[i] -= res;
            val[i ^ 1] += res;
            in -= res;
            out += res;
        }
    }
    if(!out) dep[u] = 0;
    return out;
}

// in main() :
    while(bfs()) ans += dfs(S, inf); // ans 就是最大流
//

最小割

将当前网络的点划分为两个不相交的点集\(S,T\),满足\(s\in S,t\in T\)。为此我们必须删去这两个点集之间的连边。这样的一种删法名为。在所有的割中,删去边的权值和最小的一个为最小割。

有结论: \(\large\mathtt{\bold{最大流=最小割}}\)

最小割虽然在数量上和最大流相等(这意味着他们求法一样),但放到题目中理解起来是不一样的。最小割常常跟划分不同的集合,取舍等等的问题描述有关。

费用流

最大流的前提下,我们给每一条边加上费用权值,代表这条边每通过 \(1\) 单位流量就要产生费用 \(cost[i]\) ,想知道可能的最小费用是多少。

我们将上面的 \(BFS\) 动一下手脚,改一点数组,就可以变成......

\(\huge\mathtt{某已死算法}\)

我们用 \(\rm spfa\) 来判断增广路的存在,顺便求一条 \(\sum cost[i]\) 最小的路径,然后从汇点沿着最短路往回调,最终累加答案。

inline bool aspfaedspfa() {
    deque <int> Q;
    memset(vis, 0, sizeof(vis));
    for(int i = 1; i <= T; i++) dis[i] = inf;
    dis[S] = 0; vis[S] = true; incf[S] = inf;
    Q.push_back(S);
    while(Q.size()) {
        def u = Q.front(); Q.pop_front(); vis[u] = false;
        for(def i = head[u]; i; i = nxt[i]) {
            if(!fx[i]) continue;
            def v = to[i];
            if(dis[v] > dis[u] + cost[i]) {
                dis[v] = dis[u] + cost[i];
                pre[v] = i;
                incf[v] = min(incf[u], fx[i]);
                if(!vis[v]) {
                    vis[v] = true;
                    if(!Q.empty() && dis[v] < dis[Q.front()]) Q.push_front(v); // 建议加个优化,不然它随时把你带走......
                    else Q.push_back(v);
                }
            }
        }
    }
    return dis[T] != inf;
}

inline LL MCMF() {
    while(aspfaedspfa()) {
        def x = T;
        while(x != S) {
            def i = pre[x];
            fx[i] -= incf[T];
            fx[i ^ 1] += incf[T];
            x = to[i ^ 1];
        }
        MaxFlow += 1LL * incf[T];
        MinCost += 1LL * incf[T] * dis[T];
    }
    return MinCost;
}

然后就是连边的时候要注意:

inline void LinkUp(int u, int v, int w,  int c) {
    nxt[++tot] = head[u];
    to[tot] = v;
    fx[tot] = w;
    cost[tot] = c;
    head[u] = tot;
}

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

以上内容可以当板子背

因为网络流最难的其实是建立最大流/最小割/费用流的模型,如何巧妙地套路性地将问题转化为网络流问题。所以,以下题目代码仅展示建边过程。

看题:

\(\text{Luogu P4313}\) 文理分科

题意:
小 P 所在的班级要进行文理分科。他的班级可以用一个 \(n\times m\) 的矩阵进行描述,每个格子代表一个同学的座位。每位同学必须从文科和理科中选择一科。同学们在选择科目的时候会获得一个满意值。满意值按如下的方式得到:

如果第 \(i\) 行第 \(j\) 列的同学选择了文科,则他将获得 \(art_{i,j}\) 的满意值,如果选择理科,将得到 \(science_{i,j}\) 的满意值。
如果第 \(i\) 行第 \(j\) 列的同学选择了文科,并且他相邻(两个格子相邻当且仅当它们拥有一条相同的边)的同学全部选择了文科,则他会更开心,所以会增加 \(same\text{\underline{ }}art_{i,j}\) 的满意值。
如果第 \(i\) 行第 \(j\) 列的同学选择了理科,并且他相邻的同学全部选择了理科,则增加 \(same\text{\underline{ }}science_{i,j}\) 的满意值。

小 P 想知道,大家应该如何选择,才能使所有人的满意值之和最大。请告诉他这个最大值。


Sol. 建立网络流模型。令一个同学 \((i,j)\) 的 \(\rm id\) 为 \((i-1)*m+j\) 。注意到这题和集合划分有关系,那我们就将文科生和源点 \(s\) 划在一起,理科生和汇点 \(t\) 划在一起。这样就要求一个人不能同时属于两个集合,求最小割即可。

那周围的同学还加分怎么办?我们新建一个点,将这个新点向小团体中的每一个同学连一条容量为 \(\inf\) 的边。同理,文科小团体的点连载源点上,理科小团体的点指向汇点。为什么可以这么做呢?因为这个新点指向成员的边容量均为 \(\inf\) ,所以这些边一定不会在求最小割的过程中被砍掉,而一旦有成员叛变,在 \(s\to \text{This Node}\to member[x]\to t\) 这条链上,我们删了 \(s\to member[x]\) (不在这条链上),就要连 \(member[x]\to t\),而 \(\text{This Node}\to member[x]\) 的容量为 \(\inf\),所以 \(s\to\text{This Node}\) 一定会被删,这样就排除了周围的同学还加分这一干扰项。
这样再求最小割即可。

    cin >> n >> m;
    S = 0; opt = T = n * m + 1; // 给源点、汇点赋值编号
    for(def i = 1; i <= n; i++)  //  以下是复杂的建边
        for(def j = 1; j <= m; j++){
            def art = read(), id = getid(i, j);
            sum += art;
            add(S, id, art);
        }
    for(def i = 1; i <= n; i++)
        for(def j = 1; j <= m; j++) {
            def sci = read(), id = getid(i, j);
            sum += sci;
            add(id, T, sci);
        }
    for(def i = 1; i <= n; i++) 
        for(def j = 1; j <= m; j++) {
            def artx = read(), id = getid(i, j); opt++;
            sum += artx;
            add(S, opt, artx), add(opt, id, inf);
            for(def k = 0; k < 4; k++) 
                if(valid(i + dx[k], j + dy[k])) add(opt, getid(i + dx[k], j + dy[k]), inf);
        }
    for(def i = 1; i <= n; i++) 
        for(def j = 1; j <= m; j++) {
            def scix = read(), id = getid(i, j); opt++;
            sum += scix;
            add(opt, T, scix), add(id, opt, inf);
            for(def k = 0; k < 4; k++)
                if(valid(i + dx[k], j + dy[k])) add(getid(i + dx[k], j + dy[k]), opt, inf);
        }
    cout << sum - dinic(); // 所有加成值减去最小割的值就是最大加分

\(\text{Luogu P2762}\) 太空飞行计划问题

题意:有 \(m\) 个实验,每个实验只可以进行一次,但会获得相应的奖金,有 \(n\) 个仪器,每个实验都需要一定的仪器,每个仪器可以运用于多个实验,但需要一定的价值,问奖金与代价的差的最大值是多少?同时输出一种选定方案。


Sol.
一般的思路是建立 \(s\to Experiments \to Equipments \to t\) 的网络,将 \(s\to Experiments\) 的边赋值为正,将 \(Equipments\to t\) 的边赋值为负,然而我们有另解:让 \(Equipments\to t\) 的边就赋成正值,然后跑最小割。怎么又是最小割?我们发现,这次题目给的实验和仪器变成了依赖关系,那我们能不能定义 \(s\to t\) 的不连通为依赖关系的满足呢?当然可以。我们令在 \(s\to Experiments\) 的边中,选(不删)表示决定进行该实验,获得对应的奖金,而在 \(Equipments \to t\) 的边中,不选(删去,即加入割集)表示购买该仪器。这样,决定进行一项实验的话,跟这个实验有连边的仪器和汇点的连边就必须删掉(因为我们在求最小割),这样就会损失这些边的权值,而这个“损失”就可以理解为购买仪器需要花钱。这样,我们的最小割模型就构建完成了。我们套最大流的板子求出最小割的值(最小损失),再拿全部的奖金减去最小割就是最大利润。

至于选定方案,我们知道,Dinic 算法执行的最后一个 \(bfs\) 返回了 false,这一遍搜索是没有用的。此时,\(dep\) 不为 \(0\) 的点就一定入选了实验或器材,遍历输出即可。

    int num;
    cin >> m >> n;
    int sum = 0;
    S = 0; T = n + m + 1;
    for(int i = 1; i <= m; i++)
    {
        int x;
        char c;
        scanf("%d", &x);
        sum += x;
        Struct(S, n + i, x); // 连接圆点和实验点
        while(1)
        {
            scanf("%d%c", &x, &c);
            Struct(n + i, x, inf); // 这句话是指建立实验点到器材点的有向边
            if(c == '\n' || c == '\r') break; // 这题的输入很奇怪,自动忽略
        }
    }
    for(int i = 1; i <= n; i++)
    {
        int w;
        scanf("%d", &w);
        Struct(i, T, w); // 连接器材点和汇点
    }
    
    while(bfs()) ans += dfs(S, inf); // 跑 Dinic 就用这句话

    for(def i = 1; i <= m; i++) if(dep[i + n]) cout << i << ' ';
    puts("");
    for(def i = 1; i <= n; i++) if(dep[i]) cout << i << ' ';
    puts("");
    cout << sum - ans;

\(\text{Luogu P2153}\) 晨跑

题意:现在给出一张学校附近的地图,这张地图中包含 \(n\) 个十字路口和 \(m\) 条街道,Elaxia 只能从一个十字路口跑向另外一个十字路口,街道之间只在十字路口处相交。Elaxia 每天从寝室出发跑到学校,保证寝室编号为 \(1\),学校编号为 \(n\)。Elaxia 的晨跑计划是按周期(包含若干天)进行的,一个周期内,每天的晨跑路线都不会相交(在十字路口处),寝室和学校不算十字路口。Elaxia 耐力不太好,他希望在一个周期内跑的路程尽量短,但是又希望训练周期包含的天数尽量长。(若有边\(1\to n\),则一个周期内只能走一次)

简述:求给定网络的最大流与最小费用,其中每个点仅允许经过一次。


Sol.
非常容易看到这几乎就是一道网络流的板子题,略有不同的是每个点只能经过一次(当然边也不能重复)。怎么搞?这里好像是个套路:将每个点拆成出点和入点,出点专门管理出度,入点专门管理入度。每个点的出点和入点之间也要连边。

具体的,\(id[a_{i,in}]=i,id[a_{i,out}]=i+n\),显然 \(s=n+1,t=n\)。连 \(a_{in}\to a_{out}\) 的边 \((a_{in}, a_{out}, 1, 0)\) (分别为两端点,容量,费用),连 \(u_{out}\to v_{in}\) 的边 \((u_{out}, v_{in}, 1, Len(u,v))\) ,其中 \(u\) 和 \(v\) 是原题的一条有向边 \(u\to v\) 的端点。容量赋值为 \(1\) 是因为只能经过一次。在这张新图上跑最小费用最大流的板子即可。

(我们好像将原来的一个点拉成了一条边?)

    n = read(); m = read();
    S = 1 + n; T = n;
    for(def i = 1; i <= m; i++) {
        def a = read(), b = read(), c = read();
        add(a + n, b, 1, c);
    }
    for(def i = 1; i <= n; i++) add(i, i + n, 1, 0);
    def ans = MCMF();
    cout << ans.first << ' ' << ans.second;

\(\text{Luogu P2469}\) 星际竞速

题意:题意:给一张 DAG ,边有边权(计算代价),从每个点出发有点权(途中经过节点没有点权的代价)。求最小代价使得每个点都被经过。

(题解里说这个“恰好经过一次”就套路性地想拆点???)

题目要求通过所有的点,那么如果瞬移也算走了一条边的话,整个走下来必定走了最大流。我们将点 \(i\) 拆成入点 \(i_x\) 的和出点 \(i_y\) ,然后显然对于原图的有向边 \(u\to v\) 要连 \(u_y\to v_x\)。考虑 \(i_x\) 的流量(即在入度时)是怎么来的,那只有可能是源点给的呀!(题目也并没有规定从哪里走起)于是我们搞一个超级源点,将 \(s\to i_x\) 连边 \((s,i_x,1,0)\) (显然这不需要费用)。再处理瞬移的情况:瞬移相当于直接访问到了出点(不受一个点的入点和出点之间的这条边的容量限制),于是连边 \(s\to i_y\) 即 \((s,i_y,1,a_i)\) 计算费用。

模型构建完成,套费用流板子即可。

inline auto MCMF() {
    while(aspfaedspfa()) {
        def x = T;
        while(x != S) {
            def i = pre[x];
            fx[i] -= incf[T];
            fx[i ^ 1] += incf[T];
            x = to[i ^ 1];
        }
        MaxFlow += incf[T];
        MinCost += incf[T] * dis[T];
    }
    return MinCost;
}

signed main() {
    n = read(); m = read();
    S = 0; T = 2 * n + 1;
    for(def i = 1; i <= n; i++) {
        def x = read();
        add(S, i + n, 1, x);
    }
    for(def i = 1; i <= m; i++) {
        def u = read(), v = read(), w = read();
        if(u > v) swap(u, v);
        add(u, v + n, 1, w);
    }
    for(def i = 1; i <= n; i++) 
        add(S, i, 1, 0);
    for(def i = 1; i <= n; i++)
        add(i + n, T, 1, 0);
    cout << MCMF();
}

\(\text{Luogu P1251}\) 餐巾计划问题

题意:
一个餐厅在相继的 \(N\) 天里,每天需用的餐巾数不尽相同。假设第 \(i\) 天需要 \(r_i\)块餐巾( i=1,2,...,N)。餐厅可以购买新的餐巾,每块餐巾的费用为 \(p\) 分;或者把旧餐巾送到快洗部,洗一块需 m 天,其费用为 f 分;或者送到慢洗部,洗一块需 \(n\) 天(\(n>m\)),其费用为 \(s\) 分(\(s<f\))。(这里已经\(m,n \to a,b\))

每天结束时,餐厅必须决定将多少块脏的餐巾送到快洗部,多少块餐巾送到慢洗部,以及多少块保存起来延期送洗。但是每天洗好的餐巾和购买的新餐巾数之和,要满足当天的需求量。

试设计一个算法为餐厅合理地安排好 \(N\) 天中餐巾使用计划,使总的花费最小。编程找出一个最佳餐巾使用计划。


Sol. 这个东西涉及比较复杂的费用计算。显然这题我们的“餐巾”对应到模型中就是流量。然而对于第 \(i\) 天来说,既有产生的脏的餐巾,可能送去快洗、慢洗或者托给下一天,又有若干天前开始洗而喜好的来自快洗部、慢洗部的餐巾,还有这一天新买的餐巾也要考虑,实在是太复杂了。但是我们可以发现这些操作无非就是两种:获得新餐巾,送洗旧餐巾。这启发我们还是将一天分成获得餐巾和处理餐巾两个点处理。(\(r[i]\) 表示这一天需要的餐巾数)

然后连边方法如下:(\(1\) 是早上,\(2\) 是晚上)

  • \(s\to i_1: (s,i_1,r[i],0)\)
  • \(i_2\to t: (i_2,t,r[i],0)\)
  • \(s\to i_1: (s,i_1,\inf,p)\)
  • \(i_2\to (i+a)_1: (i_2,(i+a)_1,\inf,f)\)
  • \(i_2\to (i+b)_1: (i_2,(i+b)_1,\inf,s)\)
  • \(i_2\to (i+1)_1: (i_2, (i+1)_1,\inf,0)\)

这题其实挺难的...

signed main() {
    scanf("%d", &n);
    for(def i = 1; i <= n; i++) scanf("%d", &r[i]);
    scanf("%d %d %d %d %d", &p, &a, &f, &b, &s);

    S = 0; T = 2 * n + 1;

    for(def i = 1; i <= n; i++) {
        Struct(S, i, r[i], 0); Struct(i + n, T, r[i], 0);
    }
    for(def i = 1; i <= n; i++) {
        Struct(S, i + n, inf, p);
        if(i + a <= n) Struct(i, i + n + a, inf, f);
        if(i + b <= n) Struct(i, i + n + b, inf, s);
        if(i + 1 <= n) Struct(i, i + 1, inf, 0);
    }

    cout << MCMF();
}

Task: P 4716 最小树形图

标签:int,text,餐巾,最小,网络,初步,7.7,incf,def
来源: https://www.cnblogs.com/Doge297778/p/16456013.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有