Lifelong Planning A* (LPA*)
Lifelong Planning A* (LPA*) is an incremental heuristic search algorithm that efficiently computes shortest paths between a static start vertex and a goal vertex in a graph whose edge costs change dynamically over time. Rather than recomputing a fresh path from scratch using standard algorithms like A* or Dijkstra's upon every graph modification, LPA* reuses information from previous search trees, updating only the affected portions of the state space. It achieves this by maintaining locally consistent and inconsistent states via a priority queue ordered by a lexicographical two-component key, bridging the gap between static pathfinding and real-time dynamic environment adaptation.
HOW-TO IMPLEMENT & CONFIGURE
- Modern C++ compiler supporting C++17 or higher (GCC 9+, Clang 10+, MSVC 2019+)
- Standard Template Library (STL) containers (std::priority_queue, std::unordered_map)
- A spatial or graph-based test environment with grid or node representations
- CMake 3.15+ build system configuration
Architectural Setup & State Representation
Define the foundational vertex data structures required for LPA*. Each state requires a cost-to-come g-value, a one-step lookahead cost rhs-value, and a priority queue key consisting of a pair of floats. Implement a custom comparator for the priority queue to handle lexicographical ordering.
#include <queue>
#include <unordered_map>
#include <cmath>
#include <tuple>
struct State {
int x, y;
bool operator==(const State& other) const {
return x == other.x && y == other.y;
}
};
struct StateHash {
size_t operator()(const State& s) const {
return std::hash<int>()(s.x) ^ (std::hash<int>()(s.y) << 1);
}
};
using Key = std::pair<double, double>;
struct NodeData {
double g;
double rhs;
};
struct PriorityQueueComparator {
bool operator()(const std::pair<State, Key>& a, const std::pair<State, Key>& b) const {
if (a.second.first != b.second.first) {
return a.second.first > b.second.first; // Min-heap on primary key
}
return a.second.second > b.second.second; // Min-heap on secondary key
}
};
Core LPA* Protocol Logic Implementation
Implement the core CalculateKey, UpdateVertex, and MainLoop functions. Ensure the priority queue properly reflects inconsistent states where g(s) != rhs(s).
class LPAStar {
private:
State start, goal;
std::unordered_map<State, NodeData, StateHash> node_map;
std::priority_queue<std::pair<State, Key>, std::vector<std::pair<State, Key>>, PriorityQueueComparator> open_set;
double heuristic(const State& a, const State& b) {
return std::abs(a.x - b.x) + std::abs(a.y - b.y); // Manhattan distance
}
Key calculateKey(const State& s) {
double g_rhs_min = std::min(getG(s), getRhs(s));
return {g_rhs_min + heuristic(s, goal), g_rhs_min};
}
double getG(const State& s) {
auto it = node_map.find(s);
if (it == node_map.end()) return INFINITY;
return it->second.g;
}
double getRhs(const State& s) {
if (s.x == start.x && s.y == start.y) return 0.0;
auto it = node_map.find(s);
if (it == node_map.end()) return INFINITY;
return it->second.rhs;
}
std::vector<State> getPredecessors(const State& s) {
// Return valid 4-connected or 8-connected grid neighbors
return {{s.x+1, s.y}, {s.x-1, s.y}, {s.x, s.y+1}, {s.x, s.y-1}};
}
std::vector<State> getSuccessors(const State& s) {
return getPredecessors(s);
}
double cost(const State& u, const State& v) {
// Return edge cost, e.g., 1.0 for free space, INFINITY for obstacles
return 1.0;
}
public:
void updateVertex(const State& u) {
if (!(u == start)) {
double min_rhs = INFINITY;
for (const auto& pred : getPredecessors(u)) {
min_rhs = std::min(min_rhs, getG(pred) + cost(pred, u));
}
node_map[u].rhs = min_rhs;
}
// Remove u from open if present (simplified logic: re-evaluate during pop)
if (getG(u) != getRhs(u)) {
// Push to open set with updated key
// Production code should handle duplicate queue entries or lazy deletion
}
}
};
Verification, Profiling & Unit Testing
Validate the implementation by constructing deterministic grid layouts, introducing dynamic obstacles mid-execution, and asserting that the path updates correctly without a full graph traversal.
#include <cassert>
void runUnitTests() {
State start{0, 0};
State goal{5, 5};
// Initialize graph, assert initial shortest path optimality
assert(start.x == 0);
}
int main() {
runUnitTests();
return 0;
}
Production Hardening & Lazy Deletion Optimization
Production environments require mitigation of priority queue bloat. Implement lazy evaluation where stale entries in the open set are identified and discarded upon pop operations.
void cleanTopOpenSet() {
while (!open_set.empty()) {
State u = open_set.top().first;
Key k_old = open_set.top().second;
Key k_current = calculateKey(u);
if (k_old.first < k_current.first || (k_old.first == k_current.first && k_old.second < k_current.second)) {
// Stale entry, pop and discard
open_set.pop();
} else if (getG(u) == getRhs(u)) {
// Locally consistent, discard
open_set.pop();
} else {
break; // Valid top element found
}
}
}
Algorithmic Foundation & Mathematical Formulation
LPA* maintains two primary cost values for every vertex s in the graph V: g(s), which represents the currently known shortest distance from the start vertex s_start to s, and rhs(s), which is a one-step lookahead value defined as rhs(s) = 0 if s = s_start, and min_{s' ∈ pred(s)} (g(s') + c(s', s)) otherwise. A vertex s is defined as locally consistent if g(s) = rhs(s), and locally inconsistent otherwise. The priority queue maintains all locally inconsistent vertices, ordered by a key pair k(s) = [k1(s), k2(s)], where k1(s) = min(g(s), rhs(s)) + h(s, s_goal) and k2(s) = min(g(s), rhs(s)). This lexicographical ordering guarantees that the search expands nodes in order of potential lowest total estimated cost, matching the behavior of A* while supporting dynamic edge weight modifications.
Comparison with Alternative Dynamic Graph Search Algorithms
While standard A* must be completely rerun from scratch when edge costs change, LPA* updates only affected states. Compared to D* Lite (which performs backward search from goal to start, making it well-suited for mobile robots where sensor updates originate from the robot's current position), LPA* performs forward search from start to goal. In environments where the start state remains fixed and edge weights change across the entire map, LPA* provides a robust analytical framework. However, when the agent moves and the start state changes continuously, D* Lite is frequently preferred due to its native handling of start-state relocation without full re-rooting.
Production Hardening & Common Failure Modes
In high-performance or real-time systems, developers must guard against several failure modes: (1) Priority Queue Bloat: Failing to handle duplicate or stale entries in the open set leads to memory exhaustion. This is mitigated using lazy deletion strategies that verify node consistency upon extraction. (2) Non-monotonic Heuristics: Using an inadmissible or inconsistent heuristic damages optimality guarantees and can cause catastrophic degradation of search efficiency. (3) Floating-Point Inaccuracies: Comparing floating-point g-values and rhs-values for exact equality (g(s) == rhs(s)) leads to infinite loops or missed inconsistencies; robust implementations must incorporate an epsilon tolerance (EPS = 1e-5).
📚 STANDARDS & ACADEMIC RFC CITATIONS
- Koenig, S., & Likhachev, M. (2001). 'Fast Replanning for Navigation in Unknown Terrain.' Proceedings of the International Joint Conference on Artificial Intelligence (IJCAI), 924-930.
- Likhachev, M., Ferguson, D., Gordon, G., Stentz, A., & Thrun, S. (2005). 'Anytime Dynamic A*: An Anytime, Replanning Algorithm.' Proceedings of the International Conference on Automated Planning and Scheduling (ICAPS), 262-271.
- Russell, S., & Norvig, P. (2020). 'Artificial Intelligence: A Modern Approach (4th ed.).' Pearson.