[教程]网络流详解: 从入门到放弃

亡梦爱人 提交于 2020-08-05 22:46:53

网络流从入门到放弃

by 沉迷流体力学无心刷题的rvalue

€€£ WARNING: 前方多图杀猫

关于这篇文章

本来这个是18年12月在校内讲课用的课件...本来打算当时就放到博客上但是因为里面有大量mermaid图就没敢放qaq...然而突然发现cnblogs是滋磁mermaid图的于是就丢出来了qaq

如果有错漏之处还请在评论区指正.

好像由于目录结构太大所以cnblogs生成不出来了...大纲是长这样的:

+ 网络流从入门到放弃
    + 何谓网络流
    + 最大流
        + Ford-Fulkerson算法(增广路算法)
            + 实现
            + 局限
        + Edmonds-Karp算法(EK算法)
            + 代码实现
            + 局限
        + Dinic算法
            + 举个栗子
            + 阻塞流
            + 代码实现
            + 时间复杂度分析
            + 真·时间复杂度分析
            + 真·代码实现
            + 一些题外问题
        + 最大流建模举例
            + 圆桌问题
            + 公平分配问题
            + 星际转移问题
            + 收集者的难题
    + 最大流与最小割
        + 流与割的关系
        + 最大流最小割定理
        + 最小割建模举例
            + 花园的守护之神
            + 王者之剑
            + Number
            + 最小割
            + 最大权闭合子图
            + 切糕
    + 最小费用最大流
        + EK费用流
            + 反向边权
            + 关于SPFA
            + 时间复杂度分析
        + 消圈算法
            + 消圈定理
        + ZKW费用流
            + 代码实现
            + 关于当前弧优化
            + 时间复杂度分析
        + Dijkstra费用流
        + 最小费用最大流建模举例
            + 南极科考旅行
            + 餐巾
            + 负载平衡
            + 最长k可重区间集
            + 平方费用最小费用最大流

文章长度比较劝退...但是应该能真正让读者入门(至少能打出复杂度比较正确的板子).

上下界网络流部分由于一些历史原因咕给另一个聚聚了...

何谓网络流

假设 \(G = (V,E)\) 是一个有限的有向图,它的每条边 \((u,v) \in E\) 都有一个非负值实数的容量$ c(u, v)\(。如果\) (u, v) \not \in E\(,我们假设\) c(u, v) = 0$。我们区别两个顶点:一个源点 \(s\) 和一个汇点$ t\(。一道网络流是一个对于所有结点\) u$ 和$ v \(都有以下特性的实数函数\) f:V \times V \rightarrow \mathbb{R}\(: **容量限制(Capacity Constraints)**:\) f(u, v) \leq c(u, v)\(一条边的流不能超过它的容量。 **斜对称(Skew Symmetry)**:\) f(u, v) = - f(v, u)$由 \(u\)\(v\)的净流必须是由 \(v\)\(u\)的净流的相反(参考例子)。
流守恒(Flow Conservation): 除非 \(u = s\)\(u = t\),否则 \(\sum_{w \in V} f(u, w) = 0\)一结点的净流是零,除了“制造”流的源点和“消耗”流的汇点。
即流守恒意味着:$ \sum_{(u,v) \in E} f(u,v) = \sum_{(v,z) \in E} f(v,z)$ ,对每个顶点$ v \in V\setminus{s,t}\( 注意\) f(u,v)$ 是由 \(u\) 到 $v \(的净流。如果该图代表一个实质的网络,由\) u \(到\) v \(有4单位的实际流及由\) v$ 到 $u $有3单位的实际流,那么 $f(u, v) = 1 $及 \(f(v, u) = -1\)
基本上,我们可以说,物理网络的网络流是从$ s = \sum_{(s,v)\in E} f(s,v) \(出发的流 边的**剩余容量(residual capacity)**是\) c_f(u, v) = c(u, v) - f(u, v)$。这定义了以 $G_f(V, E_f) $表示的剩余网络(residual network),它显示可用的容量的多少。留意就算在原网络中由 \(u\) 到 $v \(没有边,在剩余网络仍可能有由\) u \(到\) v $的边。因为相反方向的流抵消,减少由 $v \(到\) u$ 的流等于增加由$ u$ 到 $v \(的流。**增广路(augmenting path)**是一条路径\) (u_1, u_2, \dots, u_k)$,而 \(u_1 = s\)、$ u_k = t $及 \(c_f(u_i, u_{i+1}) > 0\),这表示沿这条路径发送更多流是可能的。当且仅当剩余网络$ G_f $没有增广路时处于最大流。
因此如下使用图 \(G\) 创建 $ G_f \(: \)G_f = V $的顶点
定义如下的 $G_f = E_f \(的边 对每条边\) (x,y) \in E$
若$ f(x,y) < c(x,y)\(,创建容量为\) c_f = c(x,y) - f(x,y)$ 的前向边 \((x,y) \in E_f\)
若$ f(x,y) > 0\(,创建容量为\) c_f = f(x,y) $的后向边 \((y, x) \in E_f\)
这个概念用在计算流量网的最大流的Ford–Fulkerson算法中。
有时需要对有多于一个源点的网络,于是就引入了图的超源点。这包含了一个与无限容量的边连接的每个源点,以作为一个整体源点。汇点也有类似的构造称作超汇点








以上是摘自Wikipedia的定义

实际上重点有三:

  • 容量限制(Capacity Constraints):$ f(u, v) \leq c(u, v)$一条边的流不能超过它的容量。
  • 斜对称(Skew Symmetry):$ f(u, v) = - f(v, u)$由 \(u\)\(v\)的净流必须是由 \(v\)\(u\)的净流的相反(参考例子)。
  • 流守恒(Flow Conservation): 除非 \(u = s\)\(u = t\),否则 \(\sum_{w \in V} f(u, w) = 0\)一结点的净流是零,除了“制造”流的源点和“消耗”流的汇点。

想象一个不可压缩的流体的运输管网, 每个管道都有防倒流阀门 (保证有向) , 每个管道还有一个单位时间内的流量限制, 那就是一个网络流模型的样子了

最大流

最大流问题其实就是字面意思, 求 \(s\)\(t\) 的最大流量.

比如对于下面这个流量网络:

graph LR; s-->|16|1 s-->|13|2 2-->|4|1 1-->|12|3 1-->|10|2 3-->|9|2 2-->|14|4 4-->|7|3 3-->|20|t 4-->|4|t

它的最大流是 \(23\):

graph LR; s-->|11/16|1 s-->|12/13|2 2-->|1/4|1 1-->|12/12|3 1-->|0/10|2 3-->|0/9|2 2-->|11/14|4 4-->|7/7|3 3-->|19/20|t 4-->|4/4|t

Ford-Fulkerson算法(增广路算法)

对于最大流问题, 我们有一个直观的思路, 就是找一条从 \(s\)\(t\) 而且剩余容量非空的路径, 然后把它跑满并累加答案

而这个"从 \(s\)\(t\) 而且剩余容量非空的路径"就是增广路. 因为它将原有的流扩充(或者说"增广")了.

但是这个时候我们会发现一些问题: 如果不巧找到了一个增广路把本来不该在最大流里的边给增广了怎么破?

比如下图中的增广路:

graph LR; s==>|7/16|1 s-->|0/13|2 2-->|0/4|1 1-->|0/12|3 1==>|7/10|2 3-->|0/9|2 2==>|7/14|4 4==>|7/7|3 3==>|7/20|t 4-->|0/4|t

如果我们继续增广, 则我们得到的"最大流"只有 \(20\).

graph LR; subgraph end; e.s[s]-->|16/16|e.1 e.s[s]-->|4/13|e.2 e.2[2]-->|4/4|e.1 e.1[1]-->|10/12|e.3 e.1[1]-->|10/10|e.2 e.3[3]-->|1/9|e.2 e.2[2]-->|11/14|e.4 e.4[4]-->|7/7|e.3 e.3[3]-->|16/20|e.t[t] e.4[4]-->|4/4|e.t[t] end subgraph +5; 4.s[s]==>|16/16|4.1 4.s[s]-->|4/13|4.2 4.2[2]-->|4/4|4.1 4.1[1]==>|10/12|4.3 4.1[1]-->|10/10|4.2 4.3[3]-->|1/9|4.2 4.2[2]-->|11/14|4.4 4.4[4]-->|7/7|4.3 4.3[3]==>|16/20|4.t[t] 4.4[4]-->|4/4|4.t[t] end subgraph +1; 3.s[s]==>|11/16|3.1 3.s[s]-->|4/13|3.2 3.2[2]-->|4/4|3.1 3.1[1]==>|5/12|3.3 3.1[1]-->|10/10|3.2 3.3[3]==>|1/9|3.2 3.2[2]==>|11/14|3.4 3.4[4]-->|7/7|3.3 3.3[3]-->|11/20|3.t[t] 3.4[4]==>|4/4|3.t[t] end subgraph +3; 2.s[s]==>|10/16|2.1 2.s[s]-->|4/13|2.2 2.2[2]-->|4/4|2.1 2.1[1]-->|4/12|2.3 2.1[1]==>|10/10|2.2 2.3[3]-->|0/9|2.2 2.2[2]==>|10/14|2.4 2.4[4]-->|7/7|2.3 2.3[3]-->|11/20|2.t[t] 2.4[4]==>|3/4|2.t[t] end subgraph +4; 1.s[s]-->|7/16|1.1 1.s[s]==>|4/13|1.2 1.2[2]==>|4/4|1.1 1.1[1]==>|4/12|1.3 1.1[1]-->|7/10|1.2 1.3[3]-->|0/9|1.2 1.2[2]-->|7/14|1.4 1.4[4]-->|7/7|1.3 1.3[3]==>|11/20|1.t[t] 1.4[4]-->|0/4|1.t[t] end

