NP=P 图着色问题 线性解法

图着色问题,很容易理解。

我们看的地图不同地区和国家之间有共用的边界的时候,总是用两种不同的颜色来表示两个区域,以便于一眼就能识别出两者的差异。

我们这里说的图着色问题,并不是地图的着色问题,也不涉及四色定理一类的。此图非彼图,Graph!=Map,尽管众多的关于地图着色的问题也可以抽象到图着色的问题上来,但我们此时并不关心这一点。我们说的图,是这种东西。下面的PNG图像是用一个叫DOT的语言描述并用DOT软件自动生成的:

我们要求这个由节点和边组成的图,用边相连的两个节点具有不同的颜色。就像这个(PNG图像所描绘的)图一样。

知道的人知道,这又是一个NP问题,具体是NP-Complete还是NP-Hard我就不清楚了。

这个问题现在大家是怎么解的呢?

比如从1开始,把节点1画成蓝色,和它相邻的节点2,3,4,都用和1不同的颜色。比如都用绿色,这时候2,3,4,都是绿色。可是看到节点3的时候,发现3和4都是绿色显然不行,而且2和3也都是绿色,当然也不行,相邻的节点不能用一样的颜色。

这种情况显然是不允许的。现在的算法看到这种情况,就回到上一步(称为回溯),至少不能让2,3,4都标上绿色。可是我们知道,如果这个图相当的大,回溯的可能性也相当的大,因为不知道新遇到的节点是不是已经标注颜色,也不知道新节点是否会和先前标注的颜色冲突,所以回溯会经常发生,而这种尝试的次数,也是指数级别的,所以才叫NP问题。

让我们再想一遍,为啥要回溯呢?因为要标注的新节点的颜色,可能会和周围的颜色相冲突,比如我们要求用5种颜色来标注这个图上的节点,但是现在这个节点周围的节点已经占据了所有的五种颜色,那么新节点只能用第六种颜色,才能避免和周围任何一个节点的颜色一样。

可是我们怎么知道这件事什么时候会发生,发生的时候又是怎么样的?毕竟在最开始的时候,所有的节点都没有标注颜色的时候,后面到底走怎样的路径显然不是一目了然的,就算是在过程中,哪怕就是上一步,也没法预知未来的情况。所以这样的尝试似乎是无法避免的。

一个节点的颜色决定于那些它所关联的节点的颜色。要是能让它所关联的节点先着色,它就自然知道了应当着什么颜色,或者在什么颜色范围里面可以选择。所以问题要向前推,可是就算是这样,我们仍然无法预知未来。

既然前提前推无法实现,那么决定后推是否可以做到呢?比如说,让那些可能发生冲突的情况尽量发生在未来,而确定性的处理好现在发生的问题。换句话说,是不是某些个节点的颜色决定得太早了,才导致了在未来的颜色选择上面无路可走?

有了这个想法,我们就要考虑,着色的时序问题。DOT语言有一个功能,叫做Rank。也就是设置级别的意思。一个节点可以被对齐在和它同样级别的节点上。比如从左到右的布局中,若不特别指出,则按照级别排列。上图中2和3从左到右的方向上并没有垂直对齐,而是有先后关系,这就是所谓级别的体现,之所以3在2后面,是因为3并不仅仅被1连接,还被2连接,而且2也被1连接,所以它在1后面的一级的后面的一级上,而不是和2一样就在后面。

我们变个魔术,让它们纵向对齐在一起:

这是DOT语言中写出{ rank=same; 2;3};产生的结果。

我们提到DOT语言,是因为这种Rank的想法,实际上可以帮助我们建立节点遍历的时序。

现在大家显然都开始习惯性的用面向对象的方式来描述图了。虽然链表和邻接矩阵也不是问题,但面向对象的方法,建立节点对象和边对象,能给我们提供更多的操作选项。我们用计算机中的对应的物件(object)来描述现实问题中的对应的事物,用物件之间的关系和操作来对应现实中事物的交互作用。而这里,我们用类和对象来构建对象网络,描述图论问题,显然要比用矩阵这种抽象的方式更容易理解。(这一步的要点在于:具象好过抽象,容易理解和掌握)。

