0Pricing
Coding Interview Prep · Урок

DFS: компоненты связности и заливка

Применяйте DFS для подсчёта компонент связности, решайте задачу number-of-islands на двумерной сетке и реализуйте заливку при обработке изображений

«DFS: компоненты связности и заливка» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.

Определение связных компонент

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

from collections import defaultdict

# Graph with 3 components: {0,1,2}, {3,4}, {5}
graph = defaultdict(list)
for u, v in [(0,1),(0,2),(1,2),(3,4)]:
    graph[u].append(v)
    graph[v].append(u)
# Node 5 is isolated (no edges)
for node in [0,1,2,3,4,5]:
    if node not in graph:
        graph[node] = []

# We need DFS or BFS from each unvisited node
# to discover all components
print('Graph has nodes 0-5 with components: {0,1,2}, {3,4}, {5}')

Подсчёт связных компонент с помощью DFS

Переберите все вершины. Для каждой непосещённой вершины запустите DFS, чтобы отметить посещёнными все достижимые вершины. Каждый запуск DFS соответствует обнаружению новой компоненты. Чтобы получить количество компонент, подсчитайте число запусков DFS. Этот алгоритм со сложностью O(V + E) корректно работает независимо от того, является граф связным или нет.

from collections import defaultdict

def count_components(n, edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)

    visited = set()
    count = 0

    def dfs(node):
        visited.add(node)
        for nb in graph[node]:
            if nb not in visited:
                dfs(nb)

    for node in range(n):
        if node not in visited:
            dfs(node)
            count += 1

    return count

print(count_components(6, [(0,1),(0,2),(1,2),(3,4)]))  # 3
print(count_components(5, [(0,1),(1,2),(3,4)]))          # 2

Количество островов

