Coverage for optimates/combinatorics.py: 0%
128 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-16 11:19 -0500
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-16 11:19 -0500
1"""This module defines classes for search problems on spaces of standard combinatorial objects (subsets, permutations, etc.)."""
3from collections.abc import Iterable
4from dataclasses import dataclass
5import itertools
6import math
7import random
9from optimates.search import EmptyNeighborSetError, SearchProblem
12Perm = tuple[int, ...]
14def num_permutations(n: int, k: int) -> int:
15 """Gets the number of permutations of n of size k."""
16 return math.factorial(n) // math.factorial(n - k)
18def random_combo2(n: int) -> tuple[int, int]:
19 """Gets a random ordered pair (i, j), where i != j, and 0 <= i, j < n."""
20 i = random.randint(0, n - 1)
21 j = random.randint(0, n - 2)
22 if j >= i:
23 j += 1
24 return tuple(sorted([i, j])) # type: ignore[return-value]
27@dataclass
28class SubsetSearchProblem(SearchProblem[int]):
29 """A search problem defined on the set of subsets of {0, 1, ..., n - 1}.
30 Encodes a subset as an n-bit unsigned integer.
31 A neighbor of a subset is considered to be any subset with "a bit flipped" (i.e. either one element was added or removed from the set)."""
32 n: int
33 initial: int = 0 # by default, start off empty
35 def initial_nodes(self) -> Iterable[int]:
36 return [self.initial]
38 def is_solution(self, node: int) -> bool:
39 return True
41 def iter_nodes(self) -> Iterable[int]:
42 return range(2 ** self.n)
44 def random_node(self) -> int:
45 return random.randint(0, 2 ** self.n - 1)
47 def get_neighbors(self, node: int) -> Iterable[int]:
48 return (node ^ (1 << i) for i in range(self.n))
50 def num_neighbors(self, node: int) -> int:
51 return self.n
53 def random_neighbor(self, node: int) -> int:
54 i = random.randint(0, self.n - 1)
55 return node ^ (1 << i)
58@dataclass
59class PermutationSearchProblem(SearchProblem[Perm]):
60 """A search problem defined on the set of length-n permutations.
61 A permutation is represented as an integer vector x, where x[i] = j means that i is mapped to j under the permutation.
62 A neighbor of a permutation is one where a single swap has occurred (n choose 2 total).
63 If adjacent_only = True, only includes neighbors where the swap is adjacent (n - 1 total)."""
64 n: int
65 adjacent_only: bool = False
67 def initial_nodes(self) -> Iterable[Perm]:
68 return [tuple(range(self.n))]
70 def is_solution(self, node: Perm) -> bool:
71 return True
73 def iter_nodes(self) -> Iterable[Perm]:
74 return itertools.permutations(range(self.n))
76 def random_node(self) -> Perm:
77 perm = list(range(self.n))
78 random.shuffle(perm)
79 return tuple(perm)
81 def get_neighbors(self, node: Perm) -> Iterable[Perm]:
82 if self.adjacent_only:
83 pair_gen: Iterable[tuple[int, int]] = ((i, i + 1) for i in range(self.n - 1))
84 else:
85 pair_gen = itertools.combinations(range(self.n), 2)
86 for (i, j) in pair_gen:
87 nbr = list(node)
88 nbr[i], nbr[j] = nbr[j], nbr[i]
89 yield tuple(nbr)
91 def num_neighbors(self, node: Perm) -> int:
92 return (self.n - 1) if self.adjacent_only else math.comb(self.n, 2)
94 def random_neighbor(self, node: Perm) -> Perm:
95 if (self.n <= 1):
96 raise EmptyNeighborSetError()
97 if self.adjacent_only:
98 i = random.randint(0, self.n - 2)
99 j = i + 1
100 else:
101 (i, j) = random_combo2(self.n)
102 nbr = list(node)
103 nbr[i], nbr[j] = nbr[j], nbr[i]
104 return tuple(nbr)
107@dataclass
108class PermutedSubsequenceSearchProblem(SearchProblem[Perm]):
109 """A search problem defined on permutations of subsequences of (0, 1, ..., n - 1).
110 Each node is a sequence (i_1, ..., i_k), where k = 0, ..., n, and each i_j is a distinct element in {0, 1, ..., n - 1}.
111 A neighbor of a permuted sequence is a sequence that swaps one element of the original sequence with another element of either the sequence or its complement, discards one element, or inserts one element."""
112 n: int
114 def initial_nodes(self) -> Iterable[Perm]:
115 return [()]
117 def is_solution(self, node: Perm) -> bool:
118 return True
120 def iter_nodes(self) -> Iterable[Perm]:
121 return itertools.chain.from_iterable(itertools.permutations(range(self.n), k) for k in range(self.n + 1))
123 def random_node(self) -> Perm:
124 # choose k in proportion to the number of permutations of that size
125 num_perms = [num_permutations(self.n, k) for k in range(self.n + 1)]
126 k = random.choices(range(self.n + 1), num_perms)[0]
127 # get a random permutation of n elements, then take the first k
128 vals = list(range(self.n))
129 random.shuffle(vals)
130 return tuple(vals)[:k]
132 def get_complement(self, node: Perm) -> list[int]:
133 """Gets the complement of a node (as a list of integers, ordered, which are not in the subsequence)."""
134 node_set = set(node)
135 return [i for i in range(self.n) if (i not in node_set)]
137 def get_neighbors(self, node: Perm) -> Iterable[Perm]:
138 k = len(node)
139 complement = self.get_complement(node)
140 # internal swaps
141 for (i, j) in itertools.combinations(range(k), 2):
142 nbr = list(node)
143 nbr[i], nbr[j] = nbr[j], nbr[i]
144 yield tuple(nbr)
145 # external swaps
146 for (i, j) in itertools.product(range(k), range(self.n - k)):
147 nbr = list(node)
148 nbr[i] = complement[j]
149 yield tuple(nbr)
150 # discards
151 for i in range(k):
152 yield node[:i] + node[i + 1:]
153 # insertions
154 for (i, j) in itertools.product(range(k + 1), range(self.n - k)):
155 nbr = list(node)
156 nbr.insert(i, complement[j])
157 yield tuple(nbr)
159 def num_neighbors(self, node: Perm) -> int:
160 k = len(node)
161 return math.comb(k, 2) + k * (self.n - k + 1) + (k + 1) * (self.n - k)
163 def random_neighbor(self, node: Perm) -> Perm:
164 k = len(node)
165 num_internal_swaps = math.comb(k, 2)
166 num_external_swaps = k * (self.n - k)
167 num_discards = k
168 num_insertions = (1 + k) * (self.n - k)
169 r = random.choices(range(4), [num_internal_swaps, num_external_swaps, num_discards, num_insertions])[0]
170 nbr = list(node)
171 if (r == 0): # internal swap
172 (i, j) = random_combo2(k)
173 nbr[i], nbr[j] = nbr[j], nbr[i]
174 elif (r == 1): # external swap
175 i = random.randint(0, k - 1)
176 j = random.randint(0, self.n - k - 1)
177 nbr[i] = self.get_complement(node)[j]
178 elif (r == 2): # discard
179 i = random.randint(0, k - 1)
180 nbr = nbr[:i] + nbr[i + 1:]
181 else: # insert
182 i = random.randint(0, k)
183 j = random.randint(0, self.n - k - 1)
184 nbr.insert(i, self.get_complement(node)[j])
185 return tuple(nbr)