views:

1853

answers:

16

You are going on a one-way indirect flight trip that includes billions an unknown very large number of transfers.

  • You are not stopping twice in the same airport.
  • You have 1 ticket for each part of your trip.
  • Each ticket contains src and dst airport.
  • All the tickets you have are randomly sorted.
  • You forgot the original departure airport (very first src) and your destination (last dst).

Design an algorithm to reconstruct your trip with minimum big-O complexity.


Attempting to solve this problem I have started to use a symmetric difference of two sets, Srcs and Dsts:

1)Sort all src keys in array Srcs
2)Sort all dst keys in array Dsts
3)Create an union set of both arrays to find non-duplicates - they are your first src and last dst
4)Now, having the starting point, traverse both arrays using the binary search.

But I suppose there must be another more effective method.

+5  A: 

It's basically a dependency graph where every ticket represents a node and the src and dst airport represents directed links, so use a topological sort to determine the flight order.

EDIT: Although since this is an airline ticket and you know you actually made an itinerary you could physically perform, sort by departure date and time in UTC.

EDIT2: Assuming each airport you have a ticket to uses a three character code, you can use the algorithm described here (http://stackoverflow.com/questions/3003021/find-three-numbers-appeared-only-once-using-bit-manipulation/3003112#3003112) to determine the two unique airports by xoring all the airports together.

EDIT3: Here's some C++ to actually solve this problem using the xor method. The overall algorithm is as follows, assuming a unique encoding from airport to an integer (either assuming a three letter airport code or encoding the airport location in an integer using latitude and longitude):

First, XOR all the airport codes together. This should be equal to the initial source airport XOR the final destination airport. Since we know that the initial airport and the final airport are unique, this value should not be zero. Since it's not zero, there will be at least one bit set in that value. That bit corresponds to a bit that is set in one of the airports and not set in the other; call it the designator bit.

Next, set up two buckets, each with the XORed value from the first step. Now, for every ticket, bucket each airport according to whether it has the designator bit set or not, and xor the airport code with the value in the bucket. Also keep track for each bucket how many source airports and destination airports went to that bucket.

After you process all the tickets, pick one of the buckets. The number of source airports sent to that bucket should be one greater or less than the number of destination airports sent to that bucket. If the number of source airports is less than the number of destination airports, that means the initial source airport (the only unique source airport) was sent to the other bucket. That means the value in the current bucket is the identifier for the initial source airport! Conversely, if the number of destination airports is less than the number of source airports, the final destination airport was sent to the other bucket, so the current bucket is the identifier for the final destination airport!

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;
}
MSN
Not quite - it's really a linked list (which is a DAG I suppose). The problem is that you don't have the pointers, just names for elements that are in random order.
Niki Yoshiuchi
@Niki, you can still do a topological sort on it to get a correct ordering. Or you could sort by flight arrival or departure times.
MSN
Right, but you initially don't have a structure to sort. There aren't any edges in the initial set up. If I look at a ticket that says "Boston -> New York" I have to find "New York -> whatever" before I can start sorting. You make a good point about departure times, but I suspect that the actual problem is not about airline tickets.
Niki Yoshiuchi
+billions for sorting by date
John Rasch
@Niki, the tickets are the edges (src->dst) and the nodes (trip). Given that they form a DAG, you have to perform a topological sort to determine their ordering. All the solutions presented here are specific implementations of a topological sort. And yes, you need the entire graph to do a topological sort. As you would for any potential solution, since in the worst case the last two tickets you process are the start and end tickets.
MSN
You are correct. I worded my initial comment poorly - what I meant was it's not quite as simple as just a topological sort because you can't look up an edge in O(1) time without doing something else first. You need to build an adjacency list first, then you do a topological sort. Sorry if that was unclear.
Niki Yoshiuchi
-trillions for sorting by non existant data 'date/time'
Greg Domjan
@Greg, duh, it was a joke. Although not really since if you were to do this for real the ticket would have a departure date and time.
MSN
+24  A: 

Construct a hashtable and add each airport into the hash table.

<key,value> = <airport, count>

Count for the airport increases if the airport is either the source or the destination. So for every airport the count will be 2 ( 1 for src and 1 for dst) except for the source and the destination of your trip which will have the count as 1.

You need to look at each ticket at least once. So complexity is O(n).

srikfreak
Getting the starting and ending location is O(n), but you still have to link up the rest of the trip...
dmckee
I was just about to write that same answer :P
Wayne Werner
Haha, same here. Now given that you know the source and destination if you had all of the tickets loaded in to a map<source, dest> you could reconstruct the trip fairly easily.
Ben Burnett
@dmckee - nope, once you have them in the hash table you have your trip reconstructed, because each ticket holds two pieces of information - itself, and where it's going. At the end even if you need to look through the whole table to find the src and dst you still only have 2N => O(n).
Wayne Werner
You don't even need to find the starting airport. You can just choose one at random, construct as much as possible, then choose another at random, etc. until you've constructed the entire trip.
Niki Yoshiuchi
Complexity is `O(n^2)` - inserting into hash table is not O(1)...
BlueRaja - Danny Pflughoeft
@blueraja - you are only inserting each element once. O(n) to construct the table, O(n) to construct the trip. O(n + n) total, which is O(n).
Niki Yoshiuchi
@Niki: Constructing the table is `O(n^2)`. There are n elements and O(n) for inserting each one (hash tables are not O(1) insertion!)
BlueRaja - Danny Pflughoeft
@blueraja - Inserting into a hash table is O(1). Hashing a key is typically O(k) where k is the length of the key. So the algorithm is O(nk) which is effectively O(n) since airport names aren't that long (we can choose a costant c such that c > k for all k in n and c << n).
Niki Yoshiuchi
@Niki: That is simply incorrect. [Inserting and retrieving from a hash table](http://en.wikipedia.org/wiki/Hash_table#Performance_analysis) is `O(n/t)` average (t = size of table) and `O(n)` worst case. When simply "O(n)" is stated, we usually mean worst case.
BlueRaja - Danny Pflughoeft
Your link also points out that the O(n/t) bound is constant if you use table resizing to maintain n/t < c for c < 1. You are correct that the worst case is O(n) but I doubt there are many implementations of hash tables that can be expected to exhibit worst case behavior (especially inputs that haven't been constructed for such a purpose). As such it's a fair assumption that insertions are going to take O(k) (where k is the key length) in this particular example.
Niki Yoshiuchi
Hashtable insertions are usually given as amortized O(1).
Lasse V. Karlsen
In the real world, each airport is represented by a 3 upper case letter sequence which can be packed into 15 bits. So an array of 32768 short integers will do as well as a hash table. Array access is O(1), I'm pretty sure. In a hypothetical world with billions of airports, there must be the resources for huge amounts of RAM too, so you can still use an array, or a hash table large enough to have a low probability of collisions, so the answer for the data structure access is still O(1) - or near enough and the algorithm O(n).
JeremyP
BTW I am assuming short int is 16 bits,.
JeremyP
I would suggest a minor modification. count++ when you encounter an airport as a source, count-- when it's a destination. That way you can tell right away which is which.
patros
The answer as written does not actual solve the problem; there is no route reconstruction, i.e., the entire step-by-step path is needed.
Joel Hoff
"You need to look at each ticket at least once" - this means Ω(n), not O(n) (but yes, the algorithm is O(N)). By the way, see my answer for a single-pass algorithm.
Dimitris Andreou
@Lasse: "expected" O(1), not "amortized" O(1).
Dimitris Andreou
+9  A: 

Construct two hash tables (or tries), one keyed on src and the other on dst. Choose one ticket at random and look up its dst in the src-hash table. Repeat that process for the result until you hit the end (the final destination). Now look up its src in the dst-keyed hash table. Repeat the process for the result until you hit the beginning.

Constructing the hash tables takes O(n) and constructing the list takes O(n), so the whole algorithm is O(n).

EDIT: You only need to construct one hash table, actually. Let's say you construct the src-keyed hash table. Choose one ticket at random and like before, construct the list that leads to the final destination. Then choose another random ticket from the tickets that have not yet been added to the list. Follow its destination until you hit the ticket you initially started with. Repeat this process until you have constructed the entire list. It's still O(n) since worst case you choose the tickets in reverse order.

Edit: got the table names swapped in my algorithm.

Niki Yoshiuchi
I also started thinking about two hashes, but indeed one hash is sufficient. Good idea, +1.
Patrick
That's a three-pass algorithm. A single pass is enough!
Dimitris Andreou
+1  A: 

Each airport is a node. Each ticket is an edge. Make an adjacency matrix to represent the graph. This can be done as a bit field to compress the edges. Your starting point will be the node that has no path into it (it's column will be empty). Once you know this you just follow the paths that exist.

Alternately you could build a structure indexable by airport. For each ticket you look up it's src and dst. If either is not found then you need to add new airports to your list. When each is found you set a the departure airport's exit pointer to point to the destination, and the destination's arrival pointer to point to the departure airport. When you are out of tickets you must traverse the entire list to determine who does not have a path in.

Another way would be to have a variable length list of mini-trips that you connect together as you encounter each ticket. Each time you add a ticket you see if the ends of any existing mini-trip match either the src or dest of you ticket. If not, then your current ticket becomes it's own mini-trip and is added to the list. If so then the new ticket is tacked on to the end(s) of the existing trip(s) that it matches, possibly splicing two existing mini-trips together, in which case it would shorten the list of mini-trips by one.

nategoose
A: 

Easiest way is with hash tables, but that doesn't have the best worst-case complexity (O(n2))

Instead:

  1. Create a bunch of nodes containing (src, dst) O(n)
  2. Add the nodes to a list and sort by src O(n log n)
  3. For each (destination) node, search the list for the corresponding (source) node O(n log n)
  4. Find the start node (for instance, using a topological sort, or marking nodes in step 3) O(n)

Overall: O(n log n)

(For both algorithms, we assume the length of the strings is negligible ie. comparison is O(1))

BlueRaja - Danny Pflughoeft
If you are truly worried about getting worst case performance from a hash table then you can use a trie which will give you O(k) insert/lookup.
Niki Yoshiuchi
Worst-case O(n²) complexity for hash tables is simply not true for this problem, as we can use a perfect hash function (airports encodings are a well known list). That is only true when no such function is known (and for specially designed poor keys, not in real world).
kriss
+2  A: 

Put in two Hashes: to_end = src -> des; to_beg = des -> src

Pick any airport as a starting point S.

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

S is now your final destination. Repeat with the other map to find your starting point.

Without properly checking, this feels O(N), provided you have a decent Hash table implementation.

wowest
you don;t need to know the final destination. following the ticket chain you'll eventually get to it
George
This is what I came up with as well, it should be O(n) for building each dictionary, and O(n) for finding the start (you don't need to find the end airport this way), and O(n) for reconstructing the trip. So basically 4 * O(n) which basically means O(n).
Lasse V. Karlsen
True, upon re-reading its not really necessary to find the end, I just saw that it was forgotten.
wowest
+2  A: 

A hash table won't work for large sizes (such as the billions in the original question); anyone who has worked with them knows that they're only good for small sets. You could instead use a binary search tree, which would give you complexity O(n log n).

The simplest way is with two passes: The first adds them all to the tree, indexed by src. The second walks the tree and collects the nodes into an array.

Can we do better? We can, if we really want to: we can do it in one pass. Represent each ticket as a node on a liked list. Initially, each node has null values for the next pointer. For each ticket, enter both its src and dest in the index. If there's a collision, that means that we already have the adjacent ticket; connect the nodes and delete the match from the index. When you're done, you'll have made only one pass, and have an empty index, and a linked list of all the tickets in order.

This method is significantly faster: it's only one pass, not two; and the store is significantly smaller (worst case: n/2 ; best case: 1; typical case: sqrt(n)), enough so that you might be able to actually use a hash instead of a binary search tree.

SRobertJames
A few citations would be nice (where does the sqrt(n) come from? Why are hash tables only good for "small" sets? A how small is "small"?) Other than that, nice answer, especially for a first post. -> +1
meriton
Sure: First, let's look at why this store is smaller than the other proposals. Every time we make a match, we delete two entries from the store. The most unmatched entries we can have at once is n/2 - the next one needs to match _something_. Worst case = n/2. Best case is every entry matches something, so we fluctuate between 1 ticket in store and none. Usually where in between those extremes. The larger our store is, the more likely the next entry is to match _something_ in it; so as it grows larger, so does it's likelihood of growing _smaller_. No space to elaborate, but think sqrt(n).
SRobertJames
I understood best and worst case analysis. It's your average case analysis I doubt, because my intuition says it is linear in n. Attempted proof: Let i be the number of segments in the store. If a new segment chosen at random over all segments is inserted, it is adjacent to 2 * i / n nodes on average. That is, E[new_size] = E[old_size] + 1 - 2 * old_size / n. That is, the fix point s of this iteration satisfies s = s + 1 - 2s/n, which is equivalent to s = n/2. Yes, I made numerous simplifying assumptions, but I doubt any will be asymptotically relevant?
meriton
"2 * i / n" isn't correct; more like "2 * i / (n - d)" where d number of pairs dropped until now. As for hashes - to be efficient, they can't have too many collisions. As n grows, the likelihood of large clumps increases; space needed to avoid collisions is roughly order n^2. See http://theory.cs.uiuc.edu/~jeffe/teaching/algorithms/notes/07-hashing.pdf . Finally, if we need to use disk, forget about hashes - only trees are viable.
SRobertJames
sqrt(n) is indeed incorrect; it's actually n/6. Don't look at it as a sequential process; instead, ask, if I've taken x portion of n, how many do I have expect in my hand? With no matches it's xn, but, with dropping matches, it's (x)(x-1)n. Integrating that over x ranging from 0 to 1, we get n/6.
SRobertJames
I think your math may be a bit off for space complexity. Sure, your hashtable will be half the size, but your partial trips data structure will be twice as big as the ticket objects. Your average memory usage will be lower, but your worst case will be just as bad.
Jason
+3  A: 

Create two data structures:

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

List of Routes

And then:

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
  }
}
Skizz
+11  A: 

Summary: below a single-pass algorithm is given. (I.e., not just linear, but looks each ticket exactly once, which of course is optimal number of visits per ticket). I put the summary because there are many seemingly equivalent solutions and it would be hard to spot why I added another one. :)

I was actually asked this question in an interview. The concept is extremely simple: each ticket is a singleton list, with conceptually two elements, src and dst.

We index each such list in a hashtable using its first and last elements as keys, so we can find in O(1) if a list starts or ends at a particular element (airport). For each ticket, when we see it starts where another list ends, just link the lists (O(1)). Similarly, if it ends where another list starts, another list join. Of course, when we link two lists, we basically destroy the two and obtain one. (The chain of N tickets will be constructed after N-1 such links).

Care is needed to maintain the invariant that the hashtable keys are exactly the first and last elements of the remaining lists.

All in all, O(N).

And yes, I answered that on the spot :)

Edit Forgot to add an important point. Everyone mentions two hashtables, but one does the trick as well, because the algorithms invariant includes that at most one ticket list starts or begins in any single city (if there are two, we immediately join the lists at that city, and remove that city from the hashtable). Asymptotically there is no difference, it's just simpler this way.

Edit 2 Also of interest is that, compared to solutions using 2 hashtables with N entries each, this solution uses one hashtable with at most N/2 entries (which happens if we see the tickets in an order of, say, 1st, 3rd, 5th, and so on). So this uses about half memory as well, apart from being faster.

Dimitris Andreou
I like this. This also seems to be how you would go about solving the problem if you actually had physical tickets and were doing it by hand.
Daniel Straight
Thank you. Hopefully the idea comes through even with me being lazy to write the required details in, well, detail :) It's the kind of thing that a simple image could make the algorithm immediately clear, but the textual description is rather awkward.
Dimitris Andreou
A: 

No need for hashes or something alike. The real input size here is not necessarily the number of tickets (say n), but the total 'size' (say N) of the tickets, the total number of char needed to code them.

If we have a alphabet of k characters (here k is roughly 42) we can use bucketsort techniques to sort an array of n strings of a total size N that are encoded with an alphabet of k characters in O(n + N + k) time. The following works if n <= N (trivial) and k <= N (well N is billions, isn't it)

  1. In the order the tickets are given, extract all airport codes from the tickets and store them in a struct that has the code as a string and the ticket index as a number.
  2. Bucketsort that array of structs according to their code
  3. Run trough that sorted array and assign an ordinal number (starting from 0) to each newly encountered airline code. For all elements with the same code (they are consecutive) go to the ticket (we have stored the number with the code) and change the code (pick the right, src or dst) of the ticket to the ordinal number.
  4. During this run through the array we may identify original source src0.
  5. Now all tickets have src and dst rewritten as ordinal numbers, and the tickets may be interpreted as one list starting in src0.
  6. Do a list ranking (= toplogical sort with keeping track of the distance from src0) on the tickets.
Jens Gustedt
A: 

If you assume a joinable list structure that can store everything (probably on disk):

  1. Create 2 empty hash tables S and D
  2. grab the first element
  3. look up its src in D
  4. If found, remove the associated node from D and link it to the current node
  5. If not found, insert the node into S keyed on src
  6. repeat from 3 the other way src<->des, S<->D
  7. repeat from 2 with the next node.

O(n) time. As for space, the birthday paradox (or something much like it) will keep your data set a lot smaller than the full set. In the bad luck case where it still gets to large (worst case is O(n)), you can evict random runs from the hash table and insert them at the end of the processing queue. Your speed could go to pot but as long as you can far excede the threashold for expecting collisions (~O(sqrt(n))) you should expect to see your dataset (the tables and input queue combined) regularly shrink.

BCS
A: 

It seems to me like a graph-based approach is based here.

Each airport is a node, each ticket is an edge. Let's make every edge undirected for now.

In the first stage you are building the graph: for each ticket, you lookup the source and destination and build an edge between them.

Now that the graph is constructed, we know that it is acyclical and that there is a single path through it. After all, you only have tickets for trips you took, and you never visited the same airport once.

In the second stage, you are searching the graph: pick any node, and initiate a search in both directions until you find you cannot continue. These are your source and destination.

If you need to specifically say which was source and which was destination, add a directory property to each edge (but keep it an undirected graph). Once you have the candidate source and destination, you can tell which is which based on the edge connected to them.

The complexity of this algorithm would depend on the time it takes to lookup a particular node. If you could achieve an O(1), then the time should be linear. You have n tickets, so it takes you O(N) steps to build the graph, and then O(N) to search and O(N) to reconstruct the path. Still O(N). An adjacency matrix will give you that.

If you can't spare the space, you could do a hash for the nodes, which would give you O(1) under optimal hashing and all that crap.

Uri
A: 

Note that if the task were only to determine the source and destination airports (instead of reconstructing the whole trip), the puzzle would probably become more interesting.

Namely, assuming that airport codes are given as integers, the source and destination airports can be determined using O(1) passes of the data and O(1) additional memory (i.e. without resorting to hashtables, sorting, binary search, and the like).

Of course, once you find the source, it also becomes a trivial matter to index and traverse the full route, but from that point on the whole thing will require at least O(n) additional memory anyway (unless you can sort the data in place, which, by the way, allows to solve the original task in O(n log n) time with O(1) additional memory)

KT
A: 

Let's forget the data structures and graphs for a moment.

First I need to point out that everybody made an assumption that there are no loops. If the route goes through one airport twice than it's a much larger problem.


But let's keep the assumption for now.

The input data is in fact an ordered set already. Every ticket is an element of the relation that introduces order to a set of airports. (English is not my mother tongue, so these might not be correct math terms)

Every ticket holds information like this: airportX < airportY, so while doing one pass through the tickets an algorithm can recreate an ordered list starting from just any airport.


Now let's drop the "linear assumption". No order relation can be defined out of that kind of stuff. The input data has to be treated as production rules for a formal grammar, where grammar's vocabulary set is a set of ariport names. A ticket like that:

src: A
dst: B

is in fact a pair of productions:

A->AB
B->AB

from which you only can keep one.

Now you have to generate every possible sentence, but you can use every production rule once. The longest sentence that uses every its production only once is a correct solution.

naugtur
From the question: You are not stopping twice in the same airport.
MikeD
Damn my lazy reading...
naugtur
A: 

This is the simple case of a single path state machine matrix. Sorry for the pseudo-code being in C# style, but it was easier to express the idea with objects.

First, construct a turnpike matrix. Read my description of what a turnpike matrix is (don't bother with the FSM answer, just the explanation of a turnpike matrix) at http://stackoverflow.com/questions/2691073/testing-finite-state-machines/2698241#2698241.

However, the restrictions you describe make the case a simple single path state machine. It is the simplest state machine possible with complete coverage.

For a simple case of 5 airports,
vert nodes=src/entry points,
horiz nodes=dst/exit points.

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

Notice that for each row, as well as for each column, there should be no more than one transition.

To get the path of the machine, you would sort the matrix into

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

Or sort into a diagonal square matrix - an eigen vector of ordered pairs.

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

where the ordered pairs are the list of tickets:

a2:a1, a5:a2, a1:a3, a3:a4, a4:a5.

or in more formal notation,

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

Hmmm .. ordered pairs huh? Smelling a hint of recursion in Lisp?

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

There are two modes of the machine,

  1. trip planning - you don't know how many airports there are, and you need a generic trip plan for an unspecified number of airports
  2. trip reconstruction - you have all the turnpike tickets of a past trip but they are all one big stack in your glove compartment/luggage bag.

I am presuming your question is about trip reconstruction. So, you pick one ticket after another randomly from that pile of tickets.

We presume the ticket pile is of indefinite size.

     tak mnx cda 
bom    0
daj       0
phi           0

Where 0 value denotes unordered tickets. Let us define unordered ticket as a ticket where its dst is not matched with the src of another ticket.

The following next ticket finds that mnx(dst) = kul(src) match.

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

At any moment you pick the next ticket, there is a possibility that it connects two sequential airports. If that happen, you create a cluster node out of that two nodes:

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

and the matrix is reduced,

     tak cda kul
bom    0
daj          L1
phi       0

where

L1 = <daj,<mnx,kul>>

which is a sublist of the main list.

Keep on picking the next random tickets.

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

Match either existent.dst to new.src
or existent.src to 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>

The above topological exercise is for visual comprehension only. The following is the algorithmic solution.

The concept is to cluster ordered pairs into sublists to reduce the burden on the hash structures we will use to house the tickets. Gradually, there will be more and more pseudo-tickets (formed from merged matched tickets), each containing a growing sublist of ordered destinations. Finally, there will remain one single pseudo-ticket containing the complete itinerary vector in its sublist.

As you see, perhaps, this is best done with Lisp.

However, as an exercise of linked lists and maps ...

Create the following structures:

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

So that effectively,

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

When a ticket is randomly picked from the pile,

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

Check for existent 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;
}

Check for existent 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;
}

