LeetCode 1436 - Destination City

In this problem, we are given a list of directed paths between cities. Each entry in paths has the form [cityA, cityB],

LeetCode Problem 1436

Difficulty: 🟢 Easy
Topics: Array, Hash Table, String

Solution

Problem Understanding

In this problem, we are given a list of directed paths between cities. Each entry in paths has the form [cityA, cityB], which means there is a one way path from cityA to cityB.

The graph formed by these paths is guaranteed to represent a straight travel chain with no cycles. This guarantee is extremely important because it means:

  • There is exactly one city where the journey ends.
  • Every other city has at least one outgoing edge.
  • No city can lead back to a previously visited city.

The task is to find the destination city, which is the only city that never appears as a starting point in any path.

For example, if we have:

London -> New York
New York -> Lima
Lima -> Sao Paulo

then Sao Paulo is the destination city because no path starts from it.

The input size is very small, at most 100 paths, so efficiency is not a major concern. However, the problem is designed to test whether we can recognize the key graph property instead of overcomplicating the solution.

Some important observations from the constraints and guarantees:

  • Every path is directed.
  • There is exactly one valid answer.
  • No loops exist.
  • City names are strings, so hash based structures such as sets or maps are ideal.
  • Since the graph forms a line, we do not need graph traversal algorithms like DFS or BFS.

Important edge cases include:

  • A single path such as ["A", "Z"]
  • Cities appearing multiple times as intermediate stops
  • The destination city appearing only once in the entire input
  • Paths given in arbitrary order

Because the input order is not guaranteed to represent the travel order, we cannot simply return the last city from the final path.

Approaches

Brute Force Approach

A straightforward approach is to examine every destination city and check whether it ever appears as a starting city.

For every path [fromCity, toCity], we can scan the entire list again to determine whether toCity exists as any fromCity.

If we find a city that never appears as a source, that city must be the destination city.

This works correctly because the destination city is defined precisely as the city with zero outgoing paths.

However, this approach performs unnecessary repeated scans. For each city, we may traverse the entire list again, leading to quadratic time complexity.

Optimal Approach

The key insight is that the destination city is the only city that never appears as a starting city.

Instead of repeatedly scanning the input, we can store all source cities in a hash set. Then we iterate through the destination cities and return the one that is not present in the set.

Hash sets provide average O(1) lookup time, making the solution very efficient and simple.

Approach Comparison

Approach Time Complexity Space Complexity Notes
Brute Force O(n²) O(1) Repeatedly scans all paths for each destination city
Optimal O(n) O(n) Uses a hash set for constant time lookups

Algorithm Walkthrough

  1. Create an empty hash set called starting_cities.

This set will store every city that appears as a source city in the paths. We use a hash set because membership checks are very fast. 2. Iterate through all paths.

For each path [from_city, to_city], insert from_city into the set. 3. Iterate through all paths again.

For each path [from_city, to_city], check whether to_city exists in starting_cities. 4. If to_city is not in the set, return it immediately.

Since the graph guarantees exactly one destination city, the first such city we encounter is the correct answer. 5. If no city is found, return an empty string.

In practice, this line is never reached because the problem guarantees a valid destination city.

Why it works

The destination city is defined as the city with no outgoing paths. Every city with outgoing paths appears as a source city somewhere in the input. Therefore, the only city missing from the set of source cities must be the destination city.

Because the problem guarantees exactly one such city, the algorithm always returns the correct answer.

Python Solution

from typing import List

class Solution:
    def destCity(self, paths: List[List[str]]) -> str:
        starting_cities = set()

        for from_city, to_city in paths:
            starting_cities.add(from_city)

        for from_city, to_city in paths:
            if to_city not in starting_cities:
                return to_city

        return ""

The implementation follows the algorithm directly.

First, we create a set named starting_cities. During the first loop, every source city is inserted into the set.

In the second loop, we examine each destination city. If a destination city is absent from the set, it means that city never appears as a starting point, so it must be the final destination.

The final return "" exists only for completeness. According to the problem constraints, a valid destination city always exists.

Go Solution

func destCity(paths [][]string) string {
    startingCities := make(map[string]bool)

    for _, path := range paths {
        fromCity := path[0]
        startingCities[fromCity] = true
    }

    for _, path := range paths {
        toCity := path[1]

        if !startingCities[toCity] {
            return toCity
        }
    }

    return ""
}

The Go solution uses a map[string]bool to simulate a hash set because Go does not have a built in set type.