这时我们考虑以前讲过的"可撤销贪心", 建立一条反向边来允许撤销.

记得定义中的一句"斜对称"么? 也就是 \(f(u,v)=-f(v,u)\), 于是我们可以定义反向边上增加一单位流量都代表原边上减少一单位流量. 如果增广出了 \((u,v)\) 之间的双向流量实际上可以将经过 \(u,v\) 的流量交换来抵消成单向流量. 比如下图:

graph TD; subgraph 转化; 3.s[s]-->3.1[1] 3.s==>3.3[3] 3.3==>3.2[2] 3.1-->3.2[2] 3.1==>3.4[4] 3.2==>3.1 3.2-->3.t[t] 3.4==>3.t end subgraph 实际优的流; 2.s[s]-->2.1[1] 2.1-->2.4[4] 2.2-->2.t 2.4-->2.t[t] 2.3-->2.2[2] 2.1-.->2.2 2.s-->2.3[3] end subgraph 开始增广出的流; 1.1[1]-->1.2[2] 1.s[s]-->1.1 1.2-->1.t[t] end

实际上我们相当于是增加反方向的流量来把原来的正向流量"推回去".

或者一个更形象化的解释, 每条边代表着一个里面流动着不可压缩流体的具有流速限制的管道, 那么反向增广就有点像是反向加压来尝试把流体压回原来的点, 总的效果就是让这条边里的流速减缓或反向, 而让流返回原来的点重新决策.

这其实也告诉我们, 最大流必定是无环的. 因为循环流无源无汇, 不会对 \(s\verb|-|t\) 流量产生贡献, 却会白白占用容量, 一定不会优.

实际上我们对这个"消除环流"的过程的实现方式是"只要给一条边加载上流量, 就必须在其对应的反向边扩充等量的容量".

这个过程中引入一个概念: 残量网络. 可以理解为意思就是把满流的边扔掉之后, 边权为剩余流量的图.

于是我们加载流量的过程就可以转化为边权降低, 扩容的过程就可以转化为边权增加.

实际上整个最大流的过程中我们我们并没有必要一直在容量和当前流量这两个值上做文章. 我们在整个算法中全程只关心它们的差值: 剩余容量. 所以我们其实只记录它就可以了. 然后残量网络就可以定义成只包含剩余容量非 \(0\) 的边的图.

实现

建立流量网络, 对于每条边记录一下它的出入结点/剩余容量/反向边, 然后我们进行一次只走剩余流量非 \(0\) 的边的DFS来查找增广路径, 过程中顺便维护一下路径上的最小剩余容量 (显然我们只要找到一条增广路径之后把它压榨干净再继续找下一条是最优策略), 回溯的时候进行 "减少当前边剩余容量, 增加反向边剩余容量" 的操作, 结束后把答案累加起来就好了. 代码实现就是返回一个值表示找到的增广路的流量, 如果为 \(0\) 表示没有找到增广路. 只要返回非 \(0\) 值就执行缩小剩余容量的操作并回溯. 单次增广是 \(O(V+E)\) 的.

不难发现整个过程中一直保持着流量守恒的原则: 每次我们都是在一整条路径上搬移流量, 所以中间结点完全不会累积流量.

局限

我们发现增广路算法的运行时间基本上全靠RP: 看你增广路选得怎么样. 选得好没准一次就跑出来了, 选差了就得一直把流推来推去. 比如下面这张图:

graph LR; s-->|23333333|1 1-->|23333333|t 1-->|1|2 2-->|23333333|t s-->|23333333|2

我们一眼就能看出这图最大流是 \(46666666\) , 但是假如你RP爆炸一直在和边 \((1,2)\) 斗智斗勇的话...请允许我做一个悲伤的表情...

不过增广路算法是一定会结束的, 因为你每次找到一个增广路都会让流量增加, 最终一定能达到最大流.

时间复杂度是 O(值域) 的, 属于指数级算法. (题外话: 其实O(值域)的算法全是指数算法...我才不会告诉你今年联赛day1考了个NPC问题呢)

但是这个算法是几乎所有实用网络流算法的基础. 它们本质上都是增广路算法的优化.

Edmonds-Karp算法(EK算法)

EK算法其实就是在增广路算法的基础上加了一层优化: 每次增广最短的增广路.

这样的话它的时间复杂度便有了保障, 是 \(O(VE^2)\) 的. 大体的证明思路是这样的:

首先每次我们找到一条增广路的时候肯定会把它压榨干净, 也就是说至少有一条边在这个过程中被跑满了. 我们把这样的边叫做关键边. 增广之后, 这些边在残量网络中都会被无视掉. 也就是说这条路径就这么断掉了. 而我们每次都增广最短路, 也就是说我们每次都在破坏最短路. 所以 \((s,t)\) 之间的最短路单调递增.

因为增广路必然伴随至少一条关键边出现, 所以我们可以把增广过程的迭代次数上界转化为每条边成为关键边的次数.

因为关键边会从残量网络中消失, 直到反向边上有了流量才会恢复成为关键边的可能性, 而当反向边上有流量时最短路长度一定会增加. 而最短路长度不会超过 \(V\), 所以总迭代次数是 \(O(VE)\) 的.

因为EK算法中残量网络上的最短路是 \(0/1\) 最短路, 直接BFS实现的话时间复杂度是 \(O(E)\) 的, 于是EK算法总时间复杂度 \(O(VE^2)\) , 证毕.

实现上把DFS改成BFS并且在第一次访问到 \(t\) 时就跳出就行了, 实际上没啥好讲的...

代码实现

下面这个实现是去年粘的某刚哥届学长的课件里的...懒得自己写了(其实是不会)

int Bfs() {
    memset(pre, -1, sizeof(pre));
    for(int i = 1 ; i <= n ; ++ i) flow[i] = INF;
    queue <int> q;
    pre[S] = 0, q.push(S);
    while(!q.empty()) {
        int op = q.front(); q.pop();
        for(int i = 1 ; i <= n ; ++ i) {
            if(i==S||pre[i]!=-1||c[op][i]==0) continue;
            pre[i] = op; //找到未遍历过的点
            flow[i] = min(flow[op], c[op][i]); // 更新路径上的最小值 
            q.push(i);
        }
    }
    if(flow[T]==INF) return -1;
    return flow[T];
}
int Solve() {
    int ans = 0;
    while(true) {
        int k = Bfs();
        if(k==-1) break;
        ans += k;
        int nw = T;
        while(nw!=S) {//更新残余网络 
            c[pre[nw]][nw] -= k, c[nw][pre[nw]] += k;
            nw = pre[nw];
        }
    }
    return ans;
}

局限

虽然EK算法成功把时间复杂度降到了一个多项式级别, 但是它 \(O(VE^2)\) 的上界实际上相当于 \(O(V^5)\) 的级别(\(E\)\(V^2\) 可以是同阶的), 并不是非常能够令人接受.

我们来看一看为啥EK依然这么慢.

为啥呢?

我们再看最开始的流量网络, 跑一跑EK的BFS求最短路, 找到一条最短增广路 \(s\rightarrow 1 \rightarrow 3 \rightarrow t\) 之后:

graph LR; s["s(d=0)"]==>|12/16|1 s-->|13|2 2["2(d=1)"]-->|4|1 1["1(d=1)"]==>|12/12|3 1-->|10|2 3["3(d=2)"]-->|9|2 2-->|14|4 4["4(d=2)"]-->|7|3 3==>|12/20|t["t(d=3)"] 4-->|4|t

我们增广掉这条路径上的 \(12\) 单位流量...然后再来一遍BFS求最短路...

先等一等!

我们一眼就能就发现: 这 \(s \rightarrow 2 \rightarrow 4 \rightarrow t\) 明明也是一条最短增广路啊!

EK算法的劣势就在于, 每次遍历了一遍残量网络之后只能求出一条增广路来增广.

于是我们针对这一点进行优化, 我们就得到了一个更加优秀的替代品.

Dinic算法

没有什么是一个BFS或一个DFS解决不了的;如果有,那就两个一起。

Dinic算法又称为"Dinic阻塞流算法", 这个算法的关键就在于"阻塞流".

首先我们顺着EK算法的思路, 每次增广最短路上的边来在一定程度上保证时间复杂度.

这时我们引入一个概念: 分层图. 它的一个不严谨的定义就是: 对于每个点, 按照从 \(s\) 到它的最短路长度分组, 每组即为"一层".

其实这个"分层"也可以理解为深度...

举个栗子

回到最开始的那个流量网络, 我们把它BFS一遍按照 \(s\) 最短路分层, 得到下面的图:

graph LR; s-->|16|1 s-->|13|2 2-->|4|1 1-->|12|3 1-->|10|2 3-->|9|2 2-->|14|4 4-->|7|3 3-->|20|t 4-->|4|t subgraph depth=0; s end subgraph depth=1; 1 2 end subgraph depth=2 3 4 end subgraph depth=3 t end

层内的边和反向边不在最短路上所以增广时都会被我们被无视, 我们在示意图中删掉它们, 于是就变成:

graph LR; s-->|16|1 s-->|13|2 1-->|12|3 2-->|14|4 3-->|20|t 4-->|4|t subgraph depth=0; s end subgraph depth=1; 1 2 end subgraph depth=2 3 4 end subgraph depth=3 t end

分好层之后, 我们非常偷税地发现:

  • 所有存在于分层图上的残余边都在一条残量网络的最短路上
  • 反向边都去和梁非凡共进晚餐了

因为不用担心在处理分层图的时候流会被反向边退回, 所以我们只管放心大胆地只用一遍DFS来增广就好了.

但是这并不意味着我们在边上加载流量的时候可以不管反向边, 反向边的残量变更还是要算的, 因为以后的DFS可能会把流再推回去.

阻塞流

这一节刚开始的时候说Dinic的关键就在于"阻塞流". 阻塞流是啥?

我们尝试在上图中增广, 然后得到下面的残量网络:

graph LR; s-->|4|1 s-->|9|2 2-->|10|4 3-->|8|t subgraph depth=0; s end subgraph depth=1; 1 2 end subgraph depth=2 3 4 end subgraph depth=3 t end

我们发现把当前分层图上的最大流完全增广之后, \(s\)\(t\) 在分层图上一定会不连通 (增广路算法找不到新的增广路就是因为残量网络不连通), 我们称其为阻塞增广. 这样增广出来的流就是阻塞流.

代码实现

int Dinic(int s,int t){
    int ans=0;
    while(BFS(s,t))
        ans+=DFS(s,INF,t);
    return ans;
}
 
int DFS(int s,int flow,int t){
    if(s==t||flow<=0)
        return flow;
    int rest=flow;
    for(Edge* i=head[s];i!=NULL&&rest>0;i=i->next){
        if(i->flow>0&&depth[i->to]==depth[s]+1){
            int k=DFS(i->to,std::min(rest,i->flow),t);
            rest-=k;
            i->flow-=k;
            i->rev->flow+=k;
        }
    }
    return flow-rest;
}
 
bool BFS(int s,int t){
    memset(depth,0,sizeof(depth));
    std::queue<int> q;
    q.push(s);
    depth[s]=1;
    while(!q.empty()){
        s=q.front();
        q.pop();
        for(Edge* i=head[s];i!=NULL;i=i->next){
            if(i->flow>0&&depth[i->to]==0){
                depth[i->to]=depth[s]+1;
                if(i->to==t)
                    return true;
                q.push(i->to);
            }
        }
    }
    return false;
}

BFS函数非常好说, 就是一个纯粹的 \(0/1\) 最短路

DFS函数有几个点需要说一下. 先说参数. 首先 s, t是字面意思, 然后是flow参数, 代表"上游的边对增广路上的流量的限制". 因为增广路上任意一条边都不能超流. 接着有一个局部变量 rest, 表示"上游的流量限制还有rest单位没有下传". 因为要满足流量守恒, 我们只能把流入的流量分配到后面, 所以这个 rest 实际上保存的就是最大可能的流入流量.

最后返回的是flow-rest, 把所有后继结点都访问过之后的 rest 值即为无法排出的流入量的值, 我们返回的是增广量所以肯定不能让它不能排出, 所以我们把上游流入量减去不能排出的量即为可行流量.

或者说, 参数 flow 是"推送量", 返回的是"接受量"或"成功传递给 \(t\) 的流量", rest 是"淤积量".

(能量流动学傻了.png)

时间复杂度分析

由于阻塞增广之后不再存在原长度的最短路, 最短路的长度至少 \(+1\). 所以阻塞增广会进行进行 \(O(V)\) 次.

进行一次阻塞增广只要一次DFS就可以实现, 而一次DFS的时间复杂度小学生都知道是 \(O(E)\) 的.

于是Dinic的时间复杂度是 \(O(VE)\) 的! 比EK优秀到不知道哪里去了!






然而是假的

真·时间复杂度分析

其实多路增广的同时我们发现一个问题: 这个DFS求阻塞增广的复杂度其实和普通DFS完全不同. 想一想, 为什么.

EK算法中计算一条增广路是严格BFS一遍, 时间复杂度严格 \(O(E)\), 但是这次的DFS就不是这样了.

我们会有重复DFS一个结点这种操作.

比如下面这个分层图:

graph LR; s-->|12|1 s-->|8|2 1-->|4|3 2-->|10|3 3-->|5|4 3-->|20|5 4-->|7|t 5-->|6|t subgraph depth=0 s end subgraph depth=1 1 2 end subgraph depth=2 3 end subgraph depth=3 4 5 end subgraph depth=4 t end

我们就会发现一开始从 \(s \rightarrow 1 \rightarrow 3\) 这条路径过来的时候, 上游的残量已经把最大增广量卡到了 \(4\). 所以我们只能在 \(3\) 号点后增广 \(4\) 个单位的流量, 但是 \(3\) 号点依然有继续增广的空间, 我们不能打个vis就跑路. 接着从 \(s \rightarrow 2 \rightarrow 3\) 过来的时候, 就会继续从 \(3\) 号点向下增广.

然而不加vis的DFS是指数级的.

所以这个鬼Dinic又是一个辣鸡指数算法?

没那么简单.

我们在DFS的时候加两个非常简单但是很重要的优化:

int DFS(int s,int flow,int t){
    if(s==t||flow<=0)
        return flow;
    int rest=flow;
    for(Edge*& i=cur[s];i!=NULL;i=i->next){
        if(i->flow>0&&depth[i->to]==depth[s]+1){
            int tmp=DFS(i->to,std::min(rest,i->flow),t);
            if(tmp<=0)
                depth[i->to]=0;
            rest-=tmp;
            i->flow-=tmp;
            i->rev->flow+=tmp;
            if(rest<=0)
                break;
        }
    }
    return flow-rest;
}

如果我们令 depth=0, 那么不可能会有哪个前驱结点满足 \(d(u)+1=d(v)\) 了, 也就是说我们把这个点无视掉了.

为啥呢?

因为你现在找不到妹子以后也一样找不到

(这是刚哥的比喻(逃))

因为如果你现在尝试给 i->to 推送一个大小非 \(0\) 的流量, 然而它却无情地返回了一个 \(0\) 作为接受量的话, 只能说明: i->to 结点从此刻开始无法再推送更多流量到 \(t\) 了. 现在不能, 以后也不能. 于是我们删掉它作为优化.

然后最关键的决定时间复杂度的优化是当前弧优化, 我们每次向某条边的方向推流的时候, 肯定要么把推送量用完了, 要么是把这个方向的容量榨干了. 除了最后一条因为推送量用完而无法继续增广的边之外其他的边一定无法继续传递流量给 \(t\) 了. 这种无用边会在寻找出边的循环中增大时间复杂度(记得那个欧拉路题么?), 必须删除.

最后再看重新这一整个DFS的过程, 如果当前路径的最后一个点可以继续扩展, 则肯定是在层间向汇点前进了一步, 最多走 \(V\) 步就会到达汇点. 在前进过程中, 我们发现一个点无法再向 \(t\) 传递流量, 我们就删掉它. 根据我们在分析EK算法时间复杂度的时候得到的结论, 我们会找到 \(O(E)\) 条不同的增广路, 每条增广路又会前进或后退 \(O(V)\) 步来更新流量, 又因为我们加了当前弧优化所以查找一条增广路的时间是和前进次数同阶的, 于是单次阻塞增广DFS的过程的时间上界是 \(O(VE)\) 的.

于是Dinic算法的总时间复杂度是 \(O(V^2E)\) 的.

这个上界非常非常松 (王逸松的松). 松到什么程度?

LOJ最大流板子, \(V=100, E=5000\) , 计算得 \(V^2E=5\times 10^7\) , 而我这份充满STL和递归的板子代码实际上跑得最慢的点只跑了 \(25\texttt{ms}\).

顺便说这个Dinic在容量 \(0/1\) 以及层数不多的图上跑得更快, 二分图匹配问题上甚至被证明了一个 \(O(\sqrt{V}E)\) 的上界

实战应用网络流建模的时候因为是自己构图, 一般层数都不会非常大而且结构是自己定的, 所以跑得会更快~ (一般\(800\)\(1000\) 个点, 边数 \(1\times 10^4\) 左右的图都是能跑的)

真·代码实现

以下是LOJ#101 最大流的AC板子

#include <bits/stdc++.h>

const int MAXV=110;
const int MAXE=10010;
const long long INF=1e15;

struct Edge{
    int from;
    int to;
    int flow;
    Edge* rev;
    Edge* next;
};
Edge E[MAXE];
Edge* head[MAXV];
Edge* cur[MAXV];
Edge* top=E;

int v;
int e;
int s;
int t;
int depth[MAXV];

bool BFS(int,int);
void Insert(int,int,int);
long long Dinic(int,int);
long long DFS(int,long long,int);

int main(){
    scanf("%d%d%d%d",&v,&e,&s,&t);
    for(int i=0;i<e;i++){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        Insert(a,b,c);
    }
    printf("%lld\n",Dinic(s,t));
    return 0;
}

long long Dinic(int s,int t){
    long long ans=0;
    while(BFS(s,t))
        ans+=DFS(s,INF,t);
    return ans;
}

