Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize solving efficiency. #94

Open
wants to merge 54 commits into
base: dev
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
a532dee
Logical descriptions
dclamage Sep 26, 2021
4d2b8e8
Improved AIC behavior.
dclamage Sep 29, 2021
7709d21
Generic "N"-Wings (XYZ-Wings, WXYZ-Wings, etc)
dclamage Sep 29, 2021
837f644
Fixed labeling of constraint logic
dclamage Sep 29, 2021
a3ddfee
Fix for constraints adding empty lines to logical steps
dclamage Sep 29, 2021
dfe336b
CountSolutions behaves the same single and multithreaded.
dclamage Sep 30, 2021
f81de7d
Added direct cell forcing chains.
dclamage Oct 8, 2021
d1159fe
Improved Little Killer summation logic by splitting it into groups wh…
dclamage Oct 8, 2021
dd5d2ac
Improved and combined killer/little killer logic, including a generic…
dclamage Oct 15, 2021
5b0894d
Finned fishes, improved unorthodox tuples.
dclamage Oct 18, 2021
cee52ef
Various fixes in Arrow logic where grid > 9*9
madformuse Oct 20, 2021
74212bb
Add unit tests for ArrowSumConstraint
madformuse Oct 20, 2021
b2eaca8
Correct 9*9 assumptions in InitCandidates
madformuse Oct 20, 2021
c226605
Stop abusing Strings when calculating digits in number
madformuse Oct 21, 2021
96e8fcc
Improve performance and useability of Int32 subsections
madformuse Oct 22, 2021
13cff30
Region Sum Lines Constraint support
dclamage Oct 25, 2021
2d190ff
Added indexer constraints (159)
dclamage Nov 6, 2021
ca5099f
Github actions from master
dclamage Nov 6, 2021
371e3e1
Updated version to 0.4.0
dclamage Nov 6, 2021
518dee0
Considering addtional constraints from comand line in f-puzzles integ…
mutualtape Nov 7, 2021
050d8ec
Updated to .NET 6.0 with C#10
dclamage Nov 9, 2021
2c98111
Added two missing constraints.
dclamage Nov 9, 2021
c30ecb0
Cell and Region Forcing Chains
dclamage Nov 10, 2021
3ae5459
Added weak links to indexer, killer, and little killer constraints.
dclamage Nov 11, 2021
a791965
Weak link evaluation for many more constraints.
dclamage Nov 11, 2021
6681cb0
Pointing within constraints that must contain a value.
dclamage Nov 11, 2021
8390ab5
Added x-sum and skyscraper constraints to the user script
dclamage Nov 12, 2021
eb0b9d5
Improved conflict highlighting and visual look of skyscraper/xsums
dclamage Nov 12, 2021
8a47319
Fix location in the constraint list.
dclamage Nov 12, 2021
7330a35
Fix for forcing chains reporting phantom eliminations.
dclamage Nov 25, 2021
95b0e33
DNL no longer eliminates the candidates which will eliminate after th…
dclamage Nov 25, 2021
9aba42a
Don't report a strong link as ALS if between only two cells (for brev…
dclamage Nov 25, 2021
8412f34
Chess constraint can be limited to specific cells.
dclamage Nov 28, 2021
c0e648a
X-Sums and Skyscraper constraints
dclamage Jan 25, 2022
c0c485e
Replaced zip action with one that isn't deleted.
dclamage Jan 25, 2022
4a12f7e
Better fix for zipping
dclamage Jan 25, 2022
5c81d32
True candidates button now plays nicely with other user scripts that …
dclamage Jan 29, 2022
b70f703
Additional improvement to the true candidates button layouting.
dclamage Jan 29, 2022
0056f98
Console overlap fix
dclamage Jan 30, 2022
2770e58
Removed unused, incorrect CellValue function
dclamage Feb 2, 2022
2ee31d3
Escaping unicode characters
dclamage Feb 20, 2022
33fead9
Fix for skyscraper constraint incorrectly reporting invalid when cell…
dclamage Feb 23, 2022
7ec25b9
Additional skyscraper contraint fixes.
dclamage Feb 24, 2022
7f6d38d
Multi-sum killer cage constraint
dclamage Mar 25, 2022
06dc44c
Added: Entropic Line Constraint to javascript
mosswg Mar 26, 2022
7892adf
Fixed: Logic Error in JavaScript
mosswg Mar 27, 2022
0f6e3cf
Added: Entropic Line Constraint to C#
mosswg Mar 27, 2022
321ada5
Added: Basic Solving Logic
mosswg Mar 27, 2022
8ce4087
Added: Logic for Groups that Share a Line or Box
mosswg Mar 27, 2022
8eb7712
Fixed: Issue with Conflict Highlighting
mosswg Mar 27, 2022
e5ae62a
Added: Test Puzzle for Entropic Lines
mosswg Mar 27, 2022
0fcd10d
Fixed: Auto-formatting Issues
mosswg Mar 28, 2022
9989928
Improve performance of PermutationsHelper
marknn3 Mar 6, 2022
d06eea4
Update data type of weaklink, in order to optimize the speed of AIC s…
BloodmageThalnos Apr 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Generic "N"-Wings (XYZ-Wings, WXYZ-Wings, etc)
dclamage committed Oct 15, 2021
commit 7709d21b17821a67c4c6001ff832a50a088db239
374 changes: 199 additions & 175 deletions SudokuSolver/Solver.cs
Original file line number Diff line number Diff line change
@@ -487,7 +487,7 @@ public bool FinalizeConstraints()
timers["FindPointingTuples"] = new();
timers["FindUnorthodoxTuples"] = new();
timers["FindFishes"] = new();
timers["FindYWings"] = new();
timers["FindWings"] = new();
timers["FindAIC"] = new();
timers["FindSimpleContradictions"] = new();
}
@@ -2024,7 +2024,7 @@ public LogicResult PrepForBruteForce()
DisableTuples = false;
DisablePointing = false;
DisableFishes = false;
DisableWings = false;
DisableWings = true;
DisableAIC = true;
DisableContradictions = false;
DisableFindShortestContradiction = true;
@@ -2200,11 +2200,11 @@ public LogicResult StepLogic(List<LogicalStepDesc> logicalStepDescs)
if (!DisableWings)
{
#if PROFILING
timers["FindYWings"].Start();
timers["FindWings"].Start();
#endif
result = FindYWings(logicalStepDescs);
result = FindWings(logicalStepDescs);
#if PROFILING
timers["FindYWings"].Stop();
timers["FindWings"].Stop();
#endif
if (result != LogicResult.None)
{
@@ -2426,7 +2426,7 @@ private LogicResult FindHiddenSingle(List<LogicalStepDesc> logicalStepDescs)
setMask &= ~valueSetMask;
if (numCells == MAX_VALUE && (atLeastOnce | setMask) != ALL_VALUES_MASK)
{
logicalStepDescs?.Add(new($"{group} has nowhere to place {MaskToString(ALL_VALUES_MASK & ~atLeastOnce)}.", group.Cells));
logicalStepDescs?.Add(new($"{group} has nowhere to place {MaskToString(ALL_VALUES_MASK & ~(atLeastOnce | setMask))}.", group.Cells));
return LogicResult.Invalid;
}

@@ -2831,9 +2831,9 @@ private LogicResult FindFishes(List<LogicalStepDesc> logicalStepDescs)
return LogicResult.None;
}

private LogicResult FindYWings(List<LogicalStepDesc> logicalStepDescs)
private LogicResult FindWings(List<LogicalStepDesc> logicalStepDescs)
{
if (true || isBruteForcing)
if (isBruteForcing)
{
return LogicResult.None;
}
@@ -2847,11 +2847,7 @@ private LogicResult FindYWings(List<LogicalStepDesc> logicalStepDescs)
for (int j = 0; j < WIDTH; j++)
{
uint mask = board[i, j];
if (IsValueSet(mask))
{
continue;
}
if (ValueCount(mask) == 2)
if (!IsValueSet(mask) && ValueCount(mask) == 2)
{
candidateCells.Add((i, j));
}
@@ -2937,181 +2933,207 @@ private LogicResult FindYWings(List<LogicalStepDesc> logicalStepDescs)
}
}

// Look for XYZ-Wings
for (int c0 = 0; c0 < candidateCells.Count - 1; c0++)
// Look for (N)-Wings [XYZ-Wings, WXYZ-Wings, VWXYZ-Wings, etc]
// An (N)-Wing is N candidates limited to N cells.
// Looking at each candidate, all but one of them cannot repeat within those cells.
// This implies that any cell seen by the instances of that last candidate can be eliminated.
for (int wingSize = 3; wingSize <= MAX_VALUE; wingSize++)
{
var (i0, j0) = candidateCells[c0];
uint mask0 = board[i0, j0];
for (int c1 = c0 + 1; c1 < candidateCells.Count; c1++)
var logicResult = FindNWing(logicalStepDescs, wingSize);
if (logicResult != LogicResult.None)
{
var (i1, j1) = candidateCells[c1];
uint mask1 = board[i1, j1];
if (mask0 == mask1)
{
continue;
}
return logicResult;
}
}

uint combinedMask = mask0 | mask1;
if (ValueCount(combinedMask) != 3)
return LogicResult.None;
}

private LogicResult FindNWing(List<LogicalStepDesc> logicalStepDescs, int wingSize)
{
List<(int, int)> candidateCells = new();
for (int i = 0; i < HEIGHT; i++)
{
for (int j = 0; j < WIDTH; j++)
{
uint mask = board[i, j];
int valueCount = ValueCount(mask);
if (!IsValueSet(mask) && valueCount > 1 && valueCount <= wingSize)
{
continue;
candidateCells.Add((i, j));
}
}
}

// These two cells are potentially pincers.
// They will have exactly one shared value between them.
uint sharedMask = mask0 & mask1;
int sharedVal = GetValue(sharedMask);
int sharedCand0 = CandidateIndex((i0, j0), sharedVal);
int sharedCand1 = CandidateIndex((i1, j1), sharedVal);
int numCandidateCells = candidateCells.Count;
if (numCandidateCells < wingSize)
{
return LogicResult.None;
}

// Skip these cells if they have a weak link on this candidate, since pincers by definition can't see each other.
if (weakLinks[sharedCand0].Contains(sharedCand1))
{
continue;
}
int UngroupedValue(uint valuesMask, List<(int, int)> wingCells, int curUngroupedValue)
{
if (curUngroupedValue < 0 || ValueCount(valuesMask) > wingSize)
{
return -1;
}

// Look for cells which have a weak link on the shared candidate and have all three values of the combined mask.
// This is the pivot for the XYZ-wing.
List<int> elims = CalcElims(sharedCand0, sharedCand1).ToList();
if (elims.Count <= 1)
{
continue;
}
foreach (int elimCandidate in elims)
if (wingCells.Count <= 1)
{
return 0;
}
bool checkForSingle = wingCells.Count == wingSize;
if (checkForSingle && ValueCount(valuesMask) != wingSize)
{
return -1;
}

valuesMask &= ~ValueMask(curUngroupedValue);

int ungroupedValue = curUngroupedValue;
int minValue = MinValue(valuesMask);
int maxValue = MaxValue(valuesMask);
for (int v = minValue; v <= maxValue; v++)
{
uint valueMask = ValueMask(v);
if ((valuesMask & valueMask) != 0)
{
var (i2, j2, v2) = CandIndexToCoord(elimCandidate);
uint mask2 = board[i2, j2];
if (mask2 != combinedMask)
int numWithCandidate = 0;
for (int k = 0; k < wingCells.Count; k++)
{
continue;
var (i, j) = wingCells[k];
if ((board[i, j] & valueMask) != 0)
{
numWithCandidate++;
}
}

List<(int, int)> cells = new() { (i0, j0), (i1, j1), (i2, j2) };
logicalStepDescs?.Add(new(
desc: $"XYZ-Wing: {MaskToString(combinedMask)} in {CompactName(cells)} => {DescribeElims(elims)}",
sourceCandidates: CandidateIndexes(combinedMask, cells),
elimCandidates: elims
));
if (!ClearCandidates(elims))
if (checkForSingle && numWithCandidate == 1)
{
return LogicResult.Invalid;
return -1;
}

if (numWithCandidate > 1 && !IsGroup(wingCells, v))
{
if (ungroupedValue == 0)
{
ungroupedValue = v;
}
else
{
return -1;
}
}
return LogicResult.Changed;
}
}
return ungroupedValue;
}

// Look for WXYZ-Wings
// A WXYZ-Wing is 4 candidates limited to 4 cells.
// Looking at each candidate, all but one of them cannot repeat within those cells.
// This implies that any cell seen by the instances of that last candidate can be eliminated.
// This process can be extended to any N number of candidates in N cells, but it would be too slow to search for.
candidateCells.Clear();
for (int i = 0; i < HEIGHT; i++)
List<(int, int)> wingCells = new(wingSize);
(int index, (int i, int j) coord, uint accumMask, int ungroupedValue)[] wingInfoArray = new (int, (int, int), uint, int)[wingSize];

// Initialize the wing info array with the first combination even if it's invalid
{
for (int j = 0; j < WIDTH; j++)
uint accumMask = 0;
for (int k = 0; k < wingSize; k++)
{
uint mask = board[i, j];
if (!IsValueSet(mask) && ValueCount(mask) <= 4)
{
candidateCells.Add((i, j));
}
var coord = candidateCells[k];
accumMask |= board[coord.Item1, coord.Item2];
wingCells.Add(coord);

int ungroupedValue = UngroupedValue(accumMask, wingCells, k == 0 ? 0 : wingInfoArray[k - 1].ungroupedValue);
wingInfoArray[k] = (k, coord, accumMask, ungroupedValue);
}
}

for (int c0 = 0; c0 < candidateCells.Count - 3; c0++)
while (true)
{
var (i0, j0) = candidateCells[c0];
uint mask0 = board[i0, j0];
for (int c1 = c0 + 1; c1 < candidateCells.Count - 2; c1++)
// Check if this wing info is valid and performs eliminations
var (_, _, accumMask, ungroupedValue) = wingInfoArray[wingSize - 1];
if (ValueCount(accumMask) == wingSize && ungroupedValue > 0)
{
var (i1, j1) = candidateCells[c1];
uint mask1 = board[i1, j1];
uint mask01 = mask0 | mask1;
if (ValueCount(mask01) > 4)
// At least one of the ungrouped candidates is true, so eliminate any candidates with a weak link to all of them.
var elims = CalcElims(ValueMask(ungroupedValue), wingCells);
if (elims != null && elims.Count > 0)
{
continue;
if (logicalStepDescs != null)
{
StringBuilder wingName = new();
for (int k = wingSize - 1; k >= 0; k--)
{
wingName.Append((char)('Z' - k));
}
logicalStepDescs?.Add(new(
desc: $"{wingName}-Wing: {MaskToString(accumMask)} in {CompactName(wingCells)} => {DescribeElims(elims)}",
sourceCandidates: CandidateIndexes(accumMask, wingCells),
elimCandidates: elims
));
}
if (!ClearCandidates(elims))
{
return LogicResult.Invalid;
}
return LogicResult.Changed;
}
}

for (int c2 = c1 + 1; c2 < candidateCells.Count - 1; c2++)
// Find the first invalid index. This is the minimum index to increment.
int firstInvalid = wingSize;
for (int k = 0; k < wingSize; k++)
{
var wingInfo = wingInfoArray[k];
if (wingInfo.ungroupedValue < 0)
{
var (i2, j2) = candidateCells[c2];
uint mask2 = board[i2, j2];
uint mask012 = mask01 | mask2;
if (ValueCount(mask012) > 4)
{
continue;
}
firstInvalid = k;
break;
}
}

for (int c3 = c2 + 1; c3 < candidateCells.Count; c3++)
{
var (i3, j3) = candidateCells[c3];
uint mask3 = board[i3, j3];
uint mask0123 = mask012 | mask3;
if (ValueCount(mask0123) != 4)
{
continue;
}
// Find the last index which can be incremented
int lastCanIncrement = -1;
for (int k = wingSize - 1; k >= 0; k--)
{
var wingInfo = wingInfoArray[k];
if (wingInfo.index + 1 < numCandidateCells - (wingSize - k))
{
lastCanIncrement = k;
break;
}
}

List<(int, int)> cells = new() { (i0, j0), (i1, j1), (i2, j2), (i3, j3) };
bool isInvalid = false;
int ungroupedValue = 0;
for (int v = 1; v <= 9; v++)
{
uint valueMask = ValueMask(v);
if ((mask0123 & valueMask) != 0)
{
int numWithCandidate =
((mask0 & valueMask) != 0 ? 1 : 0) +
((mask1 & valueMask) != 0 ? 1 : 0) +
((mask2 & valueMask) != 0 ? 1 : 0) +
((mask3 & valueMask) != 0 ? 1 : 0);
if (numWithCandidate == 1)
{
isInvalid = true;
break;
}
// Check if done
if (lastCanIncrement == -1)
{
break;
}

if (!IsGroup(cells, v))
{
if (ungroupedValue == 0)
{
ungroupedValue = v;
}
else
{
isInvalid = true;
break;
}
}
}
}
// Increment the smaller of the first invalid one or the last that can increment.
int k0 = Math.Min(lastCanIncrement, firstInvalid);

if (!isInvalid)
{
// Eliminate the ungrouped value from any candidates with a weak link to all the cells which contain this value.
var elims = CalcElims(ValueMask(ungroupedValue), cells);
if (elims == null || elims.Count == 0)
{
continue;
}
wingCells.Clear();
for (int k = 0; k < k0; k++)
{
wingCells.Add(wingInfoArray[k].coord);
}

foreach (int elimCandidate in elims)
{
logicalStepDescs?.Add(new(
desc: $"WXYZ-Wing: {MaskToString(mask0123)} in {CompactName(cells)} => {DescribeElims(elims)}",
sourceCandidates: CandidateIndexes(mask0123, cells),
elimCandidates: elims
));
if (!ClearCandidates(elims))
{
return LogicResult.Invalid;
}
return LogicResult.Changed;
}
}
}
}
// Increment to the next combination
int nextIndex = wingInfoArray[k0].index + 1;
var nextCoord = candidateCells[nextIndex];
uint nextAccumMask = board[nextCoord.Item1, nextCoord.Item2];
if (k0 > 0)
{
nextAccumMask |= wingInfoArray[k0 - 1].accumMask;
}
wingCells.Add(nextCoord);
wingInfoArray[k0] = (nextIndex, nextCoord, nextAccumMask, UngroupedValue(nextAccumMask, wingCells, k0 == 0 ? 0 : wingInfoArray[k0 - 1].ungroupedValue));

for (int k1 = k0 + 1; k1 < wingSize; k1++)
{
nextIndex = wingInfoArray[k1 - 1].index + 1;
nextCoord = candidateCells[nextIndex];
nextAccumMask |= board[nextCoord.Item1, nextCoord.Item2];
wingCells.Add(nextCoord);
wingInfoArray[k1] = (nextIndex, nextCoord, nextAccumMask, UngroupedValue(nextAccumMask, wingCells, wingInfoArray[k1 - 1].ungroupedValue));
}
}