我们先前遍历节点,基本上不是BFS(深度遍历)就是DFS(广度)遍历,我们用隐式或者显式的栈记录我们当前视点的位置。实际上就像递归总可以展开成循环,我们可以把BFS和DFS展开成为集合操作前提下的迭代(Iteration)。比如说,我们先前用DFS,看过1节点之后,看2,然后看5,再回头看4,再回头再回头看3,这样进进出出的取挨个查看每个节点。

事实上有了对象网络之后,我们从1开始,可以同时看2和3(并不需要多线程),把2和3当作一个时间层面来理解,然后再看4所在的时间层面,里面只有4,然后再看5。这种迭代方式,除非所有的层次都只有一个节点,否则层次总数总是小于节点的总数。

有了这些准备,我们言归正传。

先用求Rank的方式,把节点都放在自己应当放在的层次上。从选定的根节点1开始,一层一层的取色着色。比如根节点1着蓝色,第二层的2就可以着绿色,但是这时候不要处理3,因为3在后面一层。直到第三层的时候,原则上只要不是绿色就行了,所以第三层又可以着蓝色。然而第三层上的3却不是,它是例外的,它同时受到1和2的影响所以它不可以着绿色或者蓝色,它必须再用一种颜色来着色。

每一层若不关系到更先前的层次,就可以选择尽可能用过的颜色,只要和前一层不同就是了。而对于特定节点,关系到更先前的层次,就要具体考虑选择的颜色,不能是它所有入度的颜色之一,必要的话,还得创造新的颜色。

原则上,相邻的两个层次颜色不同即可,甚至可以交替。比如两种颜色就可以实现一条链的标注,

而对于特定的跨层次节点,则具体问题具体处理。

用这种方式,我们就可以找到这个图的着色的最小数量(上图为2),当然这时候每个节点都已经完成着色了。如果这还不行,考虑用另一个节点当成根节点,按照Rank的方式,重新生成迭代序列,再来一遍,看看哪一遍需要的颜色数量最小 - 究竟一遍能不能获得最小的颜色数量,我并不知道,但作为一种有效的着色方法,一遍显然足够了。

本文开始的时候说到的图,着色之后是这样的,不妨检视一下,确实至少需要四种颜色。

算法到这就大体上说清楚了。

这个算法其实是受到最大流问题启发才想到的,并不是因为DOT。但是不得不说,DOT的做法实在很有启发性!

代码实在太多就不全贴了。

上github链接:

 https://github.com/yyl-20020115/GraphAlgorithmTestericon-default.png?t=M666https://github.com/yyl-20020115/GraphAlgorithmTester

贴一下核心部分,

using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace GraphAlgorithmTester;

