⚡ dixl.eu DEV LEXICON
● Spec v2026.1 RFC / MDN / POSIX

Lifelong Planning A* (LPA*)

✍️ Spec Review: Dixl Architecture Committee • 📅 Updated: October 24, 2026 • ⏱️ Reading: ~6 min
Lifelong Planning A* (LPA*) - Featured Image

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

📋 Prerequisites & Environment:

  • 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
STEP 1

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
    }
};
STEP 2

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
        }
    }
};
STEP 3

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;
}
STEP 4

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.