I have a feeling the above has quite some typos, but the concept should be right. Any typo found, someone could help correct it for, plsss.

Blessed Geek
A: 

Prerequisites

First of all, create some kind of subtrip structure that contains a part of your route.

For example, if your complete trip is a-b-c-d-e-f-g, a subtrip could be b-c-d, i.e. a connected subpath of your trip.

Now, create two hashtables that map a city to the subtrip structure the city is contained in. Thereby, one Hashtable stands for the city a subtrip is starting with, the other stands for the cities a subtrip is ending with. That means, one city can occur at most once in one of the hashtables.

As we will see later, not every city needs to be stored, but only the beginning and the end of each subtrip.

Constructing subtrips

Now, take the tickets just one after another. We assume the ticket to go from x to y (represented by (x,y)). Check, wheter x is the end of some subtrip s(since every city is visited only once, it can not be the end of another subtrip already). If x is the beginning, just add the current ticket (x,y) at the end of the subtrip s. If there is no subtrip ending with x, check whether there is a subtrip t beginning with y. If so, add (x,y) at the beginning of t. If there's also no such subtrip t, just create a new subtrip containing just (x,y).

Dealing with subtrips should be done using some special "tricks".

  • Creating a new subtrip s containing (x,y) should add x to the hashtable for "subtrip beginning cities" and add y to the hashtable for "subtrip ending cities".
  • Adding a new ticket (x,y) at the beginning of the subtrip s=(y,...), should remove y from the hashtable of beginning cities and instead add x to the hashtable of beginning cities.
  • Adding a new ticket (x,y) at the end of the subtrip s=(...,x), should remove x from the hashtable of ending cities and instead add y to the hashtable of ending cities.