public class GraphColoringProblemSolver:ProblemSolver
{
    public uint UsableColors = 5;
    public string[] Parameters = new string[1];
    public override void Solve(TextWriter writer, string start_name = null, string end_name = null)
    {
        writer.WriteLine("GraphColoringProblem:");

        if (Nodes.Count < 1 || Edges.Count < 1)
        {
            writer.WriteLine("  Nodes count should >=1 and Edges count should >=1 too.");
        }
        else
        {
            if (this.Parameters.Length == 1 && this.Parameters[0] is string uc)
            {
                if (uc.StartsWith("C=") && uc.Length > 2)
                {
                    uint.TryParse(uc[2..], out this.UsableColors);
                }
            }

            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();
            var groups = new Dictionary<HashSet<int>, Dictionary<int, SNodeSet>>();

            var edge_collection = new HashSet<SEdge>();

            foreach (var start in Nodes.Values)
            {
                if (edge_collection.Count > 0)
                {
                    this.Edges.UnionWith(edge_collection);
                }
                foreach (var n in this.Nodes.Values)
                {
                    n.Offset = null;
                }

                var nodes = new HashSet<SNode>() { start };
                var nexts = new HashSet<SNode>();
                var all = new HashSet<SNode>();
                int level_index = 1;

                do
                {
                    foreach (var node in nodes.ToArray())
                    {
                        var edges = this.Edges.Where(e => e.O == node).ToArray();
                        foreach (var edge in edges)
                        {
                            var t = edge.T;
                            t.LevelIndex = level_index;
                            nexts.Add(t);
                            //remove the directional 
                            edge_collection.UnionWith(
                                this.Edges.Where(e => e.T == node && e.O == t).ToList()
                                );
                            
                            int c = this.Edges.RemoveWhere(e => e.T == node && e.O == t);
                            if (c != 0)
                            {

                            }
                        }
                    }
                    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 - 1 < names.Count);
                if (level_index > names.Count + 1)
                {
                    //there is a loop!
                    //we should break this edge and remove this node.
                    start.LevelIndex = 0;
                    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
                start.LevelIndex = 0;
                var levels = new List<SNodeSet>();

                for (int i = 0; i < level_index; i++)
                {
                    var level = new SNodeSet(this.Nodes.Values.Where(n => n.LevelIndex == i));
                    if (level.Count > 0)
                    {
                        levels.Add(level);
                        all.UnionWith(level);
                    }
                }
                if (levels.Count == 0)
                {
                    writer.WriteLine($"    Unable to build levels!");
                    return;
                }
                int last = 0;
                int current = last;
                var colors = new HashSet<int>() { };
                var delayed = new HashLookups<int, SNode>();
                for (int idx = 0; idx < levels.Count; idx++)
                {
                    if (delayed.TryGetValue(idx, out var delays))
                    {
                        foreach (var node in delays)
                        {
                            //processing input first
                            var ins = this.Edges.Where(e => e.T == node).Select(e => e.O).ToList();
                            var cos = ins.Select(_in => _in.Color is int i ? i : -1).ToHashSet();
                            cos.Remove(-1);
                            if (cos.Count < colors.Count)
                            {
                                var ccs = colors.ToHashSet();
                                ccs.ExceptWith(cos);
                                last = current;
                                current = ccs.First();
                            }
                            else //cos.Count == colors.Count (can not be >)
                            {
                                last = current;
                                cos.Add(current = colors.Count);
                                colors.Add(colors.Count);
                            }
                            node.Offset = current;
                        }
                    }
                    var level = levels[idx];
                    foreach (var node in level)
                    {
                        if (node.Offset == null)
                        {
                            node.Offset = current;
                        }
                    }
                    //has to be
                    colors.Add(current);
                    var found = false;
                    foreach (var node in level)
                    {
                        var outs = this.Edges.Where(e => e.O == node).Select(e => e.T).ToList();
                        if (outs.Count > 0)
                        {
                            foreach (var o in outs)
                            {
                                if (node.LevelIndex + 1 < o.LevelIndex)
                                {
                                    delayed.Add(o.LevelIndex, o); //delay processing
                                }
                                else if (node.LevelIndex + 1 == o.LevelIndex)
                                {
                                    found = true;
                                }
                                else
                                {
                                    found = true;
                                }
                            }
                        }
                    }
                    if (found)
                    {
                        var ch = colors.ToHashSet();
                        ch.Remove(current);
                        current = ch.Count == 0 ? 1 : ch.First();
                    }
                }
                var group = new Dictionary<int, SNodeSet>();
                foreach (var c in colors)
                {
                    group[c] = new SNodeSet(
                        this.Nodes.Values.Where(n => n.Color == c));
                }
                groups.Add(colors, group);
            }
            
            var groups_ = new Dictionary<HashSet<int>, Dictionary<int, SNodeSet>>();
            foreach (var kv in groups.Where(g=>g.Key.Count <= UsableColors).DistinctBy(g => g.Key.Count))
            {
                groups_.Add(kv.Key, kv.Value);
            }

            groups = groups_;
            if (groups.Count > 0)
            {
                writer.WriteLine($"  NOTICE: the solution is wrong if nodes are not all connected");

                writer.WriteLine($"  Best solution, total {groups.Count} groups:");
                foreach (var g in groups)
                {
                    writer.WriteLine($"    group:");
                    foreach (var p in g.Value)
                    {
                        writer.WriteLine($"      color: {p.Key} {p.Value})");
                    }
                }
            }
            else
            {
                writer.WriteLine($"  No solution found!");
            }
        }
    }

}

这整个是一套完全不同的想法。

这个想法的核心叫做时序化,配合集合运算(交并补等等),同时处理大量的节点,这也有点像是SIMD的方式,或者物理学中平行世界运作的方式。用这种方式我们可以把所有的问题提升到最高的维数(时间)上去解答。以这种方式所产生的算法,可以很轻松的把NP问题变成P问题,而且相当多的NP问题可以直接实现O(n)解。所以说,从这个角度理解,NP问题就是P问题,就已经证明了。

也许,这就是人脑处理这类问题的真正的方式。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值