bool BFS(int s,int t){
    memset(depth,0,sizeof(depth));
    std::queue<int> q;
    q.push(s);
    depth[s]=1;
    cur[s]=head[s];
    while(!q.empty()){
        s=q.front();
        q.pop();
        for(Edge* i=head[s];i!=NULL;i=i->next){
            if(i->flow>0&&depth[i->to]==0){
                depth[i->to]=depth[s]+1;
                cur[i->to]=head[i->to];
                if(i->to==t)
                    return true;
                q.push(i->to);
            }
        }
    }
    return false;
}

long long DFS(int s,long long flow,int t){
    if(s==t||flow<=0)
        return flow;
    long long rest=flow;
    for(Edge*& i=cur[s];i!=NULL;i=i->next){
        if(i->flow>0&&depth[i->to]==depth[s]+1){
            long long tmp=DFS(i->to,std::min(rest,(long long)i->flow),t);
            if(tmp<=0)
                depth[i->to]=0;
            rest-=tmp;
            i->flow-=tmp;
            i->rev->flow+=tmp;
            if(rest<=0)
                break;
        }
    }
    return flow-rest;
}

void Insert(int from,int to,int flow){
    top->from=from;
    top->to=to;
    top->flow=flow;
    top->rev=top+1;
    top->next=head[from];
    head[from]=top++;
    
    top->from=to;
    top->to=from;
    top->flow=0;
    top->rev=top-1;
    top->next=head[to];
    head[to]=top++;
}

注意到我把当前弧优化的重赋值部分写在了 BFS 里, 这样可以做到"按需赋值". 因为Dinic本来上界就松得一匹, BFS的过程中不连通的点根本就不用再管了...

以及如果你用数组边表的话, 可以选择让下标从 \(0\) 开始, 保证一次加入两个边, 这样的话只要 \(\text{xor}\,1\) 就可以算出反向边的编号了

一些题外问题

到现在估计jjm已经吵了好几次"这是个黑盒算法不用理解"之类的话了.

为啥我要把一个Dinic讲得这么细呢? 况且它确实真的只是个建模之后跑一下的工具?

因为如果你不理解Dinic的过程与复杂度分析, 你几乎一 定 会 写 假.

有些看起来很不起眼的小细节可能影响着整个算法的时间复杂度.

首先就是当前弧优化的跳出条件, 我为啥要把"除了最后一条边之外"那句话加粗呢? 因为你如果把跳出判定写在for循环里会慢 \(10\) 倍以上, 根本不是常数问题, 是复杂度出了锅. 因为你会漏掉最后那个可能没跑满的弧, 而分层图BFS会在当前图没有被割断的时候一直跑跑跑, 于是就锅了.

其次有人把无用点优化当成当前弧优化的替代品, 实际上无用点优化并不能保证时间复杂度. 无用点优化可以看做是当前弧优化的一个弱化版, 区别只是在于是出边全都无法传递流量的时候再删还是一条边无法传递流量时就删.

甚至有人直接不加当前弧优化和无用点优化, 这是我最 \(F_2\) 的...

网上把Dinic写假的真不少. 很多Dinic教程上的板子都是假的.

本着防止大家好不容易建出图来结果因为板子一直写的是假的结果炸飞的态度 (坚信一个复杂度是假的的东西复杂度是真的, 这和在普通最短路中用SPFA有什么区别) , 我决定从仔细讲好Dinic开始.

以及为啥不讲ISAP?

因为我不是ISAP选手所以不会ISAP

因为ISAP其实在很多用途上其实并没有Dinic灵活. 有些玄学建图骚操作ISAP很难跑得起来. 今年省选D2T1如果不是因为出题人是ISAP选手而特意构造了一个ISAP能跑的题的话很可能就会无意间卡掉ISAP选手 (这不是玩笑, 多半正经出题人都是Dinic选手, 一个不小心就会无意间卡掉ISAP). 所以我个人并不建议大家做ISAP选手.

当然你要是坚信ISAP跑得比较快而且不会被卡而去学ISAP我也不拦你...柱子恒就是ISAP选手

最大流建模举例

当你能熟练而正确地打出最大流板子的时候你就会发现: 你并不能做出几道网络流题目.

网络流的精髓在于建图.

我们先从最基础的"网络流24题"开始.

大家先来看几个例题理解一下建图的基本套路

圆桌问题

假设有来自 \(n\) 个不同单位的代表参加一次国际会议。每个单位的代表数分别为 \(r_i\) 。会议餐厅共有 \(m\) 张餐桌,每张餐桌可容纳 \(c_i\) 个代表就餐。 为了使代表们充分交流,希望从同一个单位来的代表不在同一个餐桌就餐。 试给出满足要求的代表就餐方案。

这题建图感觉还是很显然的.

首先可以看出一个明显的二分图模型, 单位是一种点, 餐桌是另一种点, 代表数量可以看做是流量.

\(s\) 连到所有单位点, 容量为代表数量. 单位点和餐桌点之间两两连一条容量为 \(1\) 的边代表"同一个单位来的代表不能在同一个餐桌就餐"的限制. 餐桌点再向 \(t\) 连一条容量为餐桌大小的边来限制这个餐桌的人数.

这样的话, 每一单位流量都代表着一个代表. 它们流经的点和边就代表了它们的特征. 而容量就是对代表的限制.

回想DP, DP的特点就是抽象出一个基本对象"状态", 然后用若干维度来描述这个状态的特征, 根据这些特征应用题目中的限制. 而网络流的构图, 则是用一单位流量流动的过程来刻画特征.

这种"把一个基本对象的特征用一单位流量从 \(s\) 流到 \(t\) 的过程来刻画"的思路, 就是网络流的一般建图策略.

至于输出方案, 我们只要看从单位点到餐桌点的边有哪些满载了, 一条满载的边 \((u,v)\) 意义就是在最优方案中一个来自 \(u\) 代表的单位的代表坐到了 \(v\) 代表的餐桌上.

当然如果最大流没有跑满(最大流的值不等于代表数量之和)的话肯定有代表没被分配出去, 判定无解.

公平分配问题

\(m\) 个任务分配给 \(n\) 个处理器. 其中每个任务有两个候选处理器, 可以任选一个分配. 要求所有处理器中被分配任务最多的处理器分得的任务最少. 不同任务的候选处理器保证不同.

首先我们要让最大值最小, 容易想到二分答案.

其次我们可以看到一个明显的二分图模型: \(m\) 个任务和 \(n\) 个处理器. 我们从 \(s\) 连容量为 \(1\) 的边到所有任务, 从任务连边到候选处理器, 从候选处理器连一条容量为二分答案的边到 \(t\) . 只要最大流达到了 \(m\) , 就说明我们成功在任务数量最多处理器的任务数量不大于二分答案的情况下分配出了 \(m\) 个任务, 执行 \(O(\log m)\) 次即可.

星际转移问题

现有 \(n\) 个太空站位于地球与月球之间,且有 \(m\) 艘公共交通太空船在其间来回穿梭。每个太空站可容纳无限多的人,而每艘太空船 \(i\) 只可容纳 \(H_i\) 个人。每艘太空船将周期性地停靠一系列的太空站,例如:\(1,3,4\) 表示该太空船将周期性地停靠太空站 \(134134134\cdots\) 每一艘太空船从一个太空站驶往任一太空站耗时均为 \(1\)。人们只能在太空船停靠太空站(或月球、地球)时上、下船。 初始时所有人全在地球上,太空船全在初始站。试设计一个算法,找出让 \(k\) 人尽快地全部转移到月球上的运输方案。

\(n \leq 20, k\leq 50, m\leq 13\)

在这个题目中, 我们使用图论算法的另一个套路: 把状态抽象为结点.

我们把 "每一天的空间站/星球" 抽象为状态, 从 \(s\) 连一条容量为 \(k\) 的边到第 \(0\) 天的地球, 从所有月球点连接一条容量为 \(\infty\) 的边到 \(t\) , 然后对于第 \(i\) 个飞船, 如果第 \(d\) 天停留在空间站 \(u\) 且下一轮要去 \(v\) 空间站, 则从第 \(d\) 天的 \(u\) 向第 \(d+1\) 天的 \(v\) 连一条容量为 \(H_i\) 的边, 计算最大流是否为 \(k\) 即可判定是否能够完全运输.

网络流判定, 那么二分答案根据答案建若干层点来判定?

太慢辣!

网络流题很多(不包括上一题)都有个特点: 当你发现某个题需要二分的时候, 它多数情况下并不用二分.

因为你的残量网络还是可以怼几条边进去接着跑的.

所以你只要从小到大枚举答案, 每次建当天的一层点, 増广之后判断一下是否满流就行了.

增量増广一般快的一匹.

收集者的难题

Bob和他的朋友从糖果包装里收集贴纸. 这些朋友每人手里都有一些 (可能有重复的) 贴纸, 并且只跟别人交换他所没有的贴纸. 贴纸总是一对一交换.

Bob比这些朋友更聪明, 因为他意识到指跟别人交换自己没有的贴纸并不总是最优的. 在某些情况下, 换来一张重复的贴纸会更划算.

假设Bob的朋友只和Bob交换 (他们之间不交换), 并且这些朋友只会出让手里的重复贴纸来交换它们没有的不同贴纸. 你的任务是帮助Bob算出它最终可以得到的不同贴纸的最大数量.

首先我们发现, Bob所持有的贴纸数量是一定的, 可以转化为流量, 所以我们只要建边体现一下交换就行了.

