File size: 7,523 Bytes
459fa23 | 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 | """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
# ------------------------------------------------- exhaustive reduction check
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
# ------------------------------------------------------------ negative controls
ctl = {}
# (a) robustness probe only: the paper's reduction is stated for acyclic input.
# Do not assume that the finite identity must fail outside that scope; record the
# result literally and do not use it as a destructive control.
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)
# (b) drop the "add all bidirected edges" step of the reduction
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]: # no bidirected edges added
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)
# (c) drop transitive closure from the target predicate (acyclicity only)
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
# --------------------------------------------------------- sampled n = 5 check
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)
# ----------------------------------------- reduction is polynomial-time (O(n^2))
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)}
# ------------- the paper's remark: deciding incomp(G) <= k is polynomial for fixed k
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")
|