The logic is identical to the Python version:

  • Store all starting cities in the map
  • Scan destination cities
  • Return the one not found in the map

No special handling for integer overflow is needed because the problem only involves strings and small input sizes.

Worked Examples

Example 1

paths = [
    ["London", "New York"],
    ["New York", "Lima"],
    ["Lima", "Sao Paulo"]
]

First Pass, Build Starting Cities Set

Path Set After Insertion
["London", "New York"] {"London"}
["New York", "Lima"] {"London", "New York"}
["Lima", "Sao Paulo"] {"London", "New York", "Lima"}

Second Pass, Find Destination

Destination City In Starting Set? Action
"New York" Yes Continue
"Lima" Yes Continue
"Sao Paulo" No Return "Sao Paulo"

Final answer:

"Sao Paulo"

Example 2

paths = [
    ["B", "C"],
    ["D", "B"],
    ["C", "A"]
]

First Pass

Path Set After Insertion
["B", "C"] {"B"}
["D", "B"] {"B", "D"}
["C", "A"] {"B", "D", "C"}

Second Pass

Destination City In Starting Set? Action
"C" Yes Continue
"B" Yes Continue
"A" No Return "A"

Final answer:

"A"

Example 3

paths = [["A", "Z"]]

First Pass

Path Set After Insertion
["A", "Z"] {"A"}

Second Pass

Destination City In Starting Set? Action
"Z" No Return "Z"

Final answer:

"Z"

Complexity Analysis

Measure Complexity Explanation
Time O(n) We iterate through the paths list twice
Space O(n) The hash set stores up to all starting cities

The algorithm performs two linear passes over the input. Each hash set insertion and lookup operation takes average constant time, so the total runtime is linear.

The extra memory usage comes from storing the starting cities in the hash set. In the worst case, every path has a unique source city, so the set size grows to n.

Test Cases

from typing import List

class Solution:
    def destCity(self, paths: List[List[str]]) -> str:
        starting_cities = set()

        for from_city, to_city in paths:
            starting_cities.add(from_city)

        for from_city, to_city in paths:
            if to_city not in starting_cities:
                return to_city

        return ""

solution = Solution()

# Example 1
assert solution.destCity([
    ["London", "New York"],
    ["New York", "Lima"],
    ["Lima", "Sao Paulo"]
]) == "Sao Paulo"  # standard chain

# Example 2
assert solution.destCity([
    ["B", "C"],
    ["D", "B"],
    ["C", "A"]
]) == "A"  # unordered chain

# Example 3
assert solution.destCity([
    ["A", "Z"]
]) == "Z"  # single path

# Destination appears only once
assert solution.destCity([
    ["X", "Y"],
    ["Y", "Z"]
]) == "Z"  # simple ending node

# Paths given out of logical order
assert solution.destCity([
    ["C", "D"],
    ["A", "B"],
    ["B", "C"]
]) == "D"  # ordering should not matter

# City names with spaces
assert solution.destCity([
    ["New Delhi", "Dubai"],
    ["Dubai", "Paris"]
]) == "Paris"  # handles spaces correctly

# Longer chain
assert solution.destCity([
    ["A", "B"],
    ["B", "C"],
    ["C", "D"],
    ["D", "E"]
]) == "E"  # deeper chain

print("All tests passed!")

Test Summary

Test Why
Standard chain Verifies normal functionality
Unordered chain Ensures input order does not matter
Single path Tests minimum input size
Destination appears once Confirms destination detection
Out of order paths Prevents assumptions about ordering
City names with spaces Verifies string handling
Longer chain Tests multiple intermediate cities

Edge Cases

Single Path Input

The smallest valid input contains only one path, such as ["A", "Z"]. A naive implementation might overcomplicate traversal logic or assume multiple connections exist. Our implementation handles this naturally because the source set contains only "A", and "Z" is immediately identified as the only city without outgoing paths.

Paths Given in Random Order

The input paths are not guaranteed to appear in travel sequence order. For example:

["C", "D"]
["A", "B"]
["B", "C"]

A naive approach that assumes the last destination city in the list is the answer would fail. Our implementation avoids this issue entirely because it relies only on source city membership, not path ordering.

Intermediate Cities Appearing Multiple Times

Some cities may appear both as destinations and as sources. For example:

"A" -> "B"
"B" -> "C"

Here, "B" appears in both roles. A buggy implementation might incorrectly identify "B" as the destination after seeing it as a destination once. Our algorithm correctly checks whether the city also exists in the source set before returning it.