With this structure, subtrips corresponding to a city can be done in amortized O(1).

After this is done for all tickets, we have some subtrips. Note the fact that we have at most (n-1)/2 = O(n) such subtrips after the procedure.

Concatenating subtrips

Now, we just consider the subtrips one after another. If we have a subtrip s=(x,...,y), we just look in our hashtable of ending cities, if there's a subtrip t=(...,x) ending with x. If so, we concatenate t and s to a new subtrip. If not, we know, that s is our first subtrip; then, we look, if there's another subtrip u=(y,...) beginning with y. If so, we concatenate s and u. We do this until just one subtrip is left (this subtrip is then our whole original trip).

I hope I didnt overlook somtehing, but this algorithm should run in:

  • constructing all subtrips (at most O(n)) can be done in O(n), if we implement adding tickets to a subtrip in O(1). This should be no problem, if we have some nice pointer structure or something like that (implementing subtrips as linked lists). Also changing two values in the hashtable is (amortized) O(1). Thus, this phase consumes O(n) time.
  • concatenating the subtrips until just one is left can also be done in O(n). Too see this, we just need to look at what is done in the second phase: Hashtable lookups, that need amortized O(1) and subtrip concatenation that can be done in O(1) with pointer concatenation or something.

Thus, the whole algorithm takes time O(n), which might be the optimal O-bound, since at least every ticket might need to be looked at.

phimuemue