Skip to content

Commit f091f43

Browse files
authored
Merge branch 'master' into python3.15t
2 parents 7cf500e + 52b9b91 commit f091f43

235 files changed

Lines changed: 18621 additions & 1710 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ Comments which are not on the same line as code should appear ___before___ the c
196196
return a + b
197197
```
198198

199-
[Install ty](https://docs.astral.sh/ty/installation/) and then pslease use the command `ty check` to test all files or `ty check path/to/file.py` to test a specific file.
199+
[Install ty](https://docs.astral.sh/ty/installation/) and then please use the command `ty check` to test all files or `ty check path/to/file.py` to test a specific file.
200200

201201
- [__List comprehensions and generators__](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions) are preferred over the use of `lambda`, `map`, `filter`, `reduce` but the important thing is to demonstrate the power of Python in code that is easy to read and maintain.
202202

‎DIRECTORY.md‎

Lines changed: 131 additions & 1 deletion
Large diffs are not rendered by default.

‎backtracking/coloring.py‎

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
2-
Graph Coloring also called "m coloring problem"
3-
consists of coloring a given graph with at most m colors
4-
such that no adjacent vertices are assigned the same color
2+
Graph Coloring (also called the "m coloring problem") is the problem of
3+
assigning at most 'm' colors to the vertices of a graph such that
4+
no two adjacent vertices share the same color.
55
66
Wikipedia: https://en.wikipedia.org/wiki/Graph_coloring
77
"""
@@ -11,22 +11,46 @@ def valid_coloring(
1111
neighbours: list[int], colored_vertices: list[int], color: int
1212
) -> bool:
1313
"""
14+
Check if a given vertex can be assigned the specified color
15+
without violating the graph coloring constraints (i.e., no two adjacent vertices
16+
have the same color).
17+
18+
Procedure:
1419
For each neighbour check if the coloring constraint is satisfied
1520
If any of the neighbours fail the constraint return False
1621
If all neighbours validate the constraint return True
1722
18-
>>> neighbours = [0,1,0,1,0]
23+
Parameters:
24+
neighbours: The list representing which vertices
25+
are adjacent to the current vertex.
26+
1 indicates an edge between the current vertex
27+
and the neighbour.
28+
colored_vertices: List of current color assignments for all vertices
29+
(-1 means uncolored).
30+
color: The color we are trying to assign to the current vertex.
31+
32+
Returns:
33+
True if the vertex can be safely colored with the given color,
34+
otherwise False.
35+
36+
Examples:
37+
>>> neighbours = [0, 1, 0, 1, 0]
1938
>>> colored_vertices = [0, 2, 1, 2, 0]
20-
2139
>>> color = 1
2240
>>> valid_coloring(neighbours, colored_vertices, color)
2341
True
2442
2543
>>> color = 2
2644
>>> valid_coloring(neighbours, colored_vertices, color)
2745
False
46+
47+
>>> neighbors = [1, 0, 1, 0]
48+
>>> colored_vertices = [-1, -1, -1, -1]
49+
>>> color = 0
50+
>>> valid_coloring(neighbors, colored_vertices, color)
51+
True
2852
"""
29-
# Does any neighbour not satisfy the constraints
53+
# Check if any adjacent vertex has already been colored with the same color
3054
return not any(
3155
neighbour == 1 and colored_vertices[i] == color
3256
for i, neighbour in enumerate(neighbours)
@@ -37,7 +61,7 @@ def util_color(
3761
graph: list[list[int]], max_colors: int, colored_vertices: list[int], index: int
3862
) -> bool:
3963
"""
40-
Pseudo-Code
64+
Recursive function to try and color the graph using backtracking.
4165
4266
Base Case:
4367
1. Check if coloring is complete
@@ -51,6 +75,20 @@ def util_color(
5175
2.4. if current coloring leads to a solution return
5276
2.5. Uncolor given vertex
5377
78+
Parameters:
79+
graph: Adjacency matrix representing the graph.
80+
graph[i][j] is 1 if there is an edge
81+
between vertex i and j.
82+
max_colors: Maximum number of colors allowed (m in the m-coloring problem).
83+
colored_vertices: Current color assignments for each vertex.
84+
-1 indicates that the vertex has not been colored
85+
yet.
86+
index: The current vertex index being processed.
87+
88+
Returns:
89+
True if the graph can be colored using at most max_colors, otherwise False.
90+
91+
Examples:
5492
>>> graph = [[0, 1, 0, 0, 0],
5593
... [1, 0, 1, 0, 1],
5694
... [0, 1, 0, 1, 0],
@@ -67,43 +105,68 @@ def util_color(
67105
>>> util_color(graph, max_colors, colored_vertices, index)
68106
False
69107
"""
70-
71-
# Base Case
108+
# Base Case: If all vertices have been assigned a color, we have a valid solution
72109
if index == len(graph):
73110
return True
74111

75-
# Recursive Step
76-
for i in range(max_colors):
77-
if valid_coloring(graph[index], colored_vertices, i):
78-
# Color current vertex
79-
colored_vertices[index] = i
80-
# Validate coloring
112+
# Try each color for the current vertex
113+
for color in range(max_colors):
114+
# Check if it's valid to color the current vertex with 'color'
115+
if valid_coloring(graph[index], colored_vertices, color):
116+
colored_vertices[index] = color # Assign color
117+
# Recur to color the rest of the vertices
81118
if util_color(graph, max_colors, colored_vertices, index + 1):
82119
return True
83-
# Backtrack
120+
# Backtrack if no solution found with the current assignment
84121
colored_vertices[index] = -1
85-
return False
122+
123+
return False # Return False if no valid coloring is possible
86124

87125

88126
def color(graph: list[list[int]], max_colors: int) -> list[int]:
89127
"""
90-
Wrapper function to call subroutine called util_color
91-
which will either return True or False.
92-
If True is returned colored_vertices list is filled with correct colorings
93-
128+
Attempt to color the graph with at most max_colors colors such that no two adjacent
129+
vertices have the same color.
130+
If it is possible, returns the list of color assignments;
131+
otherwise, returns an empty list.
132+
133+
Parameters:
134+
graph: Adjacency matrix representing the graph.
135+
max_colors: Maximum number of colors allowed.
136+
137+
Returns:
138+
List of color assignments if the graph can be colored using max_colors.
139+
Each index in the list represents the color assigned
140+
to the corresponding vertex.
141+
If coloring is not possible, returns an empty list.
142+
143+
Examples:
94144
>>> graph = [[0, 1, 0, 0, 0],
95145
... [1, 0, 1, 0, 1],
96146
... [0, 1, 0, 1, 0],
97147
... [0, 1, 1, 0, 0],
98148
... [0, 1, 0, 0, 0]]
99-
100149
>>> max_colors = 3
101150
>>> color(graph, max_colors)
102151
[0, 1, 0, 2, 0]
103152
104153
>>> max_colors = 2
105154
>>> color(graph, max_colors)
106155
[]
156+
157+
>>> graph = [[0, 1], [1, 0]] # Simple 2-node graph
158+
>>> max_colors = 2
159+
>>> color(graph, max_colors)
160+
[0, 1]
161+
162+
>>> graph = [[0, 1, 1], [1, 0, 1], [1, 1, 0]] # Complete graph of 3 vertices
163+
>>> max_colors = 2
164+
>>> color(graph, max_colors)
165+
[]
166+
167+
>>> max_colors = 3
168+
>>> color(graph, max_colors)
169+
[0, 1, 2]
107170
>>> color([], 2) # empty graph
108171
[]
109172
>>> color([[0]], 1) # single node, 1 color
@@ -113,9 +176,11 @@ def color(graph: list[list[int]], max_colors: int) -> list[int]:
113176
>>> color([[0, 1], [1, 0]], 2) # 2 nodes, 2 colors (possible)
114177
[0, 1]
115178
"""
179+
# Initialize all vertices as uncolored (-1)
116180
colored_vertices = [-1] * len(graph)
117181

182+
# Use the utility function to try and color the graph starting from vertex 0
118183
if util_color(graph, max_colors, colored_vertices, 0):
119-
return colored_vertices
184+
return colored_vertices # The successful color assignment
120185

121-
return []
186+
return [] # No valid coloring is possible

‎backtracking/crossword_puzzle_solver.py‎

Lines changed: 66 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,28 @@ def is_valid(
66
) -> bool:
77
"""
88
Check if a word can be placed at the given position.
9+
A cell is valid if it is empty or already contains the correct letter
10+
(enabling crossing/intersection between words).
911
10-
>>> puzzle = [
11-
... ['', '', '', ''],
12-
... ['', '', '', ''],
13-
... ['', '', '', ''],
14-
... ['', '', '', '']
15-
... ]
12+
>>> puzzle = [['', '', '', ''], ['', '', '', ''],
13+
... ['', '', '', ''], ['', '', '', '']]
1614
>>> is_valid(puzzle, 'word', 0, 0, True)
1715
True
18-
>>> puzzle = [
19-
... ['', '', '', ''],
20-
... ['', '', '', ''],
21-
... ['', '', '', ''],
22-
... ['', '', '', '']
23-
... ]
2416
>>> is_valid(puzzle, 'word', 0, 0, False)
2517
True
18+
>>> puzzle2 = [['w', '', ''], ['o', '', ''], ['r', '', ''], ['d', '', '']]
19+
>>> is_valid(puzzle2, 'word', 0, 0, True)
20+
True
21+
>>> is_valid(puzzle2, 'cat', 0, 0, True)
22+
False
2623
"""
27-
for i in range(len(word)):
28-
if vertical:
29-
if row + i >= len(puzzle) or puzzle[row + i][col] != "":
30-
return False
31-
elif col + i >= len(puzzle[0]) or puzzle[row][col + i] != "":
24+
rows, cols = len(puzzle), len(puzzle[0])
25+
for i, ch in enumerate(word):
26+
r, c = (row + i, col) if vertical else (row, col + i)
27+
if r >= rows or c >= cols:
28+
return False
29+
cell = puzzle[r][c]
30+
if cell not in ("", ch):
3231
return False
3332
return True
3433

@@ -37,95 +36,87 @@ def place_word(
3736
puzzle: list[list[str]], word: str, row: int, col: int, vertical: bool
3837
) -> None:
3938
"""
40-
Place a word at the given position.
41-
42-
>>> puzzle = [
43-
... ['', '', '', ''],
44-
... ['', '', '', ''],
45-
... ['', '', '', ''],
46-
... ['', '', '', '']
47-
... ]
39+
Place a word at the given position in the puzzle.
40+
41+
>>> puzzle = [['', '', '', ''], ['', '', '', ''],
42+
... ['', '', '', ''], ['', '', '', '']]
4843
>>> place_word(puzzle, 'word', 0, 0, True)
4944
>>> puzzle
5045
[['w', '', '', ''], ['o', '', '', ''], ['r', '', '', ''], ['d', '', '', '']]
5146
"""
52-
for i, char in enumerate(word):
47+
for i, ch in enumerate(word):
5348
if vertical:
54-
puzzle[row + i][col] = char
49+
puzzle[row + i][col] = ch
5550
else:
56-
puzzle[row][col + i] = char
51+
puzzle[row][col + i] = ch
5752

5853

5954
def remove_word(
60-
puzzle: list[list[str]], word: str, row: int, col: int, vertical: bool
55+
puzzle: list[list[str]],
56+
word: str,
57+
row: int,
58+
col: int,
59+
vertical: bool,
60+
snapshot: list[list[str]],
6161
) -> None:
6262
"""
63-
Remove a word from the given position.
64-
65-
>>> puzzle = [
66-
... ['w', '', '', ''],
67-
... ['o', '', '', ''],
68-
... ['r', '', '', ''],
69-
... ['d', '', '', '']
70-
... ]
71-
>>> remove_word(puzzle, 'word', 0, 0, True)
63+
Remove a word from the puzzle, restoring only cells that were empty
64+
before placement. Cells shared with crossing words are preserved.
65+
66+
>>> puzzle = [['w', 'o', 'r', 'd'], ['', '', '', ''],
67+
... ['', '', '', ''], ['', '', '', '']]
68+
>>> snap = [['', 'o', 'r', 'd'], ['', '', '', ''],
69+
... ['', '', '', ''], ['', '', '', '']]
70+
>>> remove_word(puzzle, 'word', 0, 0, False, snap)
7271
>>> puzzle
73-
[['', '', '', ''], ['', '', '', ''], ['', '', '', ''], ['', '', '', '']]
72+
[['', 'o', 'r', 'd'], ['', '', '', ''], ['', '', '', ''], ['', '', '', '']]
7473
"""
7574
for i in range(len(word)):
76-
if vertical:
77-
puzzle[row + i][col] = ""
78-
else:
79-
puzzle[row][col + i] = ""
75+
r, c = (row + i, col) if vertical else (row, col + i)
76+
if snapshot[r][c] == "":
77+
puzzle[r][c] = ""
8078

8179

8280
def solve_crossword(puzzle: list[list[str]], words: list[str]) -> bool:
8381
"""
8482
Solve the crossword puzzle using backtracking.
83+
Words are tried longest-first to prune the search space early.
84+
Intersections between words (shared letters) are supported.
8585
86-
>>> puzzle = [
87-
... ['', '', '', ''],
88-
... ['', '', '', ''],
89-
... ['', '', '', ''],
90-
... ['', '', '', '']
91-
... ]
92-
93-
>>> words = ['word', 'four', 'more', 'last']
94-
>>> solve_crossword(puzzle, words)
86+
>>> puzzle = [['', '', '', ''], ['', '', '', ''],
87+
... ['', '', '', ''], ['', '', '', '']]
88+
>>> solve_crossword(puzzle, ['word', 'four', 'more', 'last'])
9589
True
96-
>>> puzzle = [
97-
... ['', '', '', ''],
98-
... ['', '', '', ''],
99-
... ['', '', '', ''],
100-
... ['', '', '', '']
101-
... ]
102-
>>> words = ['word', 'four', 'more', 'paragraphs']
103-
>>> solve_crossword(puzzle, words)
90+
>>> puzzle2 = [['', '', '', ''], ['', '', '', ''],
91+
... ['', '', '', ''], ['', '', '', '']]
92+
>>> solve_crossword(puzzle2, ['word', 'four', 'more', 'paragraphs'])
10493
False
10594
"""
95+
if not words:
96+
return True
97+
98+
remaining = sorted(words, key=len, reverse=True)
99+
word, rest = remaining[0], remaining[1:]
100+
106101
for row in range(len(puzzle)):
107102
for col in range(len(puzzle[0])):
108-
if puzzle[row][col] == "":
109-
for word in words:
110-
for vertical in [True, False]:
111-
if is_valid(puzzle, word, row, col, vertical):
112-
place_word(puzzle, word, row, col, vertical)
113-
words.remove(word)
114-
if solve_crossword(puzzle, words):
115-
return True
116-
words.append(word)
117-
remove_word(puzzle, word, row, col, vertical)
118-
return False
119-
return True
103+
for vertical in (True, False):
104+
if is_valid(puzzle, word, row, col, vertical):
105+
snapshot = [r[:] for r in puzzle]
106+
place_word(puzzle, word, row, col, vertical)
107+
if solve_crossword(puzzle, rest):
108+
return True
109+
remove_word(puzzle, word, row, col, vertical, snapshot)
110+
111+
return False
120112

121113

122114
if __name__ == "__main__":
123115
PUZZLE = [[""] * 3 for _ in range(3)]
124116
WORDS = ["cat", "dog", "car"]
125-
126117
if solve_crossword(PUZZLE, WORDS):
127118
print("Solution found:")
128119
for row in PUZZLE:
129-
print(" ".join(row))
120+
print(" ".join(cell or "." for cell in row))
130121
else:
131-
print("No solution found:")
122+
print("No solution found.")

0 commit comments

Comments
 (0)