【问题标题】:One-way flight trip problem单程飞行问题
【发布时间】:2011-02-28 20:02:33
【问题描述】:

您正在进行一次单程间接飞行旅行,其中包括数十亿 未知的非常大量的转机。

  • 您不会在同一个机场停留两次。
  • 您旅行的每个部分都有 1 张车票。
  • 每张机票包含 srcdst 机场。
  • 您拥有的所有门票都是随机排序的。
  • 您忘记了最初的出发机场(第一个 src)和您的目的地(最后一个 dst)。

设计一种算法,以最小的大O复杂度重建您的行程。


试图解决这个问题我已经开始使用两个集合的symmetric difference,Srcs 和 Dsts:

1)对数组 Srcs 中的所有 src 键进行排序
2) 对数组 Dsts 中的所有 dst 键进行排序
3)创建两个数组的联合集以查找非重复项 - 它们是您的第一个 src 和最后一个 dst
4)现在,有了起点,使用二分搜索遍历两个数组。

但我想肯定还有另一种更有效的方法。

【问题讨论】:

  • “十亿转机……你不会在同一个机场停两次”我不认为有那么多机场。
  • 对我来说像家庭作业,但我知道什么......
  • +1 以抵消反对票和接近票,没有任何解释。 (如果你想脾气暴躁:别管这个问题)
  • 1) 按出发时间排序车票,2) 第一张票的起点是您的出发机场,最后一张票的到达点是您的到达机场。 ;)
  • 如果您忘记了最终目的地,那么您就没有行李(因为它在某个神秘的机场)。鉴于涉及大量门票,您将不得不将它们存放在行李中。因此,您没有门票,问题没有实际意义。

标签: c algorithm


【解决方案1】:

我在这里提供了一个更通用的解决方案:

您可以在同一个机场停留多次,但您必须将每张机票恰好使用 1 次

您可以在旅途的每个部分拥有超过 1 张票。

每张机票都包含 src 和 dst 机场。

您拥有的所有门票都是随机排序的。

您忘记了最初的出发机场(第一个 src)和您的目的地(最后一个 dst)。

如果存在这样的链,我的方法返回包含所有指定城市的城市列表(向量),否则返回空列表。当有多种方式在城市中旅行时,该方法返回按字典顺序排列的最小列表。

#include<vector>
#include<string>
#include<unordered_map>
#include<unordered_set>
#include<set>
#include<map>

using namespace std;

struct StringPairHash
{
    size_t operator()(const pair<string, string> &p) const {
        return hash<string>()(p.first) ^ hash<string>()(p.second);
    }
};

void calcItineraryRec(const multimap<string, string> &cities, string start,
    vector<string> &itinerary, vector<string> &res,
    unordered_set<pair<string, string>, StringPairHash> &visited, bool &found)
{
    if (visited.size() == cities.size()) {
        found = true;
        res = itinerary;
        return;
    }
    if (!found) {
        auto pos = cities.equal_range(start);
        for (auto p = pos.first; p != pos.second; ++p) {
            if (visited.find({ *p }) == visited.end()) {
                visited.insert({ *p });
                itinerary.push_back(p->second);
                calcItineraryRec(cities, p->second, itinerary, res, visited, found);
                itinerary.pop_back();
                visited.erase({ *p });
            }
        }
    }
}

vector<string> calcItinerary(vector<pair<string, string>> &citiesPairs)
{
    if (citiesPairs.size() < 1)
        return {};

    multimap<string, string> cities;
    set<string> uniqueCities;
    for (auto entry : citiesPairs) {
        cities.insert({ entry });
        uniqueCities.insert(entry.first);
        uniqueCities.insert(entry.second);
    }

    for (const auto &startCity : uniqueCities) {
        vector<string> itinerary;
        itinerary.push_back(startCity);
        unordered_set<pair<string, string>, StringPairHash> visited;
        bool found = false;
        vector<string> res;
        calcItineraryRec(cities, startCity, itinerary, res, visited, found);
        if (res.size() - 1 == cities.size())
            return res;
    }
    return {};
}

这是一个使用示例:

    int main()
    {
        vector<pair<string, string>> cities = { {"Y", "Z"}, {"W", "X"}, {"X", "Y"}, {"Y", "W"}, {"W", "Y"}};
        vector<string> itinerary = calcItinerary(cities); // { "W", "X", "Y", "W", "Y", "Z" }
        // another route is possible {W Y W X Y Z}, but the route above is lexicographically smaller.

        cities = { {"Y", "Z"}, {"W", "X"}, {"X", "Y"}, {"W", "Y"} };
        itinerary = calcItinerary(cities);  // empty, no way to travel all cities using each ticket exactly one time
    }

