ai-practical-kit 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_practical_kit/__init__.py +22 -0
- ai_practical_kit/cli.py +157 -0
- ai_practical_kit/experiments/__init__.py +0 -0
- ai_practical_kit/experiments/alpha_beta_pruning.py +91 -0
- ai_practical_kit/experiments/astar_8_puzzle.py +116 -0
- ai_practical_kit/experiments/eight_queens.py +62 -0
- ai_practical_kit/experiments/local_beam_tsp.py +116 -0
- ai_practical_kit/experiments/maze_dfs.py +92 -0
- ai_practical_kit/experiments/robot_path_bfs.py +98 -0
- ai_practical_kit/experiments/rule_chatbot.py +47 -0
- ai_practical_kit/experiments/sudoku_solver.py +136 -0
- ai_practical_kit/experiments/tictactoe_minimax.py +148 -0
- ai_practical_kit/experiments/tower_of_hanoi_bfs.py +100 -0
- ai_practical_kit/experiments/vacuum_cleaner.py +70 -0
- ai_practical_kit/gui.py +173 -0
- ai_practical_kit-1.1.0.dist-info/METADATA +189 -0
- ai_practical_kit-1.1.0.dist-info/RECORD +21 -0
- ai_practical_kit-1.1.0.dist-info/WHEEL +5 -0
- ai_practical_kit-1.1.0.dist-info/entry_points.txt +5 -0
- ai_practical_kit-1.1.0.dist-info/licenses/LICENSE +21 -0
- ai_practical_kit-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""AIPracticalKit: reusable, beginner-friendly AI practical algorithms."""
|
|
2
|
+
|
|
3
|
+
from .experiments.vacuum_cleaner import solve_vacuum
|
|
4
|
+
from .experiments.astar_8_puzzle import solve_astar, heuristic
|
|
5
|
+
from .experiments.alpha_beta_pruning import solve_alpha_beta
|
|
6
|
+
from .experiments.tower_of_hanoi_bfs import solve_hanoi_bfs
|
|
7
|
+
from .experiments.sudoku_solver import solve_sudoku
|
|
8
|
+
from .experiments.local_beam_tsp import local_beam_tsp, tour_cost
|
|
9
|
+
from .experiments.robot_path_bfs import solve_robot_bfs
|
|
10
|
+
from .experiments.maze_dfs import solve_maze_dfs
|
|
11
|
+
from .experiments.eight_queens import solve_8_queens
|
|
12
|
+
from .experiments.rule_chatbot import respond
|
|
13
|
+
from .experiments.tictactoe_minimax import find_best_move, minimax
|
|
14
|
+
|
|
15
|
+
__version__ = "1.1.0"
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"solve_vacuum", "solve_astar", "heuristic", "solve_alpha_beta",
|
|
19
|
+
"solve_hanoi_bfs", "solve_sudoku", "local_beam_tsp", "tour_cost",
|
|
20
|
+
"solve_robot_bfs", "solve_maze_dfs", "solve_8_queens", "respond",
|
|
21
|
+
"find_best_move", "minimax",
|
|
22
|
+
]
|
ai_practical_kit/cli.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Text menu for running and inspecting AIPracticalKit experiments."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
SOURCE_DIR = Path(__file__).resolve().parent / "experiments"
|
|
10
|
+
|
|
11
|
+
EXPERIMENTS = [
|
|
12
|
+
(1, "Simple Reflex Agent - Vacuum Cleaner", "vacuum_cleaner.py", "vacuum_cleaner"),
|
|
13
|
+
(2, "A* Search - 8-Puzzle", "astar_8_puzzle.py", "astar_8_puzzle"),
|
|
14
|
+
(3, "Alpha-Beta Pruning", "alpha_beta_pruning.py", "alpha_beta_pruning"),
|
|
15
|
+
(4, "Tower of Hanoi using BFS", "tower_of_hanoi_bfs.py", "tower_of_hanoi_bfs"),
|
|
16
|
+
(5, "Sudoku - Constraint Propagation and Backtracking", "sudoku_solver.py", "sudoku_solver"),
|
|
17
|
+
(6, "Local Beam Search - TSP", "local_beam_tsp.py", "local_beam_tsp"),
|
|
18
|
+
(7, "BFS - Robot Path Planning", "robot_path_bfs.py", "robot_path_bfs"),
|
|
19
|
+
(8, "DFS - Maze Solving", "maze_dfs.py", "maze_dfs"),
|
|
20
|
+
(9, "8-Queens using Backtracking", "eight_queens.py", "eight_queens"),
|
|
21
|
+
(10, "Rule-Based Chatbot - Regex and AND", "rule_chatbot.py", "rule_chatbot"),
|
|
22
|
+
(11, "Tic-Tac-Toe using Minimax", "tictactoe_minimax.py", "tictactoe_minimax"),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _select_experiment():
|
|
27
|
+
print("\nAI PRACTICAL KIT - EXPERIMENTS")
|
|
28
|
+
for number, title, _, _ in EXPERIMENTS:
|
|
29
|
+
print(str(number) + ". " + title)
|
|
30
|
+
try:
|
|
31
|
+
number = int(input("Experiment number (0 to cancel): ").strip())
|
|
32
|
+
except ValueError:
|
|
33
|
+
print("Please enter a number.")
|
|
34
|
+
return None
|
|
35
|
+
if number == 0:
|
|
36
|
+
return None
|
|
37
|
+
for item in EXPERIMENTS:
|
|
38
|
+
if item[0] == number:
|
|
39
|
+
return item
|
|
40
|
+
print("Invalid experiment number.")
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _open_editor(path):
|
|
45
|
+
editor = shutil.which("code")
|
|
46
|
+
try:
|
|
47
|
+
if editor:
|
|
48
|
+
subprocess.Popen([editor, "--reuse-window", str(path)])
|
|
49
|
+
elif sys.platform.startswith("win"):
|
|
50
|
+
subprocess.Popen(["notepad.exe", str(path)])
|
|
51
|
+
elif sys.platform == "darwin":
|
|
52
|
+
subprocess.Popen(["open", "-t", str(path)])
|
|
53
|
+
else:
|
|
54
|
+
opener = shutil.which("xdg-open")
|
|
55
|
+
if opener:
|
|
56
|
+
subprocess.Popen([opener, str(path)])
|
|
57
|
+
else:
|
|
58
|
+
print("Open this source in your editor:\n" + str(path))
|
|
59
|
+
return
|
|
60
|
+
print("Opened source:", path.name)
|
|
61
|
+
except OSError as error:
|
|
62
|
+
print("Could not open the editor:", error)
|
|
63
|
+
print("Source file:", path)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _copy_source(path):
|
|
67
|
+
source = path.read_text(encoding="utf-8")
|
|
68
|
+
commands = []
|
|
69
|
+
if sys.platform.startswith("win"):
|
|
70
|
+
commands.append(["clip"])
|
|
71
|
+
elif sys.platform == "darwin":
|
|
72
|
+
commands.append(["pbcopy"])
|
|
73
|
+
else:
|
|
74
|
+
commands.extend([["wl-copy"], ["xclip", "-selection", "clipboard"],
|
|
75
|
+
["xsel", "--clipboard", "--input"]])
|
|
76
|
+
|
|
77
|
+
for command in commands:
|
|
78
|
+
if shutil.which(command[0]):
|
|
79
|
+
try:
|
|
80
|
+
subprocess.run(command, input=source, text=True, check=True)
|
|
81
|
+
print("Copied source to clipboard:", path.name)
|
|
82
|
+
return
|
|
83
|
+
except (OSError, subprocess.CalledProcessError):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
import tkinter
|
|
88
|
+
window = tkinter.Tk()
|
|
89
|
+
window.withdraw()
|
|
90
|
+
window.clipboard_clear()
|
|
91
|
+
window.clipboard_append(source)
|
|
92
|
+
window.update()
|
|
93
|
+
window.destroy()
|
|
94
|
+
print("Copied source to clipboard:", path.name)
|
|
95
|
+
except Exception:
|
|
96
|
+
print("Clipboard copy is unavailable. Open this source and copy it manually:")
|
|
97
|
+
print(path)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main():
|
|
101
|
+
"""Run the installed text menu."""
|
|
102
|
+
while True:
|
|
103
|
+
print("\n" + "=" * 60)
|
|
104
|
+
print(" AI PRACTICAL KIT")
|
|
105
|
+
print(" Run algorithms or inspect full source code")
|
|
106
|
+
print("=" * 60)
|
|
107
|
+
print("1. Run an experiment")
|
|
108
|
+
print("2. View full source code")
|
|
109
|
+
print("3. Open source in an editor")
|
|
110
|
+
print("4. Copy source code")
|
|
111
|
+
print("5. Show import examples")
|
|
112
|
+
print("0. Exit")
|
|
113
|
+
choice = input("Choose an option: ").strip()
|
|
114
|
+
|
|
115
|
+
if choice == "0":
|
|
116
|
+
print("Good luck with your practical examinations!")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
if choice == "5":
|
|
120
|
+
print("\nfrom ai_practical_kit import solve_astar")
|
|
121
|
+
print('result = solve_astar("123406758")')
|
|
122
|
+
print('print(result["path"])')
|
|
123
|
+
print("\nfrom ai_practical_kit import solve_8_queens")
|
|
124
|
+
print("result = solve_8_queens()")
|
|
125
|
+
print('print(result["total_solutions"])')
|
|
126
|
+
print("\nfrom ai_practical_kit import respond")
|
|
127
|
+
print('print(respond("Tell me about admission and fees"))')
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
item = _select_experiment()
|
|
131
|
+
if item is None:
|
|
132
|
+
continue
|
|
133
|
+
number, title, filename, module_name = item
|
|
134
|
+
path = SOURCE_DIR / filename
|
|
135
|
+
|
|
136
|
+
if choice == "1":
|
|
137
|
+
module = importlib.import_module("ai_practical_kit.experiments." + module_name)
|
|
138
|
+
try:
|
|
139
|
+
module.run_cli()
|
|
140
|
+
except (KeyboardInterrupt, EOFError):
|
|
141
|
+
print("\nExperiment cancelled.")
|
|
142
|
+
elif choice == "2":
|
|
143
|
+
print("\n" + "=" * 72)
|
|
144
|
+
print(title + " — FULL SOURCE")
|
|
145
|
+
print("=" * 72)
|
|
146
|
+
print(path.read_text(encoding="utf-8"))
|
|
147
|
+
print("=" * 72)
|
|
148
|
+
elif choice == "3":
|
|
149
|
+
_open_editor(path)
|
|
150
|
+
elif choice == "4":
|
|
151
|
+
_copy_source(path)
|
|
152
|
+
else:
|
|
153
|
+
print("Invalid choice. Choose 0 to 5.")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Experiment 3: Alpha-Beta Pruning on a depth-3 binary game tree."""
|
|
2
|
+
|
|
3
|
+
INFINITY = 10**9
|
|
4
|
+
DEFAULT_LEAVES = [3, 5, 6, 9, 1, 2, 0, -1]
|
|
5
|
+
INTERNAL_NAMES = "ABCDEFG"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def solve_alpha_beta(leaf_values=None):
|
|
9
|
+
"""Return minimax value, root branch, evaluated leaves and pruned branches."""
|
|
10
|
+
if leaf_values is None:
|
|
11
|
+
leaf_values = DEFAULT_LEAVES[:]
|
|
12
|
+
leaf_values = list(leaf_values)
|
|
13
|
+
if len(leaf_values) != 8:
|
|
14
|
+
raise ValueError("Enter exactly 8 leaf values.")
|
|
15
|
+
|
|
16
|
+
evaluated = 0
|
|
17
|
+
pruned = []
|
|
18
|
+
best_root_branch = None
|
|
19
|
+
|
|
20
|
+
def node_name(node_index):
|
|
21
|
+
if node_index < 7:
|
|
22
|
+
return INTERNAL_NAMES[node_index]
|
|
23
|
+
return str(leaf_values[node_index - 7])
|
|
24
|
+
|
|
25
|
+
def search(node, depth, is_max, alpha, beta):
|
|
26
|
+
nonlocal evaluated, best_root_branch
|
|
27
|
+
|
|
28
|
+
if depth == 3:
|
|
29
|
+
evaluated += 1
|
|
30
|
+
return leaf_values[node - 7]
|
|
31
|
+
|
|
32
|
+
if is_max:
|
|
33
|
+
best = -INFINITY
|
|
34
|
+
else:
|
|
35
|
+
best = INFINITY
|
|
36
|
+
|
|
37
|
+
for child_number in range(2):
|
|
38
|
+
child = 2 * node + 1 + child_number
|
|
39
|
+
score = search(child, depth + 1, not is_max, alpha, beta)
|
|
40
|
+
|
|
41
|
+
if is_max:
|
|
42
|
+
if score > best:
|
|
43
|
+
best = score
|
|
44
|
+
if node == 0:
|
|
45
|
+
best_root_branch = node_name(child)
|
|
46
|
+
if best > alpha:
|
|
47
|
+
alpha = best
|
|
48
|
+
else:
|
|
49
|
+
if score < best:
|
|
50
|
+
best = score
|
|
51
|
+
if best < beta:
|
|
52
|
+
beta = best
|
|
53
|
+
|
|
54
|
+
if alpha >= beta:
|
|
55
|
+
if child_number == 0:
|
|
56
|
+
skipped = 2 * node + 2
|
|
57
|
+
if depth + 1 == 3:
|
|
58
|
+
pruned.append("leaf " + node_name(skipped))
|
|
59
|
+
else:
|
|
60
|
+
pruned.append("subtree " + node_name(skipped))
|
|
61
|
+
break
|
|
62
|
+
|
|
63
|
+
return best
|
|
64
|
+
|
|
65
|
+
value = search(0, 0, True, -INFINITY, INFINITY)
|
|
66
|
+
return {"value": value, "best_branch": best_root_branch,
|
|
67
|
+
"leaves_evaluated": evaluated, "pruned": pruned,
|
|
68
|
+
"leaf_values": leaf_values}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_cli():
|
|
72
|
+
print("\nEXPERIMENT 3: ALPHA-BETA PRUNING")
|
|
73
|
+
print("Enter 8 leaf values from left to right, or press Enter for the practical example.")
|
|
74
|
+
raw = input("Leaf values [3 5 6 9 1 2 0 -1]: ").strip()
|
|
75
|
+
try:
|
|
76
|
+
values = DEFAULT_LEAVES if raw == "" else [int(x) for x in raw.split()]
|
|
77
|
+
result = solve_alpha_beta(values)
|
|
78
|
+
except ValueError as error:
|
|
79
|
+
print("Input error:", error)
|
|
80
|
+
return
|
|
81
|
+
print("Minimax value at root:", result["value"])
|
|
82
|
+
print("Best branch for MAX:", result["best_branch"])
|
|
83
|
+
print("Leaves evaluated:", result["leaves_evaluated"], "out of 8")
|
|
84
|
+
if result["pruned"]:
|
|
85
|
+
print("Pruned:", ", ".join(result["pruned"]))
|
|
86
|
+
else:
|
|
87
|
+
print("Pruned: none")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
run_cli()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Experiment 2: A* Search for the 8-Puzzle using Manhattan distance."""
|
|
2
|
+
|
|
3
|
+
import heapq
|
|
4
|
+
|
|
5
|
+
GOAL = "123456780"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def heuristic(state):
|
|
9
|
+
"""Sum of Manhattan distances; the blank ('0') is ignored."""
|
|
10
|
+
distance = 0
|
|
11
|
+
for index in range(9):
|
|
12
|
+
if state[index] != "0":
|
|
13
|
+
goal_index = int(state[index]) - 1
|
|
14
|
+
distance += abs(index // 3 - goal_index // 3)
|
|
15
|
+
distance += abs(index % 3 - goal_index % 3)
|
|
16
|
+
return distance
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_solvable(state):
|
|
20
|
+
"""For a 3x3 puzzle, the inversion count must be even."""
|
|
21
|
+
numbers = [int(ch) for ch in state if ch != "0"]
|
|
22
|
+
inversions = 0
|
|
23
|
+
for i in range(len(numbers)):
|
|
24
|
+
for j in range(i + 1, len(numbers)):
|
|
25
|
+
if numbers[i] > numbers[j]:
|
|
26
|
+
inversions += 1
|
|
27
|
+
return inversions % 2 == 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def solve_astar(start):
|
|
31
|
+
"""Solve a valid 8-puzzle string. Return path, moves and expanded count."""
|
|
32
|
+
start = "".join(str(x) for x in start) if not isinstance(start, str) else start.strip().replace(" ", "")
|
|
33
|
+
if len(start) != 9 or sorted(start) != list("012345678"):
|
|
34
|
+
raise ValueError("State must contain digits 0 through 8 exactly once.")
|
|
35
|
+
if not is_solvable(start):
|
|
36
|
+
return {"found": False, "path": [], "moves": None, "expanded": 0}
|
|
37
|
+
|
|
38
|
+
# Entries are (f, g, state); smallest f is removed first.
|
|
39
|
+
open_queue = []
|
|
40
|
+
heapq.heappush(open_queue, (heuristic(start), 0, start))
|
|
41
|
+
best_g = {start: 0}
|
|
42
|
+
parent = {start: None}
|
|
43
|
+
expanded = 0
|
|
44
|
+
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
|
|
45
|
+
|
|
46
|
+
while open_queue:
|
|
47
|
+
f, current_g, current = heapq.heappop(open_queue)
|
|
48
|
+
if current_g != best_g.get(current):
|
|
49
|
+
continue # An older, worse queue entry.
|
|
50
|
+
|
|
51
|
+
expanded += 1
|
|
52
|
+
if current == GOAL:
|
|
53
|
+
path = []
|
|
54
|
+
state = current
|
|
55
|
+
while state is not None:
|
|
56
|
+
path.append(state)
|
|
57
|
+
state = parent[state]
|
|
58
|
+
path.reverse()
|
|
59
|
+
return {"found": True, "path": path, "moves": len(path) - 1,
|
|
60
|
+
"expanded": expanded}
|
|
61
|
+
|
|
62
|
+
blank = current.index("0")
|
|
63
|
+
row = blank // 3
|
|
64
|
+
col = blank % 3
|
|
65
|
+
|
|
66
|
+
for dr, dc in directions:
|
|
67
|
+
nr = row + dr
|
|
68
|
+
nc = col + dc
|
|
69
|
+
if nr < 0 or nr >= 3 or nc < 0 or nc >= 3:
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
new_blank = nr * 3 + nc
|
|
73
|
+
chars = list(current)
|
|
74
|
+
chars[blank], chars[new_blank] = chars[new_blank], chars[blank]
|
|
75
|
+
next_state = "".join(chars)
|
|
76
|
+
new_g = current_g + 1
|
|
77
|
+
|
|
78
|
+
if new_g < best_g.get(next_state, 10**9):
|
|
79
|
+
best_g[next_state] = new_g
|
|
80
|
+
parent[next_state] = current
|
|
81
|
+
heapq.heappush(open_queue,
|
|
82
|
+
(new_g + heuristic(next_state), new_g, next_state))
|
|
83
|
+
|
|
84
|
+
return {"found": False, "path": [], "moves": None, "expanded": expanded}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def print_board(state):
|
|
88
|
+
for i in range(9):
|
|
89
|
+
print(("_" if state[i] == "0" else state[i]), end=" ")
|
|
90
|
+
if i % 3 == 2:
|
|
91
|
+
print()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def run_cli():
|
|
95
|
+
print("\nEXPERIMENT 2: A* SEARCH FOR 8-PUZZLE")
|
|
96
|
+
raw = input("Enter 9 digits (0 = blank), e.g. 1 2 3 4 0 6 7 5 8: ")
|
|
97
|
+
start = "".join(raw.split())
|
|
98
|
+
try:
|
|
99
|
+
result = solve_astar(start)
|
|
100
|
+
except ValueError as error:
|
|
101
|
+
print("Input error:", error)
|
|
102
|
+
return
|
|
103
|
+
if not result["found"]:
|
|
104
|
+
print("No solution exists for this puzzle.")
|
|
105
|
+
return
|
|
106
|
+
print("\nSolution path:")
|
|
107
|
+
for step, state in enumerate(result["path"]):
|
|
108
|
+
print("\nStep", step, "(g =", step, ", h =", heuristic(state),
|
|
109
|
+
", f =", step + heuristic(state), ")")
|
|
110
|
+
print_board(state)
|
|
111
|
+
print("Number of moves:", result["moves"])
|
|
112
|
+
print("Nodes expanded:", result["expanded"])
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
run_cli()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Experiment 9: Backtracking for the 8-Queens constraint problem."""
|
|
2
|
+
|
|
3
|
+
SIZE = 8
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def solve_8_queens():
|
|
7
|
+
"""Return the first solution and total number of solutions (92)."""
|
|
8
|
+
board = [-1] * SIZE
|
|
9
|
+
first_solution = None
|
|
10
|
+
solution_count = 0
|
|
11
|
+
|
|
12
|
+
def is_safe(row, col):
|
|
13
|
+
for previous_row in range(row):
|
|
14
|
+
if board[previous_row] == col:
|
|
15
|
+
return False
|
|
16
|
+
if abs(board[previous_row] - col) == abs(previous_row - row):
|
|
17
|
+
return False
|
|
18
|
+
return True
|
|
19
|
+
|
|
20
|
+
def solve(row):
|
|
21
|
+
nonlocal first_solution, solution_count
|
|
22
|
+
if row == SIZE:
|
|
23
|
+
solution_count += 1
|
|
24
|
+
if first_solution is None:
|
|
25
|
+
first_solution = board[:]
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
for col in range(SIZE):
|
|
29
|
+
if is_safe(row, col):
|
|
30
|
+
board[row] = col
|
|
31
|
+
solve(row + 1)
|
|
32
|
+
board[row] = -1 # Backtrack and try another column.
|
|
33
|
+
|
|
34
|
+
solve(0)
|
|
35
|
+
return {"solution": first_solution, "total_solutions": solution_count}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_cli():
|
|
39
|
+
print("\nEXPERIMENT 9: 8-QUEENS USING BACKTRACKING")
|
|
40
|
+
result = solve_8_queens()
|
|
41
|
+
board = result["solution"]
|
|
42
|
+
if board is None:
|
|
43
|
+
print("No solution exists.")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
print("First solution (Q = queen, . = empty):")
|
|
47
|
+
for row in range(SIZE):
|
|
48
|
+
for col in range(SIZE):
|
|
49
|
+
if board[row] == col:
|
|
50
|
+
print("Q", end=" ")
|
|
51
|
+
else:
|
|
52
|
+
print(".", end=" ")
|
|
53
|
+
print()
|
|
54
|
+
|
|
55
|
+
print("Queen positions (row, column; numbering starts at 1):")
|
|
56
|
+
for row in range(SIZE):
|
|
57
|
+
print("Queen", row + 1, ": (", row + 1, ", ", board[row] + 1, ")", sep="")
|
|
58
|
+
print("Total solutions:", result["total_solutions"])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
run_cli()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Experiment 6: Local Beam Search for the Traveling Salesman Problem."""
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
import math
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def tour_cost(tour, distance):
|
|
9
|
+
"""Total cost of a tour, including the return to city 0."""
|
|
10
|
+
total = 0
|
|
11
|
+
n = len(tour)
|
|
12
|
+
for i in range(n - 1):
|
|
13
|
+
total += distance[tour[i]][tour[i + 1]]
|
|
14
|
+
total += distance[tour[-1]][tour[0]]
|
|
15
|
+
return total
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def local_beam_tsp(distance, beam_width=3, max_iterations=20, seed=42):
|
|
19
|
+
"""Return the best tour found by Local Beam Search.
|
|
20
|
+
|
|
21
|
+
City 0 is fixed at the beginning of each tour. `distance` is a square matrix.
|
|
22
|
+
"""
|
|
23
|
+
n = len(distance)
|
|
24
|
+
if n < 3 or any(len(row) != n for row in distance):
|
|
25
|
+
raise ValueError("Distance matrix must be square and contain at least 3 cities.")
|
|
26
|
+
if beam_width < 1 or max_iterations < 1:
|
|
27
|
+
raise ValueError("Beam width and iterations must both be at least 1.")
|
|
28
|
+
if any(distance[i][j] < 0 for i in range(n) for j in range(n)):
|
|
29
|
+
raise ValueError("Distances cannot be negative.")
|
|
30
|
+
|
|
31
|
+
beam_width = min(beam_width, math.factorial(n - 1))
|
|
32
|
+
rng = random.Random(seed)
|
|
33
|
+
city_list = list(range(1, n))
|
|
34
|
+
beam = []
|
|
35
|
+
seen = set()
|
|
36
|
+
|
|
37
|
+
# Create distinct initial tours in a repeatable random order.
|
|
38
|
+
attempts = 0
|
|
39
|
+
while len(beam) < beam_width and attempts < beam_width * 50:
|
|
40
|
+
rng.shuffle(city_list)
|
|
41
|
+
tour = tuple([0] + city_list[:])
|
|
42
|
+
if tour not in seen:
|
|
43
|
+
seen.add(tour)
|
|
44
|
+
beam.append(tour)
|
|
45
|
+
attempts += 1
|
|
46
|
+
|
|
47
|
+
# Fill any remaining slots systematically (small practical inputs).
|
|
48
|
+
if len(beam) < beam_width:
|
|
49
|
+
for order in itertools.permutations(range(1, n)):
|
|
50
|
+
tour = tuple([0] + list(order))
|
|
51
|
+
if tour not in seen:
|
|
52
|
+
seen.add(tour)
|
|
53
|
+
beam.append(tour)
|
|
54
|
+
if len(beam) == beam_width:
|
|
55
|
+
break
|
|
56
|
+
|
|
57
|
+
best_tour = min(beam, key=lambda tour: tour_cost(tour, distance))
|
|
58
|
+
best_cost = tour_cost(best_tour, distance)
|
|
59
|
+
history = [best_cost]
|
|
60
|
+
|
|
61
|
+
for _ in range(max_iterations):
|
|
62
|
+
candidates = {}
|
|
63
|
+
for tour in beam:
|
|
64
|
+
for i in range(1, n - 1):
|
|
65
|
+
for j in range(i + 1, n):
|
|
66
|
+
neighbour = list(tour)
|
|
67
|
+
neighbour[i], neighbour[j] = neighbour[j], neighbour[i]
|
|
68
|
+
neighbour = tuple(neighbour)
|
|
69
|
+
candidates[neighbour] = tour_cost(neighbour, distance)
|
|
70
|
+
|
|
71
|
+
if not candidates:
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
ordered = sorted(candidates.items(), key=lambda item: (item[1], item[0]))
|
|
75
|
+
next_beam = [tour for tour, cost_value in ordered[:beam_width]]
|
|
76
|
+
beam = next_beam
|
|
77
|
+
|
|
78
|
+
if ordered[0][1] < best_cost:
|
|
79
|
+
best_tour = ordered[0][0]
|
|
80
|
+
best_cost = ordered[0][1]
|
|
81
|
+
history.append(best_cost)
|
|
82
|
+
|
|
83
|
+
return {"tour": list(best_tour), "cost": best_cost,
|
|
84
|
+
"iterations": len(history) - 1, "best_cost_history": history}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def run_cli():
|
|
88
|
+
print("\nEXPERIMENT 6: LOCAL BEAM SEARCH FOR TSP")
|
|
89
|
+
try:
|
|
90
|
+
n = int(input("Enter number of cities (3-10 recommended): "))
|
|
91
|
+
if n < 3 or n > 10:
|
|
92
|
+
raise ValueError("Use between 3 and 10 cities for this practical.")
|
|
93
|
+
distance = []
|
|
94
|
+
print("Enter the distance matrix, one row per line:")
|
|
95
|
+
for _ in range(n):
|
|
96
|
+
row = [float(x) for x in input().split()]
|
|
97
|
+
if len(row) != n:
|
|
98
|
+
raise ValueError("Each distance row must contain exactly n values.")
|
|
99
|
+
distance.append(row)
|
|
100
|
+
beam_width = int(input("Enter beam width K: "))
|
|
101
|
+
iterations = int(input("Enter maximum iterations: "))
|
|
102
|
+
result = local_beam_tsp(distance, beam_width, iterations)
|
|
103
|
+
except ValueError as error:
|
|
104
|
+
print("Input error:", error)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
tour = result["tour"]
|
|
108
|
+
print("\nBest tour found:")
|
|
109
|
+
print(" -> ".join(str(city) for city in tour + [tour[0]]))
|
|
110
|
+
print("Best tour cost:", round(result["cost"], 3))
|
|
111
|
+
print("Iterations completed:", result["iterations"])
|
|
112
|
+
print("Note: Local Beam Search is heuristic and does not guarantee the global optimum.")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
run_cli()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Experiment 8: DFS for Maze Solving."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def solve_maze_dfs(maze):
|
|
5
|
+
"""Find a path from S to E by DFS; return path and a marked maze."""
|
|
6
|
+
if not maze or not maze[0]:
|
|
7
|
+
raise ValueError("Maze cannot be empty.")
|
|
8
|
+
rows = len(maze)
|
|
9
|
+
cols = len(maze[0])
|
|
10
|
+
if any(len(row) != cols for row in maze):
|
|
11
|
+
raise ValueError("All maze rows must have the same length.")
|
|
12
|
+
|
|
13
|
+
start = None
|
|
14
|
+
goal = None
|
|
15
|
+
for r in range(rows):
|
|
16
|
+
for c in range(cols):
|
|
17
|
+
if maze[r][c] == "S":
|
|
18
|
+
if start is not None:
|
|
19
|
+
raise ValueError("Maze must contain exactly one S.")
|
|
20
|
+
start = (r, c)
|
|
21
|
+
elif maze[r][c] == "E":
|
|
22
|
+
if goal is not None:
|
|
23
|
+
raise ValueError("Maze must contain exactly one E.")
|
|
24
|
+
goal = (r, c)
|
|
25
|
+
if start is None or goal is None:
|
|
26
|
+
raise ValueError("Maze must contain one S (start) and one E (exit).")
|
|
27
|
+
|
|
28
|
+
visited = set()
|
|
29
|
+
path = []
|
|
30
|
+
# Same order as the practical: Right, Down, Left, Up.
|
|
31
|
+
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
|
|
32
|
+
|
|
33
|
+
def dfs(r, c):
|
|
34
|
+
if r < 0 or r >= rows or c < 0 or c >= cols:
|
|
35
|
+
return False
|
|
36
|
+
if maze[r][c] == "#" or (r, c) in visited:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
visited.add((r, c))
|
|
40
|
+
path.append((r, c))
|
|
41
|
+
if (r, c) == goal:
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
for dr, dc in directions:
|
|
45
|
+
if dfs(r + dr, c + dc):
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
path.pop() # Dead end: remove this cell from the current path.
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
found = dfs(start[0], start[1])
|
|
52
|
+
if not found:
|
|
53
|
+
return {"found": False, "path": [], "maze": list(maze),
|
|
54
|
+
"visited_count": len(visited)}
|
|
55
|
+
|
|
56
|
+
marked = [list(row) for row in maze]
|
|
57
|
+
for r, c in path:
|
|
58
|
+
if marked[r][c] == ".":
|
|
59
|
+
marked[r][c] = "*"
|
|
60
|
+
return {"found": True, "path": path,
|
|
61
|
+
"maze": ["".join(row) for row in marked],
|
|
62
|
+
"visited_count": len(visited)}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run_cli():
|
|
66
|
+
print("\nEXPERIMENT 8: DFS MAZE SOLVER")
|
|
67
|
+
try:
|
|
68
|
+
rows, cols = [int(x) for x in input("Enter rows and columns: ").split()]
|
|
69
|
+
if rows < 1 or cols < 1 or rows > 50 or cols > 50:
|
|
70
|
+
raise ValueError("Rows and columns must be between 1 and 50.")
|
|
71
|
+
print("Enter each maze row using S=start, E=exit, #=wall, .=free:")
|
|
72
|
+
maze = [input().strip() for _ in range(rows)]
|
|
73
|
+
if any(len(row) != cols for row in maze):
|
|
74
|
+
raise ValueError("Each row must contain exactly the given number of columns.")
|
|
75
|
+
result = solve_maze_dfs(maze)
|
|
76
|
+
except ValueError as error:
|
|
77
|
+
print("Input error:", error)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
if not result["found"]:
|
|
81
|
+
print("No path exists in the maze.")
|
|
82
|
+
return
|
|
83
|
+
print("\nPath found (DFS does not guarantee shortest path):")
|
|
84
|
+
for r, c in result["path"]:
|
|
85
|
+
print("(" + str(r + 1) + "," + str(c + 1) + ")", end=" ")
|
|
86
|
+
print("\n\nMaze with path:")
|
|
87
|
+
for row in result["maze"]:
|
|
88
|
+
print(" ".join(row))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
run_cli()
|