Разбор задачи · LeetCode 1743

1743. Restore the Array From Adjacent Pairs

12.08.2026 · Глеб Михайлов ·Где дают: Яндекс
Условие на LeetCode ↗

1. Условие задачи

Турист совершил одно путешествие через несколько городов. Про каждый перелёт известно только, какие два города он соединял, но направление и порядок перелётов потерялись.

Например, пара [2, 3] говорит, что турист летел либо из города 2 в город 3, либо из 3 в 2.

Дано: массив tickets, где каждая пара [A, B] означает прямой перелёт между городами A и B.

Известно:

Нужно: восстановить города в порядке путешествия. Маршрут можно вернуть в любом направлении: как от начала к концу, так и от конца к началу.

На LeetCode эта же задача сформулирована как восстановление массива уникальных чисел по парам соседних элементов.

Ограничения LeetCode:

Пример

tickets = [[2, 1], [3, 4], [3, 2]]

Подходят два ответа:

[1, 2, 3, 4]
[4, 3, 2, 1]

2. Решение

Решение в лоб

Можно взять любой билет, а затем каждый раз просматривать все оставшиеся билеты и искать тот, который продолжает маршрут слева или справа.

В худшем случае для каждого следующего города придётся снова перебирать все билеты. Получится O(N²), что не подходит для N = 10^5.

Главный инсайт: это перемешанный связный список

На первый взгляд задача похожа на обычный граф: города — вершины, билеты — неориентированные рёбра.

Каждый билет [A, B] добавляет связь в обе стороны:

A <-> B

Но по условию все рёбра образуют единственный маршрут без повторного посещения городов:

начало - город - город - ... - конец

Значит, перед нами не произвольный граф с циклами и развилками, а перемешанный двусвязный список.

В обычном двусвязном списке у каждого узла явно подписаны ссылки previous и next. Здесь связи между соседями сохранились, но:

У каждого внутреннего города ровно два соседа, а у двух крайних городов — только по одному. Поэтому город, который встречается только в одном билете, является началом или концом маршрута.

Для нас между началом и концом нет разницы. Берём любой край и тем самым сами выбираем направление движения:

Если начать с противоположного края, previous и next поменяются местами, а мы получим тот же маршрут в обратном порядке. Оба ответа правильные.

После выбора направления структура ведёт себя как обычный односвязный список. Никакой DFS, рекурсии, стека вызовов и множества visited нам не нужно: достаточно пройти цепочку обычным циклом while.

Алгоритм по шагам

  1. Для каждого города создаём список его соседей.
  2. Для билета [A, B] добавляем B в соседи A, а A — в соседи B.
  3. Находим любой город с одним соседом. Это начало или конец маршрута.
  4. Добавляем в ответ найденный край и его единственного соседа.
  5. Пока в ответе меньше N городов, смотрим на два последних элемента маршрута.
  6. Последний элемент — текущий город, предпоследний — previous.
  7. Среди соседей текущего города выбираем того, кто не равен previous, и добавляем его в маршрут.

Визуализация

Возьмём:

tickets = [[2, 1], [3, 4], [3, 2]]

После построения списков соседей получим:

1: [2]
2: [1, 3]
3: [4, 2]
4: [3]

Города 1 и 4 имеют только по одному соседу, поэтому начать можно с любого. Начнём с 1.

Шаг previous Текущий город next Маршрут
1 нет 1 2 [1, 2]
2 1 2 3 [1, 2, 3]
3 2 3 4 [1, 2, 3, 4]

В городе 2 есть соседи 1 и 3. Из 1 мы только что пришли, поэтому 1 — это previous, а 3 — это next. По той же логике из 3 переходим в 4.

Код на Python

from collections import defaultdict
from typing import List


class Solution:
    def restoreArray(self, adjacentPairs: List[List[int]]) -> List[int]:
        # 1. Для каждого города сохраняем его соседей
        neighbors = defaultdict(list)

        for city_a, city_b in adjacentPairs:
            neighbors[city_a].append(city_b)
            neighbors[city_b].append(city_a)

        # 2. Находим начало или конец маршрута
        start = None

        for city in neighbors:
            if len(neighbors[city]) == 1:
                start = city
                break

        # 3. Начинаем с крайнего города и его единственного соседа
        route = [start, neighbors[start][0]]

        # 4. Идём по цепочке как по обычному связному списку
        while len(route) < len(neighbors):
            previous_city = route[-2]
            current_city = route[-1]

            for next_city in neighbors[current_city]:
                if next_city != previous_city:
                    route.append(next_city)
                    break

        return route

Почему алгоритм работает

По условию билеты образуют один маршрут без повторных посещений. Поэтому соответствующая структура является простым путём: у двух крайних вершин по одному соседу, у каждой внутренней — по два.

Мы начинаем с одного из краёв. В каждом внутреннем городе один сосед — это город, из которого мы пришли, а второй — единственное возможное продолжение маршрута. Значит, на каждом шаге алгоритм выбирает правильный следующий город и в итоге восстанавливает всю цепочку.

Если выбрать другой край, получим обратный маршрут, что тоже разрешено условием.

Почему не нужны visited, DFS и стек

В произвольном графе множество visited не даёт нам зациклиться, а стек DFS хранит точки возврата: после обхода одной ветки надо вернуться и продолжить другую.

Здесь ветвлений нет. Из внутреннего города можно пойти только:

Возвращаться к развилке никогда не придётся, потому что развилок не существует. Мы просто сдвигаемся от текущего узла к следующему, как при обычной обработке связного списка.

Краевые случаи

3. Сложность алгоритма

Обозначим через N число городов. Тогда билетов будет N - 1.

Итог по времени: O(N).

Итог по памяти: O(N). Сюда входят списки соседей. Возвращаемый массив route тоже содержит N элементов.

Справочно: избыточное решение через DFS

Официальный editorial LeetCode также предлагает рекурсивный DFS. Такое решение естественно приходит в голову, если увидеть в парах рёбра и отнестись к структуре как к настоящему графу.

В общем графе DFS действительно полезен: в вершине может быть несколько ещё не посещённых соседей, а рекурсивный стек запоминает, куда вернуться после каждой ветки. Для защиты от циклов обычно понадобится и множество visited.

Можно написать такой универсальный обход и здесь:

from collections import defaultdict
from typing import List


class Solution:
    def restoreArray(self, adjacentPairs: List[List[int]]) -> List[int]:
        # 1. Строим граф
        neighbors = defaultdict(list)

        for city_a, city_b in adjacentPairs:
            neighbors[city_a].append(city_b)
            neighbors[city_b].append(city_a)

        # 2. Находим начало или конец маршрута
        start = None

        for city in neighbors:
            if len(neighbors[city]) == 1:
                start = city
                break

        route = []
        visited = set()

        # 3. Обходим граф в глубину
        def dfs(current_city: int) -> None:
            visited.add(current_city)
            route.append(current_city)

            for next_city in neighbors[current_city]:
                if next_city not in visited:
                    dfs(next_city)

        dfs(start)
        return route

Для этой задачи код корректен, потому что граф гарантированно является одной цепочкой. Но здесь универсальность DFS только добавляет лишние механизмы:

Итеративный вариант точнее использует структуру входа: это не произвольный граф, а замаскированный связный список. Поэтому обычный цикл while здесь и проще, и надёжнее.

Вывод

Пары соседних элементов иногда выглядят как рёбра графа, но прежде чем доставать DFS, стоит проверить форму этого графа. В этой задаче он всегда является одной цепочкой — перемешанным связным списком.

Находим любой край, сами назначаем направление previous -> next и проходим маршрут обычным циклом while за O(N).

🐆