What is dynamic programming, and how do you recognise a problem that suits it?
Dynamic programming is a way of solving a problem by remembering the answers to subproblems you have already solved. It applies when the naive recursive solution computes the same subproblems many times over, and when the problem decomposes into subproblems whose answers can be combined.
Two conditions have to hold. The first is overlapping subproblems: the recursive calls must revisit the same inputs, otherwise memoisation buys nothing. The second is optimal substructure: the optimal answer to the whole problem must be composed from optimal answers to the subproblems. A shortest path has both, which is why Dijkstra and Bellman-Ford are dynamic programs in disguise.
def count_paths(rows, cols):
table = [[0] * cols for _ in range(rows)]
for r in range(rows):
for c in range(cols):
if r == 0 or c == 0:
table[r][c] = 1
else:
table[r][c] = table[r - 1][c] + table[r][c - 1]
return table[rows - 1][cols - 1]
print(count_paths(3, 4))
print(count_paths(2, 2))The outputs are 10 and 2. The count of paths to a cell is the count to the cell above plus the count to the cell on the left, since every path must arrive from one of them.
The mechanism is that you define a state, here a cell, and a recurrence that expresses the answer for that state in terms of other states. Then you either fill a table in dependency order, which is tabulation, or you compute states on demand and cache them, which is memoisation.
The trade-off to name is that dynamic programming is never asymptotically better than a well written greedy or graph algorithm when one exists, so you should look for those first. The value it adds is turning an exponential recursive search into a polynomial one, and the cost is the table, which is quadratic in the number of states for many problems. A useful heuristic is to write the recursive solution first, count how many calls it makes, and see whether the answer looks like a power of the input size.