from math import gcd
import random
def _coprime(a, n):
return gcd(a, n) == 1
def _find_linear_coeffs(N, max_tries=5000, rng_seed=None):
"""
Find (a1,b1,c1,a2,b2,c2) so that:
- a1,b1,a2,b2 are coprime to N
- det = a1*b2 - a2*b1 is nonzero mod N (orthogonality)
- a1±b1 and a2±b2 are coprime to N (diagonals are permutations)
c1=c2=0 is fine; we keep them 0.
"""
if rng_seed is not None:
random.seed(rng_seed)
# search space: units modulo N (coprime to N)
units = [x for x in range(1, N) if _coprime(x, N)]
for _ in range(max_tries):
a1, b1 = random.choice(units), random.choice(units)
a2, b2 = random.choice(units), random.choice(units)
if (a1 * b2 - a2 * b1) % N == 0:
continue
# diagonals: need slopes (a±b) to be units too
if not (_coprime((a1 + b1) % N or N, N) and _coprime((a1 - b1) % N or N, N)):
continue
if not (_coprime((a2 + b2) % N or N, N) and _coprime((a2 - b2) % N or N, N)):
continue
return a1, b1, 0, a2, b2, 0
raise RuntimeError(f"Could not find suitable coefficients for N={N} (try increasing max_tries).")
def build_magic_square(N, base=None, rng_seed=None):
"""
Build an N×N 'digit-balanced' magic square for odd N.
Values are B*L1 + L2 where L1,L2 are orthogonal Latin squares
with diagonal-permutation property.
Returns the matrix and the base B.
"""
if N <= 0 or N % 2 == 0:
raise ValueError("N must be a positive odd integer.")
B = max(10, N) if base is None else base
a1, b1, c1, a2, b2, c2 = _find_linear_coeffs(N, rng_seed=rng_seed)
A = [[0]*N for _ in range(N)]
for i in range(N):
for j in range(N):
l1 = (a1*i + b1*j + c1) % N
l2 = (a2*i + b2*j + c2) % N
A[i][j] = B*l1 + l2
return A, B, (a1, b1, a2, b2)
def verify_properties(A, B):
"""
Verify that all rows, columns, main & anti-diagonals have the same sum.
Also checks 'inverse' (swap tens/ones -> swap L1 and L2) keeps the same sum.
"""
N = len(A)
# row/col sums
row_sums = [sum(r) for r in A]
col_sums = [sum(A[i][j] for i in range(N)) for j in range(N)]
target = row_sums[0]
ok_rows = all(s == target for s in row_sums)
ok_cols = all(s == target for s in col_sums)
main = sum(A[i][i] for i in range(N))
anti = sum(A[i][N-1-i] for i in range(N))
ok_diags = (main == target) and (anti == target)
# inverse: swap digits (tens<->ones), i.e., A' = (A % B)*B + (A // B)
inv = [[(x % B)*B + (x // B) for x in row] for row in A]
inv_row = [sum(r) for r in inv]
ok_inverse = all(s == inv_row[0] == target for s in inv_row) and \
all(sum(inv[i][j] for i in range(N)) == target for j in range(N)) and \
sum(inv[i][i] for i in range(N)) == target and \
sum(inv[i][N-1-i] for i in range(N)) == target
return {
"target_sum": target,
"rows_ok": ok_rows,
"cols_ok": ok_cols,
"diagonals_ok": ok_diags,
"inverse_ok": ok_inverse
}
if __name__ == "__main__":
# demo
N = 5 # <- change this to any odd N (3,5,7,9,...)
A, B, coeffs = build_magic_square(N, rng_seed=42)
props = verify_properties(A, B)
print(f"N={N}, base B={B}, coefficients (a1,b1,a2,b2)={coeffs}")
print("Magic sum:", props["target_sum"])
print("Rows/Cols/Diagonals OK? ", props["rows_ok"], props["cols_ok"], props["diagonals_ok"])
print("Inverse (swap digits) also OK? ", props["inverse_ok"])
print("\nSquare:")
for row in A:
print(" ".join(f"{x:>3d}" for x in row))
ZnJvbSBtYXRoIGltcG9ydCBnY2QKaW1wb3J0IHJhbmRvbQoKZGVmIF9jb3ByaW1lKGEsIG4pOiAKICAgIHJldHVybiBnY2QoYSwgbikgPT0gMQoKZGVmIF9maW5kX2xpbmVhcl9jb2VmZnMoTiwgbWF4X3RyaWVzPTUwMDAsIHJuZ19zZWVkPU5vbmUpOgogICAgIiIiCiAgICBGaW5kIChhMSxiMSxjMSxhMixiMixjMikgc28gdGhhdDoKICAgICAgLSBhMSxiMSxhMixiMiBhcmUgY29wcmltZSB0byBOCiAgICAgIC0gZGV0ID0gYTEqYjIgLSBhMipiMSBpcyBub256ZXJvIG1vZCBOICAob3J0aG9nb25hbGl0eSkKICAgICAgLSBhMcKxYjEgYW5kIGEywrFiMiBhcmUgY29wcmltZSB0byBOICAgICAgKGRpYWdvbmFscyBhcmUgcGVybXV0YXRpb25zKQogICAgYzE9YzI9MCBpcyBmaW5lOyB3ZSBrZWVwIHRoZW0gMC4KICAgICIiIgogICAgaWYgcm5nX3NlZWQgaXMgbm90IE5vbmU6CiAgICAgICAgcmFuZG9tLnNlZWQocm5nX3NlZWQpCgogICAgIyBzZWFyY2ggc3BhY2U6IHVuaXRzIG1vZHVsbyBOIChjb3ByaW1lIHRvIE4pCiAgICB1bml0cyA9IFt4IGZvciB4IGluIHJhbmdlKDEsIE4pIGlmIF9jb3ByaW1lKHgsIE4pXQogICAgZm9yIF8gaW4gcmFuZ2UobWF4X3RyaWVzKToKICAgICAgICBhMSwgYjEgPSByYW5kb20uY2hvaWNlKHVuaXRzKSwgcmFuZG9tLmNob2ljZSh1bml0cykKICAgICAgICBhMiwgYjIgPSByYW5kb20uY2hvaWNlKHVuaXRzKSwgcmFuZG9tLmNob2ljZSh1bml0cykKICAgICAgICBpZiAoYTEgKiBiMiAtIGEyICogYjEpICUgTiA9PSAwOgogICAgICAgICAgICBjb250aW51ZQogICAgICAgICMgZGlhZ29uYWxzOiBuZWVkIHNsb3BlcyAoYcKxYikgdG8gYmUgdW5pdHMgdG9vCiAgICAgICAgaWYgbm90IChfY29wcmltZSgoYTEgKyBiMSkgJSBOIG9yIE4sIE4pIGFuZCBfY29wcmltZSgoYTEgLSBiMSkgJSBOIG9yIE4sIE4pKToKICAgICAgICAgICAgY29udGludWUKICAgICAgICBpZiBub3QgKF9jb3ByaW1lKChhMiArIGIyKSAlIE4gb3IgTiwgTikgYW5kIF9jb3ByaW1lKChhMiAtIGIyKSAlIE4gb3IgTiwgTikpOgogICAgICAgICAgICBjb250aW51ZQogICAgICAgIHJldHVybiBhMSwgYjEsIDAsIGEyLCBiMiwgMAoKICAgIHJhaXNlIFJ1bnRpbWVFcnJvcihmIkNvdWxkIG5vdCBmaW5kIHN1aXRhYmxlIGNvZWZmaWNpZW50cyBmb3IgTj17Tn0gKHRyeSBpbmNyZWFzaW5nIG1heF90cmllcykuIikKCmRlZiBidWlsZF9tYWdpY19zcXVhcmUoTiwgYmFzZT1Ob25lLCBybmdfc2VlZD1Ob25lKToKICAgICIiIgogICAgQnVpbGQgYW4gTsOXTiAnZGlnaXQtYmFsYW5jZWQnIG1hZ2ljIHNxdWFyZSBmb3Igb2RkIE4uCiAgICBWYWx1ZXMgYXJlIEIqTDEgKyBMMiB3aGVyZSBMMSxMMiBhcmUgb3J0aG9nb25hbCBMYXRpbiBzcXVhcmVzCiAgICB3aXRoIGRpYWdvbmFsLXBlcm11dGF0aW9uIHByb3BlcnR5LgogICAgUmV0dXJucyB0aGUgbWF0cml4IGFuZCB0aGUgYmFzZSBCLgogICAgIiIiCiAgICBpZiBOIDw9IDAgb3IgTiAlIDIgPT0gMDoKICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJOIG11c3QgYmUgYSBwb3NpdGl2ZSBvZGQgaW50ZWdlci4iKQogICAgQiA9IG1heCgxMCwgTikgaWYgYmFzZSBpcyBOb25lIGVsc2UgYmFzZQogICAgYTEsIGIxLCBjMSwgYTIsIGIyLCBjMiA9IF9maW5kX2xpbmVhcl9jb2VmZnMoTiwgcm5nX3NlZWQ9cm5nX3NlZWQpCgogICAgQSA9IFtbMF0qTiBmb3IgXyBpbiByYW5nZShOKV0KICAgIGZvciBpIGluIHJhbmdlKE4pOgogICAgICAgIGZvciBqIGluIHJhbmdlKE4pOgogICAgICAgICAgICBsMSA9IChhMSppICsgYjEqaiArIGMxKSAlIE4KICAgICAgICAgICAgbDIgPSAoYTIqaSArIGIyKmogKyBjMikgJSBOCiAgICAgICAgICAgIEFbaV1bal0gPSBCKmwxICsgbDIKICAgIHJldHVybiBBLCBCLCAoYTEsIGIxLCBhMiwgYjIpCgpkZWYgdmVyaWZ5X3Byb3BlcnRpZXMoQSwgQik6CiAgICAiIiIKICAgIFZlcmlmeSB0aGF0IGFsbCByb3dzLCBjb2x1bW5zLCBtYWluICYgYW50aS1kaWFnb25hbHMgaGF2ZSB0aGUgc2FtZSBzdW0uCiAgICBBbHNvIGNoZWNrcyAnaW52ZXJzZScgKHN3YXAgdGVucy9vbmVzIC0+IHN3YXAgTDEgYW5kIEwyKSBrZWVwcyB0aGUgc2FtZSBzdW0uCiAgICAiIiIKICAgIE4gPSBsZW4oQSkKICAgICMgcm93L2NvbCBzdW1zCiAgICByb3dfc3VtcyA9IFtzdW0ocikgZm9yIHIgaW4gQV0KICAgIGNvbF9zdW1zID0gW3N1bShBW2ldW2pdIGZvciBpIGluIHJhbmdlKE4pKSBmb3IgaiBpbiByYW5nZShOKV0KICAgIHRhcmdldCA9IHJvd19zdW1zWzBdCiAgICBva19yb3dzID0gYWxsKHMgPT0gdGFyZ2V0IGZvciBzIGluIHJvd19zdW1zKQogICAgb2tfY29scyA9IGFsbChzID09IHRhcmdldCBmb3IgcyBpbiBjb2xfc3VtcykKICAgIG1haW4gPSBzdW0oQVtpXVtpXSBmb3IgaSBpbiByYW5nZShOKSkKICAgIGFudGkgPSBzdW0oQVtpXVtOLTEtaV0gZm9yIGkgaW4gcmFuZ2UoTikpCiAgICBva19kaWFncyA9IChtYWluID09IHRhcmdldCkgYW5kIChhbnRpID09IHRhcmdldCkKCiAgICAjIGludmVyc2U6IHN3YXAgZGlnaXRzICh0ZW5zPC0+b25lcyksIGkuZS4sIEEnID0gKEEgJSBCKSpCICsgKEEgLy8gQikKICAgIGludiA9IFtbKHggJSBCKSpCICsgKHggLy8gQikgZm9yIHggaW4gcm93XSBmb3Igcm93IGluIEFdCiAgICBpbnZfcm93ID0gW3N1bShyKSBmb3IgciBpbiBpbnZdCiAgICBva19pbnZlcnNlID0gYWxsKHMgPT0gaW52X3Jvd1swXSA9PSB0YXJnZXQgZm9yIHMgaW4gaW52X3JvdykgYW5kIFwKICAgICAgICAgICAgICAgICBhbGwoc3VtKGludltpXVtqXSBmb3IgaSBpbiByYW5nZShOKSkgPT0gdGFyZ2V0IGZvciBqIGluIHJhbmdlKE4pKSBhbmQgXAogICAgICAgICAgICAgICAgIHN1bShpbnZbaV1baV0gZm9yIGkgaW4gcmFuZ2UoTikpID09IHRhcmdldCBhbmQgXAogICAgICAgICAgICAgICAgIHN1bShpbnZbaV1bTi0xLWldIGZvciBpIGluIHJhbmdlKE4pKSA9PSB0YXJnZXQKCiAgICByZXR1cm4gewogICAgICAgICJ0YXJnZXRfc3VtIjogdGFyZ2V0LAogICAgICAgICJyb3dzX29rIjogb2tfcm93cywKICAgICAgICAiY29sc19vayI6IG9rX2NvbHMsCiAgICAgICAgImRpYWdvbmFsc19vayI6IG9rX2RpYWdzLAogICAgICAgICJpbnZlcnNlX29rIjogb2tfaW52ZXJzZQogICAgfQoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgICMgZGVtbwogICAgTiA9IDUgICAgICAgICAgICAgICAgICAgICAgIyA8LSBjaGFuZ2UgdGhpcyB0byBhbnkgb2RkIE4gKDMsNSw3LDksLi4uKQogICAgQSwgQiwgY29lZmZzID0gYnVpbGRfbWFnaWNfc3F1YXJlKE4sIHJuZ19zZWVkPTQyKQogICAgcHJvcHMgPSB2ZXJpZnlfcHJvcGVydGllcyhBLCBCKQoKICAgIHByaW50KGYiTj17Tn0sIGJhc2UgQj17Qn0sIGNvZWZmaWNpZW50cyAoYTEsYjEsYTIsYjIpPXtjb2VmZnN9IikKICAgIHByaW50KCJNYWdpYyBzdW06IiwgcHJvcHNbInRhcmdldF9zdW0iXSkKICAgIHByaW50KCJSb3dzL0NvbHMvRGlhZ29uYWxzIE9LPyAiLCBwcm9wc1sicm93c19vayJdLCBwcm9wc1siY29sc19vayJdLCBwcm9wc1siZGlhZ29uYWxzX29rIl0pCiAgICBwcmludCgiSW52ZXJzZSAoc3dhcCBkaWdpdHMpIGFsc28gT0s/ICIsIHByb3BzWyJpbnZlcnNlX29rIl0pCiAgICBwcmludCgiXG5TcXVhcmU6IikKICAgIGZvciByb3cgaW4gQToKICAgICAgICBwcmludCgiICIuam9pbihmInt4Oj4zZH0iIGZvciB4IGluIHJvdykp