Artificial Intelligence

Last Modified: 2/12/2025

1.1 Agent Design

There are mainly two types of agents: reflex agents and planning agents.

  1. 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.
  2. 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:

Search algorithms can be divided into two categories:

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.

ucs.java
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;
}
}
ucs.h
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
ucs.cpp
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
ucs.py
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]

A* search is a kind of heuristic search algorithms. It combines uniform-cost search and greedy algorithm.

The A* search algorithm uses the following formula to calculate the cost of a node:

f(n)=g(n)+h(n)f\left(n\right) = g\left(n\right) + h\left(n\right)

Admissible heuristics: A heuristic hh is admissible (optimistic) if:

h(n)h(n)h\left(n\right) \leq h^*\left(n\right)

where h(n)h^*\left(n\right) is the true cost to reach the goal from node nn. 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 h(n)h\left(n\right):

  1. Manhattan Distance: The sum of the absolute differences of the xx and yy coordinates.
h(n)=xgoalxcurrent+ygoalycurrenth\left(n\right) = \left|x_{\text{goal}} - x_{\text{current}}\right| + \left|y_{\text{goal}} - y_{\text{current}}\right|
  1. Euclidean Distance: The straight-line distance between two points.
h(n)=(xgoalxcurrent)2+(ygoalycurrent)2h\left(n\right) = \sqrt{\left(x_{\text{goal}} - x_{\text{current}}\right)^2 + \left(y_{\text{goal}} - y_{\text{current}}\right)^2}
  1. Chebyshev Distance: The maximum of the absolute differences of the xx and yy coordinates.
h(n)=max(xgoalxcurrent,ygoalycurrent)h\left(n\right) = \max\left(\left|x_{\text{goal}} - x_{\text{current}}\right|, \left|y_{\text{goal}} - y_{\text{current}}\right|\right)
  1. Octile Distance: The diagonal distance between two points.
h(n)=(21)min(xgoalxcurrent,ygoalycurrent)+max(xgoalxcurrent,ygoalycurrent)h\left(n\right) = \left(\sqrt{2} - 1\right) \cdot \min\left(\left|x_{\text{goal}} - x_{\text{current}}\right|, \left|y_{\text{goal}} - y_{\text{current}}\right|\right) + \max\left(\left|x_{\text{goal}} - x_{\text{current}}\right|, \left|y_{\text{goal}} - y_{\text{current}}\right|\right)
  1. 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.");
}
}
}