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
| import numpy as np from scipy.sparse import lil_matrix from scipy.optimize import Bounds, LinearConstraint, milp
def solve_with_scipy_milp(guesses, counts, allow_leading_zero=True, options=None): """ guesses: list of equal-length digit-strings, e.g. ["90342","70794",...] counts: list of integers (match counts for each guess) allow_leading_zero: if False, forbid digit 0 at position 0 options: dict passed to milp(..., options=options) Returns: (solution_string or None, milp_result) """ G = len(guesses) N = len(guesses[0]) assert all(len(g) == N for g in guesses) nvars = N * 10
c = np.zeros(nvars)
integrality = np.ones(nvars, dtype=int)
lb = np.zeros(nvars, dtype=float) ub = np.ones(nvars, dtype=float) if not allow_leading_zero: ub[0] = 0.0
bounds = Bounds(lb, ub)
m = N + G A = lil_matrix((m, nvars), dtype=int) bl = np.zeros(m, dtype=float) bu = np.zeros(m, dtype=float)
for i in range(N): row = i for d in range(10): idx = i*10 + d A[row, idx] = 1 bl[row] = bu[row] = 1.0
for gi, g in enumerate(guesses): row = N + gi cnt = counts[gi] bl[row] = bu[row] = float(cnt) for i, ch in enumerate(g): d = ord(ch) - ord('0') idx = i*10 + d A[row, idx] += 1
A = A.tocsc() lincon = LinearConstraint(A, bl, bu)
res = milp(c, integrality=integrality, bounds=bounds, constraints=[lincon], options=options) if not res.success: return None, res
x = res.x sol_digits = [] for i in range(N): chosen = -1 for d in range(10): idx = i*10 + d if x[idx] > 0.5: chosen = d break if chosen == -1: return None, res sol_digits.append(str(chosen)) solution = ''.join(sol_digits) return solution, res
def check_uniqueness(guesses, counts, solution, options=None): """ Add a constraint forbidding 'solution' and re-solve. If infeasible, original solution was unique. """ N = len(solution) nvars = N * 10 c = np.zeros(nvars) integrality = np.ones(nvars, dtype=int) lb = np.zeros(nvars, dtype=float) ub = np.ones(nvars, dtype=float) bounds = Bounds(lb, ub)
m = N + len(guesses) A = lil_matrix((m+1, nvars), dtype=int) bl = np.zeros(m+1, dtype=float) bu = np.zeros(m+1, dtype=float)
for i in range(N): row = i for d in range(10): idx = i*10 + d A[row, idx] = 1 bl[row] = bu[row] = 1.0
for gi, g in enumerate(guesses): row = N + gi cnt = counts[gi] bl[row] = bu[row] = float(cnt) for i, ch in enumerate(g): d = ord(ch) - ord('0') idx = i*10 + d A[row, idx] += 1
forbid_row = m for i, ch in enumerate(solution): d = ord(ch) - ord('0') idx = i*10 + d A[forbid_row, idx] = 1 bl[forbid_row] = -np.inf bu[forbid_row] = N - 1
A = A.tocsc() lincon = LinearConstraint(A, bl, bu) res2 = milp(c, integrality=integrality, bounds=bounds, constraints=[lincon], options=options) return res2
if __name__ == "__main__": guesses = ["90342","70794","39458","34109","51545","12531"] counts = [2,0,2,1,2,1] sol, r = solve_with_scipy_milp(guesses, counts, allow_leading_zero=True, options={"presolve": True, "disp": False}) print("solution:", sol) if sol is not None: r2 = check_uniqueness(guesses, counts, sol, options={"presolve": True}) if r2.status == 2: print("Unique solution confirmed (no other solution).") else: print("Another solution exists or solver returned feasible alternative; check r2.status:", r2.status)
|