NP=P 最大流问题 线性算法

有些事真的是一通百通。

这些想法其实最早来自于关于神经编译的项目:如何让那么多的神经元彼此交互又并行不悖,而且能产生确定性的效果(不同于大家常说的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();
        }
    }
}

还有项目链接

GitHub - yyl-20020115/GraphAlgorithmTester: NP=P as proven by traveller and hamiton cycle problemsNP=P as proven by traveller and hamiton cycle problems - GitHub - yyl-20020115/GraphAlgorithmTester: NP=P as proven by traveller and hamiton cycle problemshttps://github.com/yyl-20020115/GraphAlgorithmTester具体细节,请阅读代码。

最大流算法据说是NP-Hard,也确实很Hard。第一个版本的时候没有考虑到单个节点不平衡对层面造成的影响。把题目出给儿子,得出了不同的结果,仔细检查后发现是我错了。看来还是人比机器强!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值