You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

133 lines
3.4KB

  1. import collections
  2. import functools
  3. import itertools
  4. import math
  5. import re
  6. import sys
  7. def render(grid):
  8. lines = []
  9. for y in range(size):
  10. groups = [tiles[grid[y, x]].splitlines()[1:-1] for x in range(size)]
  11. for lns in zip(*groups):
  12. lines.append(''.join(ln[1:-1] for ln in lns))
  13. return '\n'.join(lines)
  14. def get_borders(content):
  15. a, *_, b = content.splitlines()
  16. c, *_, d = [''.join(ln) for ln in zip(*content.splitlines())]
  17. return [a, b, c, d, a[::-1], b[::-1], c[::-1], d[::-1]]
  18. def transpose(string):
  19. return '\n'.join(''.join(ln) for ln in zip(*string.splitlines()))
  20. def flipV(string):
  21. return '\n'.join(ln for ln in string.splitlines()[::-1])
  22. def flipH(string):
  23. return '\n'.join(ln[::-1] for ln in string.splitlines())
  24. def rot90(string):
  25. return flipH(transpose(string))
  26. def noop(string):
  27. return string
  28. def variants(string):
  29. alts = [noop, flipH], [noop, flipV], [noop, transpose]
  30. for ops in itertools.product(*alts):
  31. yield functools.reduce(lambda s, f: f(s), ops, string)
  32. def UD(A, B):
  33. return A.splitlines()[-1] == B.splitlines()[0]
  34. def LR(A, B):
  35. return UD(rot90(A), rot90(B))
  36. def search_in(source, target):
  37. source_lns = source.splitlines()
  38. target_lns = target.splitlines()
  39. h, w = len(target_lns), len(target_lns[0])
  40. out = []
  41. for y in range(len(source_lns)):
  42. for x in range(len(source_lns[0])):
  43. window = '\n'.join(ln[x:x + w] for ln in source_lns[y:y + h])
  44. if re.fullmatch(target, window):
  45. out.append((x, y))
  46. return out
  47. def reconcile(A, B, condition):
  48. tiles[A], tiles[B] = next(
  49. (X, Y)
  50. for X, Y in itertools.product(variants(tiles[A]), variants(tiles[B]))
  51. if condition(X, Y)
  52. )
  53. text = sys.stdin.read().strip()
  54. tiles = {}
  55. borders = {}
  56. for line in text.split('\n\n'):
  57. title, content = line.split(':\n')
  58. tid = int(title.split()[1])
  59. tiles[tid] = content
  60. borders[tid] = get_borders(content)
  61. size = int(len(tiles) ** 0.5)
  62. adj = collections.defaultdict(set)
  63. for (aid, A), (bid, B) in itertools.combinations(borders.items(), 2):
  64. if set(A) & set(B):
  65. adj[aid].add(bid)
  66. adj[bid].add(aid)
  67. corners = [v for v, ks in adj.items() if len(ks) == 2]
  68. print(math.prod(corners))
  69. A = min(corners)
  70. B, C = adj.pop(A)
  71. grid = {(y, x): None for x in range(size) for y in range(size)}
  72. grid[0, 0] = A
  73. grid[0, 1] = B
  74. grid[1, 0] = C
  75. reconcile(A, B, condition=LR)
  76. reconcile(A, C, condition=UD)
  77. seen = {A, B, C}
  78. for z in range(2, 2 * size - 1):
  79. # determine new pieces from intersection between 2 neighbors
  80. for y, x in {(y, z - y) for y in range(1, z)}.intersection(grid):
  81. U, L = (y - 1, x), (y, x - 1)
  82. grid[y, x], = adj[grid[U]] & adj[grid[L]] - seen
  83. reconcile(grid[L], grid[y, x], condition=LR)
  84. seen.add(grid[y, x])
  85. # determine new piece from only one alternative left
  86. for y, x in {(0, z), (z, 0)}.intersection(grid):
  87. opts = {(y - 1, x): UD, (y, x - 1): LR}
  88. P, fn = next(p for p in opts.items() if p[0] in grid)
  89. grid[y, x], = adj[grid[P]] - seen
  90. reconcile(grid[P], grid[y, x], condition=fn)
  91. seen.add(grid[y, x])
  92. monster = '''
  93. ..................#.
  94. #....##....##....###
  95. .#..#..#..#..#..#...
  96. '''[1:-1]
  97. src = render(grid)
  98. found = max([search_in(pic, monster) for pic in variants(src)], key=len)
  99. print(src.count('#') - len(found) * monster.count('#'))