Equality by Exact Cover

Table of Contents

Using an exact-cover solver like DLX from Python , how can we solve simple equations like equals_by_exact_cover_dd43a860e400f45655c32e305ea48ca17fece4fa.svg where equals_by_exact_cover_e53ced4d13ef6b33296c81aa902b3e4dbc7621a0.svg ?

Covering Bits

We'll restrict ourselves to equals_by_exact_cover_a50177f38f8704755bf7e445f3f261535c035a0d.svg so equals_by_exact_cover_ea1aa6a693761df2411ef862e1fa848d5d00915a.svg come from the set equals_by_exact_cover_b6d93a12a78b1c2aed9757fb560b48dde87258bd.svg . Written in binary it contains all 3-bit numbers: equals_by_exact_cover_229ad13e9318f3b39845279ddd9fa8f4fde8d3f2.svg .

If we create one column per bit, we can represent the assignment equals_by_exact_cover_6b0b591bbc887a1d5bcd9edda02a3fadb8d8cf3e.svg as equals_by_exact_cover_2526af77b9c024d6fcf20aebbd223410b7998a13.svg since only the least significant bit is set. The columns equals_by_exact_cover_21bc3fd8307a2de4b1ec4c7f106989eb5e1b35ba.svg still need to be covered.

Now we need to encode equals_by_exact_cover_6b541d93a8914b48580a59c286bfc04b8cf2bc24.svg in a way that covers these missing columns. The bitwise inverse is equals_by_exact_cover_cd25e6f30ee5d42a136653270557e88ed3e96c10.svg , covering exactly equals_by_exact_cover_21bc3fd8307a2de4b1ec4c7f106989eb5e1b35ba.svg .

Handling Variable Names

So far, both equals_by_exact_cover_6b0b591bbc887a1d5bcd9edda02a3fadb8d8cf3e.svg and equals_by_exact_cover_3a3849bb3d4292e507b1fa364a09b0ef2abcd0b9.svg would be encoded as a row equals_by_exact_cover_2526af77b9c024d6fcf20aebbd223410b7998a13.svg with no way to recover the variable name from the output of DLX.

Similarly equals_by_exact_cover_1ae72d2c97604f8550a95adc8c0303b49235640b.svg would be encoded as a row equals_by_exact_cover_038d095bcf4e01566f38b4375ce47f72231a143f.svg already covering all columns, with no need to include equals_by_exact_cover_96d4c69c6a1fc83a2ba315a970276a7bd222cbca.svg encoded as the empty row equals_by_exact_cover_73b8e6cec3827bc13e1e1e726bc21c1a5aafdcb9.svg into the solution.

To avoid these issues, we can add a column equals_by_exact_cover_f25b2c497e7cfffa4c002a59939e09e21af1bc1e.svg covered by all assignments to equals_by_exact_cover_7c41eff3e5dffe9e398dbb4ed0123ec7f2fcee4e.svg and a column equals_by_exact_cover_a487dd9d4c9d73644deea7a4d87526094c046ea5.svg covered by all assignments to equals_by_exact_cover_37517dc1e685aebfb9cc4ce54992a2aaf8b9e5df.svg . Now equals_by_exact_cover_6b0b591bbc887a1d5bcd9edda02a3fadb8d8cf3e.svg is encoded as equals_by_exact_cover_24b41d7791aed0a28dd5cb834496e334674336a5.svg and equals_by_exact_cover_3a3849bb3d4292e507b1fa364a09b0ef2abcd0b9.svg is encoded as equals_by_exact_cover_ddb2b3d440e7ac0cd20ff9e0926cb1e1a4525149.svg .

As a quick check we can set up the rows for equals_by_exact_cover_6b0b591bbc887a1d5bcd9edda02a3fadb8d8cf3e.svg and equals_by_exact_cover_a18d52792edc2bda908caa23636d126728951712.svg by hand and verify that the solver finds one solution.

<<init_problem>>

s = Solver()
p = Problem({"va", "vb", "c0", "c1", "c2"})
p.add_row(["va", "c0"])
p.add_row(["vb", "c1", "c2"])

print(s.solve(p))
{'1:': [{'vb', 'c2', 'c1'}, {'c0', 'va'}]}

Encoding and Decoding

For larger sets of input numbers, we'll want to automate the encoding and decoding of rows.

Inverting a k-bit binary number is equivalent to subtracting it from equals_by_exact_cover_94bb8a0ecddc4c48f8781e290e6acb77a9c52c1f.svg , a number where the lowest equals_by_exact_cover_58114053a2cae6a1fdb2aadd41d1c92936a9c72e.svg bits a all 1.

For example equals_by_exact_cover_50f996f0d797c8097868969cb029baa7afa8d66a.svg , so the inverse of equals_by_exact_cover_f28e2a3d895c38b238b68eb2bc2e9429cac83ca0.svg is equals_by_exact_cover_6e3876679f4d841d9faba110dc1320d6fc6ae055.svg .

def invert(val: int, n_bits: int) -> int:
    full = (2**n_bits) - 1
    return full - val

To encode an assignment of var = val , we invert val if var is "b", then read bits one by one to determine which columns are covered.

def encode_row(var: str, val: int, n_bits: int):
    if var == "b":
        val = invert(val, n_bits)

    res = {f"v{var}"}
    for i in range(n_bits):
        if val & 1 == 1:
            res.add(f"c{i}")
        val //= 2
    return res

When decoding, we add the powers of two corresponding to the covered columns and invert again val if var is "b".

def decode_row(row: set[str], n_bits: int) -> tuple[str, int]:
    var = None
    val = 0

    for col in row:
        if col[0] == "v":
            var = col[1:]
        elif col[0] == "c":
            val += 2**int(col[1:])

    assert var in ("a", "b")
    if var == "b":
        val = invert(val, n_bits)
    return (var, val)

To decode a solution, we decode rows one by one and collect the values assigned to each variable (here just "a" and "b").

def decode_solution(solution: Solution, n_bits: int):
    res = {}
    for row in solution:
        var, val = decode_row(row, n_bits)
        if var in res:
            raise KeyError("Duplicate assignment")
        res[var] = val
    return res

Finally we can connect everything and verify that the solutions to equals_by_exact_cover_a756f6b354b3278fb7efb1e0e2239b57ff7626bf.svg are in fact equals_by_exact_cover_d3ac3e52ad80a4b30447bedce0a9fe59687dda58.svg , equals_by_exact_cover_e42ab7c371357ed393e52cf55c395ae64728c068.svg , …, equals_by_exact_cover_707751ca2c19a391b38518656f04aba84b8bf3c3.svg .

<<init_problem>>
<<invert>>
<<encode>>
<<decode>>
<<decode-solution>>

s = Solver()
p = Problem({"va", "vb", "c0", "c1", "c2"})

n_bits = 3
for i in range(2**n_bits):
    p.add_row(encode_row("a", i, n_bits))
    p.add_row(encode_row("b", i, n_bits))

solutions = s.solve(p)

for solution in solutions.values():
    print(decode_solution(solution, n_bits))
{'b': 3, 'a': 3}
{'a': 6, 'b': 6}
{'b': 0, 'a': 0}
{'a': 7, 'b': 7}
{'b': 1, 'a': 1}
{'a': 4, 'b': 4}
{'b': 2, 'a': 2}
{'a': 5, 'b': 5}

If you have an idea how this page could be improved or a comment send me a mail.