因为某单位流量到底是什么类型的贴纸与交换过程有关, 所以我们对于每一种贴纸都建立一个结点, 代表"流量流到这里之后即为对应类型的贴纸". 然后我们只要把可能的类型转移带上容量建上去就行了. 从 \(s\) 连边到贴纸类型点代表Bob一开始拥有的贴纸, 从贴纸类型点连边到 \(t\) 代表到此为止不再继续交换. 注意从 \(s\) 连出的边容量为拥有的贴纸数量, 但因为我们要最大化种类数量, 所以连到 \(t\) 的边必须容量为 \(1\). 最大流值即为最大种类数.

最大流与最小割

有时候我们会发现用最大流的思路并不能建出模型...考虑这样的一个题目:

看着正在被上古神兽们摧残的花园,花园的守护之神――小Bug同学泪流满面。然而,OIER不相信眼泪,小bug与神兽们的战争将进行到底!
通过google,小Bug得知,神兽们来自遥远的戈壁。为了扭转战局,小Bug决定拖延神兽增援的速度。从戈壁到达花园的路径错综复杂,由若干段双向的小路组成。神兽们通过每段小路都需要一段时间。小Bug可以通过向其中的一些小路投掷小xie来拖延神兽。她可以向任意小路投掷小Xie,而且可以在同一段小路上投掷多只小xie。每只小Xie可以拖延神兽一个单位的时间。即神兽通过整段路程的总时间,等于没有小xie时他们通过同样路径的时间加上路上经过的所有小路上的小xie数目总和。
神兽们是很聪明的。他们会在出发前侦查到每一段小路上的小Xie数目,然后选择总时间最短的路径。小Bug现在很想知道最少需要多少只小Xie,才能使得神兽从戈壁来到花园的时间变长。作为花园中可爱的花朵,你能帮助她吗?

这™是网络流?

流与割的关系

我们发现其实上面那个题的本质就是: 找一些边满足从 \(s\)\(t\) 的任意路径都必须至少经过一条这些边, 同时让这些选中的边的权值最小.

或者换一个说法, 将这些选中的边删去之后, \(s\)\(t\) 不再连通, 点集 \(V\) 被分割为两部分 \(V_s\)\(V_t\) . 我们称点集 \((V_s,V_t)\) 为流网络的一个割, 定义它的容量为所有满足 \(u\in V_s, v\in V_t\) 的边 \((u,v)\) 的容量之和.

而对于一个割 \((V_s,V_t)\), 我们定义一个流的净流量为所有满足 \(u\in V_s, v\in V_t\) 的边 \((u,v)\) 上加载的流量减去所有满足 \(v\in V_s, u\in V_t\) 的边 \((u,v)\) 上加载的流量.

我们重新拿出最开始的那个流网络, 我们就可以做出这样的一个割 \((S,T)\):

graph LR; s-->|11/16|1 s==>|12/13|2 2==>|1/4|1 1==>|12/12|3 1==>|0/10|2 3-->|0/9|2 2-->|11/14|4 4-->|7/7|3 3-->|19/20|t 4-->|4/4|t subgraph S; s 1 end subgraph T; 2 3 4 t end

不难看出,这个割的容量为 \(35\), 最大流的净流量为 \(12+12-1=23\) .

注意到净流量可以因为反向流量而为负, 但是容量一定是非负的 (反向边和 \(s\rightarrow t\) 的连通性无关)

我们换一种方式来割:

graph LR; s-->|11/16|1 s-->|12/13|2 2-->|1/4|1 1==>|12/12|3 1-->|0/10|2 3==>|0/9|2 2-->|11/14|4 4==>|7/7|3 3-->|19/20|t 4==>|4/4|t subgraph S; s 1 2 4 end subgraph T; 3 t end

不难发现净流量依然与网络流的流量相等, 依然是 \(23\). 而这个割的容量则是 \(23\).

为啥净流量一直是一样的呢? 我们可以用下面这个不太严谨的证明感性理解一下

根据网络流的定义,只有源点 \(s\) 会产生流量,汇点 \(t\) 会接收流量。因此任意非 \(s\)\(t\) 的点 \(u\) ,其净流量一定为 \(0\),也即是\(\sum f(u,v)=0\)。而源点 \(s\) 的流量最终都会通过割 \((S,T)\) 的边到达汇点 \(t\),所以网络流的流 \(f\) 等于割的静流 \(f(S,T)\)

也就是说任意一个割的净流一定都等于当前网络的流量.

而因为割的容量将所有可能的出边都计入了, 所以任意一个割的净流一定都小于等于这个割的容量. 而在所有的割中, 一定存在一个容量最小的割, 它限制了最大流的上界. 于是我们得到一个结论: 对于任意一个流网络, 其最大流必定不大于其最小割.

然而这还不够, 我们相当于只能用最大流算出最小割的一个下界. 我们如何证明这个下界一定能取到呢?

最大流最小割定理

最小割最大流定理的内容:

对于一个网络流图 \(G=(V,E)\),其中有源点 \(s\) 和汇点 \(t\),那么下面三个条件是等价的:

  1. \(f\) 是图 \(G\) 的最大流
  2. 残量网络 \(G_f\) 不存在增广路
  3. 对于 \(G\) 的某一个割 \((S,T)\) ,此时流 \(f\) 的流量等于其容量

证明如下:

首先证明 \(1\Rightarrow 2\):

