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

9

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

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

你如何建议实施行程算法?
编辑:如果您愿意,您可以根据自己的喜好提出最多4、5、...、最多10个城镇的解决方案。


2
@tom 正在提到 P=NP 的问题。更多阅读请参见:https://en.wikipedia.org/wiki/P%3DNP 这并不意味着这个问题是无解的,只是意味着需要大量的资源,并且随着问题集合中城市数量的增加,处理时间将呈指数级增长。这也意味着,在某些问题集合大小时,它变得几乎无法解决,因为需要花费数个世纪或更长时间来处理。 - Taylor
对于4个城镇的例子,可能的解决方案之一是0131230203210,也就是说巴黎->伦敦->柏林->伦敦->马德里->柏林->巴黎->马德里->巴黎->柏林->马德里->伦敦->巴黎。 - Bludzee
6个回答

1
这不是一个旅行商问题,我个人认为它不是NP完全问题,可以在O(N^2)的时间内完成。您可以从任何节点开始进行简单的递归深度优先搜索(带有回溯)到所有节点。例如,如果节点是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 )

1

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

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个城镇后,由于递归调用对于默认堆栈大小配置来说太深,因此会出现StackOverflowError错误(请参见:Java中深度递归导致堆栈溢出?)。


1

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

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 循环。乍一看,它有效... - Bludzee

0

如前所述,这是一个严重的研究问题,随着城市数量的增加,解决起来可能会变得非常乏味。然而,对于表示欧几里得距离的图节点之间的距离的情况,有近似可用。

近似算法可以在多项式时间内提供解决方案(而不是指数时间),但问题在于存在误差界限。这些解决方案并不简单,需要相当大的努力来实现。大多数算法从几何角度来处理问题,而不是将其视为图形,因此假设距离表示欧几里得距离。


0

这个问题看起来像是在有向图中确定欧拉回路[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->B, B->C, C->B, B->A,你还缺少了 A->C 和 C->A - bobbel
谢谢@bobbel,这是我在这里遗漏的一个重要点。我已经删除了错误的部分。 - Mengdong Yang

-1

根据您的约束条件,该算法似乎生成了一个可接受的解决方案:

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;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接