KenKen Solver

From the SETL Wiki

Jump to: navigation, search

There is now an online demo of this program. Unfortunately this has been taken down as the virtual machine that hosts this site did not have enough CPU capacity.

program kenken;

const
  operators := {'+', '-', '*', '/', '?', '='};

$ Identities (0 for additive operators, 1 for multiplicative operators)
const
  opIdent := {['+', 0], ['-', 0], ['*', 1], ['/', 1], ['?', 0], ['?', 1]};

var size;
var digitsUsed;
var cellXY;
var cellsByX, cellsByXY, cellsByY;
var cellRange: back;
var cages;
var cagesByCell;
var changedCells: back;
var changedRows: back;
var changedCols: back;
var changedCages: back;
var cagesBySize;
var paraPerms: back;

cageFile := command_line(1);
h := open(cageFile, 'TEXT-IN');
reada(cageFile, size);
loop
init
  cellXY := {};
  cages := [];
  cagesByCell := {};
  cagesBySize := {};
doing
  reada(cageFile, cage);
while
  cage /= om
do
  [value, oper, cgCoords] := cage;
  cgAtoms := {};
  (for xy in cgCoords)
    cell := newat;
    cellXY(cell) := xy;
    cgAtoms with:= cell;
    cagesByCell with:= [cell, 1 + #cages];
  end;
  cages with:= [value, oper, cgAtoms];
  cagesBySize with:= [#cgCoords, #cages];
end;
close(cageFile);

changedCages := {};
changedRows := {};
changedCols := {};
paraPerms := {};

nrSolutions := 0;
setupBoard(size);
(while ok)
  solve();
  showBoard();
  nrSolutions +:= 1;
  fail;
end;
print(nrSolutions, "solutions found");

proc solve();
  (until is_smap cellRange and
         {changedCells, changedCols, changedRows, changedCages} = {{}})
    refine();
  end;
end proc;

proc showBoard();
  (for y in [1..size])
    (for x in [1..size])
      digits := {digit: cell in cellsByXY{[x,y]}, digit in cellRange{cell}};
      nprint(' ' + arb digits);
    end;
    print();
  end;
  print();
end proc;

proc showBoardLong();
  (for y in [1..size])
    row :=
      ['' +/ [d in [1..size] |
              d in cellRange{cellsByXY([x, y])}]:
       x in [1..size]];
    (for yy in [1..3])
      (for x in [1..size])
        cell := row(x);
        nprint(rpad('' +/ cell(1..5), 6));
        cell(1..5) := '';
        row(x) := cell;
      end;
      print();
    end;
  end;
  print();
end proc;

proc isolateRCPerms(cells, rcIdx, oAxis);
  ranges := {cellRange{cell}: cell in cells};
  (for r in ranges | #r < size)
    cellsInPerm := {cell in cells | cellRange{cell} subset r};
    if #cellsInPerm >= #r then
      (for cell in cells | not cellRange{cell} subset r)
        restrict(cell, digitsUsed - r);
      end;
    end if;
  end;
  rngInv := {[d, cell]: cell in cells, d in cellRange{cell}};
  rngDII := {[cells, d]: cells = rngInv{d}};
  (for digits = rngDII{cells})
    if #cells <= #digits and #digits < #digitsUsed then
      (for cell in cells)
        restrict(cell, digits);
      end;
      if #cells > 1 then
        oCoords := {cellXY(cell)(oAxis): cell in cells};
        paraPerms +:= {[[oAxis, oCoords, d], rcIdx]: d in digits};
      end if;
    end if;
  end;
end proc;

proc ppBand(axis, oi);
  case axis of
    (1):
      return [cellsByXY([oi, i]): i in [1..size]];
    (2):
      return [cellsByXY([i, oi]): i in [1..size]];
  end case;
end proc;

proc applyParaPerms(rw changeFlag);
  changeFlag ?:= false;
  (for ppk in domain paraPerms)
    ppi := paraPerms{[axis, oCoords, digit] := ppk};
    if #ppi >= #oCoords then
      changeFlag := true;
      (for oi in oCoords)
        band := ppBand(axis, oi);
        (for cell = band(i) | i notin ppi)
          restrict(cell, digitsUsed less digit);
        end;
      end;
      paraPerms lessf:= ppk;
    end;
  end;
  (for [ppk, aCoord] in paraPerms)
    [axis, oCoords, digit] := ppk;
    if exists oi in oCoords |
              #cellRange{ppBand(axis, oi)(aCoord)} = 1 then
      paraPerms lessf:= ppk;
    end if;
  end;
end proc;

proc refine();
  changedCages +:= {} +/ {cagesByCell{c}: c in changedCells};
  changedRows +:= {cellXY(c)(2): c in changedCells};
  changedCols +:= {cellXY(c)(1): c in changedCells};
  changedCells := {};

  if exists y in changedRows | true then
    isolateRCPerms(cellsByY{y}, y, 1);
    changedRows less:= y;
  elseif exists x in changedCols | true then
    isolateRCPerms(cellsByX{x}, x, 2);
    changedCols less:= x;
  elseif exists gs in [1..size*size],
                g in cagesBySize{gs} |
                g in changedCages then
    applyCageConstraint(g);
    changedCages less:= g;
  else
    applyParaPerms(ppUsed);

    if not is_smap cellRange then
      $ No luck narrowing the cell range using the basic rules?
      $ We must resort to a brute-force search.
      if not ppUsed then
        $ Choose an arbitrary cell, preferring cells with fewer possible values
        crsm := {[#cellRange{cell}, cell]: cell in domain cellRange};
        cell := arb crsm{min/ ((domain crsm) less 1)};

        $ Arbitrarily choose a digit for this cell
        if exists digit in cellRange{cell} | ok then
          restrict(cell, {digit});
        else
          fail;
        end if;
      end if;
    end if;
  end if;
end proc;

proc setupBoard(size);
  cellsByX := {};
  cellsByY := {};
  cellsByXY := {};
  cellRange := {};
  changedCells := {};

  digitsUsed := {1..size};
  (for x in digitsUsed, y in digitsUsed)
    if [x,y] notin range cellXY then
      cell := newat;
      cellXY(cell) := [x, y];
    end if;
  end;

  (for [cell, [x, y]] in cellXY)
    cellsByXY([x, y]) := cell;
    cellsByX{x} with:= cell;
    cellsByY{y} with:= cell;
    cellRange{cell} := digitsUsed;
    changedCells with:= cell;
  end;
end proc;

proc applyCageConstraint(cageNr);
  [value, opSym, cells] := cages(cageNr);
  case opSym of
    ('+', '-', '*', '/'):
      restrictAll(cells, findCombs(opSym, opIdent{opSym}, value, cells, {}));
    ('?'):
      a := operators - {'?', '='};
      if #cells /= 2 then
        a -:= {'-', '/'};
      end if;
      combs :=
        {} +/ {findCombs(opSym, opIdent{opSym}, value, cells, {}): opSym in a};
      restrictAll(cells, combs);
    ('='):
      (for cell in cells)
        restrict(cell, {value});
      end;
  end case;
end proc;

proc restrictAll(cells, cvms);
  cvm := {} +/ cvms;
  (for cell in cells)
    restrict(cell, cvm{cell});
  end;
end proc;

proc restrict(cell, ub);
  oldRange := cellRange{cell};
  newRange := oldRange * ub;
  cellRange{cell} := newRange;

  if newRange /= oldRange then
    changedCells with:= cell;
    result := true;
  end if; 
  if #newRange = 0 then
    fail;
  end if;
end proc;

proc adjust(sum, opSym, trm);
  result := {};
  case opSym of
    ('+'):
      return {x in {sum - trm} | x >= 0};
    ('-'):
      return {x in {trm + sum, trm - sum} | x >= 0};
    ('*'):
      return {x in {sum div trm} | sum mod trm = 0};
    ('/'):
      return {x in {trm div sum} | trm mod sum = 0} with (trm * sum);
  end case;
end proc;

proc findCombs(opSym, identities, value, cells, part);
  if #cells > 0 then
    result := {};
    nextCell := arb cells;
    (for ncv in cellRange{nextCell},
         newVal in adjust(value, opSym, ncv))
      result +:=
        {c in findCombs(opSym,
                        identities,
                        newVal,
                        cells less nextCell,
                        part with [nextCell, ncv]) |
         conflictFree(c) and
         sufficient(opSym, newVal, cells less nextCell)};
    end;
  elseif value in identities and conflictFree(part) then
    result := {part};
  else
    result := {};
  end if;
  $print('findCombs returning', result, 'for', opSym, identities, value, cells, part);
  return result;
end proc;

proc sufficient(opSym, value, cells);
  case opSym of
    ('+'):
      result := 0 +/ [0 max/ cellRange{cell}: cell in cells] >= value;
    ('*'):
      result := 1 */ [1 max/ cellRange{cell}: cell in cells] >= value;
    else
      result := true;
  end;
  $print('sufficient returning', result, 'for', opSym, value, cells);
  return result;
end;

proc conflictFree(cvm);
  result := is_smap({[[d, cellXY(c)(1)], cellXY(c)(2)]: [c, d] in cvm}) and
            is_smap({[[d, cellXY(c)(2)], cellXY(c)(1)]: [c, d] in cvm});
  $print('conflictFree returning', result, 'for', cvm);
  return result;
end proc;

end program;

Input format:

The input is a text file. The first line contains the grid size/largest digit used. Each subsequent line contains a cage constraint in the following format:

[ value operator {[x1 y1] [x2 y2] ...}]

Where value is the sum/product etc. of the digits in the cage and (x1,y1), (x2,y2) are the coordinates of cells in the cage and operator is the operator to apply (+, -, * or / for known operators, ? to represent an unknown operator or = if the value is simply the digit that goes in the cell.)

Example:

6
[ 11 '+' {[1 1] [1 2]}]
[  2 '/' {[2 1] [3 1]}]
[ 20 '*' {[4 1] [4 2]}]
[  6 '*' {[5 1] [6 1] [6 2] [6 3]}]
[  3 '-' {[2 2] [3 2]}]
[  3 '/' {[5 2] [5 3]}]
[240 '*' {[1 3] [2 3] [1 4] [2 4]}]
[  6 '*' {[3 3] [4 3]}]
[  6 '*' {[3 4] [3 5]}]
[  7 '+' {[4 4] [4 5] [5 5]}]
[ 30 '*' {[5 4] [6 4]}]
[  6 '*' {[1 5] [2 5]}]
[  9 '+' {[6 5] [6 6]}]
[  8 '+' {[1 6] [2 6] [3 6]}]
[  2 '/' {[4 6] [5 6]}] 

The above file represents the puzzle on the Wikipedia page:

If it is saved as "puzzle1.txt" then the program would be invoked with:

setl kenken.stl -- puzzle1.txt

Notes:

  • Uses backtracking. No currently-available stable SETL implementation supports backtracking.
  • Tested under fcsetl, which is pre-alpha but has just enough functionality to allow it to run this program.

[edit] Other KenKen-solving programs:

Personal tools