増广路算法那的基础, 正确性显然, 不证了(咕咕咕

然后证明 \(2\Rightarrow 3\):

假设残留网络 \(G_f\) 不存在增广路,所以在残留网络 \(G_f\) 中不存在路径从 \(s\) 到达 \(t\) 。我们定义 \(S\) 集合为:当前残留网络中 \(s\) 能够到达的点。同时定义 \(T=V-S\)
此时 \((S,T)\) 构成一个割 \((S,T)\) 。且对于任意的 \(u\in S,v\in T\),边 \((u,v)\) 必定满流。若边 \((u,v)\) 不满流,则残量网络中必定存在边 \((u,v)\),所以 \(s\) 可以到达 \(v\),与 \(v\) 属于 \(T\) 矛盾。
因此有 \(f(S,T)=\sum f(u,v)=\sum c(u,v)=C(S,T)\)

最后证明 \(3\Rightarrow 1\):

割的容量是流量的上界, 正确性显然.

于是, 图的最大流的流量等于最小割的容量.

最小割建模举例

花园的守护之神

就是开头那题

首先我们发现不在最短路上的边都没卵用, 于是我们把它们扔进垃圾桶.

剩下的边组成的图中求最小割.

有了最大流最小割定理, 直接跑一遍最大流就好辣~

王者之剑

这是在阿尔托利亚·潘德拉贡成为英灵前的事情,她正要去拔出石中剑成为亚瑟王,在这之前她要去收集一些宝石。

宝石排列在一个n*m的网格中,每个网格中有一块价值为v(i,j)的宝石,阿尔托利亚·潘德拉贡可以选择自己的起点。

开始时刻为0秒。以下操作,每秒按顺序执行

1.在第i秒开始的时候,阿尔托利亚·潘德拉贡在方格(x,y)上,她可以拿走(x,y)中的宝石。

2.在偶数秒,阿尔托利亚·潘德拉贡周围四格的宝石会消失

3.若阿尔托利亚·潘德拉贡第i秒开始时在方格(x,y)上,则在第i+1秒可以立即移动到(x+1,y),(x,y+1),(x-1,y)或(x,y-1)上,也可以停留在(x,y)上。

求阿尔托利亚·潘德拉贡最多可以获得多少价值的宝石

首先有一个非常重要的套路: 对于网格图网络流来说, 黑白染色是一个很重要的事情. 因为你需要选一部分点和 \(s\) 相连, 一部分点和 \(t\) 相连, 然后再在它们之间各种连边来表示贡献.

然后我们来看题...

这出题人语文真棒

因为你可以任意停留或者行动而且行动时间不受限制, 其实意思就是选了一个点之后周围的点都不能选了...

这个时候我们引入最小割建图的一个重要元素: 无穷边.

无穷边显然是不可能出现在最小割里的, 于是一条 \((u,v)\) 容量为 \(\infty\) 的边的意思就是: \((u,v)\) 必须连通, 不可割断.

同时由于我们要求这个图上的一个割, 所以上面这句话等价于: 强制 \(s\verb|-| u\) 之间在最小割中被割断或者 \(v\verb|-|t\) 在最小割中被割断.

我们再来看这个题. 我们可以把问题从"我们最多拿多少"转化为"我们最少要扔掉多少". 由于相邻的点不能同时拿, 所以我们在它们之间连接一条 \(\infty\) 边代表扔掉任意一个. 所以总的建图就是:

\(s\) 向所有白点连边, 从所有黑点向 \(t\) 连边, 容量均为点权. 相邻点之间从白点向黑点连接 \(\infty\) . 容易证明这张图的最小割值即为最少需要放弃的点的价值.

总和减去这个最小放弃值即为答案.

实际上上面的最小割求的就是二分图最小权值覆盖, 减出来的答案就是二分图的最大权独立集.

Number

\(n\) 个正整数,需要从中选出一些数,使这些数的和最大。
若两个数 \(a,b\)同时满足以下条件,则 \(a,b\) 不能同时被选

  1. 存在正整数\(c\),使 \(a^2+b^2=c^2\)
  2. \(\gcd(a,b)=1\)

首先 "满足一定条件则不能同时被选" 是一个典型的最小割问题, 计算舍弃掉的最小值即可.

但是网络流建图必须要在合适的地方放 \(s\)\(t\) (于是全局最小割就成了个高端问题), 咋办?

首先我们发现偶数和偶数之间不可能有限制条件, 因为它们的 \(\gcd\)\(2\), 不满足第二个条件.

接着我们发现奇数和奇数之间不可能满足第一个条件. 为啥?

奇数的平方和在模 \(4\) 意义下一定是 \(2\), 而完全平方数在模 \(4\) 意义下必须是 \(0/1\). 枚举 \([0,4)\) 之间的数字就证完了.

所以它是个二分图! 我们就可以愉快地从 \(s\) 连边到奇数, 从偶数连边到 \(t\) , 中间同时满足条件的再连几条 \(\infty\) 边就好了...么?

emmmm...

题面漏了一句话:

\(n \leq 3000\)

然而这并不是单位容量简单网络所以不适用 \(O(\sqrt{V}E)\) 的复杂度证明...

GG了?

我们必须要知道, Dinic是一种敢写就会有奇迹的算法!

它A了.

最小割

A,B两个国家正在交战,其中A国的物资运输网中有N个中转站,M条单向道路。设其中第i (1≤i≤M)条道路连接了vi,ui两个中转站,那么中转站vi可以通过该道路到达ui中转站,如果切断这条道路,需要代价ci。现在B国想找出一个路径切断方案,使中转站s不能到达中转站t,并且切断路径的代价之和最小。 小可可一眼就看出,这是一个求最小割的问题。但爱思考的小可可并不局限于此。现在他对每条单向道路提出两个问题: 问题一:是否存在一个最小代价路径切断方案,其中该道路被切断? 问题二:是否对任何一个最小代价路径切断方案,都有该道路被切断? 现在请你回答这两个问题。

首先我们为了求出这个最小割肯定要先跑最大流, 跑完最大流之后的残量网络就是这题的突破口.

我们分析一下这个残量网络有什么性质:

  1. \(s\)\(t\) 一定不在同一SCC里
  2. 对于某条满流边 \((u,v)\) , 若 \(u\)\(s\) 在同一SCC, \(v\)\(t\) 在同一SCC, 则它必定会出现在最小割中.
  3. 对于某条满流边 \((u,v)\) , 若 \(u\)\(v\) 不在同一SCC, 则它可能出现在最小割中.

结论1非常显然, 最大流的话 \(s\)\(t\) 直接不连通更不要说在同一SCC里了.

结论2的话, 如果将一条满足该限制的边容量增大, 那么 \(s\rightarrow t\) 重新连通, 于是就会增加最大流的流量, 也相当于增加了最小割的容量. 所以这条边必定会出现在最小割中.

结论3的话, 我们把SCC缩到一个点里, 得到的新图就只有满足条件的满流边了. 缩点后的任意一个割显然就对应着原图的一个割, 所以这些满流边都可以出现在最小割中.

这三条结论有时候在最小割建图输出方案的时候会用到.

最大权闭合子图

给定一个有向图, 顶点带权值. 你可以选中一些顶点, 要求当选中一个点 \(u\) 的时候, 若存在边 \(u\rightarrow v\)\(v\) 也必须选中. 最大化选中的点的总权值.

选中一个点后可以推导出另一个点也必须选中, 同时最优化一个值, 我们考虑用最小割的 \(\infty\) 边来体现这一点.

我们像刚刚王者之剑那道题一样将价值转化为代价. 这样当不选一个正权点时相当于付出 \(val_i\) 的代价, 选中一个负权点时相当于付出 \(-val_i\) 的代价.

我们设割断后和 \(s\) 相连的点是被选中的, 和 \(t\) 相连的点是被扔掉的, 那么当一个正权点和 \(s\) 割断时要付出 \(val_i\) 的代价, 我们从 \(s\) 连一条容量为 \(val_i\) 的边到这个点. 类似的, 当一个负权点和 \(t\) 割断时(等价于和 \(s\) 相连, 也就是被选中了)要付出 \(-val_i\) 的代价, 从这个点连一条容量为 \(-val_i\) 的边到 \(t\) 即可.

原图中的边不能割断, 所以我们连 \(\infty\) 边.

最后用所有正权点的权值和减去最小割就是答案了.

切糕

经过千辛万苦小 A 得到了一块切糕,切糕的形状是长方体,小 A 打算拦腰将切糕切成两半分给小 B 。出于美观考虑,小 A 希望切面能尽量光滑且和谐。于是她找到你,希望你能帮她找出最好的切割方案。
出于简便考虑,我们将切糕视作一个长 \(P\) 、宽 \(Q\) 、高 \(R\) 的长方体点阵。我们将位于第 \(z\) 层中第 \(x\) 行、第 \(y\) 列上 \((1 \le x \le P, 1 \le y \le Q, 1 \le z \le R)\) 的点称为 \((x,y,z)\),它有一个非负的不和谐值 \(v(x,y,z)\) 。一个合法的切面满足以下两个条件:

  • 与每个纵轴(一共有 \(P\times Q\) 个纵轴)有且仅有一个交点。即切面是一个函数 \(f(x,y)\),对于所有 \(1 \le x \le P, 1 \le y \le Q\) ,我们需指定一个切割点 \(f(x,y)\) ,且 \(1 \le f(x,y) \le R\)
  • 切面需要满足一定的光滑性要求,即相邻纵轴上的切割点不能相距太远。对于所有的 \(1 \le x,x’ \le P\)\(1 \le y,y’ \le Q\) ,若 \(|x-x’|+|y-y’|=1\) ,则 \(|f(x,y)-f(x’,y’)| \le D\)∣ ,其中 \(D\) 是给定的一个非负整数。
    可能有许多切面 \(f\) 满足上面的条件,小 A 希望找出总的切割点上的不和谐值最小的那个,即 \(\sum\limits_{x,y}{v(x, y, f (x, y))}\) 最小。

给定一个立方体, 然后你要对于每一个 \((x,y)\) 选择一个切割高度 \(f(x,y)\) , 而选中某个特定切点之后会产生一定的花费, 同时相邻两个切点的高度差不能超过 \(D\) , 求花费最小的切割方案.

题都告诉你要求一个花费最小的切割方案了当然要选择最小割啦

首先我们不考虑高度差限制, 这样的话我们从每个位置开始挂一条长链, 链上边的权值大小表示割断对应位置的花费.

然后愉快地贪心最小割就好了啊~

然而现在考虑高度差限制.

这时候我们再次使用 \(\infty\) 边来解决这个限制.

假设我们建出了这样的两条链:

graph LR; s-->1 1-->2 2-->3 3-->4 4-->t s-->5 5-->6 6-->7 7-->8 8-->t

然后假设高度差限制是 \(1\) , 则我们割这样的两条边是不合法的:

graph LR; s-->1 1-->2 2-->3 3==>4 4-->t s-->5 5==>6 6-->7 7-->8 8-->t

回忆 \(\infty\) 边的作用: 强制令 \(s\verb|-|u\)\(v\verb|-|t\) 中任选一个割断. 那么我们可以这样建一条 \(\infty\) 边:

graph LR; s-->1 1-->2 2-->3 3==>4 4-->t s-->5 5==>6 6-->7 7-->8 8-->t 3-.->6

那么我们发现, 这种情况变得不合法了: \(s\)\(t\) 依然连通.

我们发现, 只要上面那条链上割断的是 \((3,4)\), 那么 \((5,6)\) 必定不会出现在最小割中. 因为下面那条链里显然只会割掉 \(6\verb|-|t\) 上的某一条边, 割两条边肯定没有割一条边更优.

或者说, \(s\verb|-|u\) 割断的话, 对第二条链在这个高度上不起限制, 第二条链依然可以割断任意一条边.

如果 \(s\verb|-|u\) 未割断的话, 一定是 \(u\verb|-|t\) 割断了, 那么这条边就会强制 \(v\verb|-|t\) 割断, 那么 \(s\verb|-|v\) 割断一定不优于是不会出现在最小割中.

上限同理, 但是实际上如果所有方向都考虑的话就是换个方向的下限限制, 建边都一样就可以了.

最小费用最大流

有时候我们会发现这样的一类问题:

公司有 \(m\) 个仓库和 \(n\) 个零售商店。第 \(i\) 个仓库有 \(a_i\) 个单位的货物;第 \(j\) 个零售商店需要 \(b_j\) 个单位的货物。货物供需平衡,即 \(\sum\limits_{i = 1} ^ m a_i = \sum\limits_{j = 1} ^ n b_j\) 。从第 \(i\) 个仓库运送每单位货物到第 \(j\) 个零售商店的费用为 \(c_{ij}\) 。试设计一个将仓库中所有货物运送到零售商店的运输方案,使总运输费用最少。

我们看到"货物供需平衡"这个关键字其实就已经可以料想到这是个流问题了. 但是不同的是它给每单位流量都增加了一个费用, 怎么办呢?

EK费用流

首先我们需要意识到一个事情: 最小费用最大流, 是在最大流的基础上取最小费用, 所以我们是必须要跑到最大流的(这一点也可以在构图中用来表示限制). 于是我们假装没有费用先算増广路.

对于一条増广路 \(p\) 来说, 我们设増广了 \(f\) 单位的流量, 则总花费为 \(\sum\limits_{i\in p} d_if\), 实际上就等于 \(f\sum\limits_{i\in p}d_i\), 也就等于把费用看做距离的路径长度乘以流量大小.

所以我们继续沿用EK最大流的思路, 每次増广以费用为边权的最短路径即可.

反向边权

我们尝试扩展最大流时的反向边建法: 建立一个残量为 \(0\) 的边. 费用呢? 最大流里面所有的边都相当于是单位边权的费用, 所以我们可以假装反向边的费用和正向边一样~




裆燃是假的辣!

费用流里求的流量在最终累加答案的时候都乘了一个费用系数, 于是这次我们推流的时候不仅要把流推走, 还要把贡献的费用减少. 所以反向边的权值其实是正向边的相反数.

于是费用流中, 负权边不可避免. 所以我们使用SPFA来求最短路.

关于SPFA

它死了(划掉

Q: 为啥要用SPFA呢? 这玩意复杂度不是玄学么?

A: 现在有负权你不得不用了...SPFA复杂度虽然是玄学但是它还是有一个 \(O(VE)\) 的科学上界的, 所以在负权图上跑SPFA也不失为一个很好的选择(当然也有更优秀但是有一些限制的Dijkstra费用流, 不过一般用不着...)

Q: 你这个费用流里既然会冒出负权来, 那要是推反向边的时候在残量网络里増广出负环了怎么破?

A: EK费用流的过程每次只増广最短路, 所以任意时刻増广出来的流一定都是最小费用流. 但是如果残量网络中存在负环, 那么我们显然可以让一部分流量改道流经这个负环来让费用减少, 这样就矛盾了. 所以一定不会増广出负环.

时间复杂度分析

因为要把一次BFS寻找増广路换成SPFA+DFS, 于是复杂度上界从 \(O(E)\) 升高到 \(O(VE)\) , 其余的EK最大流复杂度分析理论上依然使用于此, 所以总时间复杂度上界为 \(O(V^2E^2)\).

消圈算法

这锅本来扔给chr了...然而他觉得这很毒瘤就又丢回来了...

这个算法基于下面这个定理:

消圈定理

流量为 \(f\) 的流是最小费用流当且仅当对应的残量网络中不存在负费用增广圈。

感性证明:

如果在一个流网络中求出了一个最大流,但对于一条增广路上的某两个点之间有负权路,那么这个流一定不是最小费用最大流,因为我们可以让一部分流从这条最小费用路流过以减少费用,所以根据这个思想,可以先求出一个最大初始流,然后不断地通过负圈分流以减少费用,直到流网络中不存在负圈为止。关键在于负圈内所有边流量同时增加是不会改变总流量的,却会降低总费用。

于是我们就可以直接先跑一遍最大流, 然后在残量网络里用 Bellman-Ford 找负环然后沿着负环増广一发就可以了. 当然如果用SPFA的话大概会跑得比 \(\varTheta(VE)\) 的 Bellman-Ford 要快点吧.

网上说按照一定顺序消圈的话复杂度是 \(O(VE^2\log V)\) 的...但是本来这不是我的锅所以并没有仔细搞

具体实现找chr锔锅.

ZKW费用流

其实ZKW费用流和EK费用流的关系就跟Dinic和EK最大流的关系差不多...

就是加了个对所有符合 \((u,v)\) 在最短路上的边进行多路増广...

但是由于存在负权, 所以増广时可行的边组成的图并不像Dinic那样是分层图.

Dinic的BFS 部分直接改成SPFA求最短路就好了, 这个没啥好说的.

DFS部分要注意一点, 因为可行边组成的图(以下简称"可行图")并不一定是DAG, 于是我们可能可以从一个费用为负的边跑回原来的地方, 于是就会在一个 \(0\) 环上转来转去死递归.

解决方案是加一个 vis 数组, 只要DFS到了这个点就打个标记, 同时在DFS的时候判断一下出边是否会跑到一个已经打了标记的点, 如果没有打上标记再DFS.

为了保证多路増广的优越性, 这个 vis 需要在回溯时撤销.

代码实现

以下是 LOJ #102 最小费用流的AC代码

#include <bits/stdc++.h>

const int MAXV=5e2+10;
const int MAXE=1e5+10;
const int INFI=0x7F7F7F7F;

struct Edge{
	int from;
	int to;
	int dis;
	int flow;
	Edge* rev;
	Edge* next;
};
Edge E[MAXE];
Edge* head[MAXV];
Edge* top=E;

int v;
int e;
int p,m,f,n,s;
int dis[MAXV];
bool vis[MAXV];

bool SPFA(int,int);
int DFS(int,int,int);
void Insert(int,int,int,int);
std::pair<int,int> Dinic(int,int);

int main(){
	scanf("%d%d",&v,&e);
	for(int i=0;i<e;i++){
		int a,b,c,d;
		scanf("%d%d%d%d",&a,&b,&c,&d);
		Insert(a,b,d,c);
	}
	std::pair<int,int> ans=Dinic(1,v);
	printf("%d %d\n",ans.first,ans.second);
	return 0;
}

std::pair<int,int> Dinic(int s,int t){
	std::pair<int,int> ans;
	while(SPFA(s,t)){
		int flow=DFS(s,INFI,t);
		ans.first+=flow;
		ans.second+=flow*dis[t];
	}
	return ans;
}

int DFS(int s,int flow,int t){
	if(s==t||flow<=0)
		return flow;
	int rest=flow;
	vis[s]=true;
	for(Edge* i=head[s];i!=NULL;i=i->next){
		if(i->flow>0&&dis[s]+i->dis==dis[i->to]&&(!vis[i->to])){
			int k=DFS(i->to,std::min(rest,i->flow),t);
			rest-=k;
			i->flow-=k;
			i->rev->flow+=k;
			if(rest<=0)
				break;
		}
	}
	vis[s]=false; // 这里不太对头
	return flow-rest;
}

bool SPFA(int s,int t){
	memset(dis,0x7F,sizeof(dis));
	std::queue<int> q;
	vis[s]=true;
	dis[s]=0;
	q.push(s);
	while(!q.empty()){
		s=q.front();
		for(Edge* i=head[s];i!=NULL;i=i->next){
			if(i->flow>0&&dis[s]+i->dis<dis[i->to]){
				dis[i->to]=dis[s]+i->dis;
				if(!vis[i->to]){
					vis[i->to]=true;
					q.push(i->to);
				}
			}
		}
		q.pop();
		vis[s]=false;
	}
	return dis[t]<INFI;
}

inline void Insert(int from,int to,int dis,int flow){
	// printf("Insert %d -> %d : dis=%d flow=%d\n",from,to,dis,flow);
	top->from=from;
	top->to=to;
	top->dis=dis;
	top->flow=flow;
	top->rev=top+1;
	top->next=head[from];
	head[from]=top++;

	top->from=to;
	top->to=from;
	top->dis=-dis;
	top->flow=0;
	top->rev=top-1;
	top->next=head[to];
	head[to]=top++;
}

关于当前弧优化

大家发现我的代码里并没有加当前弧优化. 为什么?

刚刚说到了, ZKW费用流有一个sb特点就是可行图不是分层图. 那么就有可能有这种操作(下面只画可行图, 没有带权):

graph LR s-->2 2-->3 3-->t 3-->4 4-->2 s-->5 5-->4

然后我们DFS増广, 可能会遇到这样的东西:

graph LR s==>2 2==>3 3-->t 3==>4 4==>2 s-->5 5-->4

我们发现 \((4,2)\) 这条边到达了一个已经被标记过的点, 于是它不会产生流量贡献. 但是如果我们加了当前弧优化, 我们就会在以后访问到 \(4\) 结点时跳过 \((4,2)\) 这条出弧, 于是我们就否定掉了下面这种情况:

graph LR s-->2 2==>3 3==>t 3-->4 4==>2 s==>5 5==>4

于是我们DFS的过程就会提前认为当前已经阻塞, 返回进行新一轮SPFA. 于是我们便进行了多余的SPFA操作, 这与多路増广"为了减少BFS次数"的出发点相悖, 同时也不能保证一个较低的时间复杂度.

实测结果的话, 使用LOJ #102的最后三个测试点, 发现不加当前弧优化时SPFA执行次数为 \(400\) 左右, 而加入当前弧优化之后执行次数上升到了 \(2700\) 次, 实际运行时间慢了一倍多.

时间复杂度分析

证了证发现因为不能当前弧优化所以并不能保证比EK更优的时间复杂度...但是可以假装它上界在非源非汇点度数比较小的时候可以有一个接近 \(O(VE^2)\) 的依然很松的上界. 实测在LOJ板子上比EK费用流要快, 网络流构图可能会更快一些.

还有就是关于 visit 标记的清空问题. 上面的板子里在DFS结束之后清空了 visit 标记, 这在一些情况下会让程序变快, 但是有些情况下可能会被卡成指数(捂脸)...建议大家DFS后不清空上面的 visit 标记而是在每次DFS前清空.

Dijkstra费用流

为啥要讲一下这玩意呢?

因为这玩意可以提高你的费用流暴力在费用流转贪心的题中得到的期望分数

整个的过程和EK/ZKW其实是一样的, 不过这次把最短路换成Dijkstra了.

为啥能用Dijkstra呢? 它其实基于一个事实: 你已经求过原来的一个最短路了, 而最短路是满足三角形不等式的. 其次加入的反向边的权值都刚好是正向边的相反数.

我们为每个点\(i\)赋点权\(h[i]\)\(dis[i]\),并视每条连接\(u,v\)的边\(i\)的边权\(w'[i]\)\(w[i]+h[u]-h[v]\),由于对于任意两点\(u,v\),有\(h[u]-h[v]>=w[u][v]\),所以\(w'[i]>=0\),这样一来新图上的\(dis'[i]\)就等于\(dis[i]+h[S]-h[i]\)(对于路径上除起点和终点以外的点\(i\),其入边的\(-h[i]\)与出边的\(+h[i]\)抵消),由于每次跑最短路时\(h[i]\)都是不变的,所以求出了\(dis'[i]\)也就求出了\(dis[i]\)(\(dis[i]=dis'[i]-h[S]+h[i]\),其实很显然\(h[S]=0\))

但是跑完之后需要加入反向边,原来的\(h[i]\)可能会不适用,所以我们需要更新\(h[i]\) 对于最短路上每一条连接\((u,v)\)的边,显然有

\[dis'[u]+w'[u][v]=dis'[v] \]

从而

\[dis'[u]+h[u]-h[v]+w[u][v]=dis'[v] \]

\[(dis'[u]+h[u])-(dis'[v]+h[v])+w[u][v]=0 \]

\[∵w[u][v]=-w[v][u] \]

\[∴(dis'[v]+h[v])-(dis'[u]+h[u])+w[v][u]=0 \]

所以我们只要对于每个点\(i\)\(h[i]\)加上\(dis'[i]\)即可.

所以整个的实现就是: 先SPFA跑最短路算出 \(h\) , 然后在Dijkstra中把参考的边权加上一个\(h[u]\)再减去一个\(h[v]\), 最后把计算出的 \(dis\) 累加上 \(h\) 就行了.

最小费用最大流建模举例

当你会了费用流之后你能做的题就会瞬间多一个数量级了...

南极科考旅行

小美要在南极进行科考,现在她要规划一下科考的路线。

地图上有 N 个地点,小美要从这 N 个地点中 x 坐标最小的地点 A,走到 x 坐标最大的地点 B,然后再走回地点 A。

请设计路线,使得小美可以考察所有的地点,并且在从 A 到 B 的路程中,考察的地点的 x 坐标总是上升的,在从 B 到 A 的过程中,考察的地点的 x 坐标总是下降的。

求小美所需要走的最短距离(欧几里得距离)。

3 <= N <= 300

1 <= x <= 10000, 1 <= y <= 10000

乍一看好像是DP?

实际上网络流是可以做的

首先肯定是要按横坐标排序, 然后我们发现一去一回的方向并没有什么卵用, 找到一个环和找到两条路径是等价的. 这样我们可以发现, 每个结点都必须选择两条边, 其中 \(x\) 最小的结点两条都是出边, 最大的结点两条都是入边, 其他结点一条入边一条出边. 我们可以尝试分配这些入度和出度让总费用最小. 这样的话我们可以得到这样的构图:

graph LR s==>1-1 s-->2-1 s-->3-1 1-1-->2-2 1-1-->3-2 2-1-->3-2 1-1-->4-2 2-1-->4-2 3-1-->4-2 2-2-->t 3-2-->t 4-2==>t

其中较粗的边容量为 \(2\), 较细的边容量为\(1\), \(s\) 连出的边和连向 \(t\) 的边费用为 \(0\) , 实际结点间边的距离即为欧几里得距离.

餐巾

一个餐厅在相继的 \(n\) 天里,每天需用的餐巾数不尽相同。假设第 \(i\) 天需要 \(r_i\) 块餐巾。餐厅可以购买新的餐巾,每块餐巾的费用为 \(P\) 分;或者把旧餐巾送到快洗部,洗一块需 \(M\) 天,其费用为 \(F\) 分;或者送到慢洗部,洗一块需 \(N\) 天,其费用为 \(S\) 分(\(S < F\))。

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

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

这道题使用了另一个费用流套路: 强行补充流量.

首先按照以往的套路, 我们会选择用一单位流量来表示一块餐巾的整个生命周期: 从被购买到被使用再到被清洗最后被丢弃.

于是我们从 \(s\) 连接容量为 \(\infty\) 费用为购买餐巾单价的边到每天的决策点, 然后再乱搞?

然而这题直接这么建模会GG.

注意最小费用最大流是在最大流基础上最小费. 如果餐巾量和流量相等的话那可就变成买的餐巾越多越好了.

接着我们发现这题的最大难点在于: 洗了之后的餐巾可以再用.

于是我们不再用一单位流量流经的边来表示花费: 我们用每一单位流量表示一个餐巾被用了一次. 而这个一单位流量的来源则用来表达这一份餐巾的花费, 不管是买的还是洗的还是从地上捡的. 于是我们就可以用最大流这个限制来强制每天的餐巾必须够用.

其次, 因为每天都会产生一定量的脏餐巾, 我们直接从源点 \(0\) 费用把这些流量送达指定时间段日期之后的决策点来决定是否要洗. 因为网络流有流守恒的限制, 我们可以像 "抽水" 一样来控制以前的决策.

但是直接连边给以后的决策点还是会GG. 因为我们发现因为转运的过程不能增加费用, 于是我们直接 \(0\) 费用把 \(s\) 连到了 \(t\).

如果我们直接从 \(s\) 连一些费用为快/慢洗的边的话又无法控制它们的总流量了(你总不能洗出来的比当天用的还多吧)

这个时候我们按照套路选择拆点. 我们拆一些新的点负责把洗出来的脏餐巾流量转运到后面, 从 \(s\) 连接一条容量为当天餐巾用量的边到这个 tRNA 点, 然后从这个 tRNA 点出发连接两条费用为洗餐巾花费的边到当天快洗/慢洗洗完那天的决策点.

不过因为洗完的餐巾以后还能用, 为了体现这一点, 每天的决策点还应该向第二天连一条 \(0\) 费用 \(\infty\) 容量的边.

于是总的建图就是长这样的:

graph LR; subgraph Transport T1 T2 T3 end subgraph Decision D1 D2 D3 end s-->|P/INF|D1 s-->|P/INF|D2 s-->|P/INF|D3 s-->|0/r|T1 s-->|0/r|T2 s-->|0/r|T3 D1-->|0/r|t D2-->|0/r|t D3-->|0/r|t D1-->|0/INF|D2 D2-->|0/INF|D3 T1-->|f/INF|D2 T1-->|s/INF|D3 T2-->|f/INF|D3

负载平衡

公司有 \(n\) 个沿铁路运输线环形排列的仓库,每个仓库存储的货物数量不等。如何用最少搬运量可以使 \(n\) 个仓库的库存数量相同。搬运货物时,只能在相邻的仓库之间搬运。

注1: 搬运量即为货物量与搬运距离之积.

注2: (大概) 保证货物数量的总和是 \(n\) 的倍数.

这道题建图其实比较直观.

首先求出目标货物数量(也就是平均数), 然后我们发现, 对于 \(x_i>\bar{x}\), 它一定需要向外运输, 我们从 \(s\) 引一条流量为 \(x_i - \bar x\) 的边到这个点. 而对于 \(x_i < \bar x\), 它一定是要接受货物的. 于是我们引一条流量为 \(\bar x - x_i\) 的边到 \(t\) , 最后把相邻的仓库之间都连一条费用为 \(1\) 容量为 \(\infty\) 的边跑费用流就可以了.

最长k可重区间集

给定实直线 \(L\)\(n\) 个开区间组成的集合 \(I\),和一个正整数 \(k\),试设计一个算法,从开区间集合 \(I\) 中选取出开区间集合 \(S\in I\),使得在实直线 \(L\) 的任何一点 \(x\)\(S\) 中包含点 \(x\) 的开区间个数不超过 \(k\) 。且 \(∑\limits_{z\in S}|z|\) 达到最大。这样的集合 \(S\) 称为开区间集合 \(I\) 的最长 \(k\) 可重区间集。 \(∑\limits_{z∈S}|z|\) 称为最长 \(k\) 可重区间集的长度。 对于给定的开区间集合 \(I\) 和正整数 \(k\),计算开区间集合 \(I\) 的最长 \(k\) 可重区间集的长度。

首先因为是实数所以得离散化...这可真蠢...

然后由于这是开区间, 我们不需要考虑端点覆盖的次数, 于是我们可以对于每个区间, 从左端点到右端点连一条容量为 \(1\) , 费用为区间价值 (也就是长度) 的边, 然后从源点到第一个点/从第 \(i\) 个点到第 \(i+1\) 个点/从最后一个点到汇点都连一条容量为 \(k\) 费用为 \(0\) 的边, 跑最大费用最大流即可.

撕烤这样为啥是对的?

每个点最多覆盖 \(k\) 次, 而一次覆盖相当于一次分流, 最多分流 \(k\) 次(因为你并不能搞出负流来).

这个用分流次数来表示限制的思想有时候也会用到.

平方费用最小费用最大流

给定一个流网络, 每条边 \((u,v)\) 有一个容量 \(c\) 和一个费用系数 \(w\) . 当边 \((u,v)\) 上加载了 \(f\) 单位流量的时候, 产生的花费为 \(f^2w\) . 求最小费用最大流.

\(c\leq 100\)

题意非常明确, 但是这费用并不是线性的...

这题用到了费用流建图的一个技巧: 拆费用. 我们把一条 \((u,v)\) 的容量为 \(c\) 的平方费用边拆成 \(c\) 条容量为 \(1\) 的线性费用边. 就像这样:

graph LR; u-->|w|v u-->|3w|v u-->|5w|v u-->|7w|v u-->|9w|v

\(i\) 条边的费用正好是平方费用边加载上第 \(i\) 单位流量的时候产生的费用贡献.

因为 \(c\) 并不大, 所以直接按照上面方法暴力建边就可以了.

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!