Advertisement
Guest User

SquareSwap4Inverted.cs

a guest
Oct 11th, 2021
73
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 41.23 KB | None | 0 0
  1. using System;
  2. using System.Linq;
  3. using System.Text;
  4. using System.Collections.Generic;
  5.  
  6. // SquareSwap4Inverted.cs
  7. // used for a breadth-first exhaustive search for an answer at
  8. // https://puzzling.stackexchange.com/questions/111932/can-you-reactivate-a-4x4-magic-square
  9. // still "a work in progress" so not very tidied up...
  10.  
  11.  
  12. using IntCode = System.UInt64;
  13.  
  14. public struct CellState : IEquatable<CellState>
  15. {
  16.   public byte Value { get; set; }
  17.   public byte MovesLeft { get; set; }
  18.  
  19.   public override bool Equals(object obj)
  20.   {
  21.     return obj is CellState other && Equals(other);
  22.   }
  23.  
  24.   public bool Equals(CellState other)
  25.   {
  26.     return Value == other.Value && MovesLeft == other.MovesLeft;
  27.   }
  28.  
  29.   public override int GetHashCode()
  30.   {
  31.     return Value * 256 + MovesLeft;
  32.   }
  33.  
  34.   public bool IsSwapValid(CellState other)
  35.   {
  36.     if (Value > other.Value)
  37.       return MovesLeft >= other.Value && other.MovesLeft > 0;
  38.     else
  39.       return other.MovesLeft >= Value && MovesLeft > 0;
  40.   }
  41. }
  42.  
  43.  
  44.  
  45.  
  46.  
  47. public class GridState : IEquatable<GridState>
  48. {
  49.   private static CellState[] MakeCellStates(params string[] initialRows)
  50.   {
  51.     int columnCount = initialRows[0].Length;
  52.     var result = new CellState[initialRows.Length * columnCount];
  53.     for (int row = 0; row < initialRows.Length; row++)
  54.     {
  55.       if (initialRows[row].Length != columnCount)
  56.         throw new ArgumentException("All rows must be the same length", nameof(initialRows));
  57.       for (int col = 0; col < columnCount; col++)
  58.       {
  59.         int value = _mainNumbers.IndexOf(initialRows[row][col]);
  60.         if (value < 0)
  61.           throw new ArgumentException("Row contained invalid character: " + initialRows[row]);
  62.  
  63.         result[row * columnCount + col] = new CellState { Value = (byte)value, MovesLeft = (byte)value };
  64.       }
  65.     }
  66.     return result;
  67.   }
  68.  
  69.   public GridState(params string[] initialRows)
  70.     : this(initialRows[0].Length, MakeCellStates(initialRows))
  71.   { }
  72.  
  73.   public GridState(int columnCount, CellState[] cellStates)
  74.   {
  75.     if (cellStates.Length % columnCount != 0)
  76.       throw new ArgumentOutOfRangeException();
  77.     ColumnCount = columnCount;
  78.     CellStates = cellStates;
  79.   }
  80.  
  81.   public int SetMagicConstant()
  82.   {
  83.     MagicConstant = 0;
  84.     if (RowCount == ColumnCount)
  85.     {
  86.       int totalValues = CellStates.Sum(c => (int)c.Value);
  87.       if (totalValues % ColumnCount == 0)
  88.         MagicConstant = totalValues / ColumnCount;
  89.     }
  90.     return MagicConstant;
  91.   }
  92.  
  93.   public int ColumnCount { get; }
  94.   public int RowCount => CellStates.Length / ColumnCount;
  95.  
  96.   private static int MagicConstant { get; set; }
  97.  
  98.   public CellState[] CellStates { get; }
  99.  
  100.   public CellState this[int row, int col]
  101.   {
  102.     get
  103.     {
  104.       if (row < 0 || row > RowCount)
  105.         throw new ArgumentOutOfRangeException(nameof(row));
  106.       if (col < 0 || col > ColumnCount)
  107.         throw new ArgumentOutOfRangeException(nameof(col));
  108.       return CellStates[row * ColumnCount + col];
  109.     }
  110.   }
  111.  
  112.   private static readonly string _mainNumbers = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ#";
  113.   private static readonly string _subscriptNumbers = "₀₁₂₃₄₅₆₇₈₉₊₊₊₊₊₊₊₊₊₊₊₊₊₊₊₊₊₊₊₊₊₊₊₊₊₊₊";
  114.  
  115.   public override string ToString()
  116.   {
  117.     int rowLength = ColumnCount * 5;
  118.  
  119.     StringBuilder result = new StringBuilder(rowLength * RowCount * 2);
  120.  
  121.     for (int row = 0; row < RowCount; row++)
  122.     {
  123.       for (int col = 0; col < ColumnCount; col++)
  124.       {
  125.         var state = CellStates[row * ColumnCount + col];
  126.         result.Append(new[] { ' ', _mainNumbers[state.Value], _subscriptNumbers[state.MovesLeft], ' ', col < ColumnCount - 1 ? '|' : '\n' });
  127.       }
  128.       if (row < RowCount - 1)
  129.       {
  130.         result.Append(string.Join("+", Enumerable.Repeat("----", ColumnCount)));
  131.         result.Append("\n");
  132.       }
  133.     }
  134.     return result.ToString();
  135.   }
  136.  
  137.   public override bool Equals(object obj)
  138.   {
  139.     return base.Equals(obj) || obj is GridState other && Equals(other);
  140.   }
  141.  
  142.   public bool Equals(GridState other)
  143.   {
  144.     var otherCellStates = other.CellStates;
  145.     if (otherCellStates.Length != CellStates.Length)
  146.       return false;
  147.     for (int i = 0; i < CellStates.Length; i++)
  148.       if (!CellStates[i].Equals(otherCellStates[i]))
  149.         return false;
  150.     return true;
  151.   }
  152.  
  153.   public bool EqualValues(GridState other)
  154.   {
  155.     var otherCellStates = other.CellStates;
  156.     if (otherCellStates.Length != CellStates.Length)
  157.       return false;
  158.     for (int i = 0; i < CellStates.Length; i++)
  159.       if (!CellStates[i].Value.Equals(otherCellStates[i].Value))
  160.         return false;
  161.     return true;
  162.   }
  163.  
  164.   public override int GetHashCode()
  165.   {
  166.     int result = 0;
  167.     for (int i = 0; i < CellStates.Length; i++)
  168.       result = (result >> 18) ^ (result * 0x1413 + CellStates[i].GetHashCode());
  169.     return result;
  170.   }
  171.  
  172.   public int TotalMovesLeft => CellStates.Sum(c => c.MovesLeft);
  173.  
  174.   public GridState GetStateAfterSwap(SwapInfo swapInfo)
  175.   {
  176.     CellState stateFrom = this[swapInfo.rowFrom, swapInfo.colFrom];
  177.     CellState stateTo = this[swapInfo.rowTo, swapInfo.colTo];
  178.  
  179.     if (!stateFrom.IsSwapValid(stateTo))
  180.       throw new ArgumentOutOfRangeException(nameof(swapInfo), $"Invalid swap R{swapInfo.rowFrom + 1}C{swapInfo.colFrom + 1} with R{swapInfo.rowTo + 1}C{swapInfo.colTo + 1} for grid:\n{this.ToString()}");
  181.  
  182.     var cellStatesAfter = CellStates.ToArray();
  183.  
  184.     CellState stateFromAfter = new CellState { Value = (byte)stateTo.Value, MovesLeft = (byte)(stateTo.MovesLeft - (stateTo.Value > stateFrom.Value ? stateFrom.Value : 1)) };
  185.     CellState stateToAfter = new CellState { Value = (byte)stateFrom.Value, MovesLeft = (byte)(stateFrom.MovesLeft - (stateFrom.Value > stateTo.Value ? stateTo.Value : 1)) };
  186.  
  187.     cellStatesAfter[swapInfo.rowFrom * ColumnCount + swapInfo.colFrom] = stateFromAfter;
  188.     cellStatesAfter[swapInfo.rowTo * ColumnCount + swapInfo.colTo] = stateToAfter;
  189.  
  190.     return new GridState(ColumnCount, cellStatesAfter);
  191.   }
  192.  
  193.   public bool IsSwapValid(SwapInfo info) => this[info.rowFrom, info.colFrom].IsSwapValid(this[info.rowTo, info.colTo]);
  194.  
  195.   public bool IsOrdered()
  196.   {
  197.     for (int i = 0; i < CellStates.Length - 1; i++)
  198.       if (CellStates[i].Value > CellStates[i + 1].Value)
  199.         return false;
  200.     return true;
  201.   }
  202.  
  203.   public bool IsMagic()
  204.   {
  205.     if (MagicConstant == 0)
  206.       throw new InvalidOperationException("Cannot be magic - wrong dimensions or total...");
  207.     int count = ColumnCount;
  208.     return Enumerable.Range(0, count).Sum(i => (int)this[i, i].Value) == MagicConstant &&
  209.            Enumerable.Range(0, count).Sum(i => (int)this[count - 1 - i, i].Value) == MagicConstant &&
  210.            Enumerable.Range(0, count).All(i => Enumerable.Range(0, count).Sum(col => (int)this[i, col].Value) == MagicConstant &&
  211.                                                Enumerable.Range(0, count).Sum(row => (int)this[row, i].Value) == MagicConstant);
  212.   }
  213.  
  214.  
  215.   private byte[] _valueBytes;
  216.   private byte[] GetOrCacheValueBytes()
  217.   {
  218.     return _valueBytes ?? (_valueBytes = CellStates.Select(c => c.Value).ToArray());
  219.   }
  220.  
  221.   // cached on target, which doesn't change. Effectively this could be static.
  222.   private byte[] _movesToFarCorner;
  223.   private byte[] GetOrCacheMovesToFarCorner()
  224.   {
  225.     return _movesToFarCorner ?? (_movesToFarCorner = CreateMovesToFarCornerArray());
  226.   }
  227.   private byte[] CreateMovesToFarCornerArray()
  228.   {
  229.     byte[] result = new byte[CellStates.Length];
  230.  
  231.     for (int i = 0; i < CellStates.Length; i++)
  232.     {
  233.       int row = i / ColumnCount;
  234.       int col = i % ColumnCount;
  235.       result[i] = (byte)(Math.Max(row, RowCount - 1 - row) + Math.Max(col, ColumnCount - 1 - col));
  236.     }
  237.     return result;
  238.   }
  239.  
  240.  
  241.   // returns false if any of the cells has insufficient swaps remaining to reach the correct position in the target.
  242.   // this ignores whether valid swaps are possible (e.g. a blocking cell with no moves in between)
  243.   public bool IsTargetWithinReach(GridState target) => IsTargetWithinReach(target.GetOrCacheValueBytes(), target.GetOrCacheMovesToFarCorner());
  244.  
  245.   private bool IsTargetWithinReach(byte[] targetValues, byte[] movesToFarCorner)
  246.   {
  247.     if (targetValues.Length != CellStates.Length)
  248.       throw new ArgumentException($"Given target is not compatible - {targetValues.Length} values instead of {CellStates.Length}");
  249.  
  250.     for (int i = 0; i < CellStates.Length; i++)
  251.     {
  252.       byte movesLeft = CellStates[i].MovesLeft;
  253.       if (movesLeft == 0 && targetValues[i] != CellStates[i].Value)
  254.         return false;
  255.       if (movesLeft < movesToFarCorner[i])
  256.       {
  257.         byte value = CellStates[i].Value;
  258.         int targetIndex = Array.IndexOf(targetValues, value);
  259.         int row = i / ColumnCount;
  260.         int col = i % ColumnCount;
  261.         int targetRow = targetIndex / ColumnCount;
  262.         int targetCol = targetIndex % ColumnCount;
  263.         int movesNeeded = Math.Abs(targetRow - row) + Math.Abs(targetCol - col);
  264.         if (movesNeeded > movesLeft)
  265.           return false;
  266.         // TODO: consider checking if a move that is "exactly" possible would need to pass through an immovable cell?
  267.       }
  268.     }
  269.     // nothing "out of reach" with the simple checks done here.
  270.     return true;
  271.   }
  272. }
  273.  
  274.  
  275. public struct SwapInfo : IEquatable<SwapInfo>
  276. {
  277.   public byte rowFrom { get; set; }
  278.   public byte colFrom { get; set; }
  279.   public byte rowTo { get; set; }
  280.   public byte colTo { get; set; }
  281.  
  282.   static Dictionary<SwapInfo, byte> _toByteDict = new Dictionary<SwapInfo, byte>(256);
  283.   static List<SwapInfo> _knownValidSwaps = new List<SwapInfo>(256); // only because dict isn't indexable...
  284.   static System.Threading.ReaderWriterLockSlim _byteDictLock = new System.Threading.ReaderWriterLockSlim();
  285.  
  286.   public byte CompressToByte()
  287.   {
  288.     if (!_toByteDict.TryGetValue(this, out byte result))
  289.     {
  290.       // Attention - this is a potential major bug if the same pattern used anywhere else... Dictionary is not thread safe.
  291.       // However, we avoid hash bucket re-organisation by pre-allocating 256 entries (enough for anything which can be converted to/from byte)
  292.       // and in this specific case, the lock isn't even needed because we're really doing all writing from the single thread that
  293.       // handles finding swaps from the starting position, but kept lock because
  294.       // later tweaks to the program might potentially make the lock necessary...
  295.       // the alternatives of concurrentdictionary or readerwriterlockslim would add more overhead for many millions of calls.
  296.       lock (_toByteDict)
  297.         if (!_toByteDict.TryGetValue(this, out result))
  298.         {
  299.           if (_toByteDict.Count > 255)
  300.             throw new InvalidOperationException("_toByteDict is full. refactoring required!");
  301.           result = (byte)_toByteDict.Count;
  302.           Console.WriteLine($"Allocated byte {result} to swap {this}");
  303.           _toByteDict.Add(this, result);
  304.           _knownValidSwaps.Add(this);
  305.         }
  306.     }
  307.     return result;
  308.   }
  309.  
  310.   public static SwapInfo ExpandFromByte(byte compressed)
  311.   {
  312.     return _knownValidSwaps[compressed];
  313.   }
  314.  
  315.  
  316.   public override bool Equals(object obj)
  317.   {
  318.     return obj is SwapInfo other && Equals(other);
  319.   }
  320.  
  321.   public bool Equals(SwapInfo other)
  322.   {
  323.     return rowFrom == other.rowFrom && colFrom == other.colFrom && rowTo == other.rowTo && colTo == other.colTo ||
  324.            rowFrom == other.rowTo && colFrom == other.colTo && rowTo == other.rowFrom && colTo == other.colFrom;
  325.   }
  326.  
  327.   public override int GetHashCode()
  328.   {
  329.     return Math.Min(rowFrom, rowTo) + Math.Max(rowFrom, rowTo) * 64 + Math.Min(colFrom, colTo) * 4096 + Math.Max(colFrom, colTo) * 262144;
  330.   }
  331.  
  332.   public override string ToString()
  333.   {
  334.     return $"R{rowFrom + 1}C{colFrom + 1}<>R{rowTo + 1}C{colTo + 1}";
  335.   }
  336. }
  337.  
  338.  
  339. public class PuzzleState
  340. {
  341.   public static PuzzleState CreateStartingState(GridState gridState) => new PuzzleState(gridState);
  342.  
  343.   private PuzzleState(GridState gridState, params SwapInfo[] movesDone)
  344.   {
  345.     GridState = gridState;
  346.     MovesDone = movesDone;
  347.   }
  348.  
  349.   public GridState GridState { get; private set; }
  350.  
  351.   public PuzzleState(params string[] lines)
  352.     : this(new GridState(lines))
  353.   {
  354.   }
  355.  
  356.   public PuzzleState(CompressedPuzzleState compressed, GridState gridState)
  357.     : this(gridState, compressed.MovesDone)
  358.   { }
  359.  
  360.   // parent and move count were never used, so removed these.
  361.  
  362.   public SwapInfo[] MovesDone { get; }
  363.  
  364.   private static SwapInfo[] ConcatSwapInfo(PuzzleState parent, SwapInfo move)
  365.   {
  366.     int parentLength = parent.MovesDone.Length;
  367.     var result = new SwapInfo[parentLength + 1];
  368.     Array.Copy(parent.MovesDone, result, parentLength);
  369.     result[parentLength] = move;
  370.     return result;
  371.   }
  372.  
  373.   private PuzzleState(PuzzleState parent, SwapInfo move, GridState gridState)
  374.     : this(gridState, ConcatSwapInfo(parent, move))
  375.   {
  376.     // bug in previous version before major refactoring left out this (now redundant!) line
  377.     AllowWrapAround = parent.AllowWrapAround;
  378.   }
  379.  
  380.   public bool IsSwapValid(SwapInfo info) => GridState.IsSwapValid(info);
  381.  
  382.   public bool AllowWrapAround { get; set; } = true;
  383.  
  384.   public IEnumerable<PuzzleState> GetStatesAfterValidMoves()
  385.   {
  386.     int fromRowLimit = AllowWrapAround ? GridState.RowCount : (GridState.RowCount - 1);
  387.     int fromColLimit = AllowWrapAround ? GridState.ColumnCount : (GridState.ColumnCount - 1);
  388.     for (int fromRow = 0; fromRow < fromRowLimit; fromRow++)
  389.     {
  390.       int toNextRow = (fromRow + 1) % GridState.RowCount;
  391.       for (int fromCol = 0; fromCol < GridState.ColumnCount; fromCol++)
  392.       {
  393.         CellState fromState = GridState[fromRow, fromCol];
  394.         if (fromState.MovesLeft < 1)
  395.           continue;
  396.  
  397.         CellState toState = GridState[toNextRow, fromCol];
  398.         if (fromState.IsSwapValid(toState))
  399.         {
  400.           var swapInfo = new SwapInfo { colFrom = (byte)fromCol, rowFrom = (byte)fromRow, colTo = (byte)fromCol, rowTo = (byte)toNextRow };
  401.           yield return new PuzzleState(this, swapInfo, GridState.GetStateAfterSwap(swapInfo));
  402.         }
  403.       }
  404.     }
  405.     for (int fromCol = 0; fromCol < fromColLimit; fromCol++)
  406.     {
  407.       int toNextCol = (fromCol + 1) % GridState.ColumnCount;
  408.       for (int fromRow = 0; fromRow < GridState.RowCount; fromRow++)
  409.       {
  410.         CellState fromState = GridState[fromRow, fromCol];
  411.         if (fromState.MovesLeft < 1)
  412.           continue;
  413.  
  414.         CellState toState = GridState[fromRow, toNextCol];
  415.         if (fromState.IsSwapValid(toState))
  416.         {
  417.           var swapInfo = new SwapInfo { colFrom = (byte)fromCol, rowFrom = (byte)fromRow, colTo = (byte)toNextCol, rowTo = (byte)fromRow };
  418.           yield return new PuzzleState(this, swapInfo, GridState.GetStateAfterSwap(swapInfo));
  419.         }
  420.       }
  421.     }
  422.   }
  423.  
  424.   static GridState targetState = new GridState("867G", "143A", "9D2E", "BC5F");
  425.   // static GridState targetState = new GridState("1FE4", "AB85", "769C", "G32D"); // test target state similar to "given" magic square
  426.  
  427.   public bool IsTargetState => GridState.EqualValues(targetState);
  428.  
  429.  
  430.   public bool CheckCompatibleWithTargetState()
  431.   {
  432.     // a simple check - assume all squares can move the number of moves remaining...
  433.     // so those with at least 6 moves remaining can move anywhere in the grid.
  434.     // with 5 moves remaining in a corner, cannot move to opposite corner
  435.     // ...
  436.     // with 0 moves remaining anywhere, must stay in current position.
  437.  
  438.     return GridState.IsTargetWithinReach(targetState);
  439.   }
  440.  
  441.  
  442.   public static IEnumerable<PuzzleState> GenerateMagicSquares(int columnCount)
  443.   {
  444.     int limit = columnCount * columnCount;
  445.     int magicConstant = (limit + 1) * columnCount / 2;
  446.     // copy of set that is NOT modified
  447.     HashSet<byte> initialValuesAvailable = new HashSet<byte>(Enumerable.Range(1, limit).Select(i => (byte)i));
  448.     // copy of set that is modified in first iteration
  449.     HashSet<byte> valuesAvailableAfter1 = new HashSet<byte>(initialValuesAvailable);
  450.     var resultValues = new byte[limit];
  451.  
  452.     Console.WriteLine($"Looking for magic squares with {columnCount} columns, magicConstant={magicConstant}");
  453.  
  454. /*    Console.WriteLine("Precheck debug!");
  455.     foreach (var debugLine in GetPossibleMagicRows(new byte[] { 8, 0, 0, 1 }, 34, new byte[] { 13, 2, 12, 7 }))
  456.       Console.WriteLine($"Debug: [{string.Join(",", debugLine)}]");
  457.  
  458.     yield break;*/
  459.  
  460.  
  461.     // first populate the leading diagonal
  462.     foreach (var diag1 in GetPossibleMagicRows(new byte[columnCount], magicConstant, initialValuesAvailable))
  463.     {
  464.       //Console.WriteLine($"Leading diagonal: [{string.Join(",", diag1)}]");
  465.  
  466.       valuesAvailableAfter1.ExceptWith(diag1);
  467.       for (int i = 0; i < columnCount; i++)
  468.         resultValues[i * (1 + columnCount)] = diag1[i];
  469.       var diag2Template = new byte[columnCount];
  470.       if (columnCount % 2 == 1) // copy centre cell to other diagonal if needed.
  471.         diag2Template[columnCount / 2] = diag1[columnCount / 2];
  472.  
  473.       // for each possible leading diagonal populate all possible trailing diagonals
  474.       foreach (var diag2 in GetPossibleMagicRows(diag2Template, magicConstant, valuesAvailableAfter1))
  475.       {
  476.         //Console.WriteLine($"Trailing diagonal: [{string.Join(",", diag2)}]");
  477.         // to avoid complication of selectively including or excluding centre cell,
  478.         // just make a new hashset every time.
  479.         var valuesAvailableAfter2 = new HashSet<byte>(valuesAvailableAfter1);
  480.         valuesAvailableAfter2.ExceptWith(diag2);
  481.         for (int i = 0; i < columnCount; i++)
  482.           resultValues[(i + 1) * (columnCount - 1)] = diag2[i];
  483.  
  484.         var valuesAvailableAfterCol1 = new HashSet<byte>(valuesAvailableAfter2);
  485.         var firstColumnTemplate = new byte[columnCount];
  486.         firstColumnTemplate[0] = resultValues[0];
  487.         firstColumnTemplate[columnCount - 1] = resultValues[columnCount * (columnCount - 1)];
  488.         foreach(var firstColumn in GetPossibleMagicRows(firstColumnTemplate, magicConstant, valuesAvailableAfter2))
  489.         {
  490.           //Console.WriteLine($"First column: [{string.Join(",", firstColumn)}]");
  491.           for (int i = 1; i < columnCount - 1; i++)
  492.           {
  493.             valuesAvailableAfterCol1.Remove(firstColumn[i]);
  494.             resultValues[i * columnCount] = firstColumn[i];
  495.           }
  496.  
  497.           // now we have the two main diagonals, and the first column, all adding up to magic constant.
  498.           // fill in all rows except the top one...
  499.           if (columnCount > 5)
  500.             // for 5 or more columns, a separate "fill rows" function that may need to check more than one possibility per row is needed...
  501.             // but initial implementation only needs 4, so we'll skip implementation of that!
  502.             throw new NotImplementedException();
  503.           else
  504.           {
  505.             // fast path - middle 2 rows have only 1 empty cell, so we either have a possible magic row or we don't.
  506.             // so can use SingleOrDefault as a shortcut to nested loops or recursion.
  507.             bool resultOk = true;
  508.             var valuesAvailableAfterRows = new HashSet<byte>(valuesAvailableAfterCol1);
  509.             var resultValuesAfterRows = resultValues.ToArray();
  510.             var rowValues = new byte[columnCount];
  511.             for (int i = 1; i < columnCount - 1 && resultOk; i++)
  512.             {
  513.               Array.Copy(resultValuesAfterRows, i * columnCount, rowValues, 0, columnCount);
  514.               var rowValuesFull = GetPossibleMagicRows(rowValues, magicConstant, valuesAvailableAfterRows).SingleOrDefault();
  515.               if (rowValuesFull != null)
  516.               {
  517.                 //Console.WriteLine($"row {i}: [{string.Join(",", rowValuesFull)}]");
  518.                 Array.Copy(rowValuesFull, 0, resultValuesAfterRows, i * columnCount, columnCount);
  519.                 valuesAvailableAfterRows.ExceptWith(rowValuesFull);
  520.               }
  521.               else
  522.                 resultOk = false;
  523.             }
  524.             if (resultOk)
  525.             {
  526.               // final row has 2 empty cells, so consider each possibility for filling them...
  527.               Array.Copy(resultValuesAfterRows, (columnCount - 1) * columnCount, rowValues, 0, columnCount);
  528.               foreach (var finalRow in GetPossibleMagicRows(rowValues, magicConstant, valuesAvailableAfterRows))
  529.               {
  530.                 //Console.WriteLine($"final row: [{string.Join(",", finalRow)}]");
  531.                 Array.Copy(finalRow, 0, resultValuesAfterRows, (columnCount - 1) * columnCount, columnCount);
  532.                 var valuesAvailableForTopRow = valuesAvailableAfterRows.Except(finalRow).ToList();
  533.  
  534.                 // only top and bottom rows remain, with first and last column complete.
  535.                 // after setting bottom row, magic square property will ensure that any arrangement of the remaining values
  536.                 // is valid for the top row, but are they good for the columns?
  537.                 if (valuesAvailableForTopRow.Count != columnCount - 2)
  538.                   throw new InvalidOperationException($"Something went wrong - there should be exactly {columnCount} values remaining now, but I found {valuesAvailableForTopRow.Count}!");
  539.  
  540.                 var columnValues = new byte[columnCount];
  541.                 bool columnsOk = true;
  542.                 for (int i = 1; i < columnCount - 1 && columnsOk; i++)
  543.                 {
  544.                   for (int row = 1; row < columnCount; row++)
  545.                     columnValues[row] = resultValuesAfterRows[row * columnCount + i];
  546.                   columnValues[0] = 0;
  547.                   var columnValuesFull = GetPossibleMagicRows(columnValues, magicConstant, valuesAvailableForTopRow).SingleOrDefault();
  548.                   if (columnValuesFull != null)
  549.                   {
  550.                     resultValuesAfterRows[i] = columnValues[0];
  551.                     valuesAvailableForTopRow.Remove(resultValuesAfterRows[i]);
  552.                   }
  553.                   else
  554.                     columnsOk = false;
  555.                 }
  556.                 if (columnsOk)
  557.                 {
  558.                   // yay! we found a magic square. Now convert to PuzzleState.
  559.                   yield return new PuzzleState(new GridState(columnCount, resultValuesAfterRows.Select(v => new CellState { Value = v, MovesLeft = v }).ToArray()));
  560.                 }
  561.               }
  562.             }
  563.           }
  564.  
  565.           for (int i = 1; i < columnCount - 1; i++)
  566.             valuesAvailableAfterCol1.Add(firstColumn[i]);
  567.             // resultValues[i * columnCount] = 0; // not needed because nothing checks value of this cell within this function.
  568.         }
  569.  
  570.       }
  571.  
  572.       // re-add leading diagonal so that the same hashset can be re-used for next iteration.
  573.       valuesAvailableAfter1.UnionWith(diag1);
  574.     }
  575.   }
  576.  
  577.   // NB caller should expect rowTemplate may be corrupted, but must not modify the array contents itself!
  578.   // and the same object will usually be returned multiple times with different values (and may be the same object as rowTemplate)
  579.   private static IEnumerable<byte[]> GetPossibleMagicRows(byte[] rowTemplate, int magicConstant, ICollection<byte> valuesAvailable)
  580.   {
  581.     //Console.WriteLine($"GetPossibleMagicRows([{string.Join(",", rowTemplate)}], {magicConstant}, [{string.Join(",", valuesAvailable)}]");
  582.     int templateTotal = 0;
  583.     int[] emptyIndex = new int[rowTemplate.Length];
  584.     int emptyCount = 0;
  585.     for (int i = 0; i < rowTemplate.Length; i++)
  586.     {
  587.       byte value = rowTemplate[i];
  588.       templateTotal += value;
  589.       if (value == 0)
  590.         emptyIndex[emptyCount++] = i;
  591.       else if (valuesAvailable.Contains(value))
  592.         throw new ArgumentException("valuesAvailable mentions values found in rowTemplate!");
  593.     }
  594.  
  595.     // from this point, we're free to "corrupt" the empty cells of row template, and re-use it as a return value.
  596.     int missingTotal = magicConstant - templateTotal;
  597.  
  598.     switch (emptyCount)
  599.     {
  600.       case 0: throw new ArgumentException($"Row template didn't have any empty numbers: {string.Join(",", rowTemplate)}]");
  601.  
  602.       case 1:
  603.         // very fast path - our missing number can be calculated directly.
  604.         byte missingValue = (byte)missingTotal;
  605.         if (valuesAvailable.Contains(missingValue))
  606.         {
  607.           rowTemplate[emptyIndex[0]] = missingValue;
  608.           yield return rowTemplate;
  609.         }
  610.         break;
  611.  
  612.       case 2:
  613.         // fast path - final 2 numbers to be determined together
  614.         // loop will have no iterations if (missingTotal >= 3), so no explicit check needed
  615.         for (byte lowerValue = 1; lowerValue < (missingTotal + 1) / 2; lowerValue++)
  616.         {
  617.           if (!valuesAvailable.Contains(lowerValue))
  618.             continue;
  619.           byte upperValue = (byte)(missingTotal - lowerValue);
  620.           if (!valuesAvailable.Contains(upperValue))
  621.             continue;
  622.           rowTemplate[emptyIndex[0]] = lowerValue;
  623.           rowTemplate[emptyIndex[1]] = upperValue;
  624.           yield return rowTemplate;
  625.           rowTemplate[emptyIndex[0]] = upperValue;
  626.           rowTemplate[emptyIndex[1]] = lowerValue;
  627.           yield return rowTemplate;
  628.         }
  629.         break;
  630.  
  631.       default:
  632.         // pick any available value for first cell, and let the recursive call deal with the totals...
  633.         foreach (byte nextValue in valuesAvailable)
  634.         {
  635.           // create new copy of row template for recursive call, which recursive call will corrupt
  636.           // then next iteration can have a fresh copy, without having to explicitly re-empty the empty cells.
  637.           var nextRowTemplate = rowTemplate.ToArray();
  638.           nextRowTemplate[emptyIndex[0]] = nextValue;
  639.           var nextValuesAvailable = new HashSet<byte>(valuesAvailable);
  640.           nextValuesAvailable.Remove(nextValue);
  641.           foreach (var result in GetPossibleMagicRows(nextRowTemplate, magicConstant, nextValuesAvailable))
  642.             yield return result;
  643.         }
  644.         break;
  645.     }
  646.   }
  647.  
  648.  
  649.   public IEnumerable<SwapInfo> GetSwaps()
  650.   {
  651.     return MovesDone;
  652.   }
  653. }
  654.  
  655. public struct CompressedGridState4 : IEquatable<CompressedGridState4>
  656. {
  657.   public static implicit operator CompressedGridState4(GridState source) => new CompressedGridState4(source);
  658.   public static implicit operator GridState(CompressedGridState4 source) => source.GetGridState();
  659.  
  660.   // thoretically, we only "need" just under 96 bits integer, but code for that seems unlikely to be much of an improvement...
  661.   // limit is based on 16! * 17!
  662.   UInt64 _compressedData1;
  663.   UInt64 _compressedData2;
  664.  
  665.   public CompressedGridState4(GridState source)
  666.   {
  667.     if (source.ColumnCount != 4 || source.CellStates.Length != 16)
  668.       throw new InvalidOperationException("This compression function assumes a 4 x 4 grid containing precisely the numbers 1-16");
  669.  
  670.     var cellStates = source.CellStates;
  671.     UInt64 multiplier1 = 1;
  672.     UInt64 multiplier2 = 1;
  673.     _compressedData1 = 0;
  674.     _compressedData2 = 0;
  675.  
  676.     List<int> possiblePositions = Enumerable.Range(0, 16).ToList();
  677.  
  678.     for (uint i = 1; i <= 16; i++)
  679.     {
  680.       // find index of value i
  681.       int pos = 0;
  682.       while (cellStates[pos].Value != i)
  683.       {
  684.         if (pos >= cellStates.Length - 1)
  685.           throw new InvalidOperationException($"Value {i} not found in grid state");
  686.         else
  687.           pos++;
  688.       }
  689.       int posIndex = possiblePositions.IndexOf(pos);
  690.       if (posIndex == -1)
  691.         throw new InvalidOperationException($"Value {i} found at position {pos} which isn't possible! Possible: [{string.Join(",", possiblePositions)}]");
  692.  
  693.       _compressedData1 += multiplier1 * (uint)posIndex;
  694.       multiplier1 *= (uint)possiblePositions.Count;
  695.  
  696.       possiblePositions.RemoveAt(posIndex);
  697.  
  698.       _compressedData2 += multiplier2 * (uint)cellStates[pos].MovesLeft;
  699.       multiplier2 *= i + 1;
  700.  
  701.       //Console.WriteLine($"Compression diagnostic: i={i}, pos={pos} index {posIndex}, moves = {cellStates[pos].MovesLeft}, multiplier = {multiplier}, data = {_compressedData}");
  702.     }
  703.   }
  704.  
  705.   public GridState GetGridState()
  706.   {
  707.     var cellStates = new CellState[16];
  708.     UInt64 multiplier1 = 1;
  709.     UInt64 multiplier2 = 1;
  710.  
  711.     List<int> possiblePositions = Enumerable.Range(0, 16).ToList();
  712.  
  713.     for (byte i = 1; i <= 16; i++)
  714.     {
  715.       UInt64 nextMultiplier1 = multiplier1 * (uint)possiblePositions.Count;
  716.       int posIndex = (int)(_compressedData1 % nextMultiplier1 / multiplier1);
  717.       int pos = possiblePositions[posIndex];
  718.       possiblePositions.RemoveAt(posIndex);
  719.  
  720.       multiplier1 = nextMultiplier1;
  721.  
  722.       UInt64 nextMultiplier2 = multiplier2 * ((uint)i + 1);
  723.       byte movesLeft = (byte)(_compressedData2 % nextMultiplier2 / multiplier2);
  724.       multiplier2 = nextMultiplier2;
  725.  
  726.       cellStates[pos] = new CellState { Value = i, MovesLeft = movesLeft };
  727.  
  728.       //Console.WriteLine($"Decompression diagnostic: i={i}, pos={pos} index {posIndex}, moves = {cellStates[pos].MovesLeft}, multiplier = {multiplier}, data = {_compressedData}");
  729.     }
  730.  
  731.     return new GridState(4, cellStates);
  732.   }
  733.  
  734.   public override bool Equals(object obj)
  735.   {
  736.     return base.Equals(obj) || obj is CompressedGridState4 other && Equals(other) || obj is GridState otherExpanded && GetGridState().Equals(otherExpanded);
  737.   }
  738.  
  739.   public bool Equals(CompressedGridState4 other)
  740.   {
  741.     return _compressedData2 == other._compressedData2 && _compressedData1 == other._compressedData1;
  742.   }
  743.  
  744.   public override int GetHashCode()
  745.   {
  746.     return _compressedData1.GetHashCode() + _compressedData2.GetHashCode();
  747.   }
  748.  
  749. }
  750.  
  751.  
  752. public struct CompressedPuzzleState
  753. {
  754.   public static implicit operator CompressedPuzzleState(PuzzleState source) => new CompressedPuzzleState(source);
  755.  
  756.   public CompressedPuzzleState(PuzzleState source)
  757.   {
  758.     byte[] compressedMovesDone = source.MovesDone.Select(m => m.CompressToByte()).ToArray();
  759.     // now further compress the byte array into an int...
  760.     _compressedState = MakeCompressedState(compressedMovesDone);
  761.   }
  762.  
  763.   public static int ShortCodeCount => ShortCodesDict.Count;
  764.  
  765.   static Dictionary<IntCode, UInt32> ShortCodesDict { get; } = new Dictionary<IntCode, uint>();
  766.   static List<IntCode> ShortCodesList { get; } = new List<IntCode>();
  767.  
  768.   static private UInt32 AllocateShortCode(IntCode compressedState)
  769.   {
  770.     UInt32 result;
  771.     lock (ShortCodesDict)
  772.     {
  773.       if (!ShortCodesDict.TryGetValue(compressedState, out result))
  774.       {
  775.         ShortCodesDict.Add(compressedState, result = (UInt32)ShortCodesDict.Count);
  776.         ShortCodesList.Add(compressedState);
  777.         //Console.WriteLine($"Allocated short code {result} for swaps: {string.Join(", ", GetMovesFromCompressedState(compressedState))}");
  778.       }
  779.     }
  780.     return result;
  781.   }
  782.  
  783.   static private IntCode GetCompressedStateFromShortCode(UInt32 code)
  784.   {
  785.     if (code > ShortCodesList.Count)
  786.       throw new ArgumentOutOfRangeException(nameof(code), $"Value {code} larger than number of codes allocated: {ShortCodesList.Count}");
  787.     lock (ShortCodesDict)
  788.       return ShortCodesList[(int)code];
  789.   }
  790.  
  791.  
  792.   static private IntCode MakeCompressedState(byte[] compressedMoves)
  793.   {
  794.     // in fact each byte has a value no higher than 23.
  795.     // so we can fit 6 bytes into a 32 bit int, with 2 bits spare to indicate other encoding modes for longer sequences.
  796.     // for up to 6 bytes, the top 2 bits are set using this initial simple compression
  797.     IntCode result = IntCode.MaxValue;
  798.  
  799.     int limit = maxSymbols;
  800.  
  801.     int i = 0;
  802.  
  803.     compressMore:
  804.     limit = Math.Min(limit, compressedMoves.Length);
  805.  
  806.     for (; i < limit; i++)
  807.       result = (result << bitsPerSymbol) | compressedMoves[i];
  808.  
  809.     if (compressedMoves.Length > limit)
  810.     {
  811.       // compress the result so far...
  812.       result = AllocateShortCode(result);
  813.       if ((result >> (bitsPerSymbol * 4)) == 0)
  814.       {
  815.         // short code can fit in space of 4 "normal" moves (applies to the first few that we find, which were therefore the "least costly" moves, and likely to be re-used a lot)
  816.         // use code 30 to mark this, which can be done simply by shifting an extra bit.
  817.         result += IntCode.MaxValue << (bitsPerSymbol * 4 + 1);
  818.         limit += maxSymbols - 5;
  819.         goto compressMore;
  820.       }
  821.       else if ((result >> (bitsPerSymbol * 5 + 1)) == 0)
  822.       {
  823.         // codes 28 and 30 mark a 5-symbol code, with 1 bit of the code stored in "parent" - i.e. 26 bits total.
  824.         result += IntCode.MaxValue << (bitsPerSymbol * 5 + 2);
  825.         limit += maxSymbols - 6;
  826.         goto compressMore;
  827.       }
  828.       else if ((result >> (bitsPerSymbol * 6 + 2)) == 0)
  829.       {
  830.         // codes 24 to 27 mark a 6-symbol code, with 2 bits of the code stored in "parent" - i.e. 32 bits total.
  831.         // this should be enough for all further codes we could conceivably need before running out of memory,
  832.         // and also within the limit of Dictionary<,>.Count!
  833.         result += IntCode.MaxValue << (bitsPerSymbol * 6 + 3);
  834.         limit += maxSymbols - 7; // for 64-bit we still have enough space for 5 more symbols before allocating a new code.
  835.         goto compressMore;
  836.       }
  837.       else
  838.         throw new NotImplementedException();
  839.     }
  840.     try
  841.     {
  842.       DecodeCompressedState(result);
  843.     }
  844.     catch
  845.     {
  846.       Console.Write($"I got an exception when trying to encode byte array [{string.Join(",", compressedMoves)}]");
  847.     }
  848.     return result;
  849.   }
  850.  
  851.   const int bitsPerSymbol = 5;
  852.   const int maxSymbols = 64 / bitsPerSymbol;
  853.   const IntCode singleMask = (1 << bitsPerSymbol) - 1;
  854.   const IntCode maskForCode2 = (1 << (4 * bitsPerSymbol)) - 1;
  855.   const IntCode maskForCode3 = (1 << (5 * bitsPerSymbol + 1)) - 1;
  856.   const IntCode maskForCode4 = (1 << (6 * bitsPerSymbol + 2)) - 1;
  857.  
  858.   static private byte[] DecodeCompressedState(IntCode state)
  859.   {
  860.     List<byte> resultList = new List<byte>(16);
  861.     switch(state >> bitsPerSymbol * maxSymbols) // i.e. 30 in current implementation, giving results 0-3
  862.     {
  863.       case 15: // simple encoding mode in 64 bit int.
  864.       case 3: // simple encoding mode - 6 blocks of 5 bits, possibly containing "embedded multibyte codes"
  865.         for (int shift = bitsPerSymbol * (maxSymbols-1); shift >= 0; shift -= bitsPerSymbol)
  866.         {
  867.           IntCode next = (state >> shift) & singleMask;
  868.           if (next < 24)
  869.           {
  870.             resultList.Add((byte)next);
  871.             continue;
  872.           }
  873.           UInt32 code;
  874.           bool debug = false;
  875.           switch(next)
  876.           {
  877.             case 31: continue; // padding bits for short sequences.
  878.             case 30:
  879.               shift -= bitsPerSymbol * 4;
  880.               if (shift < 0)
  881.                 throw new InvalidOperationException("Bad position for code '30'");
  882.               code = (uint)((state >> shift) & maskForCode2);
  883.               debug = false;
  884.               break;
  885.  
  886.             case 29:
  887.             case 28:
  888.               shift -= bitsPerSymbol * 5;
  889.               if (shift < 0)
  890.                 throw new InvalidOperationException("Bad position for code '28'");
  891.               code = (uint)((state >> shift) & maskForCode3);
  892.               debug = false;
  893.               break;
  894.  
  895.             default:
  896.               shift -= bitsPerSymbol * 6;
  897.               if (shift < 0)
  898.                 throw new InvalidOperationException("Bad position for code '24'");
  899.               code = (uint)((state >> shift) & maskForCode4);
  900.               debug = next > 26;
  901.               break;
  902.           }
  903.           var decoded = DecodeCompressedState(GetCompressedStateFromShortCode(code));
  904.           resultList.AddRange(decoded);
  905.           if (debug)
  906.             Console.WriteLine($"Decoded short code {code:X} from {state:X} as swaps: {string.Join(", ", decoded.Select(SwapInfo.ExpandFromByte))}");
  907.  
  908.         }
  909.         break;
  910.       default:
  911.         throw new NotImplementedException($"Wrong top bits for state {state:X}"); // other encoding system to be defined....
  912.     }
  913.  
  914.     return resultList.ToArray();
  915.   }
  916.  
  917.   public PuzzleState GetPuzzleState(GridState gridState) => new PuzzleState(this, gridState) { AllowWrapAround = false };
  918.  
  919.   private IntCode _compressedState;
  920.  
  921.   private static SwapInfo[] GetMovesFromCompressedState(IntCode state) => DecodeCompressedState(state).Select(SwapInfo.ExpandFromByte).ToArray();
  922.  
  923.   public SwapInfo[] MovesDone => GetMovesFromCompressedState(_compressedState);
  924.  
  925. }
  926.  
  927.  
  928. public class SquareSwap
  929. {
  930.   static public void Main()
  931.   {
  932.     Console.OutputEncoding = System.Text.Encoding.UTF8;
  933.  
  934.     HashSet<GridState> knownMagicSquares = new HashSet<GridState>();
  935.  
  936.     foreach(var magic in PuzzleState.GenerateMagicSquares(4))
  937.     {
  938.       magic.GridState.SetMagicConstant(); // must be called before first call to IsMagic - doesn't hurt to call again.
  939.       if (!magic.GridState.IsMagic())
  940.         throw new InvalidOperationException("But it's not magic!");
  941.       if (!knownMagicSquares.Add(magic.GridState))
  942.         throw new InvalidOperationException("But we already had that one!");
  943.     }
  944.  
  945.     Console.WriteLine($"Total magic squares found: {knownMagicSquares.Count}");
  946.  
  947.     foreach (var magic in knownMagicSquares)
  948.     {
  949.       var reversed = new GridState(4, magic.CellStates.Reverse().ToArray());
  950.       if(!knownMagicSquares.Contains(reversed))
  951.       {
  952.         throw new InvalidOperationException($"We have {magic.GetHashCode()}:\n{magic}\nbut not {reversed.GetHashCode()}\n{reversed}");
  953.       }
  954.     }
  955.  
  956.     Console.WriteLine("Sanity check complete - all magic squares found when reversed.");
  957.  
  958.     var startingGrid = knownMagicSquares.First();
  959.  
  960.    
  961.     /*var startingState = new PuzzleState("867G", "143A", "9D2E", "BC5F") { AllowWrapAround = false };
  962.  
  963.     //    var startingState = new PuzzleState("1FE4", "BA58", "G69C", "732D") { AllowWrapAround = false }; // test starting state similar to "given" magic square
  964.  
  965.     //    var startingState = new PuzzleState("1FE4", "AB85", "769C", "G32D") { AllowWrapAround = false }; // test starting state similar to "given" magic square
  966.  
  967.  
  968.     var magicConstant = startingState.GridState.SetMagicConstant();
  969.  
  970.     Console.WriteLine($"Starting state [total moves left: {startingState.GridState.TotalMovesLeft}, magic constant = {magicConstant}]:");
  971.     Console.WriteLine(startingState.GridState);
  972.  
  973.     // test case for compression and decompression functions.
  974.     CompressedGridState4 comp = startingState.GridState;
  975.     Console.WriteLine($"Equality check: {comp.Equals((object)startingState.GridState)}");
  976.  
  977.     var startingGrid = startingState.GridState;
  978.     */
  979.     var workingSets = Enumerable.Range(0, startingGrid.TotalMovesLeft + 1).Select(i => new Dictionary<CompressedGridState4, CompressedPuzzleState>()).ToArray();
  980.  
  981.     // workingSets[startingState.GridState.TotalMovesLeft].Add(startingGrid, startingState);
  982.  
  983.     foreach (var magic in knownMagicSquares)
  984.       workingSets[startingGrid.TotalMovesLeft].Add(magic, PuzzleState.CreateStartingState(magic));
  985.  
  986.     bool found = false;
  987.  
  988.     for (int i = workingSets.Length - 1; i > 0 /*&& !found*/; i--)
  989.     {
  990.       Console.WriteLine($"Starting on working set #{i}, which has {workingSets[i].Count} members... [{Enumerable.Range(0, i).Sum(x => workingSets[x].Count)} already in later working sets, {CompressedPuzzleState.ShortCodeCount} short codes used]");
  991.  
  992.       int rejectedCount = 0;
  993.  
  994.       System.Threading.Tasks.Parallel.ForEach(workingSets[i], pair =>
  995.       {
  996.         GridState gridState = pair.Key; // implicit conversion to decompress
  997.         var puzzleState = pair.Value.GetPuzzleState(gridState);
  998.         // non-compressed puzzle state is now only scoped to this block.
  999.  
  1000.         if (puzzleState.IsTargetState)
  1001.         {
  1002.           found = true;
  1003.           lock (workingSets[i])
  1004.           {
  1005.             Console.WriteLine("Found solution!");
  1006.             Console.WriteLine($"... after swaps: {string.Join(", ", puzzleState.GetSwaps())}");
  1007.             Console.WriteLine(puzzleState.GridState);
  1008.           }
  1009.         }
  1010.         else if (puzzleState.CheckCompatibleWithTargetState())
  1011.         {
  1012.           foreach (var nextState in puzzleState.GetStatesAfterValidMoves().Where(s => s.CheckCompatibleWithTargetState()))
  1013.           {
  1014.             int movesLeft = nextState.GridState.TotalMovesLeft;
  1015.             //var swapInfo = nextState.MovesDone.Last();
  1016.             //Console.WriteLine($"Possible next state after swapping R{swapInfo.rowFrom + 1}C{swapInfo.colFrom + 1} with R{swapInfo.rowTo + 1}C{swapInfo.colTo + 1} [total moves left: {movesLeft}]:");
  1017.             //Console.WriteLine(nextState.GridState);
  1018.  
  1019.             if (movesLeft >= i)
  1020.               throw new InvalidOperationException("Total moves left didn't decrease???!");
  1021.  
  1022.             // implicit conversion to compressed data.
  1023.             CompressedGridState4 nextGridStateCompressed = nextState.GridState;
  1024.  
  1025.             lock (workingSets[movesLeft])
  1026.             {
  1027.               if (workingSets[movesLeft].TryGetValue(nextGridStateCompressed, out var firstEquivalentState))
  1028.               {
  1029.                 //Console.WriteLine("Duplicate found!");
  1030.               }
  1031.               else
  1032.                 workingSets[movesLeft].Add(nextGridStateCompressed, nextState);
  1033.             }
  1034.           }
  1035.         }
  1036.         else
  1037.           System.Threading.Interlocked.Increment(ref rejectedCount);
  1038.       });
  1039.       if (rejectedCount > 0)
  1040.         Console.WriteLine($"Rejected {rejectedCount} members as incompatible with target state");
  1041.       // allow dictionaries of compresed state to be reclaimed by garbage collector
  1042.       workingSets[i] = null;
  1043.     }
  1044.   }
  1045. }
  1046.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement