GitHub - yyl-20020115/GraphAlgorithmTester: NP=P as proven by traveller problemNP=P as proven by traveller problem. Contribute to yyl-20020115/GraphAlgorithmTester development by creating an account on GitHub.https://github.com/yyl-20020115/GraphAlgorithmTester
推销员/旅行商 问题的并行算法简介:
从初始城市开始,并行跟踪链接它的所有的边,到可能去到的所有城市,到达之后再基于这个城市跟踪所有的边, 到达它之后的所有城市,这时候需要一个关于路径的记录对象(记录所到过的城市以及城市间的距离并更新当前路径的长度), 而遇到城市之后,路径会分裂,一个路径变长的过程中, 也变成多个分裂的路径。删除那些“上一步”留下的路径,只保留最新的路径。直到路径遇到初始城市为止。 过程中可能会收取大量的路径对象,但最终它们都是首尾相接的闭合路径,而且除了首尾之外,节点都不重复 (不重复经过任何中间城市),且路径包含城市的个数等于城市总数加一。 获得这些闭合路径之后,再根据这些路径的物理长度排序,选取最短的就是想要的结果。
这种算法,实际上只需要城市数量那么多的步骤,也就是说,线性时间O(n)即可解决。
详细算法请阅读代码。
逸霖
以上的描述似乎有点太专业化了。我们用普通人容易理解的方式来把这个算法再描述一遍。
推销员(旅行商/Traveller)他要去一系列的城市做生意,比如城市A,B,C,D,E,F,G这七个城市。他显然是都想去的,而且城市之间有一些有道路相连,有一些则没有。两个相连的城市之间的物理距离是已知的。推销员想要走最短的物理距离,遍历所有这些城市(一个都不少,也都不重复),然后再回到出发点,那么他要怎么规划这个旅程才能做到距离最短又能遍历且能回归呢?这就是所谓的推销员问题。
比如他要从A出发,A和B,A和C有路可走,C和F,C和D有路可走……一直到G和A有路可走。但是他除非去走(计算机模拟也行)不然他就没法知道那条最短的路有多长。可是他完全可能走错了,比如从A走到C确实可以走,但是A先到B才是最短的那条路的一部分,而从C那条路走的尝试以及随后的所有尝试都是白费功夫的,可是不尝试(包括计算机模拟)就肯定不可能知道。一旦城市的数量增大,这个尝试的次数将是一个巨大的天文数字(而且大多数尝试都是无意义的)。
这个问题始终被认为是NP问题,也就是非多项式时间可解(也可能无解)的问题,一般来说,需要指数时间(需要的算法步骤的数量达到问题规模的某个指数级别)才能解开。形象的说,如果要每一种情况都考察一遍,就是用现在的计算机来算,也得几百上千年,这个时间肯定是没法接受的。
但有理论认为,NP问题其实就是P问题,也就是说,没找到一个好方法,把这种特别费时的问题化简到可以接受的程度。那么,到底有没有这样一种方法呢?若找到一个,就是有了。
现在让我们看看这个平行算法。
推销员要尝试所有的可能性才能找出最短的路径,但是这是一个人啊。如果我们多叫一些人来呢?情况会不会有变化?
比如推销员从A出发去B和C这件事被替换为推销员在A城招聘了两个二级推销员,并让他们两个去B和C城,同时告诉这两个人要记得距离,以及一级推销员是谁以及在哪个城市(就是他们从哪出发)。然后,等这两个二级推销员到了目的地,他们再各自招聘他们自己的二级推销员(也就是全局的三级推销员),也要求他们记得距离,一级推销员和二级推销员是谁以及他们的出发地。当这些三级推销员到达目的城市之后,再招聘下一级推销员,并让他们记得一级二级以及三级推销员是谁经历的城市以及每一段城市间的距离和总的距离……这样一直下去,如果确实有一条途径,使得最后一级推销员可以到达A城,那么最终就会找到最初的推销员,并汇总所有的距离信息,以及他们和他们前级所走过的路径信息(经过哪些城市)。
最初的推销员获得这些信息之后,根据总的距离,对这些信息进行排序,就可以找到物理路径最短的那一条路径(也就是哪个推销员从哪个城市出发到哪个城市经过多少距离的一个序列)。
由于城市之间的道路四通八达,其中完全可能出现过早的回归初始城市或者重复进入某个城市,这种情况一旦出现,推销员就停止招聘了。只有那些带着完成了所有城市的序列信息的推销员才能向最初的推销员报告他的序列路径。
一个人尝试这么多可能性显然要付出巨大的时间成本,但是,这么多人一起去做,并行不悖,就可以把这个时间成本压缩到线性的程度,也就是用空间换时间。你可能会说,这样的话要求的空间也及其的大啊,但是我们并不需要记录所有的可能性,因为那些过早回归的以及重入某城市的情况都被中间过程丢弃了。所以剩下的记录数量并不多,而且被不断的裁剪。
这种并行性也不需要多个CPU多个线程来实现,而只是按照时间层面的原则,“现在大家都在哪”以及“下一步去哪”就构成了当前步骤的路径集合,并最终构成所有路径可能性的解的集合。
这个想法其实和平行世界的概念非常相似。每到一个城市,路径就会因为城市所连接的目的地而分叉,每个分叉既继承了前面的路径,又增加了新的分支节点。随着分叉的增加,路径也呈指数增加,但是我们并不需要记录所有的路径,因为那些过早回归和重入的路径压根就不会存在,而我们也不需要记录前后两个步骤之外的其它路径情况,换句话说,如果最多有n个城市,我们只需要最多记录n(n+1)+n = (n+1)^2-1 那么多个路径,也就是说,空间复杂度至多只有O(n^2),若算上路径的长度也作为空间复杂度,则至多只有O(n^3),这个规模在现代计算机上来说基本上是可以接受的,更何况本身并行算法就是可以分裂在多台计算机上平行计算的。这种利用城市连接而构造路径和分叉算法,就像平行世界中因为特定事件造成的时间分叉一样,由于符合物理世界的规律,显然也能够在计算机中有效的实现。
这个算法的提出,其实已经说明了此类NP问题本质上确实就是P问题,因为时间复杂度就是O(n),空间复杂度不超过O(n^3),而这就是多项式。
旅行商问题的一个简化版本,称为哈密顿(Hamiltonian Cycle Problem)回路问题。就是在一个无向图中寻找遍历每个节点的回路,实际上这个问题比旅行商问题还简单一些,因为不用最终比较物理路径的长度。其它部分实质上都是一样的。
再具体的东西,请看代码吧,目前就只能这样了。下面这不到一百行就是核心算法。完整的代码在github上,是一个C#的项目,对C#有一些了解的小伙伴应当不难读懂。Java/C++的fans读起来应该也没有问题。
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace GraphAlgorithmTester;
public class TravellerProblemSolver
{
public SortedDictionary Nodes = new();
public HashSet Edges = new();
public record class Path(List Nodes)
{
public List NodeCopies = new();
public SNode Start => this.Nodes.FirstOrDefault();
public SNode End => this.Nodes.LastOrDefault();
public int Length = 0;
public bool HasVisited(SNode node) => this.Nodes.Any(n => n.Name == node.Name);
public bool IsPreTerminated(HashSet names) => this.Nodes.Count < names.Count + 1 && names.Any(name => this.Nodes.Count(n => n.Name == name) >= 2);
public Path Copy() =>
new(this.Nodes.ToList()) { Length = this.Length, NodeCopies = this.NodeCopies.ToList() };
public override string ToString() => string.Join(" -> ", this.NodeCopies) + $" = {this.Length}";
}
public void Solve(TextWriter writer)
{
if (Nodes.Count > 0 && Edges.Count > 0)
{
writer.WriteLine("Total {0} nodes", Nodes.Count);
writer.WriteLine("Total {0} edges", Edges.Count);
var names = Nodes.Keys.ToHashSet();
var start = Nodes.First().Value;
var paths = new List();
var solutions = new List();
var outs = this.Edges.Where(e => e.O == start).ToList();
foreach (var _out in outs)
{
paths.Add(new(
new() { start, _out.T })
{
Length = _out.Length,
NodeCopies = new() { start.Copy(), _out.T.Copy(_out.Length) }
});
}
//NP=P
int step = 0;
while (step++ < names.Count)
{
paths.RemoveAll(p => p.Nodes.Count <= step || p.IsPreTerminated(names));
foreach (var _path in paths.ToArray())
{
var current = _path.End;
foreach (var _out in this.Edges.Where(e => e.O == current))
{
var se = _out;
var sn = _out.T;
if ((_path.HasVisited(sn) && sn.Name == start.Name) || !_path.HasVisited(sn))
{
var branch = _path.Copy();
var snode = sn.Copy();
snode.Offset = se.Length;
branch.Length += se.Length;
branch.Nodes.Add(sn);
branch.NodeCopies.Add(snode);
paths.Add(branch);
}
}
}
}
paths.Sort((x, y) => x.Length - y.Length);
if (paths.Count > 0)
{
var d0 = paths[0].Length;
for (int i = 0; i < paths.Count; i++)
{
if (paths[i].Length <= d0)
{
solutions.Add(paths[i]);
}
}
}
writer.WriteLine($"Total {solutions.Count} solutions");
foreach (var solution in solutions)
{
writer.WriteLine($" {solution}");
}
}
}
}