Advertisement
Guest User

generator-dunnit

a guest
Feb 16th, 2025
28
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.63 KB | None | 0 0
  1. from __future__ import annotations
  2.  
  3. import random
  4. import string
  5. import sys
  6. from collections import Counter
  7. from itertools import product
  8.  
  9.  
  10. def E(*args):
  11.     print(*args, file=sys.stderr)
  12.  
  13.  
  14. class GenError(Exception):
  15.     pass
  16.  
  17.  
  18. V = int
  19. """Type of the variable: int for simplicity but usually a string."""
  20. U = set[V]
  21. """Universe: set of variables."""
  22. S = list[set[V]]
  23. """Solution type. Same type as the "Exact Cover Problem"."""
  24.  
  25.  
  26. def generate_solution(n: int, universe: U) -> S:
  27.     return generate_solution_with_bound(n, universe, bound=6)
  28.     elements = list(universe)
  29.     random.shuffle(elements)
  30.  
  31.     # The n // 2 is not needed, it just makes the sets larger on average
  32.     # This affects the length of the first clue
  33.     num_partitions = random.randint(1, n // 2)
  34.     partition_sizes = [1] * num_partitions
  35.     for _ in range(len(elements) - num_partitions):
  36.         partition_sizes[random.randint(0, num_partitions - 1)] += 1
  37.  
  38.     solution = S()
  39.     idx = 0
  40.     for size in partition_sizes:
  41.         solution.append({*elements[idx : idx + size]})
  42.         idx += size
  43.  
  44.     return solution
  45.  
  46.  
  47. def generate_solution_with_bound(n: int, universe: U, *, bound: int) -> S:
  48.     """Guarantee that every subset of the exact cover is of size <= bound."""
  49.     elements = list(universe)
  50.     random.shuffle(elements)
  51.  
  52.     num_partitions = random.randint(1, n // 20)
  53.     num_partitions = n // 4
  54.     partition_sizes = [1] * num_partitions
  55.     available_partitions = set(range(num_partitions))
  56.  
  57.     for _ in range(n - num_partitions):
  58.         if not available_partitions:
  59.             break
  60.             raise ValueError("No available partitions left")
  61.         idx = random.choice(list(available_partitions))
  62.         partition_sizes[idx] += 1
  63.         if partition_sizes[idx] >= bound:
  64.             available_partitions.remove(idx)
  65.  
  66.     solution = []
  67.     idx = 0
  68.     for size in partition_sizes:
  69.         subset = set(elements[idx : idx + size])
  70.         solution.append(subset)
  71.         idx += size
  72.  
  73.     if not all(1 < len(ss) <= bound for ss in solution):
  74.         m, *_, M = sorted([len(ss) for ss in solution])
  75.         raise GenError(f"solution OOB. NOT ( (1 < {m}) OR ({M} <= 6) )")
  76.  
  77.     return solution
  78.  
  79.  
  80. def generate_decoys(n: int, universe: U, solution: S) -> S:
  81.     """Note that this can invalidate the unicity of the solution."""
  82.     return generate_decoys_uniform(n, universe, solution)
  83.     # Number of tries to inset a decoy, different from len(decoy_sets)
  84.     n_iterations = 3 * n // 2
  85.     n_iterations = n
  86.     decoy_sets = S()
  87.     for _ in range(n_iterations):
  88.         # size_decoy = random.randint(5, 10)
  89.         size_decoy = 5
  90.         decoy = set(random.sample(sorted(universe), size_decoy))
  91.         if decoy in decoy_sets:
  92.             continue
  93.  
  94.         # Naive: to guarante single solutions
  95.         if not any(decoy.issubset(s) for s in solution):
  96.             decoy_sets.append(decoy)
  97.     return decoy_sets
  98.  
  99.  
  100. def generate_decoys_uniform(n: int, universe: U, solution: S) -> S:
  101.     n_iterations = n
  102.     decoy_sets = []
  103.     element_counts = Counter(universe)
  104.  
  105.     elements = list(universe)
  106.     random.shuffle(elements)
  107.  
  108.     for _ in range(n_iterations):
  109.         size_decoy = 2
  110.         decoy = set()
  111.  
  112.         while len(decoy) < size_decoy:
  113.             min_count = min(element_counts.values(), default=0)
  114.             candidates = [e for e in elements if element_counts[e] == min_count]
  115.             chosen = random.choice(candidates)
  116.  
  117.             decoy.add(chosen)
  118.             element_counts[chosen] += 1
  119.  
  120.         # The minimum is size_decoy + 1, but in order to have unique solution...
  121.         if random.randint(0, 1) == 1:
  122.             decoy.add(random.choice(list(universe)))
  123.  
  124.         if decoy in decoy_sets:
  125.             continue
  126.  
  127.         if not any(decoy.issubset(s) for s in solution):
  128.             decoy_sets.append(decoy)
  129.  
  130.     return decoy_sets
  131.  
  132.  
  133. def try_generate_exact_cover(n: int, universe: U) -> tuple[S, S]:
  134.     solution = generate_solution(n, universe)
  135.     decoy_sets = generate_decoys(n, universe, solution)
  136.     exact_cover = solution + decoy_sets
  137.     random.shuffle(exact_cover)
  138.     return solution, exact_cover
  139.  
  140.  
  141. def generate_exact_cover(n: int, universe: U) -> tuple[S, S]:
  142.     """Generates an exact cover instance with exactly one solution."""
  143.     max_iterations = 10
  144.     for _ in range(max_iterations):
  145.         solution, exact_cover = try_generate_exact_cover(n, universe)
  146.         if len(exact_cover) > 1:
  147.             return solution, exact_cover
  148.     raise GenError(f"Reached {max_iterations =}")
  149.  
  150.  
  151. def random_str() -> str:
  152.     return "".join(random.choice(string.ascii_letters) for _ in range(3))
  153.  
  154.  
  155. def random_labels(n: int) -> list[str]:
  156.     assert n < (26 * 2) ** 3, "Too many labels requested!"
  157.     upto = 3 if n < (26 * 2) ** 2 else 4
  158.  
  159.     possible_labels = (
  160.         "".join(letters)
  161.         for size in range(1, upto)
  162.         for letters in product(string.ascii_letters, repeat=size)
  163.     )
  164.  
  165.     labels = random.sample(list(possible_labels), n)
  166.     random.shuffle(labels)
  167.  
  168.     return labels
  169.  
  170.  
  171. def diagnostics(exact_cover):
  172.     cnt = Counter()
  173.     tsize = 0
  174.     for clause in exact_cover:
  175.         tsize += len(clause)
  176.         cnt += Counter(clause)
  177.     tcnt = Counter()
  178.     for v in cnt.values():
  179.         tcnt[v] += 1
  180.     return tsize, cnt, tcnt
  181.  
  182.  
  183. Dunnit = tuple[str, list[str], str]
  184. """Testcase, solution, culprit."""
  185.  
  186.  
  187. def xcc_to_dunnit(n: int, universe: U, xcc: S, solution: S) -> Dunnit:
  188.     """The culprit is also returned for debugging."""
  189.     # Optional: sorting for visibility
  190.     xcc = sorted(sorted(cl) for cl in xcc)  # type: ignore
  191.  
  192.     n_requirements = max(max(cl) for cl in xcc)
  193.     dunnit = [set() for _ in range(n_requirements)]
  194.     solution_dunnit: list[str] = []
  195.     labels = random_labels(len(xcc))
  196.     for clause, action in zip(xcc, labels):
  197.         for requirement in clause:
  198.             dunnit[requirement - 1].add(action)
  199.         if {*clause} in solution:
  200.             solution_dunnit.append(action)
  201.  
  202.     tsize, cnt, tcnt = diagnostics(dunnit)
  203.     # We only avoid this case because OP seems to have excluded it in its solution
  204.     # but it's perfectly fine if one where to look at the statement
  205.     if min(tcnt) <= 1:
  206.         raise GenError(f"{min(tcnt)=} <= 1")
  207.     # This is to match last testcases
  208.     if max(tcnt) > 6:
  209.         raise GenError(f"{max(tcnt)=} > 6")
  210.  
  211.     # E(f"{tsize = }")
  212.     # E(cnt)
  213.     # E(tcnt)
  214.     # E(len(solution_dunnit))
  215.     # E(min(tcnt), max(tcnt))
  216.  
  217.     # Test: put everything relevant at the end
  218.     # fst, *rst = dunnit
  219.     # rst.sort(key=lambda clause: {*clause} in solution)
  220.     # dunnit = [fst] + rst
  221.  
  222.     # Test: put something small for candidates to abide the original constraints
  223.     min_sz = min(len(cl) for cl in dunnit)
  224.     for idx, cl in enumerate(dunnit):
  225.         if len(cl) == min_sz:
  226.             dunnit.insert(0, dunnit.pop(idx))
  227.             break
  228.  
  229.     out = ""
  230.     out += f"{len(dunnit)} {len(dunnit[0])}\n"
  231.     E(f"{len(dunnit)} {len(dunnit[0])}")
  232.     for clause in dunnit:
  233.         out += ", ".join(sorted(clause))
  234.         out += "\n"
  235.  
  236.     culprit = "none"
  237.     for action in solution_dunnit:
  238.         if action in dunnit[0]:
  239.             culprit = action
  240.  
  241.     return out.strip(), solution_dunnit, culprit
  242.  
  243.  
  244. SEP = "=" * 20
  245.  
  246. G = tuple[int, Dunnit, S]
  247. """Generated result."""
  248.  
  249.  
  250. def try_generate() -> G:
  251.     n = 200
  252.     seed = random.randint(0, 1 << 16)
  253.     random.seed(seed)
  254.     universe = set(range(1, n + 1))
  255.     solution, exact_cover = generate_exact_cover(n, universe)
  256.     # E(f"{n} {len(exact_cover)}")
  257.     # for s in exact_cover:
  258.     #     E(*s)
  259.     # E(SEP)
  260.     # E(solution)
  261.     # E(SEP)
  262.     dunnit = xcc_to_dunnit(n, universe, exact_cover, solution)
  263.     return seed, dunnit, exact_cover
  264.  
  265.  
  266. def generate() -> G:
  267.     max_iterations = 2000
  268.     for _ in range(max_iterations):
  269.         try:
  270.             gen = try_generate()
  271.             seed, dunnit, exact_cover = gen
  272.             dunnit, solution_dunnit, culprit = dunnit
  273.             if len(solution_dunnit) < 40:
  274.                 raise GenError
  275.  
  276.             tsize, cnt, tcnt = diagnostics(exact_cover)
  277.             E(f"{tsize = }")
  278.             E(tcnt)
  279.             E(min(tcnt), max(tcnt))
  280.             return gen
  281.         except GenError as ge:
  282.             E(ge)
  283.             pass
  284.     raise GenError(f"Max iterations {max_iterations} reached in generate")
  285.  
  286.  
  287. def main() -> None:
  288.     seed, (dunnit, solution_dunnit, culprit), _ = generate()
  289.     print(dunnit)
  290.     E(seed)
  291.     E(SEP)
  292.     E(*solution_dunnit)
  293.     E(f"{len(solution_dunnit) = }")
  294.     E(SEP)
  295.     E("culprit:", culprit)
  296.  
  297.  
  298. if __name__ == "__main__":
  299.     main()
  300.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement