Advanced Search Algorithms

Advanced search algorithms extend beyond basic DFS and BFS to include specialized techniques like A* search, bidirectional search, and iterative deepening. These algorithms are essential for solving complex pathfinding and optimization problems efficiently.

A* Search Algorithm

A* search uses heuristics to guide the search towards the goal, combining the benefits of Dijkstra's algorithm with informed search strategies.

A* Search Implementation

#include <iostream>
#include <vector>
#include <queue>
#include <unordered_map>
#include <cmath>
using namespace std;

struct Point {
    int x, y;
    
    Point(int x = 0, int y = 0) : x(x), y(y) {}
    
    bool operator==(const Point& other) const {
        return x == other.x && y == other.y;
    }
    
    // Manhattan distance heuristic
    int manhattanDistance(const Point& other) const {
        return abs(x - other.x) + abs(y - other.y);
    }
    
    // Euclidean distance heuristic
    double euclideanDistance(const Point& other) const {
        return sqrt((x - other.x) * (x - other.x) + (y - other.y) * (y - other.y));
    }
};

// Hash function for Point
struct PointHash {
    size_t operator()(const Point& p) const {
        return hash<int>()(p.x) ^ (hash<int>()(p.y) << 1);
    }
};

struct AStarNode {
    Point pos;
    int gCost; // cost from start
    int hCost; // heuristic cost to goal
    Point parent;
    
    AStarNode(Point p, int g, int h, Point par) 
        : pos(p), gCost(g), hCost(h), parent(par) {}
    
    int fCost() const { return gCost + hCost; }
    
    bool operator>(const AStarNode& other) const {
        return fCost() > other.fCost();
    }
};

class AStar {
private:
    vector<vector<int>> grid;
    int rows, cols;
    vector<Point> directions = {{0,1}, {1,0}, {0,-1}, {-1,0}, 
                               {1,1}, {1,-1}, {-1,1}, {-1,-1}}; // 8-directional
    
public:
    AStar(vector<vector<int>>& g) : grid(g), rows(g.size()), cols(g[0].size()) {}
    
    bool isValid(const Point& p) {
        return p.x >= 0 && p.x < rows && p.y >= 0 && p.y < cols && grid[p.x][p.y] == 0;
    }
    
    int getMoveCost(const Point& from, const Point& to) {
        // Diagonal moves cost more
        if (abs(from.x - to.x) + abs(from.y - to.y) == 2) {
            return 14; // approximately sqrt(2) * 10
        }
        return 10;
    }
    
    vector<Point> findPath(Point start, Point goal) {
        priority_queue<AStarNode, vector<AStarNode>, greater<AStarNode>> openSet;
        unordered_map<Point, int, PointHash> gScore;
        unordered_map<Point, Point, PointHash> cameFrom;
        
        openSet.push(AStarNode(start, 0, start.manhattanDistance(goal), Point(-1, -1)));
        gScore[start] = 0;
        
        while (!openSet.empty()) {
            AStarNode current = openSet.top();
            openSet.pop();
            
            if (current.pos == goal) {
                // Reconstruct path
                vector<Point> path;
                Point curr = goal;
                while (!(curr.x == -1 && curr.y == -1)) {
                    path.push_back(curr);
                    curr = cameFrom[curr];
                }
                reverse(path.begin(), path.end());
                return path;
            }
            
            // Skip if we found a better path already
            if (gScore.count(current.pos) && current.gCost > gScore[current.pos]) {
                continue;
            }
            
            for (const Point& dir : directions) {
                Point neighbor(current.pos.x + dir.x, current.pos.y + dir.y);
                
                if (!isValid(neighbor)) continue;
                
                int tentativeG = current.gCost + getMoveCost(current.pos, neighbor);
                
                if (!gScore.count(neighbor) || tentativeG < gScore[neighbor]) {
                    gScore[neighbor] = tentativeG;
                    cameFrom[neighbor] = current.pos;
                    
                    int hCost = neighbor.manhattanDistance(goal);
                    openSet.push(AStarNode(neighbor, tentativeG, hCost, current.pos));
                }
            }
        }
        
        return {}; // No path found
    }
    