@@ -3480,6 +3502,8 @@ internal bool HasFullWeakLinks((int, int) cell0, (int, int) cell1)
internal IEnumerable<int> CalcElims(int candIndex0, int candIndex1) =>
weakLinks[candIndex0].Where(IsCandIndexValid).Intersect(weakLinks[candIndex1].Where(IsCandIndexValid));

internal IEnumerable<int> CalcElims(params int[] candIndexes) => CalcElims(candIndexes);

internal IEnumerable<int> CalcElims(IEnumerable<int> candIndexes)
{
IEnumerable<int> result = null;
@@ -3693,8 +3717,8 @@ private LogicResult FindAIC(List<LogicalStepDesc> logicalStepDescs)
// Process each chain found, adding more chains if still viable
List<int> bestChain = null;
List<int> bestChainElims = null;
int bestChainNumSingles = 0;
int bestChainNumSingles2 = 0;
int bestChainDirectSingles = 0;
int bestChainSinglesAfterBasics = 0;
string bestChainDescPrefix = null;
int maxChainSize = 0;
bool bestChainCausesInvalidBoard = false;
@@ -3707,49 +3731,49 @@ void CheckBestChain(List<int> chain, List<int> chainElims, string chainDescPrefi
}

// Apply the eliminations to a board clone
Solver clone = Clone();
Solver directSinglesSolver = Clone();
foreach (int elimCandIndex in chainElims)
{
var (i, j, v) = CandIndexToCoord(elimCandIndex);
if (!clone.ClearValue(i, j, v))
if (!directSinglesSolver.ClearValue(i, j, v))
{
bestChain = new(chain);
bestChainElims = new(chainElims);
bestChainNumSingles = 0;
bestChainNumSingles2 = 0;
bestChainDirectSingles = 0;
bestChainSinglesAfterBasics = 0;
bestChainDescPrefix = chainDescPrefix;
bestChainCausesInvalidBoard = true;
return;
}
}
if (clone.ApplySingles() == LogicResult.Invalid)
if (directSinglesSolver.ApplySingles() == LogicResult.Invalid)
{
bestChain = new(chain);
bestChainElims = new(chainElims);
bestChainNumSingles = 0;
bestChainNumSingles2 = 0;
bestChainDirectSingles = 0;
bestChainSinglesAfterBasics = 0;
bestChainDescPrefix = chainDescPrefix;
bestChainCausesInvalidBoard = true;
return;
}

Solver clone2 = clone.Clone();
clone2.SetToBasicsOnly();
if (clone2.ConsolidateBoard() == LogicResult.Invalid)
Solver singlesAfterBasicsSolver = directSinglesSolver.Clone();
singlesAfterBasicsSolver.SetToBasicsOnly();
if (singlesAfterBasicsSolver.ConsolidateBoard() == LogicResult.Invalid)
{
bestChain = new(chain);
bestChainElims = new(chainElims);
bestChainNumSingles = 0;
bestChainNumSingles2 = 0;
bestChainDirectSingles = 0;
bestChainSinglesAfterBasics = 0;
bestChainDescPrefix = chainDescPrefix;
bestChainCausesInvalidBoard = true;
return;
}

int numSingles = clone.NumSetValues;
int numSingles2 = clone2.NumSetValues;
(int, int, int, int) chainVals = (numSingles, numSingles2, chainElims.Count, -chain.Count);
(int, int, int, int) bestChainVals = bestChain != null ? (bestChainNumSingles, bestChainNumSingles2, bestChainElims.Count, -bestChain.Count) : default;
int numDirectSingles = directSinglesSolver.NumSetValues;
int numSinglesAfterBasics = singlesAfterBasicsSolver.NumSetValues;
(int, int, int, int) chainVals = (numSinglesAfterBasics, numDirectSingles, chainElims.Count, -chain.Count);
(int, int, int, int) bestChainVals = bestChain != null ? (bestChainSinglesAfterBasics, bestChainDirectSingles, bestChainElims.Count, -bestChain.Count) : default;
if (bestChain == null || chainVals.CompareTo(bestChainVals) > 0)
{
if (bestChain == null)
@@ -3758,11 +3782,11 @@ void CheckBestChain(List<int> chain, List<int> chainElims, string chainDescPrefi
}
bestChain = new(chain);
bestChainElims = new(chainElims);
bestChainNumSingles = numSingles;
bestChainNumSingles2 = numSingles2;
bestChainDirectSingles = numDirectSingles;
bestChainSinglesAfterBasics = numSinglesAfterBasics;
bestChainDescPrefix = chainDescPrefix;

if (bestChainNumSingles == NUM_CELLS)
if (bestChainDirectSingles == NUM_CELLS)
{
maxChainSize = chain.Count;
}