4. Unique Paths II
«Любое препятствие — это не просто преграда на пути, а повод перестроить маршрут».
— Брайан Керниган
Задача Unique Paths II (LeetCode 63) развивает концепцию 3. Unique Paths, добавляя в сетку непреодолимые стены (препятствия). Это переломный момент: изящная комбинаторная формула $C_{m+n-2}^{m-1}$ разбивается о суровую реальность. Препятствия нарушают геометрическую симметрию пространства, и единственный инструмент, способный справиться с задачей, — динамическое программирование.
В бэкенде и телекоммуникациях эта задача моделирует маршрутизацию в условиях частичной деградации инфраструктуры: расчет связности сервисов, когда отдельные серверы, стойки или каналы связи выведены из строя (Dead Nodes / Outages).
1. Постановка задачи и обработка граничных условий
Условие:
Робот находится в ячейке (0, 0) сетки $m \times n$.
В сетке obstacleGrid препятствия обозначены числом 1, а свободные клетки — 0.
Робот может перемещаться только вправо или вниз. Нельзя ступать в клетки с препятствиями.
Сколько существует уникальных маршрутов в целевую клетку (m-1, n-1)?
Вход: obstacleGrid = [
[0, 0, 0],
[0, 1, 0],
[0, 0, 0]
]
Выход: 2
Граничный чек-лист:
- Старт заблокирован: если
obstacleGrid[0][0] == 1, робот не может даже начать путь — ответ0. - Финиш заблокирован: если
obstacleGrid[m-1][n-1] == 1, цель недостижима — ответ0. - Сетка $1 \times 1$: если свободна —
1, если занята —0.
2. Логика перехода: Инфицирование нулем
Правило перехода в свободной клетке остается прежним:
$$dp[i][j] = dp[i-1][j] + dp[i][j-1]$$
Если же в клетке $(i, j)$ находится стена (obstacleGrid[i][j] == 1), число способов достичь её строго равно 0:
$$dp[i][j] = 0$$
В 1D сжатом представлении это превращается в простую мутацию:
- Если клетка — препятствие:
dp[j] = 0. - Если клетка свободна:
dp[j] = dp[j] + dp[j-1].
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
Check{"obstacleGrid[i][j] == 1?<br/>(Проверка ячейки)"}:::warning
Check -- "Да (Стена!)" --> Zero["dp[j] = 0<br/>(Маршруты заблокированы)"]:::error
Check -- "Нет (Свободно)" --> Normal["dp[j] += dp[j-1]<br/>(Суммируем верх и лево)"]:::success
Zero --> Propagate["Нуль автоматически передается<br/>соседям справа и снизу"]:::process
Normal --> Next["Переход к следующей ячейке"]:::entry
3. Идиоматичная реализация на Go 1.22+ ($O(N)$ памяти)
package paths
// UniquePathsWithObstacles возвращает число путей с учетом препятствий.
// Временная сложность: O(M * N).
// Пространственная сложность: O(N) — один срез размера cols.
func UniquePathsWithObstacles(obstacleGrid [][]int) int {
m := len(obstacleGrid)
if m == 0 {
return 0
}
n := len(obstacleGrid[0])
if n == 0 {
return 0
}
// Если старт или финиш заблокированы — путей нет
if obstacleGrid[0][0] == 1 || obstacleGrid[m-1][n-1] == 1 {
return 0
}
dp := make([]int, n)
dp[0] = 1
for i := 0; i < m; i++ {
for j := 0; j < n; j++ {
if obstacleGrid[i][j] == 1 {
// Стена обнуляет любые маршруты через эту ячейку
dp[j] = 0
} else if j > 0 {
// Свободная клетка: dp[j] уже хранит значение сверху,
// прибавляем значение слева (dp[j-1])
dp[j] += dp[j-1]
}
// Для j == 0 при obstacleGrid[i][0] == 0:
// dp[0] сохраняет значение сверху (старый dp[0])
}
}
return dp[n-1]
}
4. Mechanical Sympathy: Branch Prediction vs Branchless DP
Обратите внимание на проверку if obstacleGrid[i][j] == 1 во внутреннем цикле.
Для процессора с длинным конвейером (14–19 стадий в современных Intel Raptor Lake / AMD Zen 4) каждое условное ветвление — это потенциальная угроза.
- Если препятствия распределены упорядоченно (например, сплошная стена), предсказатель ветвлений (Branch Predictor) безошибочно настраивается на ветку.
- Если стены разбросаны случайно (хаотический шум), процент промахов (Branch Mispredictions) может достигать 30–40%. Каждая ошибка сбрасывает конвейер и отнимает 15–20 тактов CPU.
Высший пилотаж: Безусловный (Branchless) код
Senior-разработчик может устранить ветвление математически с помощью битовой маски:
// Branchless вычисление: 0 проверок if внутри горячего цикла!
// mask = 0, если obstacleGrid == 1
// mask = 1, если obstacleGrid == 0
mask := 1 - obstacleGrid[i][j]
dp[j] = (dp[j] + dp[j-1]) * mask
Такой код компилируется в строго линейную последовательность инструкций сложения и умножения без единого перехода JMP. Конвейер процессора работает с предельной тактовой эффективностью.
5. System Design: Failure Domains и Circuit Breaker в сетях
В архитектуре отказоустойчивых распределенных систем алгоритм обхода препятствий лежит в основе:
- Failure Domains (Зоны доступности в облаках):
Когда в AWS или GCP падает Availability Zone (AZ-a), планировщик оркестратора помечает все виртуальные ноды этой зоны как1(препятствие). Алгоритм мгновенно пересчитывает потоки трафика в обход упавшей зоны. - Dynamic Circuit Breaker в Service Mesh (Envoy / Istio):
Если апстрим-сервис начинает отвечать ошибками 5xx, Circuit Breaker «размыкает цепь» (ставит стену в графе маршрутов). Алгоритм распределения трафика (Weight Balancing) автоматически обнуляет вес ребра и перенаправляет 100% запросов на здоровые реплики.
6. Вопросы для собеседования
Итог
- Ограничения убивают формулы: Препятствия делают комбинаторику неприменимой, требуя полного 2D/1D динамического программирования.
- Семантика обнуления: Препятствие принудительно обнуляет состояние $dp[j] = 0$, каскадно блокируя все маршруты через данную точку.
- Branchless оптимизация: Использование множителя
(1 - obstacle)устраняет ветвления и защищает конвейер CPU от сбросов.
В следующей лекции мы вернемся к поиску экстремума и объединим матричные сетки со взвешенной стоимостью ячеек: 5. Minimum Path Sum.