Policy Evaluation by Linear Solve
Problem Statement
For a fixed policy in a finite MDP, the value function satisfies the linear system V = r + gamma P V, which solves exactly to:
V=(I−γP)−1r
Here P is the n x n state-to-state transition matrix induced by the policy and r is the length-n expected-reward vector. Implement policy_eval_exact(P, r, gamma) using numpy, returning V as a list of floats.
Example:
policy_eval_exact([[1.0]], [1.0], 0.9)
[10.0]
- Identify the dimensions and parameters from the input: the state space size is n=1, the transition matrix is P=[1.0], the reward vector is r=[1.0], and the discount factor is γ=0.9.
- Construct the coefficient matrix A for the linear system using the formula A=I−γP, where I is the 1×1 identity matrix [1.0]. This yields A=1.0−(0.9×1.0)=0.1.
- Solve the linear equation AV=r for the value vector V by dividing the reward by the coefficient: V=r/A=1.0/0.1=10.0.
- The final output is [10.0]
Constraints:
Pisn x n(each row sums to 1),rlengthn,0 <= gamma < 1.- Use
numpy.linalg.solve(not an explicit inverse) for stability. - Return a list of
nfloats.
1. Background Knowledge
In a Markov Decision Process (MDP), the value function V(s) under a fixed policy π represents the expected sum of discounted future rewards starting from state s. For a finite MDP with n states, this value function satisfies the Bellman equation: V(s)=r(s)+γ∑s′​P(s,s′)V(s′), where r(s) is the immediate expected reward, γ∈[0,1) is the discount factor, and P is the transition probability matrix induced by the policy.
When written in vector form, the Bellman equation becomes the linear system V=r+γPV. By rearranging terms, we get (I−γP)V=r, where I is the n×n identity matrix. Since γ<1 and P is a stochastic matrix (rows sum to 1), the matrix (I−γP) is always invertible. This guarantees a unique solution given by V=(I−γP)−1r. This exact solution contrasts with iterative methods like policy iteration or value iteration, which approximate V through successive updates.
The key insight is that for a fixed policy, the value function is not a fixed-point iteration problem but a straightforward linear algebra problem. The matrix (I−γP) captures the "effective" transition dynamics after accounting for discounting, and solving the linear system yields the exact value function in one step.
2. Algorithm Approach
The approach is direct linear system solving using NumPy:
- Construct the coefficient matrix A=I−γP.
- Solve AV=r for V using a numerically stable linear solver.
- Return V as a list of floats.
The preferred method is numpy.linalg.solve(A, r) rather than explicitly computing the inverse, as it is more numerically stable and computationally efficient. Direct inversion via numpy.linalg.inv(A) @ r is discouraged in practice due to accumulated floating-point errors.
3. Step-by-Step Strategy
- Validate inputs: Ensure P is a 2D array of shape (n,n), r is a 1D array of length n, and 0≤γ<1.
- Construct the identity matrix: Use np.eye(n) to create I.
- Form the coefficient matrix: Compute A=I−γ⋅P using element-wise multiplication and subtraction.
- Solve the linear system: Call np.linalg.solve(A, r) to obtain V.
- Convert to list: Use V.tolist() to return a plain Python list of floats, as required by the problem signature.
import numpy as np
def policy_eval_exact(P, r, gamma):
n = len(r)
I = np.eye(n)
A = I - gamma * P
V = np.linalg.solve(A, r)
return V.tolist()
4. Common Pitfalls
- Using matrix inverse instead of solve: Computing np.linalg.inv(A) @ r introduces unnecessary numerical error and is slower. Always prefer np.linalg.solve.
- Shape mismatches: Ensure P is a 2D array and r is a 1D array. If r is passed as a column vector (shape (n,1)), np.linalg.solve will return a 2D result, breaking .tolist() expectations.
- Gamma equal to 1: If γ=1, the matrix (I−P) may be singular for recurrent MDPs, causing np.linalg.solve to raise a LinAlgError. The problem guarantees γ<1, but defensive checks are wise.
- Integer dtype issues: If P or r are integer arrays, the multiplication gamma * P may produce float results, but explicit casting to float avoids subtle type promotion bugs.
- **Forgetting **.tolist()****: Returning a NumPy array instead of a Python list will fail test cases that check type equality.
5. Time & Space Complexity
- Time Complexity: Constructing A takes O(n2) for the element-wise operations. Solving the linear system via np.linalg.solve uses LU decomposition, which costs O(n3) floating-point operations. Overall complexity is O(n3).
- Space Complexity: Storing the n×n matrix A requires O(n2) space. The LU decomposition in-place does not significantly increase this. The solution vector V uses O(n) space. Total space is O(n2).
For typical MDP sizes in coding problems (n≤103), this is perfectly tractable. For very large sparse systems, iterative methods like Gauss-Seidel or conjugate gradient would be preferable, but the problem explicitly asks for the exact linear solve.