Artificial Intelligence
Last Modified: 2/12/2025
1 Uninformed Search
1.1 Agent Design
There are mainly two types of agents: reflex agents and planning agents.
- Reflex Agents:
- Choose action based on current percept, and may have memory or a model of the world’s current state.
- Do not consider the future consequences of their actions, just consider how the world IS.
- Planning Agents:
- Decisions based on (hypothesized) consequences of actions.
- Must have a model of how the world evolves in response to actions, and formulate a goal (test), consider how the world WOULD BE
1.2 Search Problems
A search problem consists of a state space, a successor function (with actions and costs) and a start space & a goal test. A solution is a sequence of actions (a plan) which transforms the start state to a goal state.
For a search problem, we may need:
- State Space Graph: A mathematical representation of a search problem. It consists of nodes (abstarcted world configurations) and arcs (successors/action results). The goal test is a set of goal nodes (maybe only one).
- Search Trees: A “what if” tree of plans and their outcomes. Nodes show states, and children correspond to successors.
Search algorithms can be divided into two categories:
- Uninformed Search: No additional information about states beyond the definition of the problem.
- Depth-First Search
- Breadth-First Search
- Uniform-Cost Search
- Informed Search: Use heuristics to guide the search.
- A* Search
1.2.1 Uniform-cost Search
Uniform-cost search is a variant of Dijkstra’s algorithm. Not inserting all nodes in a graph makes it possible to extend Dijkstra’s algorithm to find the shortest path from a single source to the closest of a set of target nodes on infinite graphs or those too large to represent in memory, which is called uniform-cost search (UCS) in artificial intelligence literature.
106 collapsed lines
import java.util.*;
public class ucs { private final double[] costTo; private final int[] parent; private final PriorityQueue<Integer> pq; private final boolean[] visited;
/** * A directed edge constructor with a cost. */ public static class Edge { int neighbor; double cost;
public Edge(int neighbor, double cost) { this.neighbor = neighbor; this.cost = cost; } }
/** * Uniform-cost search constructor. */ public ucs(List<List<Edge>> graph, int startNode) { int numNodes = graph.size(); costTo = new double[numNodes]; parent = new int[numNodes]; visited = new boolean[numNodes];
for (int i = 0; i < numNodes; i++) { costTo[i] = Double.POSITIVE_INFINITY; parent[i] = -1; } costTo[startNode] = 0.0;
pq = new PriorityQueue<>(Comparator.comparingDouble(v -> costTo[v])); pq.offer(startNode);
while (!pq.isEmpty()) { int currentNode = pq.poll(); visited[currentNode] = true;
if (costTo[currentNode] == Double.POSITIVE_INFINITY) continue;
for (Edge edge : graph.get(currentNode)) { relax(currentNode, edge.neighbor, edge.cost); } } }
/** * Relaxation of an edge. * * @param u the source node * @param v the target node * @param cost the cost of the edge */ private void relax(int u, int v, double cost) { if (costTo[v] > costTo[u] + cost) { costTo[v] = costTo[u] + cost; parent[v] = u; pq.offer(v); } }
/** * Get the cost to a node. * * @param node the target node * @return the cost to the target node */ public double getCostTo(int node) { return costTo[node]; }
/** * Check if a path exists to a node. * * @param node the target node * @return true if a path exists, false otherwise */ public boolean hasPathTo(int node) { return costTo[node] < Double.POSITIVE_INFINITY; }
/** * Get the path to a node. * * @param targetNode the target node * @return the path to the target node */ public List<Integer> getPathTo(int targetNode) { if (!hasPathTo(targetNode)) { return null; }
List<Integer> path = new ArrayList<>(); for (int v = targetNode; v != -1; v = parent[v]) { path.add(v); }
Collections.reverse(path); return path; }}
40 collapsed lines
#ifndef UCS_H#define UCS_H
#include <vector>#include <queue>#include <functional>#include <limits>#include <cmath>#include <algorithm>
namespace ucs {
struct Node { int neighbor; double cost;
Node(const int neighbor, const double cost) : neighbor(neighbor), cost(cost) {} };
class UniformCostSearch { private: std::vector<double> costTo; std::vector<int> parent; std::vector<bool> visited; std::priority_queue<int, std::vector<int>, std::function<bool(int, int)>> pq; const std::vector<std::vector<Node>>& adj;
void relax(int u, int v, double cost);
public: UniformCostSearch(const std::vector<std::vector<Node>>& graph, int startNode);
[[nodiscard]] double getCostTo(int node) const; [[nodiscard]] bool hasPathTo(int node) const; [[nodiscard]] std::vector<int> getPathTo(int targetNode) const; };
} // namespace ucs
#endif //UCS_H
81 collapsed lines
#include "ucs.h"
namespace ucs {
UniformCostSearch::UniformCostSearch(const std::vector<std::vector<Node>>& graph, const int startNode) : costTo(graph.size(), std::numeric_limits<double>::infinity()), parent(graph.size(), -1), visited(graph.size(), false), pq([this](const int u, const int v) { return costTo[u] > costTo[v]; }), adj(graph) { costTo[startNode] = 0.0; pq.push(startNode);
while (!pq.empty()) { const int currentNode = pq.top(); pq.pop(); visited[currentNode] = true;
if (costTo[currentNode] == std::numeric_limits<double>::infinity()) continue;
for (const auto& edge : adj[currentNode]) { relax(currentNode, edge.neighbor, edge.cost); } } }
/** * Relaxation of an edge. * * @param u the source node * @param v the target node * @param cost the cost of the edge */ void UniformCostSearch::relax(const int u, const int v, const double cost) { if (costTo[v] > costTo[u] + cost) { costTo[v] = costTo[u] + cost; parent[v] = u; pq.push(v); } }
/** * Get the cost to a node. * * @param node the target node * @return the cost to the target node */ double UniformCostSearch::getCostTo(const int node) const { return costTo[node]; }
/** * Check if a path exists to a node. * * @param node the target node * @return true if a path exists, false otherwise */ bool UniformCostSearch::hasPathTo(const int node) const { return costTo[node] < std::numeric_limits<double>::infinity(); }
/** * Get the path to a node. * * @param targetNode the target node * @return the path to the target node */ std::vector<int> UniformCostSearch::getPathTo(const int targetNode) const { if (!hasPathTo(targetNode)) { return {}; } std::vector<int> path; for (int v = targetNode; v != -1; v = parent[v]) { path.push_back(v); } std::ranges::reverse(path); return path; }
} // namespace ucs
56 collapsed lines
import heapq
class Node: def __init__(self, neighbor, cost): '''A directed edge constructor with a cost.''' self.neighbor = neighbor self.cost = cost
class UniformCostSearch: def __init__(self, graph, start_node): '''Uniform-cost search constructor.''' self.cost_to = [float('inf')] * len(graph) self.parent = [-1] * len(graph) self.visited = [False] * len(graph) self.pq = [(0, start_node)] self.adj = graph
self.cost_to[start_node] = 0.0
while self.pq: current_cost, current_node = heapq.heappop(self.pq) if self.visited[current_node]: continue self.visited[current_node] = True
if self.cost_to[current_node] == float('inf'): continue
for edge in self.adj[current_node]: self.relax(current_node, edge.neighbor, edge.cost)
def relax(self, u, v, cost): '''Relaxation of an edge.''' if self.cost_to[v] > self.cost_to[u] + cost: self.cost_to[v] = self.cost_to[u] + cost self.parent[v] = u heapq.heappush(self.pq, (self.cost_to[v], v))
def get_cost_to(self, node): '''Get the cost to a node.''' return self.cost_to[node]
def has_path_to(self, node): '''Check if a path exists to a node.''' return self.cost_to[node] < float('inf')
def get_path_to(self, target_node): '''Get the path to a node.''' if not self.has_path_to(target_node): return None path = [] v = target_node while v != -1: path.append(v) v = self.parent[v] return path[::-1]
1.2.2 A* Search
A* search is a kind of heuristic search algorithms. It combines uniform-cost search and greedy algorithm.
- : The cost from the start to reach the node , which uniform-cost search relies on.
- : An estimate of the cost from node to the goal, which greedy algorithm relies on.
The A* search algorithm uses the following formula to calculate the cost of a node:
Admissible heuristics: A heuristic is admissible (optimistic) if:
where is the true cost to reach the goal from node . The admissible heuristic guarantees that A* will never stop exploring a path that could lead to a better solution. With inadmissible heuristics, A* may think a node is “too expensive” and stop exploring it, even if that node is actually on the optimal path.
For admissible heuristics, there are some methods for calculating :
- Manhattan Distance: The sum of the absolute differences of the and coordinates.
- Euclidean Distance: The straight-line distance between two points.
- Chebyshev Distance: The maximum of the absolute differences of the and coordinates.
- Octile Distance: The diagonal distance between two points.
- Zero Heuristic: The heuristic that always returns zero (which is the same as uniform-cost search).
import java.util.*;
public class AStarSearch {
public static class AStar { private final int[][] grid; private final Node[][] nodes; private final int rows; private final int cols;
/** * Constructs an AStar instance for a given grid. * The grid is a 2D array where 0 indicates a walkable cell and 1 indicates an obstacle. * * @param grid The grid for the A* search. */ public AStar(int[][] grid) { this.grid = grid; this.rows = grid.length; this.cols = grid[0].length; this.nodes = new Node[rows][cols];
for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { nodes[y][x] = new Node(x, y); } } }
/** * Resets all nodes to their initial state. * This is useful when performing multiple searches on the same grid. */ private void resetNodes() { for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { Node n = nodes[y][x]; n.g = Double.POSITIVE_INFINITY; n.h = 0; n.f = Double.POSITIVE_INFINITY; n.parent = null; } } }
/** * Finds a path from the start cell to the goal cell using the A* search * algorithm. * * @param startX The x-coordinate of the start cell. * @param startY The y-coordinate of the start cell. * @param goalX The x-coordinate of the goal cell. * @param goalY The y-coordinate of the goal cell. * @return A list of nodes representing the path (including start and goal), * or null if no path is found. */ public List<Node> findPath(int startX, int startY, int goalX, int goalY) { resetNodes();
Node start = nodes[startY][startX]; Node goal = nodes[goalY][goalX];
/* Open list to hold nodes to be evaluated. */ PriorityQueue<Node> openList = new PriorityQueue<>(); /* Closed list to mark visited cells. */ boolean[][] closed = new boolean[rows][cols];
start.g = 0; start.h = heuristic(start, goal); start.f = start.g + start.h; openList.add(start);
while (!openList.isEmpty()) { Node current = openList.poll();
// If the goal is reached, reconstruct and return the path. if (current.equals(goal)) { return reconstructPath(current); }
closed[current.y][current.x] = true;
for (Node neighbor : getNeighbors(current)) { if (closed[neighbor.y][neighbor.x]) { continue; }
double tentativeG = current.g + 1; // Cost to move to the neighbor.
// If a shorter path to neighbor is found, update its cost and parent. if (tentativeG < neighbor.g) { neighbor.parent = current; neighbor.g = tentativeG; neighbor.h = heuristic(neighbor, goal); neighbor.f = neighbor.g + neighbor.h;
// Add neighbor to open list if it's not already there. if (!openList.contains(neighbor)) { openList.add(neighbor); } } } }
return null; }
/** * Reconstructs the path from the goal node back to the start node. * * @param node The goal node. * @return A list of nodes representing the path from start to goal. */ private List<Node> reconstructPath(Node node) { List<Node> path = new ArrayList<>(); while (node != null) { path.add(node); node = node.parent; } Collections.reverse(path); return path; }
/** * The Manhattan distance heuristic function. * * @param a The current node. * @param b The goal node. * @return The Manhattan distance between node a and node b. */ private double heuristic(Node a, Node b) { return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); }
/** * Retrieves valid neighboring nodes (up, down, left, right) that * are walkable. * * @param node The current node. * @return A list of valid neighbor nodes. */ private List<Node> getNeighbors(Node node) { List<Node> neighbors = new ArrayList<>(); int x = node.x; int y = node.y; int[][] directions = { { 0, -1 }, { 0, 1 }, { -1, 0 }, { 1, 0 } };
for (int[] d : directions) { int newX = x + d[0]; int newY = y + d[1];
// Ensure new coordinates are within the grid boundaries. if (newX >= 0 && newX < cols && newY >= 0 && newY < rows) { // Check if the cell is walkable. if (grid[newY][newX] == 0) { neighbors.add(nodes[newY][newX]); } } } return neighbors; }
/** * Represents a node (cell) in the grid. */ public static class Node implements Comparable<Node> { public final int x, y; // Grid coordinates. public double g; // Cost from the start node. public double h; // Heuristic cost to the goal. public double f; // Total cost (g + h). public Node parent; // Reference to the parent node for path reconstruction.
public Node(int x, int y) { this.x = x; this.y = y; this.g = Double.POSITIVE_INFINITY; this.h = 0; this.f = Double.POSITIVE_INFINITY; this.parent = null; }
@Override public int compareTo(Node other) { return Double.compare(this.f, other.f); }
@Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Node other = (Node) obj; return x == other.x && y == other.y; }
@Override public int hashCode() { return Objects.hash(x, y); } } }
public static void main(String[] args) { int[][] grid = { { 0, 0, 0, 0, 0 }, { 0, 1, 1, 1, 0 }, { 0, 0, 0, 1, 0 }, { 0, 1, 0, 0, 0 }, { 0, 0, 0, 1, 0 } };
AStar aStar = new AStar(grid); List<AStar.Node> path = aStar.findPath(0, 0, 4, 4);
if (path != null) { System.out.println("Path found:"); for (AStar.Node node : path) { System.out.println("(" + node.x + ", " + node.y + ")"); } } else { System.out.println("No path found."); } }}