forked from microsoft/PythonProgrammingPuzzles
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgraphs.py
More file actions
464 lines (387 loc) · 18.3 KB
/
graphs.py
File metadata and controls
464 lines (387 loc) · 18.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
"""Problems related to graphs such as Conway's 99 problem, finding
[cliques](https://en.wikipedia.org/wiki/Clique_(graph_theory)) of various sizes, shortest path (Dijkstra) """
from puzzle_generator import PuzzleGenerator, Tags
from typing import List
# See https://github.com/microsoft/PythonProgrammingPuzzles/wiki/How-to-add-a-puzzle to learn about adding puzzles
class Conway99(PuzzleGenerator):
"""Conway's 99-graph problem (*unsolved*, open problem)
Conway's 99-graph problem is an unsolved problem in graph theory.
In Conway's terminology, from [Five $1,000 Problems (Update 2017)](https://oeis.org/A248380/a248380.pdf)
"Is there a graph with 99 vertices in which every edge (i.e. pair of joined vertices) belongs to a unique
triangle and every nonedge (pair of unjoined vertices) to a unique quadrilateral?"
See also this [Wikipedia article](https://en.wikipedia.org/w/index.php?title=Conway%27s_99-graph_problem).
"""
@staticmethod
def sat(edges: List[List[int]]):
"""
Find an undirected graph with 99 vertices, in which each two adjacent vertices have exactly one common
neighbor, and in which each two non-adjacent vertices have exactly two common neighbors.
"""
# first compute neighbors sets, N:
N = {i: {j for j in range(99) if j != i and ([i, j] in edges or [j, i] in edges)} for i in range(99)}
return all(len(N[i].intersection(N[j])) == (1 if j in N[i] else 2) for i in range(99) for j in range(i))
def dedup_edges(stuff):
seen = set()
return [a for a in stuff if tuple(a) not in seen and not seen.add(tuple(a))]
class AnyEdge(PuzzleGenerator):
"""Trivial [graph](https://en.wikipedia.org/w/index.php?title=Graph_(discrete_mathematics)) problem."""
@staticmethod
def sat(e: List[int], edges=[[0, 217], [40, 11], [17, 29], [11, 12], [31, 51]]):
"""Find any edge in edges."""
return e in edges
@staticmethod
def sol(edges):
return edges[0]
def gen_random(self):
n = self.random.randrange(1, self.random.choice([10, 100]))
m = self.random.randrange(1, 10 * n)
# random graph:
edges = dedup_edges([[self.random.randrange(n + 1), self.random.randrange(n + 1)] for _ in range(m)])
self.add({"edges": edges})
class AnyTriangle(PuzzleGenerator):
"""
Easy [graph](https://en.wikipedia.org/w/index.php?title=Graph_(discrete_mathematics)) problem,
see [triangle](https://en.wikipedia.org/w/index.php?title=Triangle_graph)
"""
@staticmethod
def sat(tri: List[int], edges=[[0, 17], [0, 22], [17, 22], [17, 31], [22, 31], [31, 17]]):
"""Find any triangle in the given directed graph."""
a, b, c = tri
return [a, b] in edges and [b, c] in edges and [c, a] in edges and a != b != c != a
@staticmethod
def sol(edges):
from collections import defaultdict
outs = defaultdict(set)
ins = defaultdict(set)
for i, j in edges:
if j != i:
outs[i].add(j)
ins[j].add(i)
for i in outs:
for j in outs[i]:
try:
if j in outs:
k = min(outs[j].intersection(ins[i]))
return [i, j, k]
except ValueError:
pass
def gen_random(self):
n = self.random.randrange(1, self.random.choice([10, 100]))
m = self.random.randrange(1, 10 * n)
# random graph:
edges = dedup_edges([[self.random.randrange(n + 1), self.random.randrange(n + 1)] for _ in range(m)])
tri = self.sol(edges)
if tri:
assert self.sat(tri, edges)
self.add({"edges": edges})
########################################################################################################################
class PlantedClique(PuzzleGenerator):
"""Find a [planted clique](https://en.wikipedia.org/w/index.php?title=Planted_clique) of a given size
in an undirected graph. Finding a polynomial-time algorithm for this problem has been *unsolved* for
some time."""
@staticmethod
def sat(nodes: List[int], size=3, edges=[[0, 17], [0, 22], [17, 22], [17, 31], [22, 31], [31, 17]]):
"""Find a clique of the given size in the given undirected graph. It is guaranteed that such a clique exists."""
assert len(nodes) == len(set(nodes)) >= size
edge_set = {(a, b) for (a, b) in edges}
for a in nodes:
for b in nodes:
assert a == b or (a, b) in edge_set or (b, a) in edge_set
return True
@staticmethod
def sol(size, edges):
# brute force (finds list in increasing order), but with a tiny bit of speedup
if size == 0:
return []
from collections import defaultdict
neighbors = defaultdict(set)
n = max(max(e) for e in edges)
for (a, b) in edges:
if a != b:
neighbors[a].add(b)
neighbors[b].add(a)
pools = [list(range(n + 1))]
indices = [-1]
while pools:
indices[-1] += 1
if indices[-1] >= len(pools[-1]) - size + len(pools): # since list is increasing order
indices.pop()
pools.pop()
continue
if len(pools) == size:
return [pool[i] for pool, i in zip(pools, indices)]
a = (pools[-1])[indices[-1]]
pools.append([i for i in pools[-1] if i > a and i in neighbors[a]])
indices.append(-1)
assert False, f"No clique of size {size}"
def gen_random(self):
n = self.random.randrange(1, self.random.choice([10, 20, 50, 100]))
m = self.random.randrange(1, 10 * n)
# random graph:
edges = [[self.random.randrange(n + 1), self.random.randrange(n + 1)] for _ in range(m)]
size = self.random.randrange(min(20, n))
clique = self.random.sample(range(n), size)
for a in clique: # plant clique!
for b in clique:
if a < b:
edges.append(self.random.choice([[a, b], [b, a]]))
edges = dedup_edges(edges)
self.random.shuffle(edges)
self.add({"edges": edges, "size": size}, test=(size <= 10))
class ShortestPath(PuzzleGenerator):
"""Shortest Path, see (Dijkstra's algorithm)[https://en.wikipedia.org/w/index.php?title=Dijkstra%27s_algorithm]"""
@staticmethod
def sat(path: List[int], weights=[{1: 20, 2: 1}, {2: 2, 3: 5}, {1: 10}], bound=11):
"""
Find a path from node 0 to node 1, of length at most bound, in the given digraph.
weights[a][b] is weight on edge [a,b] for (int) nodes a, b
"""
return path[0] == 0 and path[-1] == 1 and sum(weights[a][b] for a, b in zip(path, path[1:])) <= bound
@staticmethod
def sol(weights, bound):
# Dijkstra's algorithm (bound is ignored)
u, v = 0, 1 # go from 0 to 1
import heapq
queue = [(0, u, u)] # distance, node, trail
trails = {}
while queue:
dist, i, j = heapq.heappop(queue)
if i in trails:
continue
trails[i] = j
if i == v:
break
for j in weights[i]:
if j not in trails:
heapq.heappush(queue, (dist + weights[i][j], j, i))
if v in trails:
rev_path = [v]
while rev_path[-1] != u:
rev_path.append(trails[rev_path[-1]])
return rev_path[::-1]
# no path
def gen_random(self):
n = self.random.randrange(1, self.random.choice([10, 20, 50, 100]))
m = self.random.randrange(n, 5 * n)
# random graph:
weights = [{} for _ in range(n)]
for _ in range(m):
weights[self.random.randrange(n)][self.random.randrange(n)] = self.random.randrange(1000)
path = self.sol(weights, bound=None)
if path:
bound = sum(weights[a][b] for a, b in zip(path, path[1:]))
assert self.sat(path, weights, bound)
self.add(dict(weights=weights, bound=bound))
class UnweightedShortestPath(PuzzleGenerator):
"""
Unweighted Shortest Path
See (Dijkstra's algorithm)[https://en.wikipedia.org/w/index.php?title=Dijkstra%27s_algorithm]
"""
@staticmethod
def sat(path: List[int],
edges=[[0, 11], [0, 7], [7, 5], [0, 22], [11, 22], [11, 33], [22, 33]],
u=0,
v=33,
bound=3):
"""Find a path from node u to node v, of a bounded length, in the given digraph on vertices 0, 1,..., n."""
assert path[0] == u and path[-1] == v and all([i, j] in edges for i, j in zip(path, path[1:]))
return len(path) <= bound
@staticmethod
def sol(edges, u, v, bound):
# Dijkstra's algorithm
import heapq
from collections import defaultdict
queue = [(0, u, u)] # distance, node, trail
trails = {}
neighbors = defaultdict(set)
for (i, j) in edges:
neighbors[i].add(j)
while queue:
dist, i, j = heapq.heappop(queue)
if i in trails:
continue
trails[i] = j
if i == v:
break
for j in neighbors[i]:
if j not in trails:
heapq.heappush(queue, (dist + 1, j, i))
if v in trails:
rev_path = [v]
while rev_path[-1] != u:
rev_path.append(trails[rev_path[-1]])
return rev_path[::-1]
# no path
def gen_random(self):
n = self.random.randrange(1, self.random.choice([10, 20, 50, 100]))
m = self.random.randrange(n, 5 * n)
# random graph:
edges = dedup_edges([self.random.randrange(n + 1), self.random.randrange(n + 1)] for _ in range(5 * n))
u = self.random.randrange(n)
v = self.random.randrange(n)
path = self.sol(edges, u, v, bound=None)
if path:
bound = len(path)
assert self.sat(path, edges, u, v, bound)
self.add(dict(u=u, v=v, edges=edges, bound=bound))
class AnyPath(PuzzleGenerator):
"""Any Path"""
@staticmethod
def sat(path: List[int], edges=[[0, 1], [0, 2], [1, 3], [1, 4], [2, 5], [3, 4], [5, 6], [6, 7], [1, 2]]):
""" Find any path from node 0 to node n in a given digraph on vertices 0, 1,..., n."""
for i in range(len(path) - 1):
assert [path[i], path[i + 1]] in edges
assert path[0] == 0
assert path[-1] == max(max(edge) for edge in edges)
return True
@staticmethod
def sol(edges):
n = max(max(edge) for edge in edges)
paths = {0: [0]}
for _ in range(n + 1):
for i, j in edges:
if i in paths and j not in paths:
paths[j] = paths[i] + [j]
return paths.get(n)
def gen_random(self):
n = self.random.randrange(1, self.random.choice([10, 100]))
# random graph:
edges = dedup_edges([self.random.randrange(n), self.random.randrange(n)] for _ in range(2 * n))
if self.sol(edges):
self.add(dict(edges=edges))
class EvenPath(PuzzleGenerator):
@staticmethod
def sat(path: List[int], edges=[[0, 1], [0, 2], [1, 3], [1, 4], [2, 5], [3, 4], [5, 6], [6, 7], [1, 2]]):
"""Find a path with an even number of nodes from nodes 0 to n in the given digraph on vertices 0, 1,..., n."""
assert path[0] == 0 and path[-1] == max(max(e) for e in edges)
assert all([[a, b] in edges for a, b in zip(path, path[1:])])
return len(path) % 2 == 0
@staticmethod
def sol(edges):
even_paths = {}
odd_paths = {0: [0]}
n = max(max(e) for e in edges)
for _ in range(n + 1):
for i, j in edges:
if i in even_paths and j not in odd_paths:
odd_paths[j] = even_paths[i] + [j]
if i in odd_paths and j not in even_paths:
even_paths[j] = odd_paths[i] + [j]
return even_paths.get(n)
def gen_random(self):
n = self.random.randrange(1, self.random.choice([10, 100]))
# random graph:
edges = dedup_edges([self.random.randrange(n), self.random.randrange(n)] for _ in range(2 * n))
if self.sol(edges):
self.add(dict(edges=edges))
class OddPath(PuzzleGenerator):
"""To make it even more different than EvenPath, we changed to go from node 0 to node *1*."""
@staticmethod
def sat(p: List[int], edges=[[0, 1], [0, 2], [1, 3], [1, 4], [2, 5], [3, 4], [5, 6], [6, 7], [6, 1]]):
"""Find a path with an even number of nodes from nodes 0 to 1 in the given digraph on vertices 0, 1,..., n."""
return p[0] == 0 and p[-1] == 1 == len(p) % 2 and all([[a, b] in edges for a, b in zip(p, p[1:])])
@staticmethod
def sol(edges):
even_paths = {}
odd_paths = {0: [0]}
n = 1
for _ in range(max(max(e) for e in edges) + 1):
for i, j in edges:
if i in even_paths and j not in odd_paths:
odd_paths[j] = even_paths[i] + [j]
if i in odd_paths and j not in even_paths:
even_paths[j] = odd_paths[i] + [j]
return odd_paths.get(n)
def gen_random(self):
n = self.random.randrange(1, self.random.choice([10, 100]))
# random graph:
edges = dedup_edges([self.random.randrange(n), self.random.randrange(n)] for _ in range(2 * n))
if self.sol(edges):
self.add(dict(edges=edges))
class Zarankiewicz(PuzzleGenerator):
"""[Zarankiewicz problem](https://en.wikipedia.org/wiki/Zarankiewicz_problem)"""
@staticmethod
def sat(edges: List[List[int]], z=20, n=5, t=3):
"""Find a bipartite graph with n vertices on each side, z edges, and no K_3,3 subgraph."""
from itertools import combinations
edges = {(a, b) for a, b in edges if a in range(n) and b in range(n)} # convert to a set for efficiency
assert len(edges) >= z
return all(
any((a, b) not in edges for a in left for b in right)
for left in combinations(range(n), t)
for right in combinations(range(n), t)
)
@staticmethod
def sol(z, n, t):
from itertools import combinations
all_edges = [(a, b) for a in range(n) for b in range(n)]
for edges in combinations(all_edges, z):
edge_set = set(edges)
if all(any((a, b) not in edge_set for a in left for b in right)
for left in combinations(range(n), t)
for right in combinations(range(n), t)):
return [[a, b] for a, b in edges]
def gen(self, target_num_instances):
if self.num_generated_so_far() < target_num_instances:
self.add(dict(z=26, n=6, t=3), test=False)
if self.num_generated_so_far() < target_num_instances:
self.add(dict(z=13, n=4, t=3))
class GraphIsomorphism(PuzzleGenerator):
"""
The classic [Graph Isomorphism](https://en.wikipedia.org/wiki/Graph_isomorphism) problem.
It is unknown whether or not there exists a polynomial-time algorithm
for this problem, though an unpublished quasi-polynomial-time algorithm has been announced by Babai.
The classic version is a decision problem: given two graphs, determine whether or not they are isomorphic.
However, it is polynomial-time equivalent to the one below through a standard reduction. In particular, if you
could solve the search problem below (finding the actual bijection), then you can decide isomorphism because the
search solver would simply fail on non-isomorphic graphs. Conversely, if you could solve the decision problem,
then you can find a bijection as follows: if the decider determines that the graphs are isomorphic, for each node
in the first graph, find a corresponding node in the second graph as follows. Add N self-edges from the node to
itself where N is the maximum degree in the graph + 1, and do that for each candidate node in the second graph.
For each of these additions, test isomorphism. If the graphs are isomorphic then there must be a bijection that maps
the first node to the second. Repeat this for each node until you have found a bijection. (If self-loops are not
allowed, one can do this by adding N additional nodes for each test.
"""
@staticmethod
def sat(bi: List[int], g1=[[0, 1], [1, 2], [2, 3], [3, 4], [2, 5]], g2=[[0, 4], [1, 5], [4, 1], [1, 2], [2, 3]]):
"""
You are given two graphs which are permutations of one another and the goal is to find the permutation.
Each graph is specified by a list of edges where each edge is a pair of integer vertex numbers.
"""
return len(bi) == len(set(bi)) and {(i, j) for i, j in g1} == {(bi[i], bi[j]) for i, j in g2}
@staticmethod
def sol(g1, g2):
# exponentially slow
from itertools import permutations
n = max(i for g in [g1, g2] for e in g for i in e) + 1
g1_set = {(i, j) for i, j in g1}
for pi in permutations(range(n)):
if all((pi[i], pi[j]) in g1_set for i, j in g2):
return list(pi)
assert False, f"Graphs are not isomorphic {g1}, {g2}"
def gen_random(self):
n = self.random.randrange(20)
g1 = sorted({(self.random.randrange(n), self.random.randrange(n)) for _ in range((n * n) // 2)})
if not g1:
return
pi = list(range(n))
self.random.shuffle(pi)
g1 = [[i, j] for i, j in g1]
g2 = [[pi[i], pi[j]] for i, j in g1]
self.random.shuffle(g2)
self.add(dict(g1=g1, g2=g2), test=n < 10) # only test for small n
class ShortIntegerPath(PuzzleGenerator):
"""This is a more interesting version of Study_20 with an additional length constraint. One can think of the graph
defined by the integer pairs."""
@staticmethod
def sat(li: List[int]):
"""
Find a list of nine integers, starting with 0 and ending with 128, such that each integer either differs from
the previous one by one or is thrice the previous one.
"""
return all(j in {i - 1, i + 1, 3 * i} for i, j in zip([0] + li, li + [128])) and len(li) == 9
@staticmethod
def sol():
return [1, 3, 4, 12, 13, 14, 42, 126, 127]
if __name__ == "__main__":
PuzzleGenerator.debug_problems()