有些事真的是一通百通。
这些想法其实最早来自于关于神经编译的项目:如何让那么多的神经元彼此交互又并行不悖,而且能产生确定性的效果(不同于大家常说的AI,是另一种基于符号主义和连接主义的综合的一种另类的AI),那种产生非确定性结果的系统,你终究是不能拿来作编译器来用的。
一个类似的框架,你可以考虑一下早年单CPU时代的多进程时间片的分配问题,以及近来流行的co-routine(协程)是怎么处理并行性的,还有就是SIMD的想法。这些东西综合起来,大体上就可以看到时序化方法的本质了。
言归正传,回到最大流问题,它也是一个NP问题,好像还是NP-Hard,超级难的问题!
考虑如下假定的问题:
我们要发一批货,从美国的东海岸出发,送到西海岸。肯定要经过美国境内的复杂的交通网络。这个交通网络由承载能力不同的道路构成,有些道路宽,同时可以过6-8辆车,有些只能同时过2辆车。整个网络中的每条道路的承载能力都是清楚的。我们想要问的是,同时发出多少量车,才能同时出发同时到达。当然把车拍成先后的方式在现实中完全可行,只是我们目前考虑的,就是所有的车都平行前进。具体来说,如果我们同时发车的数量大于某个上限,恐怕有些车就没法过去了。这意味着整个交通网络是有一个宏观概念上的瓶颈(Bottle Neck)的,实际上我们要把这个抽象的瓶颈到底是多大求出来。
再换一个问题:
有水流从城市一端的水源出发,经过城市中的供水管道构成的网络,到达城市的另一端。而构成供水网络的管道有些很老旧,有些则很新,老管道的设计比较保守,截面上单位时间可以流过的水量(称为流量)比较少,而新管道的设计就好一些,流量较大,每个管道的流量数值都是知道的。供水管道的网络拓扑也是知道的。现在我们想要知道的是,水源给多大的流量才能不被“憋住”,正好以相同的流量到达城市的另一端。显然,如果被憋住了,水源也无法发出那么大的流量。而这是由网络中所有的管道共同决定的。
这两个问题本质上是一样的,就是所谓的最大流问题。
经典解法我就不说了,请查百度百科。我们只说时序化方法在这个问题上的原理和实现。
上图形象的给出了一个简单的网络,0是源,5是宿。节点都是管道的分叉或者道路的路口。边上的数字给出了边可以承载的流量。现在要问的是,如果从0输出的流量是19(它的所有出边的容量总和),那么输入5的流量是多少(如果是所有入边的容量总和就好了,但显然不是)?当然如果流入5的流量小于19,那该多少就是多少,而大于19显然是不可能的。
有了先前图着色问题的理解,这个最大流问题的解法,也就更容易理解了。
水流本身就是一种时序,它在什么时候到达什么地方,若图上没有环的话(如果有环,也不可能有环流),则是严格的从源到宿,从左到右,而这和时间在时间轴上的流逝是一样的,反过来说也行。水从较高的地势0开始,流向低地势5,会怎么走呢?从0显然会流到1和2,但是2又会流到1,说明2的地势要比1更高,这其实就是Rank。
我们用Rank效果来再把这个图画一遍就是这样的。
假定有一条竖线,从0开始扫描,首先到2,然后到1和4构成的层面,然后到3,然后到5。我们可以认为这条竖线,就是水流的当前位置。
但是0和1之间,没有节点,4和5之间也没有,水流是“飞过去的”。但我们有办法,因为对于任何一个节点来说,总流入量和总流出量总是相等。我们就可以设计一些单入单出的节点,放在那些让水流”飞过去“的路径上,这样水流就不能”飞过去”而是必须经历那个层面了。至于这些节点,根据流量守恒,它的入容量和出容量显然也是一样的,就是它所截断的边的流量。
如上图所示,加入假的节点f1和f2,就把这些纵向的层面填充好了。
所谓最大流问题,其实就是求网络的瓶颈。既然网络单源单宿,整个网络其实就可以当作是源和宿之间的一条边,只是这条边的粗细总是变化。而这条边最细的地方,就是这条边的最大容量之处。所以这些纵向的层面就像是把这条边切断的那些截面,找到那个容量最小的截面,就找到了这条广义上的边的最大流量。
这个道理很明显,不必多说。但是有一些细小的问题,会造成严重的影响。比如说节点1,它的入容量为11+1=12,而它的出容量却只有8,显然要由8来决定经过节点1的流量,而不是12。这-4的增量(负增量)显然影响着当前的层面。可是若我们把当前层面当作一个管子(对偶图),那么它前一个层面就是它的源节点,后一个层面就是它的宿节点。所以真正产生的增量应当放在前一级上,也就是说2和f1构成的层面上的流量要整个都减少(入容量和出容量都减少),这样才对。可是,这种减少是否影响更前面的层面呢?我们看节点3,它的出容量是19,入容量是17+8=25,这显然也会对前一个层面造成一样的问题。可是我们终究要求的是最大流,也就是截面上的最小容量,而且已经影响了相关的截面容量,所以再向前影响是不需要的,也是不正确的。(后记:这里有一个错误,向前影响是需要考虑的,但应当选取后续所有影响中最大的影响,将其减去。这个影响对于每一个层面相对来说都不同,所以需要记录,并在后来的时刻还原)。
经过宏观层面上的流量求和和微观层面上的逆向调整,我们就可以求出一系列的截面的流量,然后选择最小的那一个,就是这条宏观管道的瓶颈,也就是可以求出最大流的数值了。
这样做避免了相当多的问题,比如用浮点数而不是整数来标注边的容量。整数可以分开算,浮点数怎么分?没有办法很好的分配给分叉之后的边。我们终究计算的是数值,无需分配到具体的边,由此才使得浮点数也可以成为流量的度量。
算法很简单,首先Rank,然后增加虚节点,然后对层面求和,然后在局部调整特定节点的出入流量并修正前级截面的流量。最后找到截面流量的最小值。
因为每一步的时间复杂度都是O(n),而且只有5个步骤,也就是O(5n),任何常数都被视作为1,所以时间复杂度就是O(n)。至于空间复杂度,有可能增加的节点数目并不知道,但也不会太多,终究需要具体算一算。
算法就是这样,至于有没有其它情况,暂时不知道。
代码贴在这,
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace GraphAlgorithmTester;
public class MaxFlowsProblemResolver : ProblemSolver
{
public override void Solve(TextWriter writer, string start_name = null, string end_name = null)
{
writer.WriteLine("MaxFlowsProblem:");
if (Nodes.Count < 2 || Edges.Count == 0)
{
writer.WriteLine(" Nodes count should >=2 and Edges count should >=1 too.");
}
else
{
writer.WriteLine(" Total {0} nodes: {1}", Nodes.Count, string.Join(',',this.Nodes.Values));
writer.WriteLine(" Total {0} edges: {1}", Edges.Count, string.Join(',',this.Edges));
var names = Nodes.Keys.ToHashSet();
//first is in
if (!Nodes.TryGetValue((start_name ??= Nodes.First().Key), out var start))
start = Nodes.First().Value;
//last is out
if (!Nodes.TryGetValue((end_name ??= Nodes.Last().Key), out var end))
end = Nodes.Last().Value;
//first capacity is calculated backwards
start.OutCapacity = start.InCapacity = Edges.Where(e => e.O == start).Sum(e => e.Capacity);
end.OutCapacity = end.InCapacity = Edges.Where(e => e.T == end).Sum(e => e.Capacity);
var all = new HashSet<SNode>();
var nodes = new HashSet<SNode>() { start};
var nexts = new HashSet<SNode>();
int level_index = 1;
do
{
foreach (var node in nodes.ToArray())
{
foreach (var edge in this.Edges.Where(e => e.O == node))
{
var t = edge.T;
if (t != end &&t!=start)
{
t.InCapacity
= this.Edges.Where(e => e.T == t).Sum(e => e.Capacity);
t.OutCapacity
= this.Edges.Where(e => e.O == t).Sum(e => e.Capacity);
}
t.LevelIndex = level_index;
nexts.Add(t);
}
}
level_index++;
nodes = nexts;
nexts = new();
//if level is too many, break the loop in case
//we find loops in graph
} while (nodes.Count > 0 && level_index <= names.Count);
if (level_index > names.Count + 1)
{
//there is a loop!
//we should break this edge and remove this node.
start.LevelIndex = 0;
end.LevelIndex = names.Count - 1;
var removeds = this.Nodes.Values.Where(n => n.LevelIndex >= names.Count).ToHashSet();
writer.WriteLine($" Found a loop!");
writer.WriteLine($" Removed nodes:{string.Join(',',removeds)}");
}
//Build levels
var levels = new List<HashSet<SNode>>();
for(int i = 0; i < level_index - 1; i++)
{
var level = this.Nodes.Values.Where(n => n.LevelIndex == i).ToHashSet();
if (level.Count > 0)
{
levels.Add(level);
all.UnionWith(level);
}
}
if(levels.Count ==0)
{
writer.WriteLine($" Unable to build levels!");
return;
}
else if (!levels.Last().Contains(end))
{
writer.WriteLine($" The graph can not reach to the end!");
return;
}
foreach (var edge in this.Edges.Where(
//check edge to avoid involving loop nodes
e => all.Contains(e.O) && all.Contains(e.T)))
{
//Insert fake nodes to ensure flows connected
//We don't actually need to build edges since we just need the
//max flows value.
for (int j = edge.O.LevelIndex + 1; j < edge.T.LevelIndex; j++)
{
levels[j].Add(new($"FAKE({edge.O}-{edge.T})")
{
InCapacity = edge.Capacity,
OutCapacity = edge.Capacity,
LevelIndex = j
});
}
}
var inps = new List<int>(levels.Select(l=>l.Sum(n=>n.InCapacity)));
//NOTICE: only focus on nodes with DeltaCapacity<0
//and substract the delta from the previous level.
//BTW, first and last nodes are not necessory to be considerred.
for (var i = levels.Count - 2; i > 0; i--)
{
var level = levels[i];
int delta = level.Where(n => n.DeltaCapacity < 0)
.Sum(n => n.DeltaCapacity);
//we don't consider further influnce,
//because finally we need the min value (capacity) of all bottle necks.
//Any previous bottle neck which is wider (more capacity)
//we just let it be,
//If narrower, it's affected by its previous pipes only,
//therefore no need to back-propagate.
if (delta < 0)
{
inps[i - 1] += delta;
}
}
//find the narrowest part of the flow stream and get the flow value
var maxflows = inps.Min();
int ilevel = 0;
writer.WriteLine($" Total {inps.Count} levels:");
foreach (var level in levels)
{
writer.Write($" Level {ilevel}, flows = {inps[ilevel]}: ");
ilevel++;
var first = true;
foreach(var n in level)
{
if (!first) writer.Write(", ");
writer.Write(
$"{n.Name} (IN:{n.InCapacity}, OUT:{n.OutCapacity}, DELTA:{n.DeltaCapacity})");
first = false;
}
writer.WriteLine();
}
writer.WriteLine($" The max flows value is {maxflows}");
writer.WriteLine();
}
}
}
还有项目链接
最大流算法据说是NP-Hard,也确实很Hard。第一个版本的时候没有考虑到单个节点不平衡对层面造成的影响。把题目出给儿子,得出了不同的结果,仔细检查后发现是我错了。看来还是人比机器强!