【讨论】:

    【解决方案2】:

    我写了一个小 Python 程序,使用两个哈希表,一个用于计数,另一个用于 src 到 dst 的映射。 复杂性取决于字典的实现。如果字典有 O(1) 那么复杂度是 O(n) ,如果字典有 O( lg(n) ) 就像在 STL 映射中一样,那么复杂度是 O( n lg(n) )

    import random
    # actual journey: a-> b -> c -> ... g -> h
    journey = [('a','b'), ('b','c'), ('c','d'), ('d','e'), ('e','f'), ('f','g'), ('g','h')]
    #shuffle the journey.
    random.shuffle(journey)
    print("shffled journey : ", journey )
    # Hashmap to get the count of each place
    map_count = {}
    # Hashmap to find the route, contains src to dst mapping
    map_route = {}
    
    # fill the hashtable
    for j in journey:
        source = j[0]; dest = j[1]
        map_route[source] = dest
        i = map_count.get(source, 0)
        map_count[ source ] = i+1
        i = map_count.get(dest, 0)
        map_count[ dest ] = i+1
    
    start = ''
    # find the start point: the map entry with count = 1 and 
    # key exists in map_route.
    for (key,val) in map_count.items():
        if map_count[key] == 1 and map_route.has_key(key):
            start = key
            break
    
    print("journey started at : %s" % start)
    route = [] # the route
    n = len(journey)  # number of cities.
    while n:
        route.append( (start, map_route[start]) )
        start = map_route[start]
        n -= 1
    
    print(" Route : " , route )
    

    【讨论】:

      【解决方案3】:

      它基本上是一个依赖图,其中每张机票代表一个节点,srcdst 机场代表有向链接,因此使用拓扑排序来确定航班顺序。

      编辑:虽然这是一张机票,并且您知道您实际上制定了一个您可以实际执行的行程,但请按 UTC 的出发日期和时间排序。

      EDIT2:假设每个机场您有一张使用三字符代码的机票,您可以使用此处描述的算法 (Find three numbers appeared only once) 通过将所有机场异或在一起来确定两个唯一机场。

      EDIT3:这里有一些 C++ 使用 xor 方法实际解决了这个问题。整体算法如下,假设从机场到整数的唯一编码(假设是三个字母的机场代码或使用经纬度将机场位置编码为整数):

      首先,将所有机场代码异或在一起。这应该等于初始源机场 XOR 最终目的地机场。由于我们知道初始机场和最终机场是唯一的,因此该值不应为零。由于它不是零,因此该值中至少会设置一位。该位对应于在其中一个机场中设置而在另一个机场中未设置的位;称它为标志位。

      接下来,设置两个存储桶,每个存储桶都具有第一步中的异或值。现在,对于每张机票,根据是否设置了指定位来对每个机场进行存储,并将机场代码与存储桶中的值进行异或。还要为每个桶跟踪有多少源机场和目的地机场进入了该桶。

      处理完所有票后,选择其中一个桶。发送到该存储桶的源机场数量应该比发送到该存储桶的目的地机场数量多或少一。如果源机场的数量小于目的地机场的数量,这意味着初始源机场(唯一唯一的源机场)被发送到另一个桶。这意味着当前存储桶中的值是初始源机场的标识符!反之,如果目的机场数小于源机场数,则将最终目的机场发送到其他桶,因此当前桶就是最终目的机场的标识符!

      struct ticket
      {
          int src;
          int dst;
      };
      
      int get_airport_bucket_index(
          int airport_code, 
          int discriminating_bit)
      {
          return (airport_code & discriminating_bit)==discriminating_bit ? 1 : 0;
      }
      
      void find_trip_endpoints(const ticket *tickets, size_t ticket_count, int *out_src, int *out_dst)
      {
          int xor_residual= 0;
      
          for (const ticket *current_ticket= tickets, *end_ticket= tickets + ticket_count; current_ticket!=end_ticket; ++current_ticket)
          {
              xor_residual^= current_ticket->src;
              xor_residual^= current_ticket->dst;
          }
      
          // now xor_residual will be equal to the starting airport xor ending airport
          // since starting airport!=ending airport, they have at least one bit that is not in common
          // 
      
          int discriminating_bit= xor_residual & (-xor_residual);
      
          assert(discriminating_bit!=0);
      
          int airport_codes[2]= { xor_residual, xor_residual };
          int src_count[2]= { 0, 0 };
          int dst_count[2]= { 0, 0 };
      
          for (const ticket *current_ticket= tickets, *end_ticket= tickets + ticket_count; current_ticket!=end_ticket; ++current_ticket)
          {
              int src_index= get_airport_bucket_index(current_ticket->src, discriminating_bit);
      
              airport_codes[src_index]^= current_ticket->src;
              src_count[src_index]+= 1;
      
              int dst_index= get_airport_bucket_index(current_ticket->dst, discriminating_bit);
              airport_codes[dst_index]^= current_ticket->dst;
              dst_count[dst_index]+= 1;
          }
      
          assert((airport_codes[0]^airport_codes[1])==xor_residual);
          assert(abs(src_count[0]-dst_count[0])==1); // all airports with the bit set/unset will be accounted for as well as either the source or destination
          assert(abs(src_count[1]-dst_count[1])==1);
          assert((src_count[0]-dst_count[0])==-(src_count[1]-dst_count[1]));
      
          int src_index= src_count[0]-dst_count[0]<0 ? 0 : 1; 
          // if src < dst, that means we put more dst into the source bucket than dst, which means the initial source went into the other bucket, which means it should be equal to this bucket!
      
          assert(get_airport_bucket_index(airport_codes[src_index], discriminating_bit)!=src_index);
      
          *out_src= airport_codes[src_index];
          *out_dst= airport_codes[!src_index];
      
          return;
      }
      
      int main()
      {
          ticket test0[]= { { 1, 2 } };
          ticket test1[]= { { 1, 2 }, { 2, 3 } };
          ticket test2[]= { { 1, 2 }, { 2, 3 }, { 3, 4 } };
          ticket test3[]= { { 2, 3 }, { 3, 4 }, { 1, 2 } };
          ticket test4[]= { { 2, 1 }, { 3, 2 }, { 4, 3 } };
          ticket test5[]= { { 1, 3 }, { 3, 5 }, { 5, 2 } };
      
          int initial_src, final_dst;
      
          find_trip_endpoints(test0, sizeof(test0)/sizeof(*test0), &initial_src, &final_dst);
          assert(initial_src==1);
          assert(final_dst==2);
      
          find_trip_endpoints(test1, sizeof(test1)/sizeof(*test1), &initial_src, &final_dst);
          assert(initial_src==1);
          assert(final_dst==3);
      
          find_trip_endpoints(test2, sizeof(test2)/sizeof(*test2), &initial_src, &final_dst);
          assert(initial_src==1);
          assert(final_dst==4);
      
          find_trip_endpoints(test3, sizeof(test3)/sizeof(*test3), &initial_src, &final_dst);
          assert(initial_src==1);
          assert(final_dst==4);
      
          find_trip_endpoints(test4, sizeof(test4)/sizeof(*test4), &initial_src, &final_dst);
          assert(initial_src==4);
          assert(final_dst==1);
      
          find_trip_endpoints(test5, sizeof(test5)/sizeof(*test5), &initial_src, &final_dst);
          assert(initial_src==1);
          assert(final_dst==2);
      
          return 0;
      }
      

      【讨论】:

      • 不完全——它实际上是一个链表(我想这是一个 DAG)。问题是您没有指针,只有随机顺序的元素名称。
      • @Niki,您仍然可以对其进行拓扑排序以获得正确的排序。或者您可以按航班到达或起飞时间排序。
      • 对,但您最初没有要排序的结构。初始设置中没有任何边缘。如果我看到一张写着“波士顿 -> 纽约”的票,我必须先找到“纽约 -> 任何”,然后才能开始排序。您对出发时间提出了很好的看法,但我怀疑实际问题不在于机票。
      • +十亿按日期排序
      • @Niki,票是边(src->dst)和节点(行程)。鉴于它们形成了一个 DAG,您必须执行拓扑排序来确定它们的顺序。这里介绍的所有解决方案都是拓扑排序的具体实现。是的,您需要对整个图进行拓扑排序。就像您对任何潜在解决方案一样,因为在最坏的情况下,您处理的最后两张票是开始票和结束票。
      【解决方案4】:

      总结:下面给出了一个单遍算法。 (即,不仅是线性的,而且每张票准确查看一次,这当然是每张票的最佳访问次数)。我把这个总结,因为有许多看似相同的解决方案,而且很难发现我为什么要添加另一个解决方案。 :)

      我实际上是在一次采访中被问到这个问题的。这个概念非常简单:每张票都是一个单例列表,在概念上包含两个元素,src 和 dst。

      我们使用哈希表中的第一个和最后一个元素作为键来索引每个这样的列表,因此我们可以在 O(1) 中找到一个列表是否开始或结束于特定元素(机场)。对于每张票,当我们看到它从另一个列表结束的地方开始时,只需链接列表 (O(1))。同样,如果它在另一个列表开始的地方结束,则另一个列表加入。当然,当我们链接两个列表时,我们基本上是销毁这两个并获得一个。 (N个票的链将在N-1个这样的链接之后构建)。

      需要注意保持哈希表键恰好是剩余列表的第一个和最后一个元素的不变量。

      总而言之,O(N)。

      是的,我当场回答了这个问题:)

      编辑忘记添加重要的一点。每个人都提到了 两个 哈希表,但其中一个也能解决问题,因为算法不变性包括最多 一个 票列表在任何单个城市开始或开始(如果有两个,我们立即加入该城市的列表,并从哈希表中删除该城市)。渐近没有区别,只是这样更简单。

      编辑 2 另外有趣的是,与使用 2 个具有 N 个条目 每个 的哈希表的解决方案相比,此解决方案使用一个 最多 的哈希表> N/2 个条目(如果我们按照第 1、第 3、第 5 等顺序查看门票,就会发生这种情况)。所以除了更快之外,这也使用了大约一半的内存。

      【讨论】:

      • 我喜欢这个。如果您实际上有实体票并且是手工完成的,这似乎也是您解决问题的方法。
      • 谢谢。希望即使我懒得在细节中写下所需的细节,这个想法也能实现:)这是一种简单的图像可以使算法立即清晰的东西,但文字描述相当尴尬。
      • 您的意思是联合哈希键&lt;src, dst&gt; 还是一种具有srcdst 两个单独哈希函数的多维哈希集。因为我看不到您将如何使用联合哈希键来搜索特定的src任意 dst。假设我们要连接航班&lt;src=s, dst=d&gt;,那么我们必须对现有序列执行两次查找:&lt;src=d, _&gt;&lt;_, dst=s&gt;,对吗?你想到了什么样的哈希函数?
      【解决方案5】:

      这是单路径状态机矩阵的简单情况。 很抱歉伪代码是 C# 风格的,但是用对象来表达这个想法更容易。

      首先,构造一个收费公路矩阵。 在What are some strategies for testing large state machines?阅读我对什么是收费公路矩阵的描述(不要理会 FSM 的答案,只是对收费公路矩阵的解释)。

      但是,您描述的限制使案例成为一个简单的单路径状态机。它是覆盖范围最广的最简单状态机。

      对于 5 个机场的简单案例,
      vert nodes=src/入口点,
      水平节点=dst/出口点。

         A1 A2 A3 A4 A5
      A1        x
      A2  x
      A3           x
      A4              x
      A5     x
      

      请注意,对于每一行以及每一列,不应有多个转换。

      要获得机器的路径,您可以将矩阵排序为

         A1 A2 A3 A4 A5
      A2  x
      A1        x
      A3           x
      A4              x
      A5     x
      

      或者排序成对角方阵——有序对的特征向量。

         A1 A2 A3 A4 A5
      A2  x
      A5     x
      A1        x
      A3           x
      A4              x
      

      其中有序对是票的列表:

      a2:a1、a5:a2、a1:a3、a3:a4、a4:a5。

      或更正式的表示法,

      <a2,a1>, <a5,a2>, <a1,a3>, <a3,a4>, <a4,a5>.
      

      嗯 .. 有序对吧?在 Lisp 中闻到一丝递归的味道?

      <a2,<a1,<a3,<a4,a5>>>>
      

      本机有两种模式,

      1. 旅行计划 - 你不知道怎么做 那里有很多机场,而你 需要一个通用的旅行计划 机场数量不详
      2. 旅行重建 - 你拥有一切 过去旅行的收费公路车票 但他们都是一大堆 你的杂物箱/行李袋。

      我假设您的问题是关于旅行重建的。所以,你从那一堆票中随机挑选一张一张。

      我们假设票堆大小不定。

           tak mnx cda 
      bom    0
      daj       0
      phi           0
      

      其中 0 值表示未排序的票。让我们将无序票定义为其 dst 与另一个票的 src 不匹配的票。

      下面的下一张票发现 mnx(dst) = kul(src) 匹配。

           tak mnx cda kul
      bom    0
      daj       1
      phi           0
      mnx               0
      

      在您选择下一张机票的任何时候,它都有可能连接两个连续的机场。如果发生这种情况,您可以从这两个节点中创建一个集群节点:

      <bom,tak>, <daj,<mnx,kul>>
      

      并且矩阵被缩减,

           tak cda kul
      bom    0
      daj          L1
      phi       0
      

      在哪里

      L1 = <daj,<mnx,kul>>
      

      这是主列表的子列表。

      继续挑选下一张随机票。

           tak cda kul svn xml phi
      bom    0
      daj          L1
      phi       0
      olm               0
      jdk                   0
      klm                       0
      

      将existent.dst 匹配到new.src
      或existent.src 到new.dst:

           tak cda kul svn xml
      bom    0
      daj          L1
      olm               0
      jdk                   0
      klm      L2
      
      
      <bom,tak>, <daj,<mnx,kul>>, <<klm,phi>, cda>
      

      上述拓扑练习仅用于视觉理解。以下是算法解决方案。

      这个概念是将有序对聚集到子列表中,以减少我们将用来存放票证的哈希结构的负担。逐渐地,将会有越来越多的伪票(由合并的匹配票组成),每个伪票都包含越来越多的有序目的地子列表。最后,将在其子列表中保留一张包含完整行程矢量的伪票。

      如您所见,也许最好使用 Lisp 来完成。

      但是,作为链表和映射的练习......

      创建以下结构:

      class Ticket:MapEntry<src, Vector<dst> >{
        src, dst
        Vector<dst> dstVec; // sublist of mergers
      
        //constructor
        Ticket(src,dst){
          this.src=src;
          this.dst=dst;
          this.dstVec.append(dst);
        }
      }
      
      class TicketHash<x>{
        x -> TicketMapEntry;
      
        void add(Ticket t){
          super.put(t.x, t);
        }
      }
      

      这样有效,

      TicketHash<src>{
        src -> TicketMapEntry;
      
        void add(Ticket t){
          super.put(t.src, t);
        }
      }
      
      TicketHash<dst>{
        dst -> TicketMapEntry;
      
        void add(Ticket t){
          super.put(t.dst, t);
        }
      }    
      
      TicketHash<dst> mapbyDst = hash of map entries(dst->Ticket), key=dst
      TicketHash<src> mapbySrc = hash of map entries(src->Ticket), key=src
      

      当一张票从一堆中随机抽取时,

      void pickTicket(Ticket t){
        // does t.dst exist in mapbyDst?
        // i.e. attempt to match src of next ticket to dst of an existent ticket.
        Ticket zt = dstExists(t);
      
        // check if the merged ticket also matches the other end.
        if(zt!=null)
          t = zt;
      
        // attempt to match dst of next ticket to src of an existent ticket.
        if (srcExists(t)!=null) return;
      
        // otherwise if unmatched either way, add the new ticket
        else {
          // Add t.dst to list of existing dst
          mapbyDst.add(t); 
          mapbySrc.add(t);
        }
      }
      

      检查是否存在 dst:

      Ticket dstExists(Ticket t){
        // find existing ticket whose dst matches t.src
        Ticket zt = mapbyDst.getEntry(t.src);
      
        if (zt==null) return false; //no match
      
        // an ordered pair is matched...
      
        //Merge new ticket into existent ticket
        //retain existent ticket and discard new ticket.
        Ticket xt = mapbySrc.getEntry(t.src);
      
        //append sublist of new ticket to sublist of existent ticket
        xt.srcVec.join(t.srcVec); // join the two linked lists.
      
        // remove the matched dst ticket from mapbyDst
        mapbyDst.remove(zt);
        // replace it with the merged ticket from mapbySrc
        mapbyDst.add(zt);
      
        return zt;
      }
      
      Ticket srcExists(Ticket t){
        // find existing ticket whose dst matches t.src
        Ticket zt = mapbySrc.getEntry(t.dst);
      
        if (zt==null) return false; //no match
      
        // an ordered pair is matched...
      
        //Merge new ticket into existent ticket
        //retain existent ticket and discard new ticket.
        Ticket xt = mapbyDst.getEntry(t.dst);
      
        //append sublist of new ticket to sublist of existent ticket
        xt.srcVec.join(t.srcVec); // join the two linked lists.
      
        // remove the matched dst ticket from mapbyDst
        mapbySrc.remove(zt);
        // replace it with the merged ticket from mapbySrc
        mapbySrc.add(zt);
      
        return zt;
      }
      

      检查存在的src:

      Ticket srcExists(Ticket t){
        // find existing ticket whose src matches t.dst
        Ticket zt = mapbySrc.getEntry(t.dst);
      
        if (zt == null) return null;
      
        // if an ordered pair is matched
      
        // remove the dst from mapbyDst
        mapbySrc.remove(zt);
      
        //Merge new ticket into existent ticket
        //reinsert existent ticket and discard new ticket.
        mapbySrc.getEntry(zt);
      
        //append sublist of new ticket to sublist of existent ticket
        zt.srcVec.append(t.srcVec);
        return zt;
      }
      

      我感觉上面有很多错别字,但这个概念应该是正确的。发现任何错别字,请有人帮忙改正。

      【讨论】:

        【解决方案6】:

        先决条件

        首先,创建某种包含部分路线的子行程结构。

        例如,如果您的完整行程是a-b-c-d-e-f-g,则子行程可能是b-c-d,即您行程的连接子路径。

        现在,创建两个哈希表,将城市映射到包含该城市的子行程结构。因此,一个哈希表代表子行程开始的城市,另一个代表子行程结束的城市。这意味着,一个城市最多只能在一个哈希表中出现一次。

        正如我们稍后会看到的,不是每个城市都需要存储,而只是每个子行程的开始和结束。

        构建子行程

        现在,一张一张地拿票。我们假设票从xy(由(x,y) 表示)。检查x是否是某个子行程的结束s(因为每个城市只访问一次,它不可能是另一个子行程的结束)。如果x是开头,只需在子行程s的末尾添加当前票(x,y)即可。如果没有以x 结尾的子行程,则检查是否有以y 开头的子行程t。如果是这样,请在t 的开头添加(x,y)。如果也没有这样的子行程t,只需创建一个仅包含(x,y) 的新子行程。

        应该使用一些特殊的“技巧”来处理子行程。

        • 创建包含(x,y) 的新子行程s 应将x 添加到“子行程开始城市”的哈希表中,并将y 添加到“子行程结束城市”的哈希表中。
        • 在子行程s=(y,...)的开头添加新票(x,y),应从起始城市哈希表中删除y,而是将x添加到起始城市哈希表中。
        • 在子行程s=(...,x) 的末尾添加新票(x,y),应从结束城市哈希表中删除x,而是将y 添加到结束城市哈希表中。

        使用这种结构,对应于一个城市的子行程可以在摊销 O(1) 中完成。

        在为所有门票完成此操作后,我们将进行一些次行程。请注意,我们最多有 (n-1)/2 = O(n) 在过程之后这样的子行程。

        连接子行程

        现在,我们只考虑一个接一个的子行程。如果我们有一个子行程s=(x,...,y),我们只需查看结束城市的哈希表,如果有一个以x 结尾的子行程t=(...,x)。如果是这样,我们将ts 连接到一个新的子行程。如果不是,我们知道,s 是我们的第一个子行程;然后,我们看,如果有另一个以y 开头的子行程u=(y,...)。如果是这样,我们将su 连接起来。我们这样做直到只剩下一个子行程(这个子行程就是我们原来的整个行程)。

        我希望我没有忽略一些事情,但是这个算法应该运行在:

        • 如果我们在O(1) 中实现为子行程添加票证,则可以在O(n) 中构建所有子行程(最多O(n))。如果我们有一些不错的指针结构或类似的东西(将子行程实现为链表),这应该没问题。同样更改哈希表中的两个值是(摊销的)O(1)。因此,此阶段消耗O(n) 时间。
        • 连接子行程直到只剩下一个也可以在O(n) 中完成。也看到这一点,我们只需要看看在第二阶段做了什么:哈希表查找,需要摊销 O(1) 和子行程连接,可以在 O(1) 中通过指针连接或其他方式完成。

        因此,整个算法需要时间O(n)可能是最佳的O-bound,因为至少每张票可能都需要查看.

        【讨论】:

          【解决方案7】:

          让我们暂时忘记数据结构和图表。

          首先我需要指出,每个人都假设没有循环。如果路线经过一个机场两次,那就是一个更大的问题。


          但让我们暂时保留假设。

          输入数据实际上已经是一个有序集合。每张机票都是将订单引入一组机场的关系的一个元素。 (英语不是我的母语,所以这些可能不是正确的数学术语)

          每张机票都包含这样的信息:airportX &lt; airportY,因此在通过机票时,算法可以重新创建从任何机场开始的有序列表。


          现在让我们放弃“线性假设”。不能从那种东西中定义顺序关系。输入数据必须被视为形式语法的产生规则,其中语法的词汇集是一组 ariport 名称。 一张这样的票:

          src: A
          dst: B
          

          实际上是一对产生式:

          A->AB
          B->AB
          

          你只能保留一个。

          现在你必须生成每一个可能的句子,但你可以使用每一个产生式规则一次。每个产生式只使用一次的最长句子是正确的解决方案。

          【讨论】:

          • 来自问题:您不会在同一个机场停留两次。
          【解决方案8】:

          请注意,如果任务确定源机场和目的地机场(而不是重建整个行程),那么这个难题可能会变得更有趣。

          也就是说,假设机场代码以整数形式给出,源机场和目的地机场可以使用 O(1) 次数据传递和 O(1) 次额外内存来确定(即,无需借助哈希表、排序、二分查找、等等)。

          当然,一旦找到源头,索引和遍历整个路径也变得微不足道,但从那时起,整个事情无论如何都需要至少 O(n) 额外的内存(除非你可以排序数据到位,顺便说一句,它允许在 O(n log n) 时间内用 O(1) 额外内存解决原始任务)

          【讨论】:

            【解决方案9】:

            在我看来,基于图形的方法就是基于这里。

            每个机场都是一个节点,每张机票都是一条边。让我们现在让每条边都是无向的。

            在第一阶段,您正在构建图表:对于每张票,您查找源和目的地并在它们之间建立一条边。

            现在构建了图,我们知道它是非循环的,并且只有一条路径通过它。毕竟,您只有旅行的机票,而且您从未去过同一个机场。

            在第二阶段,您正在搜索图表:选择任何节点,并在两个方向上启动搜索,直到您发现无法继续。这些是您的来源和目的地。

            如果您需要明确说明哪个是源哪个是目标,请为每条边添加一个目录属性(但将其保留为无向图)。一旦你有了候选源和目标,你就可以根据连接到它们的边缘来判断哪个是哪个。

            此算法的复杂性取决于查找特定节点所需的时间。如果你能达到 O(1),那么时间应该是线性的。您有 n 张票,因此构建图形需要 O(N) 步,然后搜索 O(N) 步,重建路径需要 O(N) 步。仍然是 O(N)。邻接矩阵将为您提供。

            如果你不能节省空间,你可以为节点做一个散列,这会给你 O(1) 在最佳散列和所有这些废话下。

            【讨论】:

              【解决方案10】:

              如果您假设一个可连接的列表结构可以存储所有内容(可能在磁盘上):

              1. 创建 2 个空哈希表 S 和 D
              2. 抓住第一个元素
              3. 在 D 中查找其 src
              4. 如果找到,从 D 中删除关联节点并将其链接到当前节点
              5. 如果没有找到,将节点插入到 Src 键上的 S
              6. 从 3 以另一种方式重复 srcdes, SD
              7. 从 2 开始重复下一个节点。

              O(n) 时间。至于空间,birthday paradox(或类似的东西)将使您的数据集比完整集小很多。在运气不好的情况下它仍然变大(最坏的情况是O(n)),您可以从哈希表中逐出随机运行并将它们插入到处理队列的末尾。您的速度可能会下降,但只要您可以远远超过预期碰撞的阈值 (~O(sqrt(n))),您应该会看到您的数据集(表和输入队列相结合)定期缩小。

              【讨论】:

                【解决方案11】:

                不需要哈希或类似的东西。 这里真正的输入大小不一定是票的数量(比如n),而是票的总“大小”(比如N),@的总数需要 987654321@ 对其进行编码。

                如果我们有一个 k 个字符的字母表(这里 k 大约是 42 个),我们可以使用桶排序技术对 n 的数组进行排序总长度为 N 的字符串,在 O(n + N + k) 时间内用 k 个字符的字母表编码。如果 n (微不足道)和 k (嗯,N 是数十亿,不是吗) p>

                1. 按照给出票的顺序,从票中提取所有机场代码并将它们存储在struct 中,其中代码为字符串,票索引为数字。
                2. 根据代码对 structs 数组进行桶排序
                3. 通过该排序数组运行并为每个新遇到的航空公司代码分配一个序号(从 0 开始)。对于具有相同代码(它们是连续的)的所有元素,转到票证(我们已经存储了带有代码的数字)并将票证的代码(选择右边,srcdst)更改为序号.
                4. 在遍历数组期间,我们可能会识别出原始来源 src0
                5. 现在所有票证都已将srcdst 重写为序数,并且票证可以解释为以src0 开头的一个列表。
                6. 对门票进行列表排名(= 顶级逻辑排序,跟踪与 src0 的距离)。

                【讨论】:

                  【解决方案12】:

                  创建两个数据结构:

                  Route
                  {
                    start
                    end
                    list of flights where flight[n].dest = flight[n+1].src
                  }
                  
                  List of Routes
                  

                  然后:

                  foreach (flight in random set)
                  {
                    added to route = false;
                    foreach (route in list of routes)
                    {
                      if (flight.src = route.end)
                      {
                        if (!added_to_route)
                        {
                          add flight to end of route
                          added to route = true
                        }
                        else
                        {
                          merge routes
                          next flight
                        }
                      }
                      if (flight.dest = route.start)
                      {
                        if (!added_to_route)
                        {
                          add flight to start of route
                          added to route = true
                        }
                        else
                        {
                          merge routes
                          next flight
                        }
                      }
                    }
                    if (!added to route)
                    {
                      create route
                    }
                  }
                  

                  【讨论】:

                    【解决方案13】:

                    哈希表不适用于大尺寸(例如原始问题中的数十亿);任何与他们合作过的人都知道他们只适用于小系列。您可以改为使用二叉搜索树,这会给您带来 O(n log n) 的复杂度。

                    最简单的方法是通过两次:第一次将它们全部添加到树中,由 src 索引。第二个遍历树并将节点收集到一个数组中。

                    我们可以做得更好吗?如果我们真的愿意,我们可以:我们可以一次性完成。将每张票表示为喜欢列表上的一个节点。最初,每个节点的 next 指针都有空值。对于每张票,在索引中输入其 src 和 dest。如果发生碰撞,则意味着我们已经有相邻的票;连接节点并从索引中删除匹配项。完成后,您将只通过一次,并且有一个空索引和一个按顺序排列的所有票证的链接列表。

                    这种方法明显更快:它只有一次,而不是两次;并且存储明显更小(最坏情况:n/2;最佳情况:1;典型情况:sqrt(n)),足以让您实际使用哈希而不是二叉搜索树。

                    【讨论】:

                    • 一些引用会很好(sqrt(n) 来自哪里?为什么哈希表只适用于“小”集?“小”有多小?)除此之外,很好的答案,特别是对于第一篇文章。 -> +1
                    • 当然:首先,让我们看看为什么这家商店比其他提案要小。每次进行匹配时,我们都会从存储中删除两个条目。我们可以同时拥有的最不匹配的条目是 n/2 - 下一个需要匹配 something。最坏情况 = n/2。最好的情况是每个条目都匹配一些东西,所以我们在商店里的一张票和没有票之间波动。通常介于这些极端之间。我们的商店越大,下一个条目就越有可能匹配其中的something;所以随着它变大,它变得变小的可能性也会变大。没有空间详细说明,但想想 sqrt(n)。
                    • 我了解最佳和最坏情况分析。我怀疑这是你的平均案例分析,因为我的直觉说它在 n 中是线性的。尝试证明:设 i 为商店中的段数。如果插入在所有段上随机选择的新段,则它平均与 2 * i / n 个节点相邻。也就是说,E[new_size] = E[old_size] + 1 - 2 * old_size / n。即本次迭代的不动点s满足s=s+1-2s/n,相当于s=n/2。是的,我做了许多简化的假设,但我怀疑是否有任何渐近相关的假设?
                    • "2 * i / n" 不正确;更像是“2 * i / (n - d)”,其中 d 对数下降到现在。至于哈希 - 为了提高效率,它们不能有太多的冲突。随着 n 的增长,大团块的可能性增加;避免碰撞所需的空间大致为 n^2 阶。见theory.cs.uiuc.edu/~jeffe/teaching/algorithms/notes/…。最后,如果我们需要使用磁盘,请忘记哈希——只有树是可行的。
                    • sqrt(n) 确实不正确;它实际上是 n/6。不要将其视为一个顺序过程;相反,问,如果我拿了 n 的 x 部分,我手里有多少?如果没有匹配,它是 xn,但是,如果没有匹配,它是 (x)(x-1)n。对从 0 到 1 的 x 进行积分,我们得到 n/6。
                    【解决方案14】:

                    构造两个哈希表(或尝试),一个在 src 上键入,另一个在 dst 上。随机选择一张票并在 src-hash 表中查找其 dst。对结果重复该过程,直到到达终点(最终目的地)。现在在 dst-keyed 哈希表中查找它的 src。对结果重复该过程,直到开始为止。

                    构造哈希表需要O(n),构造列表需要O(n),所以整个算法是O(n)。

                    编辑:实际上,您只需要构建一个哈希表。假设您构建了 src-keyed 哈希表。随机选择一张票,像以前一样,构建通往最终目的地的列表。然后从尚未添加到列表中的票中选择另一张随机票。跟随它的目的地,直到你打到你最初开始的票。重复此过程,直到您构建了整个列表。它仍然是 O(n),因为最坏的情况是您以相反的顺序选择门票。

                    编辑:在我的算法中交换了表名。

                    【讨论】:

                    • 我也开始考虑两个哈希,但实际上一个哈希就足够了。好主意,+1。
                    • 这是一个三遍算法。一次就够了!
                    【解决方案15】:

                    放入两个哈希: to_end = src -> des; to_beg = des -> src

                    选择任意一个机场作为起点S。

                    while(to_end[S] != null)
                       S = to_end[S];
                    

                    S 现在是您的最终目的地。重复另一张地图以找到您的起点。

                    如果没有正确检查,这感觉 O(N),前提是您有一个不错的 Hash 表实现。

                    【讨论】:

                    • 您不需要知道最终目的地。按照票链,您最终会到达它
                    • 这也是我想出来的,构建每个字典应该是 O(n),寻找起点应该是 O(n)(你不需要找到终点机场这种方式),以及用于重建行程的 O(n)。所以基本上 4 * O(n) 这基本上意味着 O(n)。
                    • 是的,重读后其实没必要找到结尾,我只是看到它被遗忘了。
                    【解决方案16】:

                    最简单的方法是使用哈希表,但它没有最好的最坏情况复杂度 (O(n2))

                    改为:

                    1. 创建一堆包含 (src, dst) O(n) 的节点
                    2. 将节点添加到列表并按 src 排序 O(n log n)
                    3. 对于每个 (destination) 节点,在列表中搜索对应的 (source) 节点 O(n log n)
                    4. 找到起始节点(例如,使用拓扑排序,或在步骤 3 中标记节点)O(n)

                    总体:O(n log n)

                    (对于这两种算法,我们假设字符串的长度可以忽略不计,即比较是 O(1))

                    【讨论】:

                    • 如果您真的担心从哈希表中获得最坏情况下的性能,那么您可以使用可以为您提供 O(k) 插入/查找的 trie。
                    • 哈希表的最坏情况 O(n²) 复杂度对于这个问题根本不成立,因为我们可以使用完美的哈希函数(机场编码是一个众所周知的列表)。仅当不知道此类功能时才会如此(并且对于专门设计的不良键,而不是在现实世界中)。
                    【解决方案17】:

                    每个机场都是node。每张票都是一个edge。制作一个邻接矩阵来表示图。这可以作为压缩边缘的位字段来完成。您的起点将是没有路径的节点(它的列将为空)。了解这一点后,您只需遵循现有的路径即可。

                    或者,您可以构建一个可按机场索引的结构。对于您查找的每张票,它是 srcdst。如果没有找到任何一个,那么您需要将新机场添加到您的列表中。当每一个被找到时,你设置一个出发机场的出口指针指向目的地,而目的地的到达指针指向出发机场。当您没有门票时,您必须遍历整个列表以确定谁没有进入的路径。

                    另一种方法是创建一个可变长度的迷你旅行列表,当您遇到每张票时将这些迷你旅行连接在一起。每次添加票时,您都会查看任何现有迷你旅行的结尾是否与您的票的 src 或 dest 匹配。如果没有,那么您当前的机票将成为它自己的迷你旅行并被添加到列表中。如果是这样,则将新票附加到它匹配的现有旅行的末尾,可能将两个现有的迷你旅行拼接在一起,在这种情况下,它将把迷你旅行的列表缩短一个。

                    【讨论】:

                      【解决方案18】:

                      构造一个哈希表并将每个机场添加到哈希表中。

                      &lt;key,value&gt; = &lt;airport, count&gt;

                      如果机场是源或目的地,机场的计数会增加。因此,对于每个机场,计数将为 2(src 为 1,dst 为 1),但旅行的来源和目的地除外,计数为 1。

                      您需要至少查看每张票一次。所以复杂度是 O(n)。

                      【讨论】:

                      • 获取起点和终点位置是O(n),但是你仍然需要把剩下的行程联系起来……
                      • 您甚至不需要找到起始机场。您可以随机选择一个,尽可能多地构建,然后随机选择另一个,等等,直到您构建了整个行程。
                      • @blueraja - 您只插入每个元素一次。 O(n) 构建表,O(n) 构建行程。总共 O(n + n),也就是 O(n)。
                      • @blueraja - 插入哈希表是 O(1)。散列密钥通常是 O(k),其中 k 是密钥的长度。所以算法是 O(nk),实际上是 O(n),因为机场名称不是那么长(我们可以选择一个常数 c,使得 n 中的所有 k 的 c > k 并且 c
                      • 哈希表插入通常以摊销 O(1) 的形式给出。
                      猜你喜欢
                      • 2022-11-02
                      • 1970-01-01
                      • 1970-01-01
                      • 2017-07-17
                      • 2021-04-02
                      • 2019-09-08
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多