尝试用Java实现一种旅行者算法

Posted

技术标签:

【中文标题】尝试用Java实现一种旅行者算法【英文标题】:Trying to implement a kind of traveller algorithm in Java 【发布时间】:2014-01-04 01:08:25 【问题描述】:

我正在尝试为这种旅行者问题实现一个简单有效的算法(但这不是“旅行推销员”):

A traveller has to visit N towns, and:
1. each trip from town X to town Y occurs once and only once
2. the origin of each trip is the destination of the previous trip

所以,如果您有例如城镇 A、B、C,

A->B, B->A, A->C, **C->A, B->C**, C->B

不是解决方案,因为您不能先执行 C->A,然后执行 B->C(您需要在两者之间使用 A->B),而:

A->B, B->C, C->B, B->A, A->C, C->A

是一个可接受的解决方案(每个目的地都是下一次旅行的起点)。

下面是一个 Java 插图,以 4 个城镇为例。ItineraryAlgorithm 是在提供回答问题的算法时要实现的接口。如果您将new TooSimpleAlgo() 替换为new MyAlgorithm()main() 方法将测试您的算法是否存在重复。

package algorithm;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Traveller 

    private static final String[] TOWNS = new String[]  "Paris", "London", "Madrid", "Berlin";

    public static void main(String[] args) 
        ItineraryAlgorithm algorithm = new TooSimpleAlgo();
        List<Integer> locations = algorithm.processItinerary(TOWNS);
        showResult(locations);
    

    private static void showResult(List<Integer> locations) 
        System.out.println("The itinerary is:");
        for (int i=0; i<locations.size(); i++) 
            System.out.print(locations.get(i) + " ");
        
        /*
         * Show detailed itinerary and check for duplicates
         */
        System.out.println("\n");
        System.out.println("The detailed itinerary is:");
        List<String> allTrips = new ArrayList<String>();
        for (int m=0; m<locations.size()-1; m++) 
            String trip = TOWNS[locations.get(m).intValue()] + " to "+TOWNS[locations.get(m+1).intValue()];
            boolean duplicate = allTrips.contains(trip);
            System.out.println(trip+(duplicate?" - ERROR: already done this trip!":""));
            allTrips.add(trip);
        
        System.out.println();
    

    /**
     * Interface for interchangeable algorithms that process an itinerary to go
     * from town to town, provided that all possible trips are present in the
     * itinerary, and only once. Note that after a trip from town A to town B,
     * the traveler being in town B, the next trip is from town B.
     */
    private static interface ItineraryAlgorithm 
        /**
         * Calculates an itinerary in which all trips from one town to another
         * are done. Trip to town A to town B should occur only once.
         * 
         * @param the
         *            number of towns to visit
         * @return the list of towns the traveler visits one by one, obviously
         *         the same town should figure more than once
         */
        List<Integer> processItinerary(String[] towns);
    

    /**
     * This algorithm is too simple because it misses some trips! and generates
     * duplicates
     */
    private static class TooSimpleAlgo implements ItineraryAlgorithm 

        public TooSimpleAlgo() 
        

        public List<Integer> processItinerary(String[] towns) 
            final int nbOfTowns = towns.length;
            List<Integer> visitedTowns = new ArrayList<Integer>();
            /* the first visited town is town "0" where the travel starts */
            visitedTowns.add(Integer.valueOf(0));
            for (int i=0; i<nbOfTowns; i++) 
                for (int j=i+1; j<nbOfTowns; j++) 
                    /* travel to town "j" */
                    visitedTowns.add(Integer.valueOf(j));
                    /* travel back to town "i" */
                    visitedTowns.add(Integer.valueOf(i));
                
            
            return visitedTowns;
        

    


这个示例程序给出以下输出,第一部分是按旅行者访问它们的顺序排列的城镇索引列表(0 代表“巴黎”,1 代表“伦敦”,2 代表“马德里”,3 代表“柏林”)。

The itinerary is:
0 1 0 2 0 3 0 2 1 3 1 3 2 

The detailed itinerary is:
Paris to London
London to Paris
Paris to Madrid
Madrid to Paris
Paris to Berlin
Berlin to Paris
Paris to Madrid - ERROR: already done this trip!
Madrid to London
London to Berlin
Berlin to London
London to Berlin - ERROR: already done this trip!
Berlin to Madrid

