Memory-bounded search is a heuristic search technique in artificial intelligence that solves problems using a strict, fixed amount of memory. By dynamically managing and pruning its search data, it efficiently finds solutions even on devices with limited resources, making it ideal for real-time and embedded systems.
Implementing Memory-Bound Search
Let's implement the Memory-Bound Search for 8-Puzzle,
Step 1: Define the Node Class
Each node in the search tree keeps track of the current state, its parent (for reconstructing the solution path) and the action taken to reach it.
Each node keeps:
- Current puzzle state
- Parent node (for reconstructing solution path)
- Action leading to this state
class Node:
def __init__(self, state, parent=None, action=None):
self.state = state
self.parent = parent
self.action = action
Step 2: Manhattan Distance Heuristic
This function estimates the cost to goal (lower = closer).
def heuristic(state):
distance = 0
for i in range(9):
if state[i] != 0:
distance += abs(i // 3 - (state[i] - 1) //
3) + abs(i % 3 - (state[i] - 1) % 3)
return distance
Step 3: Memory Usage Accounting
- Counts nodes in both open & closed lists.
- Ensures search stays within memory limits.
def memory_usage(open_list, closed_list):
return len(open_list) + len(closed_list)
Step 4: Prune Open List When Memory Limit Exceeded
When memory exceeded:
- Sort open list by heuristic (worst first)
- Remove bottom half (least promising nodes)
def prune_memory(open_list, closed_list):
open_list.sort(key=lambda x: heuristic(x.state), reverse=True)
open_list[:] = open_list[:len(open_list) // 2]
Step 5: Choose the Current Best Node
Pick the node that looks most promising to expand next.
def select_best_node(open_list):
return min(open_list, key=lambda x: heuristic(x.state))
Step 6: Define the Goal Test
Check if a node’s state matches the desired goal and return True.
def is_goal(node):
return node.state == goal_state
Step 7: Generate Successor States
- Produces new nodes for each valid move (sliding the blank).
- Each move results in a new puzzle state.
def generate_successors(node):
successors = []
zero_index = node.state.index(0)
for move in moves[zero_index]:
new_state = list(node.state)
new_state[zero_index], new_state[move] = new_state[move], new_state[zero_index]
successors.append(Node(tuple(new_state), parent=node, action=move))
return successors
Step 8: Avoid Redundant States
Skip nodes that have already been seen.
def redundant(successor, open_list, closed_list):
for node in open_list + closed_list:
if node.state == successor.state:
return True
return False
Step 9: Memory-Bounded A-like Search Routine
Implements the memory management and pruning logic.
Repeat these steps in main loop:
- Prune if above memory limit.
- Stop if open list empty.
- Expand best node.
- If goal, return node.
- Otherwise, move node to closed, add new successors if not redundant.
def MemoryBoundedSearch(initial_state, memory_limit):
node = Node(initial_state)
open_list = [node]
closed_list = []
while open_list:
if memory_usage(open_list, closed_list) > memory_limit:
prune_memory(open_list, closed_list)
if not open_list:
return None
current_node = select_best_node(open_list)
if is_goal(current_node):
return current_node
open_list.remove(current_node)
closed_list.append(current_node)
for successor in generate_successors(current_node):
if not redundant(successor, open_list, closed_list):
open_list.append(successor)
return None
Step 10: Set Up Puzzle Definitions
Define the goal state and legal moves.
goal_state = (1, 2, 3, 4, 5, 6, 7, 8, 0)
moves = {
0: [1, 3],
1: [0, 2, 4],
2: [1, 5],
3: [0, 4, 6],
4: [1, 3, 5, 7],
5: [2, 4, 8],
6: [3, 7],
7: [4, 6, 8],
8: [5, 7]
}
Step 11: Test and Get Result
- Define moves for 8-puzzle and goal state.
- Run search with specified memory limit.
- Print solution path if found.
initial_state = (1, 2, 3, 4, 5, 6, 0, 7, 8)
print("Case: Memory Limit = 1")
goal_node = MemoryBoundedSearch(initial_state, memory_limit=1)
if goal_node:
print("Solution found!")
while goal_node.parent:
print("Action:", goal_node.action)
print("State:", goal_node.state)
goal_node = goal_node.parent
else:
print("Memory limit exceeded. No solution found.")
print("\nCase: Memory Limit = 10")
goal_node = MemoryBoundedSearch(initial_state, memory_limit=10)
if goal_node:
print("Solution found!")
while goal_node.parent:
print("Action:", goal_node.action)
print("State:", goal_node.state)
goal_node = goal_node.parent
else:
print("Memory limit exceeded. No solution found.")
Output:
Case: Memory Limit = 1
Memory limit exceeded. No solution found.Case: Memory Limit = 10 Solution found!
Action: 8
State: (1, 2, 3, 4, 5, 6, 7, 8, 0)Action: 7
State: (1, 2, 3, 4, 5, 6, 7, 0, 8)
Applications of Memory-Bound Search
- Robotics: Helps robots navigate efficiently and avoid obstacles with limited memory.
- Autonomous Vehicles: Enables real-time route planning and obstacle avoidance given onboard memory constraints.
- Game AI: Allows AI agents in games like Chess or Go to make strong decisions within a fixed memory budget.
- NLP Tasks: Supports tasks like translation or summarization on memory-constrained devices.
- IoT & Embedded Devices: Facilitates complex tasks, such as image recognition, on devices with minimal memory.
Benefits
- Memory Efficiency: Works well even when available memory is tight.
- Real-World Ready: Ideal for practical systems with hardware constraints.
- Quality Results: Delivers good solutions using limited resources.
- Adaptive Memory Use: Dynamically manages what to keep and what to prune, using memory wisely.
Limitations
- Potential for Suboptimal Solutions: May overlook the best solutions due to aggressive memory pruning.
- Added Computational Overhead: Frequent memory checks and pruning can slow down the search process.
- Complex Implementation Requirements: Necessitates sophisticated strategies for selective memory retention and pruning.
- Balancing Challenges: Maintaining both high solution quality and low memory usage often involves trade-offs.