Курс 3.8 — Алгоритмы · Раздел 07 · Урок 7.5

Пример 3. Задача про гвозди

Разберём поиск минимальной длины верёвки для соединения соседних гвоздей.

Разберём поиск минимальной длины верёвки для соединения соседних гвоздей.
7.5 — Пример 3. Задача про гвозди

Материал урока

Привет, друг! Это снова я — твой ИИ-помощник. Сегодня мы вместе разберём ещё одну интересную задачу на динамическое программирование.

Представь, что у нас есть числовая прямая, а в неё вбиты гвозди. Гвозди вбиты в разные места, и мы хотим связать их верёвкой так, чтобы соблюсти правила:

  1. Связывать можно только соседние гвозди.
  2. У каждого гвоздя хотя бы с одной стороны должен быть привязан сосед.

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

Размышляем пошагово

  1. Если у нас один гвоздь — ответ очевиден: верёвка не нужна, длина = 0.
  2. Если два гвоздя — единственный способ связать их: протянуть верёвку между ними. Длина будет равна расстоянию между гвоздями.

3.Если три гвоздя — связываем всех последовательно, и тоже всё ясно.

4.Если четыре гвоздя — остаётся один вариант: связываем попарно: 1–2 3–4.

5.Начиная с пяти гвоздей, появляется выбор. Например, при пяти гвоздях есть два варианта:

  • 1–2–3 4–5
  • 1–2 3–4–5
  • Нужно выбрать минимальный вариант.

Так шаг за шагом мы будем искать минимальную длину для всех гвоздей.

Алгоритм

  • Сначала отсортируем гвозди, ведь они могут быть даны в случайном порядке.
  • Используем массив ropes, где будем хранить минимальную длину верёвки на каждом шаге.
  • Для каждого нового гвоздя будем выбирать минимум из двух вариантов:
  • продолжить предыдущую схему,
  • или «перекинуться» на схему до предпоследнего гвоздя.

Решение на Python

def nails(line):
    line.sort()  # сортируем гвозди
    n = len(line)
    if n == 1:
        return 0

    ropes = [0] * n
    ropes[1] = line[1] - line[0]  # для двух гвоздей

    if n > 2:
        ropes[2] = line[2] - line[0]  # для трёх гвоздей

    for i in range(3, n):
        ropes[i] = min(ropes[i - 1], ropes[i - 2]) + (line[i] - line[i - 1])

    return ropes[n - 1]

# Пример использования
nails_positions = [7, 1, 4, 10, 13]  # гвозди в случайном порядке
print("Минимальная длина верёвки:", nails(nails_positions))

Пояснения

  • line.sort() — упорядочиваем гвозди по возрастанию.
  • ropes[i] хранит минимальную длину верёвки, если мы связываем все гвозди до позиции i.
  • На каждом шаге выбираем оптимальный вариант:
  • либо добавляем нового соседа к уже готовой схеме,
  • либо строим новую «связку» из двух последних гвоздей.

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

  • Сортировка: O(n log n)
  • Динамика: O(n)
  • Итоговая сложность: O(n log n)

Пример визуализации задачи

Гвозди на числовой прямой: схема соединений 1
Гвозди на числовой прямой: схема соединений 1
Гвозди на числовой прямой: схема соединений 2
Гвозди на числовой прямой: схема соединений 2
Гвозди на числовой прямой: схема соединений 3
Гвозди на числовой прямой: схема соединений 3
Гвозди на числовой прямой: варианты соединения пяти гвоздей
Гвозди на числовой прямой: варианты соединения пяти гвоздей
Гвозди на числовой прямой: схема соединений 5
Гвозди на числовой прямой: схема соединений 5

Допустим, у нас есть гвозди в точках: 1, 4, 7, 10, 13

После сортировки они выстраиваются так:

1     4     7     10     13
|-----|-----|------|------|

Здесь каждая вертикальная палочка — это гвоздь 🪛, а линии между ними — верёвка.

def nails_with_path(positions):
    # Сортируем гвозди
    positions = sorted(positions)
    n = len(positions)

    if n == 1:
        return 0, [[positions[0]]]

    # DP-массив для минимальной длины
    dp = [0] * n
    # Путь (как связываем)
    path = [[] for _ in range(n)]

    # Базовые случаи
    dp[1] = positions[1] - positions[0]
    path[1] = [(positions[0], positions[1])]

    if n > 2:
        dp[2] = positions[2] - positions[0]
        path[2] = [(positions[0], positions[2])]

    # Динамика
    for i in range(3, n):
        # Вариант 1: берём dp[i-1] + последний отрезок
        option1 = dp[i-1] + positions[i] - positions[i-1]
        # Вариант 2: берём dp[i-2] + последний отрезок
        option2 = dp[i-2] + positions[i] - positions[i-1]

        if option1 <= option2:
            dp[i] = option1
            path[i] = path[i-1] + [(positions[i-1], positions[i])]
        else:
            dp[i] = option2
            path[i] = path[i-2] + [(positions[i-1], positions[i])]

    return dp[-1], path[-1]

def visualize(positions, pairs):
    """ ASCII-схема связывания """
    positions = sorted(positions)
    pos_to_index = {p: i for i, p in enumerate(positions)}

    # Линия с гвоздями
    line = ""
    for p in positions:
        line += f"{p}".ljust(5)

    print(line)

    # Вторая строка со связями
    conn = ["     "] * len(positions)
    for a, b in pairs:
        i, j = pos_to_index[a], pos_to_index[b]
        for k in range(i, j):
            conn[k] = "-----"
    print("".join(conn))

# 🔨 Пример
nails = [1, 4, 7, 10, 13]
min_len, pairs = nails_with_path(nails)

print("Минимальная длина верёвки:", min_len)
print("Связи:", pairs)
visualize(nails, pairs)