Количество островов (LeetCode #200) — классическая задача на связные компоненты в двумерной сетке. Каждая ячейка со значением '1' принадлежит острову; соседние ячейки со значением '1' (сверху, снизу, слева и справа) образуют один и тот же остров. Подсчитайте количество различных островов с помощью DFS: переберите все ячейки и, найдя непосещённую ячейку со значением '1', запустите DFS, который отметит все связанные ячейки со значением '1' (выполнит заливку), а затем увеличьте счётчик.

def num_islands(grid):
    if not grid:
        return 0
    rows, cols = len(grid), len(grid[0])
    count = 0

    def dfs(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols:
            return
        if grid[r][c] != '1':
            return
        grid[r][c] = '#'  # mark visited in-place
        dfs(r+1,c); dfs(r-1,c)
        dfs(r,c+1); dfs(r,c-1)

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == '1':
                dfs(r, c)
                count += 1
    return count

grid = [['1','1','0','0','0'],
        ['1','1','0','0','0'],
        ['0','0','1','0','0'],
        ['0','0','0','1','1']]
print(num_islands(grid))  # 3

Алгоритм заливки

Заливка (LeetCode #733) заменяет цвет всех связанных ячеек, начиная с заданной, на новый цвет — точно как инструмент заливки в графических редакторах. Используйте DFS: начиная с исходного пикселя, рекурсивно перекрашивайте всех соседей, цвет которых совпадает с исходным. Важный крайний случай: если цвет начальной ячейки уже совпадает с новым цветом, немедленно верните результат, чтобы избежать бесконечной рекурсии.

def flood_fill(image, sr, sc, new_color):
    original = image[sr][sc]
    if original == new_color:
        return image  # edge case: same color, nothing to do
    rows, cols = len(image), len(image[0])

    def dfs(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols:
            return
        if image[r][c] != original:
            return
        image[r][c] = new_color
        dfs(r+1,c); dfs(r-1,c)
        dfs(r,c+1); dfs(r,c-1)

    dfs(sr, sc)
    return image

image = [[1,1,1],[1,1,0],[1,0,1]]
result = flood_fill(image, 1, 1, 2)
for row in result: print(row)
# [[2,2,2],[2,2,0],[2,0,1]]

Максимальная площадь острова

Максимальная площадь острова (LeetCode №695) расширяет задачу подсчёта островов: для каждого острова нужно вернуть размер крупнейшего. Во время заливки с помощью DFS считайте отмечаемые клетки. DFS возвращает размер текущего острова, а Вы отслеживаете максимум среди всех островов. Это простое расширение шаблона связных компонент.

def max_area_of_island(grid):
    if not grid:
        return 0
    rows, cols = len(grid), len(grid[0])
    max_area = 0

    def dfs(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols:
            return 0
        if grid[r][c] != 1:
            return 0
        grid[r][c] = 0  # mark visited
        return (1 + dfs(r+1,c) + dfs(r-1,c) +
                dfs(r,c+1) + dfs(r,c-1))

    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == 1:
                max_area = max(max_area, dfs(r, c))
    return max_area

grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],
        [0,0,0,0,0,0,0,1,1,1,0,0,0],
        [0,1,1,0,1,0,0,0,0,0,0,0,0],
        [0,1,0,0,1,1,0,0,1,0,1,0,0]]
print(max_area_of_island(grid))  # 6

Поток воды к Тихому и Атлантическому океанам

Поток воды к Тихому и Атлантическому океанам (LeetCode №417) — задача определить, из каких клеток вода может попасть и в Тихий океан (верхняя и левая границы), и в Атлантический океан (нижняя и правая границы). Вместо моделирования стока воды вниз используйте обратный DFS: двигайтесь от океанов вверх. Выполните два обхода DFS — один от границ Тихого океана, другой от границ Атлантического, собирая достижимые клетки. Пересечение множеств и есть ответ.

def pacific_atlantic(heights):
    rows, cols = len(heights), len(heights[0])
    pac = set(); atl = set()

    def dfs(r, c, visited, prev_h):
        if (r,c) in visited or r < 0 or r >= rows or c < 0 or c >= cols:
            return
        if heights[r][c] < prev_h:
            return  # water can't flow uphill in reverse
        visited.add((r,c))
        for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:
            dfs(r+dr, c+dc, visited, heights[r][c])

    for r in range(rows):
        dfs(r, 0, pac, heights[r][0])           # Pacific left
        dfs(r, cols-1, atl, heights[r][cols-1]) # Atlantic right
    for c in range(cols):
        dfs(0, c, pac, heights[0][c])            # Pacific top
        dfs(rows-1, c, atl, heights[rows-1][c]) # Atlantic bottom

    return sorted(pac & atl)  # intersection

print(pacific_atlantic([[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]]))

Итеративный DFS для связных компонент

Используйте итеративный DFS (с явным стеком), чтобы избежать ограничения глубины рекурсии в Пайтоне на больших сетках. Итеративная версия эквивалентна рекурсивному DFS, но использует стек вместо стека вызовов. Поместите начальный узел в стек, затем извлеките его, отметьте посещённым и добавьте непосещённых соседей. Такой подход безопасно обрабатывает сетки размером до нескольких миллионов клеток, тогда как рекурсивный DFS вызвал бы переполнение стека.

def count_components_iterative(n, edges):
    from collections import defaultdict
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)

    visited = set()
    count = 0

    for start in range(n):
        if start in visited:
            continue
        # Iterative DFS
        stack = [start]
        while stack:
            node = stack.pop()
            if node in visited:
                continue
            visited.add(node)
            for nb in graph[node]:
                if nb not in visited:
                    stack.append(nb)
        count += 1

    return count

print(count_components_iterative(6, [(0,1),(0,2),(1,2),(3,4)]))  # 3

Окружённые области

Окружённые области (LeetCode №130) находит все области из «O», полностью окружённые границами из «X». Область НЕ захватывается, если хотя бы одна её клетка «O» касается края поля. Хитрость заключается в следующем: вместо непосредственного поиска окружённых областей выполните DFS от всех граничных клеток «O» и пометьте всё достижимое как безопасное. Затем инвертируйте результат: все оставшиеся клетки «O» окружены и становятся «X», а безопасные клетки восстанавливаются в «O».

def solve(board):
    if not board:
        return
    rows, cols = len(board), len(board[0])

    def dfs(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols:
            return
        if board[r][c] != 'O':
            return
        board[r][c] = 'S'  # safe: connected to border
        dfs(r+1,c); dfs(r-1,c)
        dfs(r,c+1); dfs(r,c-1)

    # Mark border-connected O's as safe
    for r in range(rows):
        dfs(r, 0); dfs(r, cols-1)
    for c in range(cols):
        dfs(0, c); dfs(rows-1, c)

    # Flip: surrounded O -> X, safe S -> O
    for r in range(rows):
        for c in range(cols):
            if board[r][c] == 'O': board[r][c] = 'X'
            elif board[r][c] == 'S': board[r][c] = 'O'

board = [['X','X','X','X'],['X','O','O','X'],
         ['X','X','O','X'],['X','O','X','X']]
solve(board)
print([board[1][1], board[3][1]])  # X, O

Подсчёт подостровов

Подсчёт подостровов (LeetCode №1905) находит острова во второй сетке, полностью содержащиеся в острове первой сетки. Выполните DFS от каждой клетки «1» во второй сетке: остров является подостровом, если каждая посещённая в нём клетка также содержит «1» в первой сетке. Хитрость заключается в следующем: посетите ВСЕ клетки острова, чтобы отметить их исследованными, но отдельно отслеживайте, были ли ВСЕ они также равны «1» в первой сетке. Не прерывайте обход при первом «0» в первой сетке — иначе Вы не отметите остальные клетки того же острова.

def count_sub_islands(grid1, grid2):
    rows, cols = len(grid2), len(grid2[0])

    def dfs(r, c):
        if r < 0 or r >= rows or c < 0 or c >= cols:
            return True
        if grid2[r][c] != 1:
            return True
        grid2[r][c] = 0  # mark visited
        is_sub = grid1[r][c] == 1  # this cell must be in grid1
        is_sub = dfs(r+1,c) and is_sub  # note: AND not short-circuit OR
        is_sub = dfs(r-1,c) and is_sub
        is_sub = dfs(r,c+1) and is_sub
        is_sub = dfs(r,c-1) and is_sub
        return is_sub

    count = 0
    for r in range(rows):
        for c in range(cols):
            if grid2[r][c] == 1 and dfs(r, c):
                count += 1
    return count

print(count_sub_islands([[1,1,1],[1,0,1],[1,1,1]],
                         [[1,1,1],[1,0,1],[1,1,1]]))  # 1

DFS и BFS для связных компонент

И DFS, и BFS корректно находят все связные компоненты с одинаковой временной сложностью O(V + E) и пространственной сложностью O(V). DFS проще реализовать рекурсивно в задачах о связных компонентах, тогда как BFS предпочтителен, когда Вам также нужна информация о кратчайшем пути. В задачах на сетках DFS лучше использует кэш, поскольку он углубляется в одном направлении до возврата назад и последовательно обращается к близким областям памяти.

# DFS advantages for connected components:
# - Simpler recursive implementation
# - Lower constant factor for small graphs
# - Can restore grid state during backtracking (if needed)

# BFS advantages:
# - Finds shortest path while traversing
# - Better for wide, shallow graphs (avoids deep recursion)
# - Multi-source initialisation is natural

# Same asymptotic complexity: O(V + E) time, O(V) space
# Grid (m rows, n cols): O(mn) time and space
print('DFS and BFS: same O(V+E) complexity for component counting')

Острова с ограничениями: формы и периметры

Периметр острова (LeetCode №463) подсчитывает общий периметр единственного острова в сетке. Для каждой клетки суши («1») добавьте к периметру 4, затем вычтите 2 за каждую соседнюю клетку суши (общие грани). Этот подход на основе формулы со сложностью O(mn) не требует DFS, однако понимание того, что он эквивалентен DFS, подсчитывающему граничные рёбра, укрепляет связь между задачами на сетках и рассуждениями в терминах графов.

def island_perimeter(grid):
    rows, cols = len(grid), len(grid[0])
    perimeter = 0
    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == 1:
                perimeter += 4  # start with 4 sides
                # Subtract shared edges with adjacent land cells
                if r > 0 and grid[r-1][c] == 1:
                    perimeter -= 2  # shared top edge
                if c > 0 and grid[r][c-1] == 1:
                    perimeter -= 2  # shared left edge
    return perimeter

grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]
print(island_perimeter(grid))  # 16

Быстрая проверка

Проверьте своё понимание понятий «Структуры данных & алгоритмы — подготовка к техническому собеседованию» из этого урока.

Повторение урока

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

Часто задаваемые вопросы

Урок «DFS: компоненты связности и заливка» бесплатный?

Да — полный текст урока «DFS: компоненты связности и заливка» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.

Чему я научусь в уроке «DFS: компоненты связности и заливка»?

Применяйте DFS для подсчёта компонент связности, решайте задачу number-of-islands на двумерной сетке и реализуйте заливку при обработке изображений Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Coding Interview Prep?

Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «DFS: компоненты связности и заливка»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Coding Interview Prep?

Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Представление графов и настройка обхода
  2. BFS: кратчайший путь и обход по уровням
  3. DFS: компоненты связности и заливка
  4. Обнаружение циклов в ориентированных и неориентированных графах
← Назад к Coding Interview Prep