Алгоритмы с нуля
Dynamic programming: чтение кода
Баги в DP молчаливы — они возвращают уверенно неверное число, а не падение. Прочитайте каждый сниппет, предскажите, что он на самом деле считает, и выберите исправление, которое senior сделает первым.
Отработайте цикл, который вы запускаете на каждой DP: проверить, что ключ кэша совпадает с полным state, что transition покрывает каждый выбор, что оптимизация памяти не разрушает ещё нужное значение, и считать настоящую сложность по циклам.
Сниппет 1 — memo, который не попадает
def lcs(a, b):
memo = {}
def go(i, j):
if i == len(a) or j == len(b):
return 0
if a in memo: # баг: ключ не тот
return memo[a]
if a[i] == b[j]:
res = 1 + go(i + 1, j + 1)
else:
res = max(go(i + 1, j), go(i, j + 1))
memo[a] = res
return res
return go(0, 0)
Эта memoization LCS одновременно неверна и медленна. В чём дефект и как исправить?
Сниппет 2 — state в coin-change
def min_coins(coins, amount):
# одна монета на номинал, без повторов
dp = [0] + [float('inf')] * amount
for c in coins:
for x in range(amount, c - 1, -1): # ёмкость вниз
dp[x] = min(dp[x], dp[x - c] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
Автор хочет НЕОГРАНИЧЕННОЕ использование каждой монеты (классический coin change). Делает ли это 1D-код, и что задаёт направление обхода?
Сниппет 3 — оптимизация скользящей строки
def edit_distance(a, b):
n, m = len(a), len(b)
prev = list(range(m + 1)) # строка для i = 0
for i in range(1, n + 1):
cur = [i] + [0] * m # cur[0] = i
for j in range(1, m + 1):
if a[i - 1] == b[j - 1]:
cur[j] = prev[j - 1]
else:
cur[j] = 1 + min(prev[j], cur[j - 1], prev[j - 1])
prev = cur
return prev[m]
Этот код edit distance сворачивает 2D-таблицу до двух строк. Корректна ли оптимизация памяти и какая зависимость делает её безопасной?
Сниппет 4 — циклы interval DP
def interval_cost(n, weight):
dp = [[0] * n for _ in range(n)]
for length in range(2, n + 1): # длина интервала
for i in range(0, n - length + 1): # левый конец
j = i + length - 1 # правый конец
best = float('inf')
for k in range(i, j): # точка разбиения
best = min(best, dp[i][k] + dp[k + 1][j] + weight(i, k, j))
dp[i][j] = best
return dp[0][n - 1]
Какова временная и пространственная сложность этой interval DP и откуда берётся доминирующая стоимость?
Четыре проверки ловят большинство дефектов DP: ключ кэша должен равняться полному state подзадачи (Сниппет 1 ключевал по неверной переменной); transition и направление цикла должны совпадать с задуманной семантикой (Сниппет 2 — вниз это 0/1, вверх это unbounded); сжатие до скользящей строки безопасно, лишь когда ячейка читает только предыдущую строку и соседей текущей (Сниппет 3); а настоящая сложность — произведение границ циклов: O(n^2) state на O(n) работы на state даёт O(n^3) (Сниппет 4). Читайте циклы, а не комментарий.