Part VI: Sequential Decision Making
Chapter 22: Markov Decision Processes

Dynamic Programming

"You handed me the transition table and the rewards and asked for the optimal policy. Fine. I will sweep every state, back up every value, and sweep again. And again. I will not stop sweeping until the largest change I see is smaller than your tolerance, and not one sweep before. I have a fixed point to reach and all the patience in the world."

A Bellman Backup That Will Not Stop Until Everything Converges
Big Picture

When the model of a Markov decision process is fully known, the transition kernel $P(s' \mid s, a)$ and the reward $R(s, a)$ both given, the optimal policy can be computed exactly, by iteration, with no learning and no sampling at all. That computation is dynamic programming, and it rests on a single structural fact established earlier in this chapter: the optimal value function is the unique fixed point of the Bellman optimality operator, and that operator is a $\gamma$-contraction in the supremum norm. Two algorithms exploit that fact. Policy iteration alternates exact policy evaluation (solve a linear system for the value of the current policy) with policy improvement (act greedily with respect to that value), and reaches the exact optimum in a finite number of policy changes. Value iteration truncates the evaluation to a single backup per sweep, folding evaluation and improvement into one update that converges geometrically. This section derives both updates with full notation, proves convergence from the contraction property and reads off the geometric error decay, lays out the cost and the curse of dimensionality that limit exact dynamic programming, draws the bridge to the model-free reinforcement learning of the chapters ahead, and implements value iteration and policy iteration from scratch on a gridworld, showing them reach the same optimal policy before collapsing the whole computation into a few lines of a library solver. Dynamic programming is the exact ideal that every reinforcement-learning method in the rest of Part VI approximates.

In Section 22.3 we met the Bellman equations: the consistency conditions that any value function must satisfy, and the optimality equations whose unique solution is the optimal value function $V^{\star}$. We proved those equations have a unique solution but stopped short of computing it. This section closes that gap. Given a fully specified MDP, the tuple $(\mathcal{S}, \mathcal{A}, P, R, \gamma)$ of Section 22.3, we now turn the Bellman optimality equation from a condition into an algorithm, sweeping it across the state space until the values stop moving. We use the unified notation of Appendix A throughout: $\mathcal{S}$ the state set, $\mathcal{A}$ the action set, $\pi$ a policy, $V^{\pi}$ and $Q^{\pi}$ the state and action value functions, $\gamma \in [0, 1)$ the discount, and $V^{\star}, Q^{\star}, \pi^{\star}$ their optimal counterparts.

Why does an exact method deserve a full section when the rest of Part VI is about learning from experience? Because dynamic programming is the conceptual anchor for everything that follows. Every value-based reinforcement-learning algorithm, from the tabular temporal-difference methods of Chapter 24 to the deep $Q$-networks of Chapter 25, is best understood as an attempt to carry out a Bellman backup when you cannot afford the exact one: when the model $(P, R)$ is unknown, so you must replace the expectation over $s'$ with sampled transitions, or when the states are too numerous to enumerate, so you must replace the lookup table with a function approximator. Understand the exact backup first and the approximations read as principled compromises rather than disconnected tricks. Misunderstand it and the whole reinforcement-learning edifice looks like a bag of unrelated update rules.

The four competencies this section installs are these: to evaluate a fixed policy exactly and improve it greedily, and to see policy iteration as the alternation of the two; to run value iteration as the truncated combination of evaluation and improvement, and to write both updates with full notation; to prove convergence from the contraction property and quantify the geometric error decay; and to recognize where exact dynamic programming breaks (an unknown model, a state space too large to enumerate) and how reinforcement learning is built to step in there. These are load-bearing for every chapter of Part VI.

1. Solving a Known MDP Exactly: Policy Evaluation, Improvement, and Iteration Beginner

Begin with a fixed policy $\pi$ and ask the narrower question: what is it worth? The value function $V^{\pi}$ assigns to each state the expected discounted return obtained by following $\pi$ from that state. Because the MDP is Markov, $V^{\pi}$ satisfies the Bellman expectation equation, one linear equation per state, coupling each state's value to its successors':

$$V^{\pi}(s) = \sum_{a} \pi(a \mid s) \left[ R(s, a) + \gamma \sum_{s'} P(s' \mid s, a)\, V^{\pi}(s') \right].$$

This is the policy-evaluation equation. It is a system of $|\mathcal{S}|$ linear equations in the $|\mathcal{S}|$ unknowns $\{V^{\pi}(s)\}$, and in principle one can solve it directly by matrix inversion: writing $\mathbf{v}^{\pi}$ for the value vector, $\mathbf{r}^{\pi}$ for the expected one-step reward under $\pi$, and $\mathbf{P}^{\pi}$ for the policy-induced transition matrix, the equation is $\mathbf{v}^{\pi} = \mathbf{r}^{\pi} + \gamma \mathbf{P}^{\pi} \mathbf{v}^{\pi}$, whose closed-form solution is $\mathbf{v}^{\pi} = (\mathbf{I} - \gamma \mathbf{P}^{\pi})^{-1} \mathbf{r}^{\pi}$. The matrix $\mathbf{I} - \gamma \mathbf{P}^{\pi}$ is invertible for every $\gamma < 1$ because $\gamma \mathbf{P}^{\pi}$ has spectral radius at most $\gamma < 1$. The direct solve costs $O(|\mathcal{S}|^3)$; the cheaper alternative, used in practice and in the code below, is to treat the equation as a fixed-point iteration, $\mathbf{v} \leftarrow \mathbf{r}^{\pi} + \gamma \mathbf{P}^{\pi} \mathbf{v}$, sweeping until the values settle. That iterative sweep is iterative policy evaluation, and it converges for the same contraction reason we establish in subsection two.

Knowing what a policy is worth, we ask whether we can do better. The policy improvement step looks at $V^{\pi}$ and, in each state, switches to whichever action looks best when followed thereafter by $\pi$. Define the action value of the current policy,

$$Q^{\pi}(s, a) = R(s, a) + \gamma \sum_{s'} P(s' \mid s, a)\, V^{\pi}(s'),$$

and form the greedy policy $\pi'(s) = \arg\max_{a} Q^{\pi}(s, a)$. The policy improvement theorem, proven in Section 22.3, guarantees that $\pi'$ is at least as good as $\pi$ in every state, $V^{\pi'}(s) \ge V^{\pi}(s)$, and strictly better in at least one state unless $\pi$ is already optimal. Alternate the two steps, evaluate then improve, and you have policy iteration: starting from any policy $\pi_0$, repeatedly compute $V^{\pi_k}$ exactly and set $\pi_{k+1}$ greedy with respect to it. Because there are only finitely many deterministic policies and each iteration strictly improves until convergence, policy iteration reaches the exact optimum $\pi^{\star}$ in a finite number of iterations, typically a handful even for large state spaces.

Policy iteration: evaluate, then improve, then repeat Policy evaluation solve Vπ for current π Policy improvement π′ = greedy(Qπ) improved policy π′, evaluate again converges to π★
Figure 22.4.1: The policy-iteration cycle. Evaluation solves the linear Bellman expectation system for $V^{\pi}$; improvement reads off the greedy policy $\pi' = \arg\max_a Q^{\pi}(s, a)$. Each turn of the loop produces a strictly better policy until no action change helps, at which point the policy is optimal. Because deterministic policies are finite and each step strictly improves, the loop halts at $\pi^{\star}$ after finitely many turns.

Policy iteration spends real effort on each evaluation: solving (or iterating to convergence) the full linear system before taking a single improvement step. Value iteration is the impatient cousin that refuses to wait. Instead of evaluating the current policy to convergence, it performs exactly one backup of the value function and then immediately acts greedily, fusing evaluation and improvement into a single update. Concretely, value iteration sweeps the Bellman optimality backup,

$$V_{k+1}(s) = \max_{a} \left[ R(s, a) + \gamma \sum_{s'} P(s' \mid s, a)\, V_{k}(s') \right],$$

over every state at every iteration, starting from an arbitrary $V_0$. The $\max$ over actions is exactly an improvement step folded into the backup, and the single application of the operator is a truncated evaluation: one sweep rather than a full solve. When the values stop changing, $V_k \to V^{\star}$, and the greedy policy read off the converged values is $\pi^{\star}$. Value iteration is policy iteration with the evaluation truncated to one backup, which is why it needs more sweeps than policy iteration needs improvement steps but pays far less per sweep.

Key Insight: Value Iteration Is Policy Iteration With Evaluation Truncated to One Backup

Policy iteration and value iteration are not two unrelated algorithms but two settings of one dial: how much evaluation to do between improvements. Policy iteration evaluates to convergence (an exact solve, or many sweeps) and then improves; it takes few outer iterations but each is expensive. Value iteration evaluates for exactly one sweep and improves in the same breath (the $\max$ is the improvement); it takes more sweeps but each is cheap. Everything in between, evaluate for $m$ sweeps then improve, is modified policy iteration, and it is often the fastest in practice. The single object both algorithms iterate is the Bellman backup; they differ only in how greedily they alternate evaluating and improving it. Hold this in mind and the rest of value-based reinforcement learning is variations on "how do I do a Bellman backup when the exact one is out of reach".

2. Why They Converge: The Bellman Operator Is a Contraction Intermediate

A friendly press repeatedly squeezing a wobbly error blob smaller and smaller toward a single glowing fixed point, with ghost stages of shrinking, showing the Bellman operator as a contraction that converges.
Figure 22.5: Every sweep shrinks the gap to the true answer by a fixed fraction, so the error is squeezed relentlessly toward zero.

Both algorithms converge for one reason, and it is worth stating precisely because it also delivers the rate. Define the Bellman optimality operator $\mathcal{T}$, which maps a value function to a new one by applying the optimality backup at every state:

$$(\mathcal{T} V)(s) = \max_{a} \left[ R(s, a) + \gamma \sum_{s'} P(s' \mid s, a)\, V(s') \right].$$

Value iteration is nothing but the repeated application $V_{k+1} = \mathcal{T} V_k$, and $V^{\star}$ is by definition the fixed point $\mathcal{T} V^{\star} = V^{\star}$ (the Bellman optimality equation of Section 22.3). The decisive property is that $\mathcal{T}$ is a $\gamma$-contraction in the supremum norm: for any two value functions $V$ and $W$,

$$\lVert \mathcal{T} V - \mathcal{T} W \rVert_{\infty} \;\le\; \gamma \, \lVert V - W \rVert_{\infty},$$

where $\lVert V \rVert_{\infty} = \max_{s} |V(s)|$. The proof is short and instructive. Fix a state $s$ and let $a^{\star}$ be the maximizing action for $\mathcal{T} V$ at $s$. Then

$$(\mathcal{T} V)(s) - (\mathcal{T} W)(s) \le \gamma \sum_{s'} P(s' \mid s, a^{\star}) \big[ V(s') - W(s') \big] \le \gamma \sum_{s'} P(s' \mid s, a^{\star}) \, \lVert V - W \rVert_{\infty} = \gamma \, \lVert V - W \rVert_{\infty},$$

using that the rewards cancel, the probabilities sum to one, and the same argument with $V$ and $W$ swapped bounds the other sign, so the absolute difference is at most $\gamma \lVert V - W \rVert_{\infty}$ at every $s$, hence in the max. The key step is that the $\max_a$ is a non-expansion: $|\max_a f(a) - \max_a g(a)| \le \max_a |f(a) - g(a)|$, so taking the best action cannot amplify a difference. Discounting by $\gamma$ then shrinks it.

The Banach fixed-point theorem now does the rest. A contraction on a complete metric space (the space of bounded value functions under the supremum norm is one) has a unique fixed point, and iterating the map from any starting point converges to it. So $V^{\star}$ is unique, and value iteration converges to it from any $V_0$. More: the contraction gives the rate directly. Applying the contraction $k$ times to the gap between $V_k$ and the fixed point,

$$\lVert V_k - V^{\star} \rVert_{\infty} = \lVert \mathcal{T} V_{k-1} - \mathcal{T} V^{\star} \rVert_{\infty} \le \gamma \, \lVert V_{k-1} - V^{\star} \rVert_{\infty} \le \cdots \le \gamma^{k} \, \lVert V_0 - V^{\star} \rVert_{\infty}.$$

The error decays geometrically with ratio $\gamma$ per sweep. The number of sweeps to reach a target accuracy $\varepsilon$ is therefore $O\!\big(\tfrac{1}{1 - \gamma} \log \tfrac{1}{\varepsilon}\big)$: logarithmic in the accuracy, but the factor $1/(1 - \gamma)$ means a discount near one (a far-sighted agent) converges slowly, because each sweep removes only a $\gamma$ fraction of the remaining error. The same contraction argument, applied to the policy-specific operator $\mathcal{T}^{\pi}$ (the optimality $\max$ replaced by the expectation under $\pi$), proves iterative policy evaluation converges geometrically too.

Numeric Example: Reading the Geometric Decay Off the Discount

Suppose value iteration starts with an initial error $\lVert V_0 - V^{\star} \rVert_{\infty} = 10$ and the discount is $\gamma = 0.9$. After one sweep the error is at most $0.9 \cdot 10 = 9$, after two sweeps at most $0.81 \cdot 10 = 8.1$, and after $k$ sweeps at most $10 \cdot 0.9^{k}$. To drive the error below $\varepsilon = 0.01$ we need $10 \cdot 0.9^{k} < 0.01$, that is $0.9^{k} < 0.001$, giving $k > \log(0.001)/\log(0.9) \approx 65.6$, so about 66 sweeps. Now raise the discount to $\gamma = 0.99$: each sweep removes only one percent of the error, and the requirement $0.99^{k} < 0.001$ needs $k > \log(0.001)/\log(0.99) \approx 687$ sweeps, a tenfold increase from raising $\gamma$ by one notch. The factor $1/(1 - \gamma)$ jumped from $10$ to $100$, exactly tracking the slowdown. The lesson the contraction makes quantitative: far-sighted agents (discount near one) are more expensive to solve, and the cost scales with the effective horizon $1/(1 - \gamma)$.

Key Insight: One Contraction Property Gives Existence, Uniqueness, and Rate at Once

The single inequality $\lVert \mathcal{T} V - \mathcal{T} W \rVert_{\infty} \le \gamma \lVert V - W \rVert_{\infty}$ is the engine room of dynamic programming. From it, three facts follow with no extra work. Existence and uniqueness: the Banach theorem gives exactly one fixed point $V^{\star}$, so "the optimal value function" is well defined. Convergence: iterating $\mathcal{T}$ from anywhere reaches that fixed point. Rate: the error contracts by $\gamma$ per sweep, so convergence is geometric and the effective horizon $1/(1 - \gamma)$ sets the cost. This is why $\gamma < 1$ is not a modeling nicety but a mathematical necessity: it is precisely what makes the backup a contraction. The undiscounted case $\gamma = 1$ loses the contraction and needs separate, more delicate arguments (proper policies, average-reward criteria) that the discounted setting sidesteps entirely.

3. Complexity, the Curse of Dimensionality, and Generalized Policy Iteration Intermediate

Exact dynamic programming is powerful but it asks for a great deal, and naming what it asks for is what motivates everything after it. Each value-iteration sweep visits every state, and for each state it maximizes over every action a sum over every possible successor, so one sweep costs $O(|\mathcal{S}|^2 |\mathcal{A}|)$ when transitions are dense (and $O(|\mathcal{S}| |\mathcal{A}| \cdot \bar{b})$ with average branching factor $\bar{b}$ when sparse). Policy iteration pays $O(|\mathcal{S}|^3)$ per exact evaluation solve plus the improvement scan, over a handful of outer iterations. Either way the cost is polynomial in the size of the state space, which sounds benign until you notice how state spaces are built.

The trouble is the curse of dimensionality: in most real problems the state is a vector of features, and the number of states grows exponentially in the number of features. A robot with ten joints each discretized to a hundred positions has $100^{10} = 10^{20}$ states; a board game has astronomically many configurations; a portfolio over fifty assets is a continuum. Dynamic programming needs to enumerate and store a value for every one of these, and that is impossible past a few well-behaved dimensions. The polynomial-in-$|\mathcal{S}|$ cost is fatal precisely because $|\mathcal{S}|$ itself is exponential in the natural problem description. This single obstacle is the reason the rest of Part VI exists: we cannot enumerate the states, so we must generalize across them with function approximation, and we usually cannot write down $P$ and $R$ either, so we must learn from experience.

Requirement / costExact dynamic programmingWhat breaks at scale
model $(P, R)$must be fully known and written downusually unknown: learn from sampled transitions (Ch 24)
statesenumerable and stored in a tableexponential in features: approximate with a network (Ch 25)
per sweep$O(|\mathcal{S}|^2 |\mathcal{A}|)$ value iteration$|\mathcal{S}|$ itself astronomically large
evaluation solve$O(|\mathcal{S}|^3)$ exact, policy iterationmatrix inversion infeasible past $\sim 10^4$ states
expectation over $s'$exact sum over all successorsreplaced by a sampled next-state (TD targets)
Figure 22.4.2: What exact dynamic programming demands, and how each demand fails at scale. The two structural failures, an unknown model and an unenumerable state space, are exactly the two gaps that model-free and approximate reinforcement learning fill, which is why subsection four reads dynamic programming as the exact ideal these methods chase.

Before leaving exact methods, it helps to see policy iteration and value iteration as instances of one broader pattern, because that pattern survives the move to approximation. Generalized policy iteration (GPI) is the name for any scheme that interleaves two processes: a policy-evaluation process that drives the value estimate toward the true value of the current policy, and a policy-improvement process that drives the policy toward greedy with respect to the current value estimate. The two pull on each other, evaluation makes the value consistent with the policy, improvement makes the policy greedy for the value, and they reach a joint fixed point exactly when the policy is greedy for its own value, which is the optimality condition. Policy iteration runs each process to completion before switching; value iteration interleaves them at the finest grain (one backup each); modified policy iteration sits between. Crucially, almost every reinforcement-learning algorithm in the chapters ahead is also a GPI scheme, with the evaluation done from samples and the value stored in a network, which is why GPI, not any single algorithm, is the unifying picture to carry forward.

Fun Note: The Two Processes That Quietly Argue Until They Agree

Generalized policy iteration is best pictured as two stubborn officials sharing one office. The evaluator keeps recomputing how good the current plan is; the improver keeps rewriting the plan to exploit the latest numbers. Every time one of them finishes, the other one's work is out of date, so it starts over. It looks like an argument that could go forever, yet it cannot: each round the plan gets strictly better and there are only finitely many plans, so the two are eventually forced to agree. The point of agreement, the plan that is already optimal for its own evaluation, is the optimal policy. The whole of value-based reinforcement learning is this same office, just with the evaluator working from noisy sampled receipts instead of the exact ledger.

4. The Bridge to Reinforcement Learning: When the Model Is Unknown or Too Large Advanced

Dynamic programming assumed two luxuries: a known model $(P, R)$ and an enumerable, storable state space. Reinforcement learning is what you do when one or both are taken away, and the cleanest way to see the connection is to write the exact Bellman backup and then cross out the parts you can no longer compute, replacing each with something you can.

The first luxury removed is the known model. The optimality backup contains an expectation over successors, $\sum_{s'} P(s' \mid s, a)\, V(s')$, which needs the full transition kernel. When $P$ is unknown you cannot form that sum, but you can sample from it: take action $a$ in state $s$, observe the actual reward $r$ and next state $s'$ the environment returns, and use that single transition as an unbiased one-sample estimate of the expectation. Replacing the exact backup with a sampled one is exactly the move from dynamic programming to Monte Carlo and temporal-difference learning, the subject of Chapter 24. Temporal-difference learning in particular is the sampled, incremental cousin of the Bellman backup: where value iteration sets $V(s) \leftarrow \max_a [R(s,a) + \gamma \sum_{s'} P(s'|s,a) V(s')]$ exactly, the temporal-difference update nudges $V(s)$ toward the bootstrapped sample target $r + \gamma V(s')$, an idea that traces straight back to the contraction backup we just analyzed.

The second luxury removed is the small, enumerable state space. When states are too many to store one value each (the curse of dimensionality of subsection three), the table $V(s)$ is replaced by a parametric function $V_{\theta}(s)$, a linear model or, in the modern setting, a neural network, that generalizes a value to states it has never visited by sharing parameters across similar states. The Bellman backup becomes a regression target: fit $V_{\theta}(s)$ toward $r + \gamma \max_{a} V_{\theta}(s')$ by gradient descent. This is approximate dynamic programming, and with a deep network as the approximator it is deep reinforcement learning, the subject of Chapter 25. The deep $Q$-network is precisely a value-iteration backup with the table swapped for a convolutional or recurrent network and the exact expectation swapped for sampled transitions, the two substitutions composed.

Key Insight: Reinforcement Learning Is the Bellman Backup With Two Substitutions

Hold the exact value-iteration update $V(s) \leftarrow \max_a \big[ R(s,a) + \gamma \sum_{s'} P(s' \mid s, a) V(s') \big]$ in front of you and reinforcement learning is two principled edits to it. Edit one (unknown model): replace the exact expectation $\sum_{s'} P(\cdot) V(s')$ with a sampled next-state target $r + \gamma V(s')$ from real experience, turning the backup into temporal-difference learning (Ch 24). Edit two (too many states): replace the table $V(s)$ with a function approximator $V_{\theta}(s)$ and the assignment with a gradient step toward the target, turning the backup into approximate and deep reinforcement learning (Ch 25). Make neither edit and you have exact dynamic programming; make both and you have deep $Q$-learning; the methods between are different combinations of these two substitutions. Dynamic programming is the exact ideal; every reinforcement-learning algorithm is an approximation that recovers it in the limit of infinite data and unlimited capacity.

Research Frontier: Dynamic Programming Structure in Modern Decision Systems (2024 to 2026)

The Bellman backup is far from a museum piece; its structure keeps resurfacing at the research frontier. Offline reinforcement learning, the dominant practical setting in 2024 to 2026, runs approximate dynamic programming on a fixed dataset with no further interaction, and the central difficulty is precisely a dynamic-programming pathology: the $\max$ in the backup overestimates the value of out-of-distribution actions, so methods such as conservative $Q$-learning (CQL) and implicit $Q$-learning (IQL) modify the backup to stay pessimistic where data is thin, and the d3rlpy library has made these the standard offline baselines. A second active line connects dynamic programming to sequence models: the Decision Transformer and Trajectory Transformer (revisited at scale through 2024 to 2025) recast control as conditional sequence prediction and largely bypass the Bellman backup, sparking an ongoing debate, sharpened by 2024 to 2026 analyses, over when bootstrapped dynamic-programming targets beat supervised return-conditioning, a debate Chapter 28 takes up directly. A third thread, model-based planning in learned world models (the Dreamer line and its successors through 2024 to 2025, treated in Chapter 29), runs value iteration's spiritual descendant inside a learned latent model, closing the loop back to the known-model setting of this section but with the model itself learned. The 2026 takeaway: whenever a decision system bootstraps a value estimate from its own later estimates, the contraction analysis of subsection two is the lens that explains when it is stable and when it diverges.

5. Worked Example: Value Iteration and Policy Iteration on a Gridworld Advanced

We now make every equation of this section executable on a small but genuine MDP: a four-by-four gridworld. The agent starts anywhere, moves up, down, left, or right, pays a small step cost of $-0.04$ per move, and seeks a terminal goal cell worth $+1$ while avoiding a terminal trap worth $-1$. The model is fully known, so dynamic programming applies exactly. The plan is the cleanest demonstration of the section's claims: implement value iteration from scratch (subsection one's optimality backup swept to convergence), implement policy iteration from scratch (evaluation plus greedy improvement), confirm the two reach the same optimal policy, then collapse the whole computation into a library MDP solver. Code 22.4.1 sets up the gridworld model.

import numpy as np

# 4x4 gridworld. State = cell index 0..15 (row-major). Goal=15 (+1), trap=11 (-1),
# both terminal. Every non-terminal move costs -0.04. Discount gamma = 0.95.
N, GAMMA, STEP = 4, 0.95, -0.04
GOAL, TRAP = 15, 11
ACTIONS = {0: (-1, 0), 1: (1, 0), 2: (0, -1), 3: (0, 1)}   # up, down, left, right
n_states, n_actions = N * N, len(ACTIONS)

def step(s, a):
    """Deterministic transition: return (next_state, reward, done)."""
    if s in (GOAL, TRAP):
        return s, 0.0, True                 # terminals absorb, no further reward
    r, c = divmod(s, N)
    dr, dc = ACTIONS[a]
    nr, nc = r + dr, c + dc
    if not (0 <= nr < N and 0 <= nc < N):
        nr, nc = r, c                       # bumping a wall: stay in place
    ns = nr * N + nc
    reward = {GOAL: 1.0, TRAP: -1.0}.get(ns, STEP)
    return ns, reward, ns in (GOAL, TRAP)

# Precompute the model tables P (next state) and R (reward) for every (s, a).
P = np.zeros((n_states, n_actions), dtype=int)
R = np.zeros((n_states, n_actions))
for s in range(n_states):
    for a in range(n_actions):
        ns, rew, _ = step(s, a)
        P[s, a], R[s, a] = ns, rew

print("model built: %d states, %d actions" % (n_states, n_actions))
print("reward for moving into goal:", R[14, 3])   # cell 14, move right -> goal
Code 22.4.1: The four-by-four gridworld model. Transitions are deterministic, so the expectation $\sum_{s'} P(s' \mid s, a) V(s')$ in every backup collapses to a single successor lookup V[P[s, a]], which keeps the dynamic-programming code uncluttered while remaining a faithful instance of the general equations.
model built: 16 states, 4 actions
reward for moving into goal: 1.0
Output 22.4.1: The model is in place: sixteen states, four actions, and the reward of one for the move that reaches the goal cell. Both dynamic-programming algorithms below read only these P and R tables.

Code 22.4.2 implements value iteration: the optimality backup of subsection one swept across all states until the largest value change falls below a tolerance, then the greedy policy read off the converged values. The per-sweep error contracts by $\gamma$, exactly as subsection two proved.

def value_iteration(P, R, gamma=GAMMA, tol=1e-8):
    """Sweep the Bellman optimality backup V <- max_a [R + gamma V(s')] to convergence."""
    V = np.zeros(n_states)
    sweeps = 0
    while True:
        Q = R + gamma * V[P]                  # Q[s,a] = R[s,a] + gamma * V[next state]
        Q[GOAL, :] = Q[TRAP, :] = 0.0         # terminals have zero value
        V_new = Q.max(axis=1)                 # the max over actions IS the improvement
        sweeps += 1
        if np.max(np.abs(V_new - V)) < tol:   # stop when the largest change is tiny
            V = V_new
            break
        V = V_new
    pi = Q.argmax(axis=1)                     # greedy policy from the converged values
    return V, pi, sweeps

V_vi, pi_vi, sweeps = value_iteration(P, R)
print("value iteration converged in %d sweeps" % sweeps)
print("V(start cell 0) = %.4f" % V_vi[0])
print("greedy policy:", pi_vi.reshape(N, N))
Code 22.4.2: Value iteration from scratch. The single line V_new = (R + gamma * V[P]).max(axis=1) is the Bellman optimality operator $\mathcal{T}$ applied to the whole state space at once; the loop iterates $V_{k+1} = \mathcal{T} V_k$ until the supremum-norm change drops below tolerance, the geometric convergence of subsection two in code.
value iteration converged in 7 sweeps
V(start cell 0) = 0.5928
greedy policy:
[[1 1 1 1]
 [1 1 1 2]
 [1 1 1 0]
 [3 3 3 0]]
Output 22.4.2: Value iteration converges in 7 sweeps to a value of about 0.59 for the start cell and a greedy policy that routes toward the goal. The sweep count reflects $\gamma = 0.95$ and the $10^{-8}$ tolerance; with a discount only moderately close to one the $O(\tfrac{1}{1-\gamma}\log\tfrac{1}{\varepsilon})$ bound stays small, and slow far-sighted convergence shows up only as $\gamma$ approaches one.

Code 22.4.3 implements policy iteration: iterative policy evaluation (subsection one's expectation equation swept to convergence) alternating with greedy improvement, until the policy stops changing. We then assert it finds the same optimal policy as value iteration, the central claim that the two algorithms are two settings of one dial.

def policy_evaluation(pi, P, R, gamma=GAMMA, tol=1e-8):
    """Iterate V <- R[s, pi(s)] + gamma V(s') to the value of the fixed policy pi."""
    V = np.zeros(n_states)
    while True:
        idx = np.arange(n_states)
        V_new = R[idx, pi] + gamma * V[P[idx, pi]]    # Bellman EXPECTATION backup under pi
        V_new[GOAL] = V_new[TRAP] = 0.0
        if np.max(np.abs(V_new - V)) < tol:
            return V_new
        V = V_new

def policy_iteration(P, R, gamma=GAMMA):
    """Alternate exact evaluation and greedy improvement until the policy is stable."""
    pi = np.zeros(n_states, dtype=int)        # start from 'always go up'
    iters = 0
    while True:
        V = policy_evaluation(pi, P, R, gamma)        # evaluate current policy
        Q = R + gamma * V[P]                          # action values from that V
        pi_new = Q.argmax(axis=1)                     # greedy improvement
        iters += 1
        if np.array_equal(pi_new, pi):                # no action changed: optimal
            return V, pi_new, iters
        pi = pi_new

V_pi, pi_pi, iters = policy_iteration(P, R)
print("policy iteration converged in %d improvement steps" % iters)

# The central check: both exact methods reach the SAME optimal policy.
movable = [s for s in range(n_states) if s not in (GOAL, TRAP)]
print("same policy as value iteration:", np.array_equal(pi_pi[movable], pi_vi[movable]))
print("values agree:", np.allclose(V_pi, V_vi, atol=1e-4))
Code 22.4.3: Policy iteration from scratch, the evaluate-then-improve loop of Figure 22.4.1. Each outer step solves for $V^{\pi}$ by iterating the expectation backup, then sets $\pi$ greedy with respect to it; the loop halts when no action changes. The final check confirms it reaches the identical optimal policy that value iteration found, in far fewer outer steps.
policy iteration converged in 5 improvement steps
same policy as value iteration: True
values agree: True
Output 22.4.3: Policy iteration reaches the optimum in just five improvement steps (versus 7 value-iteration sweeps), and its policy and values match value iteration's exactly. This is the "two settings of one dial" insight made concrete: policy iteration takes few expensive outer steps, value iteration takes many cheap sweeps, both land on $\pi^{\star}$.
Numeric Example: One Value-Iteration Sweep, by Hand

Take the cell just left of the goal, state 14, after the values have partly settled so that the neighbor values are roughly $V(13) = 0.55$, $V(10) = 0.50$, and $V(14) = 0.60$ from the previous sweep. The backup at state 14 maximizes over the four actions: moving right reaches the terminal goal and collects the entry reward, $R[14,3] + \gamma V[15] = 1.0 + 0.95 \cdot 0 = 1.0$ (the goal is terminal, $V[15] = 0$). Moving left to state 13 gives $-0.04 + 0.95 \cdot 0.55 = 0.4825$; moving up to state 10 gives $-0.04 + 0.95 \cdot 0.50 = 0.435$; moving down bumps the wall and stays, $-0.04 + 0.95 \cdot 0.60 = 0.53$. The max is the move-right value $1.0$, so $V_{\text{new}}(14) = 1.0$ and the greedy action is "right", toward the goal. One state, one $\max$ over four numbers: multiply that by sixteen states and you have one full sweep of Code 22.4.2, and by a handful of sweeps the whole grid has settled.

Step back and read what the three code blocks establish together. Code 22.4.2 swept the optimality backup of subsection one to convergence and read off a policy; Code 22.4.3 alternated exact evaluation and greedy improvement and reached the identical policy in a fraction of the outer iterations; the equality check certified that policy iteration and value iteration are the same dial at two settings, exactly as subsection one claimed. The from-scratch pair is about forty lines of careful backup bookkeeping. A dedicated MDP library collapses all of it.

Library Shortcut: The Whole Solve in a Few Lines

The from-scratch value iteration and policy iteration above ran roughly forty lines of backup and convergence bookkeeping between them. A dedicated dynamic-programming library, mdptoolbox (the pymdptoolbox package), collapses each exact solve to about three lines: hand it the transition and reward arrays and call the solver, and it runs the same Bellman backups, the same convergence test, and the same greedy read-off internally, returning the optimal value and policy. (A currency note: pymdptoolbox is unmaintained, its last release predates current NumPy, and it may need a NumPy-compat shim or a pin to numpy<1.24 to import cleanly; the maintained mdptoolbox-hiive fork, or Gymnasium with a value-iteration helper, are current alternatives.)

import mdptoolbox
import numpy as np

# mdptoolbox wants P of shape (A, S, S) and R of shape (S, A). Build the
# sparse-but-dense transition tensor from our deterministic gridworld model.
T = np.zeros((n_actions, n_states, n_states))
for s in range(n_states):
    for a in range(n_actions):
        T[a, s, P[s, a]] = 1.0                # deterministic: all mass on one successor

vi = mdptoolbox.mdp.ValueIteration(T, R, GAMMA)   # construct the solver
vi.run()                                          # runs Bellman backups to convergence
print("library optimal policy:", np.array(vi.policy).reshape(N, N))
print("library value(start)  : %.4f" % vi.V[0])
Code 22.4.4: The library equivalent of Code 22.4.2 and 22.4.3. The forty lines of hand-written value and policy iteration reduce to constructing mdptoolbox.mdp.ValueIteration and calling run(); the library handles the sweep loop, the contraction-based stopping rule, and the greedy extraction, returning the same optimal policy our from-scratch code computed. Use mdptoolbox.mdp.PolicyIteration for the policy-iteration variant with an identical interface.

The shortcut is the right tool once your MDP is genuinely tabular and you trust the model: mdptoolbox also ships modified policy iteration, relative value iteration for average-reward problems, and Gauss-Seidel sweeps, each a few characters of configuration. The from-scratch version remains the right tool for understanding what those solvers do and for the moment, arriving in Chapter 24, when the model disappears and no library can simply be handed a transition tensor.

Practical Example: Exact Inventory Control at a Regional Warehouse

Who: An operations analyst at a regional distribution warehouse setting a daily reorder policy for a single high-volume stock-keeping unit, the kind of supply-chain decision problem that recurs through the industrial applications of Chapter 35.

Situation: Demand each day is random but its distribution is known from years of sales history, holding inventory costs money, stockouts cost more in lost sales and goodwill, and reordering incurs a fixed shipping charge. The state is the current on-hand quantity, capped at a warehouse limit of a few hundred units; the action is how much to reorder.

Problem: Find the reorder policy, how much to order at each inventory level, that minimizes expected long-run discounted cost. The state space is small and enumerable (a few hundred levels) and the demand distribution gives an exact transition model, so this is a textbook fully-known MDP.

Dilemma: A heuristic order-up-to rule was simple but left money on the table near the capacity boundary and during demand spikes, where its fixed target was clearly wrong. The team could keep tuning the heuristic by hand or solve the MDP exactly.

Decision: Because the model was known and the state space small, they solved it exactly with policy iteration rather than learning from simulation: with only a few hundred states, the $O(|\mathcal{S}|^3)$ evaluation solve was trivial, and policy iteration's handful of outer steps made it faster than value iteration to a clean policy.

How: They built the transition matrix from the known demand distribution (the probability of moving from each inventory level to each next level under each reorder amount), the reward as negative expected cost, and ran policy iteration exactly as in Code 22.4.3, then sanity-checked it against mdptoolbox as in Code 22.4.4.

Result: The optimal policy was a state-dependent $(s, S)$ rule, reorder up to a target $S$ whenever stock fell below a threshold $s$, with both thresholds varying near the capacity boundary in ways the hand-tuned heuristic had missed. Expected cost fell by a clear margin, and because the policy was exact there was nothing left to tune.

Lesson: When the model is genuinely known and the states genuinely enumerable, do not reach for reinforcement learning, solve the MDP exactly. Dynamic programming is not a fallback; it is the correct and optimal tool whenever its two assumptions hold. Reinforcement learning earns its keep precisely when one of those assumptions fails.

6. Constrained MDPs and Safe Reinforcement Learning Intermediate

Every MDP formulation so far has a single objective: maximize expected discounted return. Real deployments almost never work that way. A trading agent must limit its drawdown. A clinical decision system must not exceed a drug dosage threshold. A robotics agent must avoid collisions. These are constraints, not just soft penalties, and ignoring them in training produces agents that are optimal by one measure but undeployable by any real standard. The Constrained MDP (CMDP) framework (Altman 1999) extends the standard MDP by adding one or more cost functions alongside the reward, and the optimization becomes: maximize the expected cumulative reward subject to the expected cumulative cost staying below a threshold.

Formally, a CMDP augments the MDP tuple $(\mathcal{S}, \mathcal{A}, P, R, \gamma)$ with $K$ cost functions $c_1, \ldots, c_K$ and thresholds $d_1, \ldots, d_K$. The problem is

$$\max_{\pi} \; J_r(\pi) = \mathbb{E}_{\pi}\!\left[\sum_{t=0}^{\infty} \gamma^t r_t\right] \quad \text{subject to} \quad J_{c_k}(\pi) = \mathbb{E}_{\pi}\!\left[\sum_{t=0}^{\infty} \gamma^t c_{k,t}\right] \le d_k \quad \text{for each } k.$$

Two points about this formulation deserve emphasis. First, the constraint is on the expected cumulative cost, not the per-step cost: the agent may occasionally violate it if the average stays below $d_k$. Second, the feasible set of policies is generally not convex in the policy space, so naively applying policy gradient to the constrained problem requires special care. The two main algorithmic families for CMDPs are Lagrangian relaxation and constrained policy optimization.

Lagrangian Relaxation

The Lagrangian approach converts the CMDP to an unconstrained primal-dual problem. Introduce a dual variable $\lambda_k \ge 0$ for each constraint and form the Lagrangian

$$L(\pi, \lambda) = J_r(\pi) - \sum_{k} \lambda_k \left(J_{c_k}(\pi) - d_k\right).$$

The saddle-point problem $\max_{\pi} \min_{\lambda \ge 0} L(\pi, \lambda)$ recovers the constrained optimum under mild convexity conditions. In practice the primal-dual update alternates two steps. The primal step updates the policy to maximize $L$ for fixed $\lambda$: any policy gradient method applies directly because the Lagrangian is a reward with an added penalty $-\lambda_k c_k$. The dual step updates $\lambda$ to penalize constraint violations:

$$\lambda_k \leftarrow \max\!\big(0,\; \lambda_k + \alpha \left(J_{c_k}(\pi) - d_k\right)\big).$$

When the constraint is violated ($J_{c_k} > d_k$), $\lambda_k$ increases, making the penalty term heavier and pushing the policy back toward feasibility. When the constraint is satisfied with slack, $\lambda_k$ decreases. At convergence, $\lambda_k > 0$ only if the constraint is tight (complementary slackness), exactly as in classical constrained optimization.

Numeric Example: 2-State Safety Constraint

Consider a two-state MDP with $\mathcal{S} = \{\text{safe}, \text{risky}\}$ and $\mathcal{A} = \{\text{aggressive}, \text{cautious}\}$. Rewards: aggressive in risky $= +10$, cautious anywhere $= +1$. Cost: aggressive in risky $= 1$ (a safety violation), all others $= 0$. Threshold $d = 0.1$, meaning at most a 10% violation rate is allowed. The unconstrained optimal policy is always aggressive (collect $+10$ whenever possible). The constrained optimal policy is aggressive in the safe state and cautious in the risky state, which keeps the long-run cost rate near $d = 0.1$ without forgoing all reward. The Lagrangian dual variable $\lambda$ grows until it makes the cost of being aggressive in the risky state ($\lambda \cdot 1$) outweigh the reward gain ($+10 - +1 = +9$), forcing a switch to cautious once $\lambda > 9$.

Constrained Policy Optimization (CPO)

The Lagrangian approach can violate constraints during training, because the policy update step does not enforce feasibility on each iterate. CPO (Achiam et al., NeurIPS 2017) is a trust-region method that enforces the constraint on every policy update. Each step solves

$$\max_{\pi} \; J_r(\pi) \quad \text{s.t.} \quad J_{c_k}(\pi) \le d_k \;\text{ for each }k, \quad D_{\mathrm{KL}}(\pi \,\|\, \pi_{\mathrm{old}}) \le \delta,$$

simultaneously imposing the safety constraint and a KL trust-region constraint to prevent large policy steps. CPO approximates both the objective and constraints with their first-order surrogates at $\pi_{\mathrm{old}}$ and solves the resulting quadratic program analytically. The key result is a recovery step: if no feasible point exists within the trust region, CPO falls back to minimizing constraint violation rather than maximizing reward, prioritizing safety over performance. This makes CPO's training trajectory constraint-respecting from the first iteration, a meaningful property when running in a physical or clinical environment.

FOCOPS: First-Order Constrained Optimization in Policy Space

CPO's trust-region solve involves a second-order quadratic program that is expensive to compute exactly for large policy networks. FOCOPS (Zhang et al., NeurIPS 2020) achieves comparable constraint satisfaction at much lower cost using two first-order steps per iteration. The first step projects the unconstrained policy gradient onto the subspace that is consistent with the linearized constraint: if the gradient $g$ would increase $J_c$ above $d$, subtract the component of $g$ that points in the direction of increasing cost. The second step takes a standard gradient ascent step on the projected gradient. The linearization discards some of the curvature information that CPO uses, but the cost is proportional to a standard policy gradient update rather than a quadratic solve, making FOCOPS practical with deep policy networks where CPO's solve is prohibitive.

Key Insight: Lagrangian vs. Projection Methods

Lagrangian RL is simple to implement: add a penalty term to the reward and update a scalar dual variable after each rollout. The drawback is that constraint satisfaction is only guaranteed in expectation at convergence, and violations can be large during training. Projection methods (CPO, FOCOPS) enforce a linearized version of the constraint on every update, reducing mid-training violations at the cost of more algebra per step. For systems where mid-training safety violations are tolerable (simulated environments with a safety supervisor), Lagrangian RL is the practical starting point. For systems where mid-training violations are unacceptable (physical robots, clinical pilots), CPO or FOCOPS is the appropriate choice.

Evaluating Safe RL: Safety Gym

Comparing safe RL algorithms requires benchmarks that measure both performance and constraint satisfaction. Safety Gym (Ray et al., OpenAI 2019) provides standardized CMDP environments for this purpose. The three task families are PointGoal (a point-mass robot navigating to a goal while avoiding hazards), CarGoal (a car-like robot with dynamics), and AntGoal (a legged robot). Each task is parameterized by difficulty level 0, 1, or 2. The evaluation metric is a pair: the episodic return (reward) and the cost-rate (average number of constraint violations per episode). An algorithm that achieves a high return with a low cost-rate is the goal; algorithms that sacrifice all return to avoid all violations, or that achieve high return by ignoring constraints, both fail the benchmark. The Python package is safety-gymnasium, the maintained successor to Safety Gym.

Library Shortcut: Lagrangian Safe RL Loop

The code below implements a minimal primal-dual safe RL loop using a linear policy and a PyTorch-based dual variable update. Install the benchmark environment with pip install safety-gymnasium. For production use, the omnisafe library implements CPO, FOCOPS, and Lagrangian RL with the same interface.

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np

# A minimal Lagrangian safe RL loop.
# Policy: linear mapping from state to action logits (for discrete action spaces).
# Constraint threshold d: max allowed expected cumulative cost per episode.

class LinearPolicy(nn.Module):
    def __init__(self, obs_dim, act_dim):
        super().__init__()
        self.fc = nn.Linear(obs_dim, act_dim)

    def forward(self, obs):
        return torch.softmax(self.fc(obs), dim=-1)

def collect_rollout(env, policy, max_steps=200):
    """Run one episode; return rewards, costs, log-probs."""
    obs, _ = env.reset()
    rewards, costs, log_probs = [], [], []
    for _ in range(max_steps):
        obs_t = torch.tensor(obs, dtype=torch.float32)
        probs = policy(obs_t)
        dist = torch.distributions.Categorical(probs)
        action = dist.sample()
        log_probs.append(dist.log_prob(action))
        obs, reward, terminated, truncated, info = env.step(action.item())
        rewards.append(reward)
        costs.append(info.get("cost", 0.0))   # Safety Gym stores cost in info
        if terminated or truncated:
            break
    return rewards, costs, log_probs

def lagrangian_update(policy, optimizer, lam, lam_lr, rewards, costs, log_probs,
                      gamma=0.99, d=25.0):
    """One primal-dual update step.
    d: constraint threshold (cumulative cost per episode; Safety Gym default ~25).
    """
    # Discounted returns for reward and cost.
    G_r, G_c = [], []
    g_r = g_c = 0.0
    for r, c in zip(reversed(rewards), reversed(costs)):
        g_r = r + gamma * g_r
        g_c = c + gamma * g_c
        G_r.insert(0, g_r)
        G_c.insert(0, g_c)
    G_r = torch.tensor(G_r, dtype=torch.float32)
    G_c = torch.tensor(G_c, dtype=torch.float32)
    log_probs = torch.stack(log_probs)

    # Lagrangian objective: maximize J_r - lambda * (J_c - d).
    J_c_ep = G_c[0].item()          # episode cumulative cost (rough scalar)
    lagrangian_loss = -(G_r * log_probs).mean() + lam * (G_c * log_probs).mean()
    optimizer.zero_grad()
    lagrangian_loss.backward()
    optimizer.step()

    # Dual update: increase lambda if constraint violated, decrease if satisfied.
    lam = max(0.0, lam + lam_lr * (J_c_ep - d))
    return lam, J_c_ep

# --- Training loop (replace DummyEnv with a safety-gymnasium environment) ---
# import safety_gymnasium
# env = safety_gymnasium.make("SafetyPointGoal1-v0")
# obs_dim = env.observation_space.shape[0]
# act_dim = env.action_space.n

obs_dim, act_dim = 8, 4            # placeholder dimensions
policy = LinearPolicy(obs_dim, act_dim)
optimizer = optim.Adam(policy.parameters(), lr=1e-3)
lam = 0.0                          # dual variable, starts at zero
lam_lr = 0.01
d = 25.0                           # cost threshold

# Uncomment to train:
# for episode in range(500):
#     rewards, costs, log_probs = collect_rollout(env, policy)
#     lam, ep_cost = lagrangian_update(
#         policy, optimizer, lam, lam_lr, rewards, costs, log_probs, d=d)
#     if episode % 50 == 0:
#         print(f"ep {episode}: return={sum(rewards):.1f} cost={ep_cost:.1f} lam={lam:.3f}")
print("Lagrangian loop defined. Swap DummyEnv for safety_gymnasium.make(...) to run.")
Code 22.4.5: Minimal primal-dual Lagrangian safe RL. The dual variable lam increases whenever the episode cost exceeds threshold d, making the policy gradient penalize constraint violations more heavily. At convergence lam stabilizes and the expected cost sits near d. Replace the placeholder dimensions with a real safety-gymnasium environment for a complete training run.
Research Frontier: Safe RL in Deployed Temporal AI Systems

Safe RL is not merely a robotics concern. Any temporal AI pipeline that translates a forecast into an automated action inherits the CMDP structure. An algorithmic trading system whose forecasting module triggers orders must satisfy drawdown constraints (a financial regulatory constraint), position-size limits (a risk-management constraint), and execution-impact constraints (a market-microstructure constraint). A clinical decision-support system that recommends drug dosages based on a patient-state forecast must not exceed toxicity thresholds, even if a higher dose produces a better predicted outcome. The 2024 to 2026 research frontier in safe RL for these domains includes three open problems. First, constraint-conditioned generalization: how should a safe RL agent behave when the constraint threshold $d$ shifts at deployment time, for instance because a new regulation tightens the drawdown limit? Offline-to-online safe RL transfer and constraint-adaptive policies are active areas. Second, multi-constraint feasibility detection: with many simultaneous constraints, the feasible policy set may be empty or nearly empty, and identifying this before wasted training is unsolved. Third, distributional safety: the CMDP constraint is on the expected cumulative cost, but practitioners often want tail-risk guarantees, for example a 95th-percentile cost bound, which requires connecting safe RL to conditional value-at-risk objectives, an active line connecting the risk-measures literature of Part V to the safe RL methods of this section.

Thesis Thread: From MDP to Sequence Model

The equivalence between Markov decision processes and sequence models is formalized in Section 28.1: when a trajectory (s₀,a₀,r₀,...,sH) is viewed as a sequence of tokens, supervised learning on offline trajectories becomes policy learning — and the Decision Transformer, a GPT model trained on trajectories, implicitly solves an MDP by next-token prediction.

Exercises

These exercises split into conceptual, implementation, and open-ended work. Selected solutions appear in Appendix G.

  1. (Conceptual) The contraction and the rate. Starting from the $\gamma$-contraction inequality $\lVert \mathcal{T} V - \mathcal{T} W \rVert_{\infty} \le \gamma \lVert V - W \rVert_{\infty}$, derive the geometric error bound $\lVert V_k - V^{\star} \rVert_{\infty} \le \gamma^k \lVert V_0 - V^{\star} \rVert_{\infty}$. Then explain in one or two sentences why a discount of $\gamma = 0.999$ makes value iteration roughly ten times slower to a fixed accuracy than $\gamma = 0.99$, and relate this to the effective horizon $1/(1 - \gamma)$. Finally, argue why the $\max_a$ step does not break the contraction, using the non-expansion property $|\max_a f(a) - \max_a g(a)| \le \max_a |f(a) - g(a)|$.
  2. (Implementation) Modified policy iteration and a stochastic grid. Extend Code 22.4.2 and 22.4.3 in two ways. First, implement modified policy iteration: run exactly $m$ evaluation sweeps (not to convergence) between improvement steps, and plot the total number of backups to convergence as a function of $m$ for $m \in \{1, 2, 5, 20, \infty\}$, confirming that $m = 1$ recovers value iteration, $m = \infty$ recovers policy iteration, and an intermediate $m$ is fastest. Second, make the gridworld stochastic: each intended move succeeds with probability $0.8$ and slips to a perpendicular cell with probability $0.1$ each, so the backup now sums over three successors. Verify value iteration and policy iteration still agree, and report how the optimal policy changes when actions are noisy.
  3. (Open-ended) Where exact dynamic programming runs out. Take the gridworld and grow it: solve $4 \times 4$, $10 \times 10$, $30 \times 30$, and $100 \times 100$ grids with value iteration, recording wall-clock time and peak memory for each. Fit the empirical scaling and compare it to the $O(|\mathcal{S}|^2 |\mathcal{A}|)$ per-sweep cost of subsection three. Then estimate, by extrapolation, the grid size at which exact value iteration would no longer fit in your machine's memory, and write a paragraph connecting that wall to the curse of dimensionality and to why Chapter 25 replaces the value table with a neural network. Optionally, replace the tabular value with a small linear function of grid coordinates and discuss what you gain and what you lose.
Looking Back: Chapter 22 Complete, and the Road Into Part VI

This section closes Chapter 22. We built the Markov decision process from its components (states, actions, transitions, rewards, discount), wrote the Bellman equations that any value function must satisfy, established the optimality equations whose unique solution is $V^{\star}$, and in this section turned those equations into exact algorithms: policy iteration and value iteration, both converging by the same $\gamma$-contraction, both reaching the same optimal policy, both demonstrated from scratch and then in a library solver. Dynamic programming is the exact ideal of sequential decision making, the answer you would always compute if you could.

The rest of Part VI is the study of what to do when you cannot. Chapter 23 drops the assumption that the agent sees the state: under partial observability the state is hidden behind noisy observations, the agent must maintain a belief over states (the same belief-state recurrence that the Kalman filter and HMM of Section 7.6 computed), and the MDP becomes a POMDP whose dynamic programming runs over belief space. Chapter 24 drops the assumption that the model is known: when $P$ and $R$ are unavailable, the agent must learn values from sampled experience, and the Bellman backup of this section becomes the temporal-difference update that anchors all of reinforcement learning. Hold onto the contraction picture: every method ahead is a Bellman backup performed under one luxury or another removed, and the exact backup you just implemented is the target they all chase.