| """Claim 5 -- Section 3: computing incomp(G) exactly is NP-hard, because |
| "computing incomp(G) for an acyclic directed graph after adding all possible |
| bidirected edges is equivalent to solving the ACYCLIC TRANSITIVITY EDITING |
| problem" (hardness of the latter: Weller et al., 2012). |
| |
| What is reproducible here is the REDUCTION, i.e. the identity |
| |
| incomp( D + all bidirected edges ) == ATE(D) for every acyclic D, |
| |
| together with the fact that the map D -> D + all bidirected edges is computable |
| in O(n^2). The external hardness premise (Weller et al. 2012) is ASSUMED, not |
| reproduced -- see Limitations. |
| """ |
| import json, itertools, pickle, random, collections |
| import gcore |
|
|
| R = {} |
|
|
|
|
| def all_digraphs(n): |
| enc = gcore.Enc(n) |
| return enc, range(1 << enc.nd) |
|
|
|
|
| def ate_table(n): |
| """Exact ACYCLIC TRANSITIVITY EDITING optimum for every digraph on n vertices, |
| by multi-source BFS from all acyclic + transitively closed digraphs.""" |
| enc = gcore.Enc(n) |
| N = 1 << enc.nd |
| dist = bytearray([255]) * N |
| frontier = [] |
| for d in range(N): |
| succ = gcore.adj_from_dbits(n, enc.dir_pairs, d) |
| if gcore.is_acyclic(n, succ) and gcore.is_transitively_closed(n, succ): |
| dist[d] = 0 |
| frontier.append(d) |
| k = 0 |
| while frontier: |
| nxt = [] |
| for g in frontier: |
| for b in range(enc.nd): |
| h = g ^ (1 << b) |
| if dist[h] == 255: |
| dist[h] = k + 1 |
| nxt.append(h) |
| frontier = nxt |
| k += 1 |
| return enc, dist, frontier |
|
|
|
|
| |
| red = {} |
| for n in (3, 4): |
| enc = gcore.Enc(n) |
| ate_enc, ate, _ = ate_table(n) |
| dist, compat = gcore.exact_incomp_table(enc) |
| allbid = 0 |
| for e in enc.bid_pairs: |
| allbid |= 1 << enc.BID[e] |
| dags = [] |
| mism = 0 |
| hist_i, hist_a = collections.Counter(), collections.Counter() |
| for d in range(1 << enc.nd): |
| succ = gcore.adj_from_dbits(n, enc.dir_pairs, d) |
| if not gcore.is_acyclic(n, succ): |
| continue |
| dags.append(d) |
| i_val = dist[d | allbid] |
| a_val = ate[d] |
| hist_i[i_val] += 1; hist_a[a_val] += 1 |
| if i_val != a_val: |
| mism += 1 |
| red[n] = dict(n_dags=len(dags), mismatches=mism, |
| incomp_histogram=dict(sorted(hist_i.items())), |
| ate_histogram=dict(sorted(hist_a.items())), |
| max_value=max(hist_i)) |
| print("n=%d" % n, red[n], flush=True) |
| R["reduction_exhaustive"] = red |
|
|
| |
| ctl = {} |
|
|
| |
| |
| |
| for n in (3, 4): |
| enc = gcore.Enc(n) |
| _, ate, _ = ate_table(n) |
| dist, _ = gcore.exact_incomp_table(enc) |
| allbid = 0 |
| for e in enc.bid_pairs: |
| allbid |= 1 << enc.BID[e] |
| tot = mism = 0 |
| for d in range(1 << enc.nd): |
| succ = gcore.adj_from_dbits(n, enc.dir_pairs, d) |
| if gcore.is_acyclic(n, succ): |
| continue |
| tot += 1 |
| if dist[d | allbid] != ate[d]: |
| mism += 1 |
| ctl[f"cyclic inputs n={n}"] = dict(cyclic_digraphs=tot, mismatches=mism, |
| mismatch_rate=mism / tot if tot else 0) |
| print("control (cyclic inputs) n=%d" % n, ctl[f"cyclic inputs n={n}"], flush=True) |
|
|
| |
| for n in (3, 4): |
| enc = gcore.Enc(n) |
| _, ate, _ = ate_table(n) |
| dist, _ = gcore.exact_incomp_table(enc) |
| tot = mism = 0 |
| for d in range(1 << enc.nd): |
| succ = gcore.adj_from_dbits(n, enc.dir_pairs, d) |
| if not gcore.is_acyclic(n, succ): |
| continue |
| tot += 1 |
| if dist[d] != ate[d]: |
| mism += 1 |
| ctl[f"no bidirected edges added n={n}"] = dict(dags=tot, mismatches=mism, |
| mismatch_rate=mism / tot) |
| print("control (skip 'add all bidirected') n=%d" % n, |
| ctl[f"no bidirected edges added n={n}"], flush=True) |
|
|
| |
| for n in (4,): |
| enc = gcore.Enc(n) |
| N = 1 << enc.nd |
| dist_ac = bytearray([255]) * N |
| fr = [] |
| for d in range(N): |
| if gcore.is_acyclic(n, gcore.adj_from_dbits(n, enc.dir_pairs, d)): |
| dist_ac[d] = 0; fr.append(d) |
| k = 0 |
| while fr: |
| nxt = [] |
| for g in fr: |
| for b in range(enc.nd): |
| h = g ^ (1 << b) |
| if dist_ac[h] == 255: |
| dist_ac[h] = k + 1; nxt.append(h) |
| fr = nxt; k += 1 |
| _, ate, _ = ate_table(n) |
| tot = mism = 0 |
| for d in range(N): |
| if not gcore.is_acyclic(n, gcore.adj_from_dbits(n, enc.dir_pairs, d)): |
| continue |
| tot += 1 |
| if dist_ac[d] != ate[d]: |
| mism += 1 |
| ctl[f"acyclicity-only target n={n}"] = dict(dags=tot, mismatches=mism, |
| mismatch_rate=mism / tot) |
| print("control (acyclicity-only target) n=4", ctl["acyclicity-only target n=4"], flush=True) |
| R["controls"] = ctl |
|
|
| |
| enc5 = gcore.Enc(5) |
| rng = random.Random(5) |
| allbid5 = 0 |
| for e in enc5.bid_pairs: |
| allbid5 |= 1 << enc5.BID[e] |
|
|
|
|
| def ate_bounded(n, d, ub=6): |
| enc = gcore.Enc(n) |
| succ = gcore.adj_from_dbits(n, enc.dir_pairs, d) |
| if gcore.is_acyclic(n, succ) and gcore.is_transitively_closed(n, succ): |
| return 0 |
| for k in range(1, ub + 1): |
| for combo in itertools.combinations(range(enc.nd), k): |
| h = d |
| for b in combo: |
| h ^= 1 << b |
| s2 = gcore.adj_from_dbits(n, enc.dir_pairs, h) |
| if gcore.is_acyclic(n, s2) and gcore.is_transitively_closed(n, s2): |
| return k |
| return None |
|
|
|
|
| n5, mism5, vals5 = 0, 0, [] |
| tries = 0 |
| while n5 < 120 and tries < 6000: |
| tries += 1 |
| d = rng.getrandbits(enc5.nd) |
| succ = gcore.adj_from_dbits(5, enc5.dir_pairs, d) |
| if not gcore.is_acyclic(5, succ): |
| continue |
| a = ate_bounded(5, d, 4) |
| if a is None: |
| continue |
| i = gcore.bounded_incomp(enc5, d | allbid5, 4) |
| n5 += 1 |
| vals5.append(a) |
| if a != i: |
| mism5 += 1 |
| R["sampled_n5"] = dict(dags_tested=n5, mismatches=mism5, |
| ate_histogram=dict(sorted(collections.Counter(vals5).items()))) |
| print("sampled n=5:", R["sampled_n5"], flush=True) |
|
|
| |
| R["reduction_cost"] = {str(n): dict(bidirected_edges_added=n * (n - 1) // 2, |
| total_edges=n * (n - 1) + n * (n - 1) // 2) |
| for n in (3, 4, 5, 10, 50)} |
|
|
| |
| R["ball_search_sizes"] = {} |
| for n in (5, 10, 20, 50): |
| bits = n * (n - 1) + n * (n - 1) // 2 |
| R["ball_search_sizes"][str(n)] = {f"k={k}": int( |
| __import__("math").comb(bits, k)) for k in (1, 2, 3)} |
| print("ball sizes (poly in n for fixed k):", R["ball_search_sizes"]["10"], flush=True) |
|
|
| json.dump(R, open("outputs/claim5.json", "w"), indent=1, default=str) |
| print("\nwrote outputs/claim5.json") |
|
|