Blog · May 12, 2026
MDPs and the Bellman Equation: The Recursion Behind Every RL Algorithm

The throughline: The value of where I am is the reward I just got, plus a discounted value of where I’ll land next.
The RL Foundations post gave us the vocabulary (policy, reward, value) and the math toolkit (expectation, discounting, the Markov property). This post puts those pieces together into a precise machine: the Markov Decision Process (MDP) formalizes the environment, value functions score how good a state or action is, and the Bellman equation expresses value as a recursion, one equation that every RL algorithm is, at its core, a way of solving.
1. The intuition: one loop, one lookup table, one recursion
The agent–environment loop, one more time
Every RL problem is the same loop. At each discrete time step :
flowchart LR agent["Agent (policy π)"] env["Environment (dynamics p)"] agent -->|"action Aₜ"| env env -->|"reward Rₜ₊₁, next state Sₜ₊₁"| agent
The agent observes state , picks action according to its policy , the environment responds with reward and next state , and the cycle repeats. The full history is a trajectory:
What makes an environment an MDP?
An MDP is a tuple :
- : a finite set of states (every cell on a grid, every board position in chess).
- : a finite set of actions (left/down/right/up on a grid).
- , the dynamics function: the probability that action in state leads to next state with reward . This single function is the complete DNA of the environment. If you have it, you can plan (dynamic programming). If you don’t, you must learn from experience (Monte Carlo, TD).
- : the set of possible reward values.
- : the discount factor (introduced in RL Foundations, §2.5).
“Finite” just means you can count the states, actions, and rewards: you could list them all on a whiteboard. FrozenLake has 16 states, 4 actions, and rewards in .
The Markov property (quick recap)
The dynamics depend only on , not on how you got there. This isn’t magic: it’s a constraint on what counts as the state. If history matters, pack it into the state (frame-stacking in Atari, full transcript in a chatbot). The payoff: value can be a function of alone.
The agent–environment boundary
A subtle point: the boundary is control, not anatomy. A robot’s motors and gears are part of the environment: the agent sends voltage commands, but what the motors do follows physics, not the agent’s wishes. Even if the agent knows the rules perfectly (like in chess), the reward computation is still “outside”; it defines the task, it doesn’t solve it.
The reward hypothesis
All goals can be thought of as the maximization of the expected cumulative sum of a scalar reward signal.
Bold claim, but it works: chess ( win, loss), maze ( per step, so hurry!), walking robot (reward proportional to forward speed). The critical design principle: reward says what to achieve, not how. If you reward a chess agent for capturing pieces, it might sacrifice a winning position to grab a queen.
Check: The reward function is the only place you tell the agent what you want. Invent a reward for "drive safely" that an agent could obviously game. What does that reveal?
Answer. Reward for every second without a crash, and the agent games it by driving extremely slowly or never leaving the driveway: technically “safe,” completely useless. It shows the reward specifies what you measure, not what you mean, and any proxy can be exploited. That failure mode is reward hacking.
2. The math you need
2.1 The dynamics function
This is a lookup table: plug in four things (current state , action , next state , reward ) and get back a probability. For any fixed , the probabilities over all outcomes sum to 1:
In Gymnasium, this table lives in env.unwrapped.P. Each entry is a list of (probability, next_state, reward, done) tuples, exactly :
import gymnasium as gym
# Create the 4x4 FrozenLake with slippery ice (stochastic transitions).# is_slippery=True means each action has only 1/3 chance of going where intended;# the other 2/3 of the time the agent slides perpendicular.env = gym.make("FrozenLake-v1", is_slippery=True)
# P is the full dynamics table p(s', r | s, a).# P[s][a] returns a list of (probability, next_state, reward, done) tuples# for every possible outcome of taking action a in state s.P = env.unwrapped.P
# Query: "What happens if I try to go Right (action=2) from state 6?"# state 6 (row 1, col 2), action Rightstate, action = 6, 2print(f"p(s', r | s={state}, a={action}):")for prob, next_state, reward, done in P[state][action]: # Each tuple is one branch of the stochastic outcome: # prob = p(s',r|s,a), the transition probability for this specific outcome print(f" prob={prob:.4f} s'={next_state:2d} r={reward:.0f} done={done}")env.close()p(s', r | s=6, a=2): prob=0.3333 s'=10 r=0 done=False prob=0.3333 s'= 7 r=0 done=True prob=0.3333 s'= 2 r=0 done=FalseBecause the ice is slippery, action “Right” from state 6 has only a 1/3 chance of actually going right (to state 7, a hole!). The other 2/3 of the time the agent slides down or up. This is the stochasticity that makes the problem an MDP rather than a deterministic puzzle.
2.2 Returns: what exactly are we maximizing?
The agent doesn’t maximize a single reward; it maximizes the return , the cumulative discounted reward from time onward:
- Episodic tasks (chess, maze) end at some terminal step ; the sum is finite even without discounting.
- Continuing tasks (thermostat, stock trader) run forever; discounting with keeps the sum finite: .
The discount factor answers “how far-sighted is this agent?” (: myopic; : values the distant future almost equally).
A quick calculation: with and , the return is bounded by . That finite ceiling is why the Bellman equation works for infinite-horizon tasks.
2.3 The recursive return: the heartbeat of RL
The most important algebraic trick in all of reinforcement learning. Start with the definition of and factor out :
The return from now equals the immediate reward plus times the return from one step later. This one-step recursion is what makes the Bellman equation work: the whole chain of future rewards collapses into a single recursive step.
2.4 Value functions: vs , the emphasis
Two ways to score “how good”:
State-value function , one number per state:
Read: “V-pi of s equals the expected return G_t, given that the current state is s, when following policy pi.”
Interpretation: if I start in state and follow policy from here on, what return should I expect on average?
Action-value function , one number per state-action pair:
Read: “Q-pi of s, a equals the expected return G_t, given that the current state is s and the action taken is a, when following policy pi afterward.”
Interpretation: if I start in state , take action , then follow afterward, what return should I expect?
breaks open per action. tells you how good a state is; tells you which action makes it that good. Both depend on the policy: same state, different policy, different value.
The V-Q bridge (both directions)
and are two views of the same thing, connected by two equations:
Read: “V-pi of s equals the sum over all actions a of the policy probability pi(a|s) times Q-pi(s, a).”
Interpretation: is the policy-weighted average of . You average the action-values over the actions the policy would take: if the policy spreads 70% on left and 30% on right, V is 0.7·Q(left) + 0.3·Q(right).
Read: “Q-pi of s, a equals the sum over all possible next states s’ and rewards r of the transition probability p(s’,r|s,a) times [r + gamma times V-pi(s’)].”
Interpretation: is the environment-weighted average of . You average over where the environment sends you: each landing spot contributes its immediate reward plus the discounted value of landing there, weighted by how likely that transition is.
The two diagrams below visualize these equations as a tree and a bar chart.
Reading the backup diagram (tree). Start at the top: the single node is the state you’re in. Going down one level, it branches into multiple nodes, one per action the policy might take. The edges are labeled : the probability the policy gives to each action. This is the first equation (): V averages over actions using policy probabilities. Going down another level, each node branches into multiple nodes (next states). The edges are labeled : the transition probabilities from the environment. This is the second equation (): Q averages over next states using environment probabilities. Together, the full tree from top to bottom is the expanded Bellman equation: two layers of averaging (agent choice, then environment randomness).
Reading the bar chart. Each bar is for one action (Left, Down, Right, Up) at a single state. The bars have different heights because some actions lead to better outcomes than others (Right is tallest here: it leads toward the goal). The dashed horizontal line is : the policy-weighted average of all four Q-values. Under a uniform policy ( for each action), that average is simply , which lands between the bars, not at any single bar’s height. This is the V-Q bridge in a picture: V is just the expected Q under the policy.
Why matters more in practice
With alone, choosing an action requires the model: “if I take action , where do I land, and what’s there?” That needs .
With , choosing is trivial: pick . No model needed. This is why DQN learns , not , and the policy falls out of the argmax for free:
Read: “Pi-star of s equals the action a that maximizes Q-star(s, a).”
Interpretation: the optimal policy just picks whichever action has the highest Q-value. No search, no model, just compare numbers and take the biggest.
Here’s the bridge in code. The key question: how does each equation become a loop?
Equation → code for from :
The sum says: “loop over every possible outcome of taking action in state .” Each outcome has a probability and contributes to the total. In Gymnasium, P[s][a] gives exactly that list of outcomes as (prob, next_state, reward, done) tuples, so the sum becomes a for-loop accumulating prob * (reward + gamma * V[next_state]).
Equation → code for from :
The sum says: “loop over every action .” Each action has a policy probability and contributes . Under a uniform policy, every action gets weight , so V is just the average of the four Q-values.
import gymnasium as gymimport numpy as np
env = gym.make("FrozenLake-v1", is_slippery=True)P = env.unwrapped.PnS, nA = 16, 4gamma = 0.99
# Start with a rough placeholder V(s) to demonstrate the V↔Q bridge.# In practice, V would come from solving the Bellman equation; here we# seed it with small random values and set known terminal states.V = np.random.default_rng(0).uniform(0, 0.1, size=nS) # placeholder VV[15] = 1.0 # goal state has high value (reward +1 for arriving)V[[5, 7, 11, 12]] = 0.0 # holes are absorbing with zero value
def q_from_v(V: np.ndarray, s: int, a: int) -> float: """Q^π(s,a) = Σ_{s'} p(s',r|s,a) · [r + γ·V(s')]
Given I take action a in state s, what's my expected return? Averages over environment randomness (where do I land?). """ q_value: float = 0.0 # P[s][a] is the dynamics table: each entry is one possible outcome # of taking action a in state s. for prob, next_state, reward, done in P[s][a]: # This one line IS the equation: # p(s',r|s,a) · [r + γ · V(s') ] q_value += prob * (reward + gamma * V[next_state]) return q_value
def v_from_q(Q_values: list[float], pi_probs: list[float]) -> float: """V^π(s) = Σ_a π(a|s) · Q(s,a)
What's the value of state s under policy π? Averages over agent randomness (which action do I pick?). """ v_value: float = 0.0 # Loop over every action: weight each Q-value by the policy probability for pi_a, q_a in zip(pi_probs, Q_values): # π(a|s) · Q(s,a) v_value += pi_a * q_a return v_value
# Compute Q(6, a) for all 4 actions, then recover V(6) from Q.s = 6Q_s = [q_from_v(V, s, a) for a in range(nA)]pi_uniform = [0.25] * nA # uniform random policy: equal weight on all actions
print(f"Q({s}, a) = {[f'{q:.4f}' for q in Q_s]}")print(f"V({s}) via Q = {v_from_q(Q_s, pi_uniform):.4f}")# Greedy action = argmax_a Q(s,a): the model-free decision rule.# With Q in hand, acting optimally is just picking the largest number.print(f"Greedy action = {np.argmax(Q_s)} ({['Left','Down','Right','Up'][np.argmax(Q_s)]})")env.close()Q(6, a) = ['0.0283', '0.0269', '0.0283', '0.0014']V(6) via Q = 0.0212Greedy action = 0 (Left)is highest (tied with Right), so the greedy policy says “go Left” from state 6: no model needed at decision time, just compare four numbers.
Check: If you could only be handed one of V(s) or Q(s, a), which one lets you actually act without any extra computation, and why?
Answer. . It already gives a number for every action, so acting is a single with no model. is one number for the whole state; to act from it you must ask “where does each action land, and what is there?”, which needs the model . That model-free choice is exactly why DQN learns .
Check: Two states have the same value under some policy π. Does that mean they're equally good under the optimal policy? Argue both directions.
Answer. No. Equal value under one policy says nothing about the optimal values: a different policy could exploit one state far more than the other. Conversely, two states can still tie under the optimal policy (e.g. both one step from the goal). So same-value-under- neither implies nor forbids same-value-under-optimal.
2.5 The Bellman expectation equation
Now the payoff. Start from the recursive return and take the expectation conditioned on :
Read: “V-pi of s equals the expected value of [R_{t+1} plus gamma times V-pi of the next state S_{t+1}], given S_t = s, under policy pi.”
Interpretation: the value of a state is the expected immediate reward plus the discounted value of the next state. This is the Bellman equation in one line. But that still hides two sources of randomness: (1) which action the agent picks, and (2) where the environment sends it. Let’s peel them off one at a time.
Step 1: expand the expectation over the agent’s action randomness (policy ).
The expectation is an average over everything random that can happen from state . The first random thing is: which action does the agent pick? The probability of picking action is . By the law of total expectation (“split an average into cases and weight each case by its probability”):
So the full line becomes:
Why is there still an inside? Because even after fixing the action, there is still randomness left: the environment hasn’t rolled its dice yet. We know the state () and the action (), but the next state is still random (the ice is slippery!). The inner expectation averages over that remaining environment randomness.
Step 2: expand the remaining expectation over the environment’s state randomness (dynamics ).
Now we’re inside a fixed pair. The only randomness left is which next state the environment produces, with probability . Apply the same rule again (“split into cases, weight by probability”), but this time the cases are next states:
No remains because once we fix , , and , nothing is random anymore: the reward is a known number, and is a fixed value for that next state.
Substituting Step 2 into Step 1 gives the full Bellman expectation equation:
Read: “V-pi of s equals the sum over actions a of pi(a|s) times the sum over next states s’ of p(s’|s,a) times [R(s,a,s’) + gamma times V-pi(s’)].”
Interpretation: for each action I might take (weighted by my policy), for each state I might land in (weighted by transition probability), add the reward I get plus the discounted value of where I land. Two nested sums, one per source of randomness, and once both are expanded there is nothing random left.
Both steps use the same rule: . Step 1 applies it to actions (probabilities from ). Step 2 applies it to next states (probabilities from ).
This is one equation per state, and they all refer to each other: depends on , which depends on , and so on. That system of equations is what we solve in the capstone.
import gymnasium as gymimport numpy as np
env = gym.make("FrozenLake-v1", is_slippery=True)P = env.unwrapped.PnS, nA = 16, 4gamma = 0.99
def bellman_backup(V: np.ndarray, s: int, pi: list[float]) -> float: """One Bellman expectation backup for state s: V^π(s) = Σ_a π(a|s) · Σ_{s'} p(s'|s,a) · [r + γ·V(s')]
This computes a NEW estimate for V(s) by looking one step ahead: for each action (weighted by policy), for each next state (weighted by dynamics), accumulate [immediate reward + discounted future value]. """ total: float = 0.0 for a in range(nA): for prob, s2, r, done in P[s][a]: # π(a|s) · p(s'|s,a) · [r + γ·V(s')] # Agent randomness × environment randomness × one-step lookahead total += pi[a] * prob * (r + gamma * V[s2]) return total
# Initialize V to all zeros except the goal. This simulates "before any# value has propagated": only the goal knows it's valuable.V_test = np.zeros(nS)# goal state has value 1 (reward = 1 for reaching it)V_test[15] = 1.0# uniform random policypi = [0.25] * nA
# One backup at state 6: can the goal's value reach state 6 in a single step?# State 6 is several cells away from state 15, so the answer is no (0.0).# Repeated backups (or solving the full system) propagate value outward.v6 = bellman_backup(V_test, 6, pi)print(f"Bellman backup at state 6: V(6) = {v6:.6f}")env.close()Bellman backup at state 6: V(6) = 0.000000With all states at zero except the goal, the backup at state 6 gives zero because state 6 is too far from the goal for a single backup to propagate the value. Repeated backups (or solving the full system) are needed: that’s what the capstone does.
Check: The backup "looks to the future to value the present." Causally the future depends on now, so in what sense is the backup a causal inversion, and why is that legitimate?
Answer. The backup writes in terms of : it values the present using the values of the future. It is not predicting the future from the past; it defines a state’s value by the values of the states it can reach, then iterates until consistent. That’s legitimate because it is a fixed-point equation , not a causal claim. We simply solve for the self-consistent .
2.6 The Bellman optimality equation
Replace the policy’s weighted average with a maximum , and you get the equation for the optimal value:
Read: “V-star of s equals the max over actions a of the sum over next states s’ of p(s’|s,a) times [R(s,a,s’) + gamma times V-star(s’)].”
Interpretation: instead of averaging over actions according to some policy, pick the best action. The optimal value of a state is the value you get when you always choose the action that leads to the highest expected return.
The optimal action-value has its own recursion:
Read: “Q-star of s, a equals the sum over next states s’ of p(s’|s,a) times [R(s,a,s’) + gamma times the max over next actions a’ of Q-star(s’, a’)].”
Interpretation: the optimal value of taking action in state is the expected reward plus the discounted value of the next state, assuming you act optimally from that next state onward (that’s where the inner comes from).
Now notice what gives you: a number for every action in every state, and that number already accounts for optimal behavior in the entire future. So if you had in hand, choosing the best action would require zero planning: just compare the numbers and pick the largest. The optimal policy falls out for free:
Read: “Pi-star of s equals the action a that maximizes Q-star(s, a).”
Interpretation: once you have the optimal Q-values, the optimal policy is trivial: just pick the action with the highest Q. Every RL algorithm is, at its core, a way of computing or approximating these optimal values.
One more relation connects and directly (compare this to the V-Q bridge from §2.4, which used a policy-weighted average; here the average becomes a max):
Read: “V-star of s equals the max over actions a of Q-star(s, a).”
Interpretation: the optimal value of a state is simply the Q-value of the best action available there. Under the optimal policy you always take the best action, so V and the best Q coincide.
Where did go? Recall the general V-Q bridge: — a weighted average. For the optimal policy, the weighting is degenerate: for the best action and for all others. When you substitute that one-hot distribution into the sum, every term vanishes except the maximum. A weighted average with a one-hot weight vector is “pick the max.”
Check: The relation V*(s) = max_a Q*(s, a) looks innocent. What does it quietly assume about how the agent behaves after the first step?
Answer. It assumes that from the next step onward you act optimally (greedily with respect to ). The single over the first action only works because every future action is already assumed to be the best one; that is what makes the state’s value the of action-values rather than a policy-weighted average.
Worked example: the optimal policy on slippery FrozenLake
The heatmap above shows for every state (computed by value iteration, which we’ll derive in the next post). The arrows show : the greedy policy that falls out of the optimal Q-values.
Why do the arrows look wrong? Many arrows point away from the goal: Left and Up in the top rows, Down at state 14 (the cell right next to the goal). This is correct and it’s entirely because the ice is slippery. On slippery FrozenLake, every action has only a 1/3 chance of going where intended; the other 2/3 of the time the agent slides perpendicular. So the optimal strategy isn’t “aim for the goal”, it’s “aim so that your slips land somewhere safe.”
Consider state 14 (value 0.86, one cell left of the goal). Right seems obvious, but:
- Right: 1/3 reaches the goal, but 1/3 slides up to state 10 (V = 0.62, far away). Q = 0.82.
- Down: 1/3 reaches the goal as a perpendicular slip, 1/3 stays at state 14 (V = 0.86), and 1/3 slips to state 13 (V = 0.74). All slip destinations are high-value. Q = 0.86.
Down wins because its worst-case slip is much better. The same logic explains the top-row arrows: by pointing Left or Up into the walls, the agent uses them as bumpers, since it can’t slip off the grid, so two of its three outcomes keep it in the same (safe) cell instead of sliding toward a hole. The optimal policy minimizes the damage from slips, not the distance to the goal.
2.7 The same recursion on a different surface
A remarkable claim: DQN, AlphaGo, PPO, and GRPO are all the same recursion, , applied to different definitions of state, action, and reward:
| Algorithm | State | Action | Reward |
|---|---|---|---|
| DQN (Atari) | stack of game frames | joystick move | change in score |
| AlphaGo | board position | legal move | win / loss |
| PPO for RLHF | prompt + tokens so far | next token | reward-model score |
| GRPO | same as PPO | next token | group-relative reward |
The Bellman equation is the universal backbone. Everything else is engineering to make it work at scale.
Check: Pick any two of DQN, AlphaGo, PPO, and GRPO and state precisely what the state, action, and reward are in each.
Answer. For example: DQN has state = a stack of Atari frames, action = a joystick move, reward = the change in game score. AlphaGo has state = the board position, action = a legal move, reward = win / loss. Different surfaces, but both estimate value through the same recursion; only the meaning of , , changes.
Check: A Go board has roughly 10^170 positions, so you can never enumerate them. How does AlphaGo still apply the Bellman backup without a table over all states?
Answer. It never enumerates them. AlphaGo approximates (and the policy) with a neural network and applies the backup only along the states actually visited by tree search and self-play, generalizing to unseen boards. The backup is local: it needs only the current state and its successors, never the whole space. That table-to-network leap is the subject of SARSA, Q-learning & DQN.
3. Putting it all together
We’ve seen each idea in isolation. Here’s the whole vocabulary as a quick reference.
| Concept | Math | In code |
|---|---|---|
| Dynamics | P[s][a] -> [(prob, s', r, done)] | |
| Return | G += gamma**k * r | |
| Recursive return | (defines the Bellman structure) | |
| State-value | V[s] | |
| Action-value | q_from_v(V, s, a) | |
| V from Q | (pi * Q_row).sum() | |
| Optimal policy | np.argmax(Q, axis=1) |
Where this goes next
We’ve formalized the environment as an MDP, defined and , and derived the Bellman equation. But writing the equation is not the same as solving it: finding the actual values requires either exact linear algebra (feasible only for tiny state spaces) or iterative/sampling methods. The next post introduces three classical approaches:
- Dynamic Programming: iterate the Bellman backup with the model (no sampling needed).
- Monte Carlo: sample full episodes and average the returns (no model needed).
- Temporal Difference: blend the two, updating after every step using a bootstrapped estimate.
All three converge to the same , but they trade off bias (how far from the true value), variance (how noisy the estimates are), and data efficiency (how many samples are needed to converge). We’ll build a custom Mars Rover environment and watch all three converge on it from scratch.