KenKen Solver

From the SETL Wiki

Jump to: navigation, search

There is now an online demo of this program.

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