您建议如何实施 ItineraryAlgorithm? 编辑:如果您愿意,您可以根据需要为最多 4、5、...、最多 10 个城镇提出解决方案。

【问题讨论】:

@tom 指的是 P=NP 的东西。在此处阅读更多内容:en.wikipedia.org/wiki/P%3DNP 这并不意味着问题无法解决,它只是意味着您添加到问题集中的每个城市将需要大量资源并且需要成倍增长。这也意味着,在某些问题集大小下,它实际上变得无法解决,因为它需要几个世纪或更长时间才能处理。 对于4个城镇的例子,可能的解决方案之一是0131230203210,也就是说Paris->London->Berlin->London->Madrid->Berlin->Paris->Madrid->巴黎->柏林->马德里->伦敦->巴黎 【参考方案1】:

这不是旅行推销员问题,恕我直言,它不是 NP 完全的,可以在 O(N^2) 时间内完成。

您可以从任何节点到所有节点进行简单的递归 DFS(带回溯)

例如如果节点是abcde

路线应该是

abcde-dce-cbdbe-bacadaea

(Total C(5,2) * 2 = 20 edges)

复杂度为 O(n^2),因为边数 = 2*C(n,2)

完整的 C++ 工作代码:(对不起,我对 Java 不熟悉。您可以相应地修改它)

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>


using namespace std;

string cities;

void recurRoute( int prevIndex, int currIndex, vector<pair<int,int> > &traversed ) 

    // For each i > currIndex, if edge (currindex to i) not in traversed, 
    // then add the edge and recur on new index i.
    for ( int i = currIndex+1; i < cities.size(); i++ ) 

        pair<int,int> newEdge( currIndex, i );
        if ( find( traversed.begin(), traversed.end(), newEdge ) == traversed.end() ) 
            traversed.push_back( newEdge );
            recurRoute( currIndex, i, traversed );
        
    

    // if there is a previous index, 
    // then add the back edge (currIndex to prevIndex) and return.
    if ( prevIndex >= 0) 
        pair<int,int> prevEdge( currIndex, prevIndex );
        traversed.push_back( prevEdge );
    
    return;


int main()

    cin >> cities;

    vector<pair<int,int> > edges;

    recurRoute( -1, 0, edges );

    for ( int i = 0; i < edges.size(); i++ ) 
        cout << cities[ edges[i].first ] << cities[ edges[i].second ] << endl;
    

    return 0;

输入:

abc

输出:

ab
bc
cb
ba
ac
ca

输入:

abcde

输出:(将新行改为空格)

ab bc cd de ed dc ce ec cb bd db be eb ba ac ca ad da ae ea
( abcde-dce-cbdbe-bacadae as noted previously )

【讨论】:

【参考方案2】:

这是我的 Java 解决方案(使用 Backtracking 算法):

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

public class BobbelAlgo implements ItineraryAlgorithm 
    private final Stack<String> routes = new Stack<String>();

    public List<Integer> processItinerary(String[] towns) 
        routes.removeAllElements();
        final List<Integer> results = new ArrayList<Integer>();
        final int[] townIndexList = new int[towns.length];

        for (int i = 0; i < towns.length; i++) 
            townIndexList[i] = i;
        

        // add starting town to list
        results.add(0);

        // start with route 'town 0' to 'town 1'
        visitTowns(townIndexList, townIndexList[0], townIndexList[1], results);

        return results;
    

    public int visitTowns(final int[] towns, final Integer from, final Integer to, final List<Integer> results) 
        // 'from' is equals to 'to' or route already exists 
        if (from.equals(to) || routes.contains(from + "-" + to)) 
            return 2;
        

        routes.push(from + "-" + to);
        results.add(to);

        if (routes.size() == towns.length * (towns.length - 1)) 
            // finished, all ways done
            return 0;
        

        for (final int town : towns) 
            final int ret = visitTowns(towns, to, town, results);

            if (ret == 0) 
                // finished, all ways done
                return 0;
             else if (ret == 1) 
                // no new way found, go back!
                routes.pop();
                results.remove(results.size() - 1);
            
        

        // no new way found, go back!
        return 1;
    

对于一个基准,我已经通过更多的城镇循环它,请参阅:

For 10 it took 1 ms.
For 15 it took 0 ms.
For 20 it took 0 ms.
For 25 it took 15 ms.
For 30 it took 15 ms.
For 35 it took 32 ms.
For 40 it took 93 ms.
For 45 it took 171 ms.
For 50 it took 328 ms.
For 55 it took 577 ms.
For 60 it took 609 ms.
For 65 it took 905 ms.
For 70 it took 1140 ms.
For 75 it took 1467 ms.
For 80 it took 1873 ms.
For 85 it took 2544 ms.
For 90 it took 3386 ms.
For 95 it took 4401 ms.
For 100 it took 5632 ms.

在这里您可以看到 O(n^2) 的复杂性。 在大约 100 个城镇之后,它会得到一个 ***Error,因为对于默认堆栈大小配置,递归调用太深了(请参阅:Stack overflows from deep recursion in Java?)。

【讨论】:

【参考方案3】:

我想我找到了我要找的东西:

private static class SylvainSAlgo implements ItineraryAlgorithm 

    @Override
    public List<Integer> processItinerary(String[] towns) 

        List<Integer> itinerary = new ArrayList<Integer>();
        for (int i = 0; i<towns.length; i++) 
            for (int j = i + 1; j < towns.length; j++) 
                itinerary.add(Integer.valueOf(i));
                itinerary.add(Integer.valueOf(j));
            
        
        itinerary.add(Integer.valueOf(0));
        return itinerary;
    

【讨论】:

我是这样得出这个算法的:采用这个解决方案0102031213230(其中 N=4)并像这样查看它010203|1213|23|- 来猜测外部 for 循环;然后,使用-1-2-3|-2-3|-3|-,您可以看到内部 for 循环。乍一看,它有效...【参考方案4】:

如前所述,这是一个严重的研究问题,随着城市数量的增加,解决起来很快就会变得乏味。但是,approximations 可用于图形节点之间的距离表示欧几里得距离的情况。

近似算法可以在多项式时间内为您提供解决方案(相对于指数),但问题是存在相关的误差界限。解决方案并不简单,需要付出相当大的努力才能实施。大多数算法都是以几何方式解决问题,而不是将其视为图形,因此假设距离代表欧几里得距离。

【讨论】:

【参考方案5】:

这个问题看起来像是在有向图中确定欧拉循环[1]。

在您的情况下,对于 N 个城镇,每两个城镇之间总是存在两条对称的道路(例如,所有对的 A->B、B->A 等等)。 如果是这样,我认为你不需要编写这样的算法,如果你的工作只是找到其中一个循环,因为对于 1,2...N,循环 1,2..N-1 ,N,N-1..2,1 始终满足要求。

但是如果每两个城镇之间并不总是有两条对称的道路,事情可能会有所不同并且更复杂,你可能想看看有向图的欧拉路径算法(你应该可以在离散数学教科书或算法教科书的图表章节中找到),如果有这样的路径并且路径具有相同的起点和终点,则表示您的问题有解决方案.

[1]http://en.wikipedia.org/wiki/Eulerian_path

【讨论】:

重点是,你必须找到所有城镇之间的所有路线。这意味着不仅A-&gt;B, B-&gt;C, C-&gt;B, B-&gt;A,您还在这里缺少A-&gt;C and C-&gt;A 谢谢@bobbel,这是我在这里错过的重要一点。我已经删除了错误的部分。【参考方案6】:

该算法似乎可以根据您的限制生成可接受的解决方案:

private static class Algorithm implements ItineraryAlgorithm 
    public List<Integer> processItinerary(String[] towns) 

        List<Integer> sequence = new ArrayList<>(towns.length*(towns.length+1));

        for(int idx1 = 0; idx < towns.length; idx1++)
            result.add(idx1);
            for(int idx2 = idx1+1; idx2 < towns.length; idx2++)
                sequence.add(idx2);
                sequence.add(idx1);
            
        

        List<Integer> segments = new ArrayList<>(result.length*2-2);
        for(int i: sequence)
            segments.add(i);
            segments.add(i);
        
        segments.remove(0);
        segments.remove(segments.length-1);

        return segments;
    

【讨论】:

以上是关于尝试用Java实现一种旅行者算法的主要内容,如果未能解决你的问题,请参考以下文章

最短路径之迪杰斯特拉算法的Java实现

用 Java 实现一个快速排序算法

用 Java 实现一个插入排序算法

Java:ConcurrentLinkedQueue的实现原理分析

一致性hash算法及java实现

用JAVA实现选择排序