    void printPath(const vector<Point>& path) {
        if (path.empty()) {
            cout << "No path found!" << endl;
            return;
        }
        
        vector<vector<char>> display(rows, vector<char>(cols, '.'));
        
        // Mark obstacles
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (grid[i][j] == 1) display[i][j] = '#';
            }
        }
        
        // Mark path
        for (int i = 0; i < path.size(); i++) {
            if (i == 0) display[path[i].x][path[i].y] = 'S'; // Start
            else if (i == path.size() - 1) display[path[i].x][path[i].y] = 'G'; // Goal
            else display[path[i].x][path[i].y] = '*';
        }
        
        // Print grid
        for (const auto& row : display) {
            for (char c : row) {
                cout << c << " ";
            }
            cout << endl;
        }
        
        cout << "Path length: " << path.size() << endl;
    }
};

int main() {
    // Create a grid with obstacles (1 = obstacle, 0 = free)
    vector<vector<int>> grid = {
        {0, 0, 0, 0, 1, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 1, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 1, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 1, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 1, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 1, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 1, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 1, 0, 0, 0, 0, 0},
        {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
    };
    
    AStar astar(grid);
    Point start(0, 0);
    Point goal(9, 9);
    
    cout << "Finding path from (0,0) to (9,9) using A* search:" << endl;
    cout << "Legend: S=Start, G=Goal, *=Path, #=Obstacle, .=Free" << endl << endl;
    
    vector<Point> path = astar.findPath(start, goal);
    astar.printPath(path);
    
    return 0;
}

A* search uses f(n) = g(n) + h(n) where g(n) is the actual cost from start and h(n) is the heuristic estimate to goal. The algorithm guarantees optimal paths when the heuristic is admissible (never overestimates).

Bidirectional Search

Bidirectional search runs two simultaneous searches from start and goal, meeting in the middle to reduce the search space exponentially.

Bidirectional BFS Implementation

#include <iostream>
#include <vector>
#include <queue>
#include <unordered_set>
#include <unordered_map>
using namespace std;

class BidirectionalBFS {
private:
    vector<vector<int>> graph;
    int n;
    
public:
    BidirectionalBFS(vector<vector<int>>& g) : graph(g), n(g.size()) {}
    
    vector<int> findPath(int start, int goal) {
        if (start == goal) return {start};
        
        // Two queues for forward and backward search
        queue<int> forwardQueue, backwardQueue;
        unordered_set<int> forwardVisited, backwardVisited;
        unordered_map<int, int> forwardParent, backwardParent;
        
        forwardQueue.push(start);
        backwardQueue.push(goal);
        forwardVisited.insert(start);
        backwardVisited.insert(goal);
        forwardParent[start] = -1;
        backwardParent[goal] = -1;
        
        while (!forwardQueue.empty() || !backwardQueue.empty()) {
            // Expand forward search
            if (!forwardQueue.empty()) {
                int meetingPoint = expandLevel(forwardQueue, forwardVisited, backwardVisited, 
                                             forwardParent, true);
                if (meetingPoint != -1) {
                    return constructPath(meetingPoint, forwardParent, backwardParent);
                }
            }
            
            // Expand backward search
            if (!backwardQueue.empty()) {
                int meetingPoint = expandLevel(backwardQueue, backwardVisited, forwardVisited, 
                                             backwardParent, false);
                if (meetingPoint != -1) {
                    return constructPath(meetingPoint, forwardParent, backwardParent);
                }
            }
        }
        
        return {}; // No path found
    }
    
private:
    int expandLevel(queue<int>& q, unordered_set<int>& visited, 
                   unordered_set<int>& otherVisited, unordered_map<int, int>& parent, 
                   bool isForward) {
        int levelSize = q.size();
        
        for (int i = 0; i < levelSize; i++) {
            int current = q.front();
            q.pop();
            
            for (int neighbor : graph[current]) {
                if (otherVisited.count(neighbor)) {
                    // Found intersection!
                    parent[neighbor] = current;
                    return neighbor;
                }
                
                if (!visited.count(neighbor)) {
                    visited.insert(neighbor);
                    parent[neighbor] = current;
                    q.push(neighbor);
                }
            }
        }
        
        return -1; // No intersection found in this level
    }
    
    vector<int> constructPath(int meetingPoint, unordered_map<int, int>& forwardParent, 
                             unordered_map<int, int>& backwardParent) {
        vector<int> path;
        
        // Build path from start to meeting point
        vector<int> forwardPath;
        int current = meetingPoint;
        while (current != -1) {
            forwardPath.push_back(current);
            current = forwardParent[current];
        }
        reverse(forwardPath.begin(), forwardPath.end());
        
        // Build path from meeting point to goal
        vector<int> backwardPath;
        current = backwardParent[meetingPoint];
        while (current != -1) {
            backwardPath.push_back(current);
            current = backwardParent[current];
        }
        
        // Combine paths
        path = forwardPath;
        path.insert(path.end(), backwardPath.begin(), backwardPath.end());
        
        return path;
    }
};

// Iterative Deepening DFS
class IterativeDeepeningDFS {
private:
    vector<vector<int>> graph;
    int n;
    
public:
    IterativeDeepeningDFS(vector<vector<int>>& g) : graph(g), n(g.size()) {}
    
    vector<int> findPath(int start, int goal, int maxDepth = 20) {
        for (int depth = 0; depth <= maxDepth; depth++) {
            vector<bool> visited(n, false);
            vector<int> path;
            
            if (dfsLimited(start, goal, depth, visited, path)) {
                return path;
            }
        }
        
        return {}; // No path found within depth limit
    }
    
private:
    bool dfsLimited(int current, int goal, int depth, vector<bool>& visited, vector<int>& path) {
        path.push_back(current);
        
        if (current == goal) {
            return true;
        }
        
        if (depth == 0) {
            path.pop_back();
            return false;
        }
        
        visited[current] = true;
        
        for (int neighbor : graph[current]) {
            if (!visited[neighbor]) {
                if (dfsLimited(neighbor, goal, depth - 1, visited, path)) {
                    return true;
                }
            }
        }
        
        visited[current] = false;
        path.pop_back();
        return false;
    }
};

int main() {
    // Create a sample graph
    vector<vector<int>> graph = {
        {1, 2},       // 0 -> 1, 2
        {0, 3, 4},    // 1 -> 0, 3, 4
        {0, 5, 6},    // 2 -> 0, 5, 6
        {1, 7},       // 3 -> 1, 7
        {1, 7},       // 4 -> 1, 7
        {2, 8},       // 5 -> 2, 8
        {2, 8},       // 6 -> 2, 8
        {3, 4, 9},    // 7 -> 3, 4, 9
        {5, 6, 9},    // 8 -> 5, 6, 9
        {7, 8}        // 9 -> 7, 8
    };
    
    BidirectionalBFS bidiBFS(graph);
    IterativeDeepeningDFS iddfs(graph);
    
    int start = 0, goal = 9;
    
    // Test bidirectional BFS
    cout << "Bidirectional BFS path from " << start << " to " << goal << ": ";
    vector<int> bidiPath = bidiBFS.findPath(start, goal);
    if (!bidiPath.empty()) {
        for (int i = 0; i < bidiPath.size(); i++) {
            if (i > 0) cout << " -> ";
            cout << bidiPath[i];
        }
        cout << " (length: " << bidiPath.size() << ")" << endl;
    } else {
        cout << "No path found" << endl;
    }
    
    // Test iterative deepening DFS
    cout << "Iterative Deepening DFS path from " << start << " to " << goal << ": ";
    vector<int> iddfsPath = iddfs.findPath(start, goal);
    if (!iddfsPath.empty()) {
        for (int i = 0; i < iddfsPath.size(); i++) {
            if (i > 0) cout << " -> ";
            cout << iddfsPath[i];
        }
        cout << " (length: " << iddfsPath.size() << ")" << endl;
    } else {
        cout << "No path found" << endl;
    }
    
    return 0;
}

Bidirectional search reduces time complexity from O(b^d) to O(b^(d/2)) where b is branching factor and d is depth. Iterative deepening combines DFS space efficiency with BFS optimality.

Key Takeaways

  • A* search uses heuristics to guide search efficiently, guaranteeing optimal paths with admissible heuristics
  • Bidirectional search reduces exponential search space by meeting in the middle
  • Iterative deepening combines DFS memory efficiency with BFS completeness and optimality
  • Choose search algorithm based on problem constraints: memory, optimality requirements, and heuristic availability