using System; using System.Runtime.CompilerServices; namespace PerfectNumbers.Core.ByDivisor; /// /// Helper utilities for the BitContradiction divisor search. These methods implement the /// interval-based carry propagation described in the BitContradictions plan document. /// The solver itself will build on top of these primitives. /// internal static class BitContradictionSolverWithAMultiplier { private const int PingPongBlockColumns = 64; private const int TailCarryBatchColumns = 1024; private const int TailCarryBatchRepeats = 1; [ThreadStatic] private static TopDownPruneFailure? _lastTopDownFailure; private static bool _debugEnabled = true; private static int _parityZeroLogCount; internal static TopDownPruneFailure? LastTopDownFailure => _lastTopDownFailure; internal static void SetDebugEnabled(bool enabled) => _debugEnabled = enabled; internal static ColumnBounds ComputeColumnBounds( ReadOnlySpan multiplicand, ReadOnlySpan multiplier, int columnIndex) { long forced = 0; long possible = 0; int multiplicandLength = multiplicand.Length; int multiplierLength = multiplier.Length; for (int i = 0; i <= columnIndex; i++) { bool a = i < multiplicandLength && multiplicand[i]; bool q = columnIndex - i < multiplierLength && multiplier[columnIndex - i]; if (!a && !q) { continue; } if (a && q) { forced++; } possible++; } return new ColumnBounds(forced, possible); } internal static bool TryPropagateCarry( CarryRange currentCarry, long forcedOnes, long possibleOnes, int requiredResultBit, out CarryRange nextCarry, out ContradictionReason reason) { long minSum = currentCarry.Min + forcedOnes; long maxSum = currentCarry.Max + possibleOnes; int parity = requiredResultBit & 1; minSum = AlignUpToParity(minSum, parity); maxSum = AlignDownToParity(maxSum, parity); if (minSum > maxSum) { nextCarry = default; reason = ContradictionReason.ParityUnreachable; return false; } nextCarry = new CarryRange((minSum - requiredResultBit) >> 1, (maxSum - requiredResultBit) >> 1); reason = ContradictionReason.None; return true; } /// /// Full column-wise propagation for a given set of q one-bit offsets (LSB=0) and exponent p. /// Returns true if the solver could decide locally (divides or contradiction). /// internal static bool TryCheckDivisibilityFromOneOffsets( ReadOnlySpan qOneOffsets, ulong p, out bool divides, out ContradictionReason reason) { divides = false; _lastTopDownFailure = null; // These conditions will never be true on EvenPerfectBitScanner execution path. // if (p == 0 || qOneOffsets.Length == 0) // { // reason = ContradictionReason.ParityUnreachable; // return true; // } // While the scanner supports p >= 31, it's a tiny subset of all checked exponents. No need to support it. // Exact small-case evaluation to short-circuit both success and failure deterministically. // int highestBit = qOneOffsets[^1]; // if (p <= 64 && highestBit < 63) // { // ulong q = 0; // foreach (int bit in qOneOffsets) // { // q |= 1UL << bit; // } // ulong mersenne = (1UL << (int)p) - 1UL; // divides = q != 0 && (mersenne % q == 0); // reason = divides ? ContradictionReason.None : ContradictionReason.ParityUnreachable; // return true; // } int qOneOffsetsLength = qOneOffsets.Length; int maxOffset = qOneOffsets[qOneOffsetsLength - 1]; if ((ulong)(maxOffset + 1) >= p) { reason = ContradictionReason.TruncatedLength; return true; } // q from EvenPerfectBitScanner will always have the correct 2kp+1 form. // if (qOneOffsets[0] != 0) // { // reason = ContradictionReason.ParityUnreachable; // return true; // } int windowSize = maxOffset + 1; var segmentState0 = new sbyte[windowSize]; var segmentState1 = new sbyte[windowSize]; long segmentBase = 0; long maxKnownA = -1L; long pLong = (long)p; long maxAllowedA = pLong - (maxOffset + 1L); if (maxAllowedA < 0) { reason = ContradictionReason.TruncatedLength; return true; } if (!TryTopDownCarryPrune(qOneOffsets, qOneOffsetsLength, pLong, maxAllowedA, out _lastTopDownFailure)) { reason = ContradictionReason.ParityUnreachable; return true; } long lowColumn = 0; long highColumn = pLong - 1; CarryRange carryLow = CarryRange.Zero; CarryRange carryHigh = CarryRange.Zero; bool processTop = true; while (lowColumn <= highColumn) { int remaining = (int)(highColumn - lowColumn + 1); int blockSize = remaining < PingPongBlockColumns ? remaining : PingPongBlockColumns; if (processTop) { if (!TryProcessTopDownBlock(qOneOffsets, qOneOffsetsLength, maxAllowedA, highColumn, blockSize, carryHigh, out carryHigh, out _lastTopDownFailure)) { reason = ContradictionReason.ParityUnreachable; return true; } highColumn -= blockSize; } else { if (!TryProcessBottomUpBlock(qOneOffsets, maxAllowedA, lowColumn, blockSize, ref carryLow, ref maxKnownA, ref segmentBase, segmentState0, segmentState1, out reason)) { return true; } lowColumn += blockSize; } processTop = !processTop; } long carryMin = carryLow.Min > carryHigh.Min ? carryLow.Min : carryHigh.Min; long carryMax = carryLow.Max < carryHigh.Max ? carryLow.Max : carryHigh.Max; if (carryMin > carryMax) { divides = false; reason = ContradictionReason.ParityUnreachable; return true; } divides = true; reason = ContradictionReason.None; return true; } internal static bool TryCheckDivisibilityFromOneOffsets( ReadOnlySpan qOneOffsets, ulong p, out bool divides) => TryCheckDivisibilityFromOneOffsets(qOneOffsets, p, out divides, out _); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static bool TryAdvanceZeroColumns(ref CarryRange carry, int columnCount) { long carryMin = carry.Min; long carryMax = carry.Max; while (columnCount > 0) { int step = columnCount > 62 ? 62 : columnCount; long add = (1L << step) - 1L; long nextMin = (carryMin + add) >> step; long nextMax = carryMax >> step; if (nextMin > nextMax) { return false; } carryMin = nextMin; carryMax = nextMax; columnCount -= step; } carry = new CarryRange(carryMin, carryMax); return true; } private static bool TryProcessBottomUpBlock( ReadOnlySpan qOneOffsets, long maxAllowedA, long startColumn, int columnCount, ref CarryRange carry, ref long maxKnownA, ref long segmentBase, sbyte[] segmentState0, sbyte[] segmentState1, out ContradictionReason reason) { long endColumn = startColumn + columnCount; int qOneOffsetsLength = qOneOffsets.Length; int windowSize = segmentState0.Length; for (long column = startColumn; column < endColumn; column++) { long forced = 0L; long unknown = 0L; long chosenIndex = -1L; long newBase = (column / windowSize) * windowSize; if (newBase != segmentBase) { if (newBase == segmentBase + windowSize) { (segmentState0, segmentState1) = (segmentState1, segmentState0); Array.Clear(segmentState0); } else { Array.Clear(segmentState0); Array.Clear(segmentState1); } segmentBase = newBase; } for (int offsetIndex = 0; offsetIndex < qOneOffsetsLength; offsetIndex++) { int offset = qOneOffsets[offsetIndex]; long aIndex = column - offset; if (aIndex < 0) { break; } if (aIndex > maxAllowedA) { continue; // a must be zero here } if (aIndex >= segmentBase) { int slot = (int)(aIndex - segmentBase); sbyte state = segmentState0[slot]; if (state == 1) { forced++; } else if (state == 0) { unknown++; if (chosenIndex == -1L) { chosenIndex = aIndex; } } } else if (aIndex >= segmentBase - windowSize) { int slot = (int)(aIndex - (segmentBase - windowSize)); sbyte state = segmentState1[slot]; if (state == 1) { forced++; } else if (state == 0) { unknown++; if (chosenIndex == -1L) { chosenIndex = aIndex; } } } else { unknown++; if (chosenIndex == -1L) { chosenIndex = aIndex; } } } long minSum = carry.Min; long maxSum = carry.Max; if (unknown == 1 && ((minSum ^ maxSum) & 1) == 0) { int requiredUnknownParity = 1 ^ ((int)minSum & 1) ^ ((int)forced & 1); if (requiredUnknownParity != 0) { if (chosenIndex >= 0) { if (chosenIndex >= segmentBase) { int slot = (int)(chosenIndex - segmentBase); segmentState0[slot] = 1; } else { int slot = (int)(chosenIndex - (segmentBase - windowSize)); segmentState1[slot] = 1; } forced++; unknown = 0; if (chosenIndex > maxKnownA) { maxKnownA = chosenIndex; } } } else { // if (_debugEnabled && _parityZeroLogCount < 8) // { // _parityZeroLogCount++; // Console.WriteLine($"[bit-contradiction] unknown==1 parity forces zero at column={column} carry=[{minSum},{maxSum}] forced={forced} chosenIndex={chosenIndex}"); // } // Treat the single unknown as 0 for parity, but do not record it as fixed. unknown = 0; } } if (unknown == 0) { int requiredCarryParity = 1 ^ ((int)forced & 1); long carryMin = AlignUpToParity(carry.Min, requiredCarryParity); long carryMax = AlignDownToParity(carry.Max, requiredCarryParity); if (carryMin > carryMax) { reason = ContradictionReason.ParityUnreachable; return false; } carry = new CarryRange(carryMin, carryMax); minSum = carry.Min; maxSum = carry.Max; } minSum += forced; maxSum += forced + unknown; const int requiredParity = 1; if (minSum == maxSum) { if ((minSum & 1) != requiredParity) { reason = ContradictionReason.ParityUnreachable; return false; } long nextCarry = (minSum - requiredParity) >> 1; carry = CarryRange.Single(nextCarry); } else { if ((minSum & 1) != requiredParity && unknown == 0) { reason = ContradictionReason.ParityUnreachable; return false; } if (!TryPropagateCarry( carry, forced, forced + unknown, requiredParity, out var next, out var propagateReason)) { reason = propagateReason; return false; } carry = next; } } reason = ContradictionReason.None; return true; } private static bool TryProcessTopDownBlock( ReadOnlySpan qOneOffsets, int qOneOffsetsLength, long maxAllowedA, long startHighColumn, int columnCount, CarryRange carryOut, out CarryRange nextCarryOut, out TopDownPruneFailure? failure) { failure = null; long carryOutMin = carryOut.Min, carryOutMax = carryOut.Max, value; int windowStart = qOneOffsetsLength; int windowEnd = qOneOffsetsLength; long endColumn = startHighColumn - columnCount + 1; long column = startHighColumn; while (column >= endColumn) { // value is used as high column index value = column; while (windowEnd > 0 && qOneOffsets[windowEnd - 1] > value) { windowEnd--; } // value is used as low column index value = column - maxAllowedA; if (value < 0) { value = 0; } while (windowStart > 0 && qOneOffsets[windowStart - 1] >= value) { windowStart--; } if (windowStart > windowEnd) { windowStart = windowEnd; } // value is unknown bits count here value = windowEnd - windowStart; if (value == 0) { long lastEnd = windowEnd > 0 ? qOneOffsets[windowEnd - 1] : long.MinValue; long lastStart = windowStart > 0 ? qOneOffsets[windowStart - 1] + maxAllowedA : long.MinValue; long chunkEnd = lastEnd > lastStart ? lastEnd : lastStart; if (chunkEnd < endColumn) { chunkEnd = endColumn; } int steps = (int)(column - chunkEnd + 1); if (steps > 1) { if (!AdvanceTopDownUnknown0(ref carryOutMin, ref carryOutMax, steps)) { failure = new TopDownPruneFailure(column, carryOutMin, carryOutMax, value); nextCarryOut = default; return false; } column = chunkEnd - 1; continue; } } else if (value == 1) { long lastEnd = windowEnd > 0 ? qOneOffsets[windowEnd - 1] : long.MinValue; long lastStart = windowStart > 0 ? qOneOffsets[windowStart - 1] + maxAllowedA : long.MinValue; long chunkEnd = lastEnd > lastStart ? lastEnd : lastStart; if (chunkEnd < endColumn) { chunkEnd = endColumn; } int steps = (int)(column - chunkEnd + 1); if (steps > 1) { if (!AdvanceTopDownUnknown1(ref carryOutMin, ref carryOutMax, steps)) { failure = new TopDownPruneFailure(column, carryOutMin, carryOutMax, value); nextCarryOut = default; return false; } column = chunkEnd - 1; continue; } } // carryOutMin now becomes nextCarryMin to limit registry pressure carryOutMin = (carryOutMin << 1) + 1 - value; // carryOutMax now becomes nextCarryMax to limit registry pressure carryOutMax = (carryOutMax << 1) + 1; if (carryOutMin < 0) { carryOutMin = 0; } if (carryOutMax < 0) { carryOutMax = 0; } if (carryOutMin > carryOutMax) { failure = new TopDownPruneFailure(column, carryOutMin, carryOutMax, value); nextCarryOut = default; return false; } column--; } nextCarryOut = new CarryRange(carryOutMin, carryOutMax); return true; } private static long AlignUpToParity(long value, int parity) => (value & 1) == parity ? value : value + 1; private static long AlignDownToParity(long value, int parity) => (value & 1) == parity ? value : value - 1; private static bool TryTopDownCarryPrune( ReadOnlySpan qOneOffsets, int qOneOffsetsLength, long pLong, long maxAllowedA, out TopDownPruneFailure? failure) { failure = null; long carryOutMin = 0, carryOutMax = 0, value; int windowStart = qOneOffsetsLength; int windowEnd = qOneOffsetsLength; long column = pLong - 1; while (column >= 0) { // value is used as high column index value = column; while (windowEnd > 0 && qOneOffsets[windowEnd - 1] > value) { windowEnd--; } // value is used as low column index value = column - maxAllowedA; if (value < 0) { value = 0; } while (windowStart > 0 && qOneOffsets[windowStart - 1] >= value) { windowStart--; } if (windowStart > windowEnd) { windowStart = windowEnd; } // value is used as unknown bits count here value = windowEnd - windowStart; if (value == 0) { long lastEnd = windowEnd > 0 ? qOneOffsets[windowEnd - 1] : long.MinValue; long lastStart = windowStart > 0 ? qOneOffsets[windowStart - 1] + maxAllowedA : long.MinValue; long chunkEnd = lastEnd > lastStart ? lastEnd : lastStart; if (chunkEnd < 0) { chunkEnd = 0; } int steps = (int)(column - chunkEnd + 1); if (steps > 1) { if (!AdvanceTopDownUnknown0(ref carryOutMin, ref carryOutMax, steps)) { failure = new TopDownPruneFailure(column, carryOutMin, carryOutMax, value); return false; } column = chunkEnd - 1; continue; } } else if (value == 1) { long lastEnd = windowEnd > 0 ? qOneOffsets[windowEnd - 1] : long.MinValue; long lastStart = windowStart > 0 ? qOneOffsets[windowStart - 1] + maxAllowedA : long.MinValue; long chunkEnd = lastEnd > lastStart ? lastEnd : lastStart; if (chunkEnd < 0) { chunkEnd = 0; } int steps = (int)(column - chunkEnd + 1); if (steps > 1) { if (!AdvanceTopDownUnknown1(ref carryOutMin, ref carryOutMax, steps)) { failure = new TopDownPruneFailure(column, carryOutMin, carryOutMax, value); return false; } column = chunkEnd - 1; continue; } } // carryOutMin now becomes nextCarryMin to limit registry pressure carryOutMin = (carryOutMin << 1) + 1 - value; // carryOutMax now becomes nextCarryMax to limit registry pressure carryOutMax = (carryOutMax << 1) + 1; if (carryOutMin < 0) { carryOutMin = 0; } if (carryOutMax < 0) { carryOutMax = 0; } if (carryOutMin > carryOutMax) { failure = new TopDownPruneFailure(column, carryOutMin, carryOutMax, value); return false; } column--; } if (carryOutMin > 0) { failure = new TopDownPruneFailure(0, carryOutMin, carryOutMax, 0); return false; } return true; } private static bool AdvanceTopDownUnknown1(ref long carryOutMin, ref long carryOutMax, int steps) { while (steps > 0) { int step = steps > 62 ? 62 : steps; long pow = 1L << step; if (carryOutMin > (long.MaxValue >> step) || carryOutMax > ((long.MaxValue - (pow - 1)) >> step)) { carryOutMin = 0; carryOutMax = long.MaxValue; return true; } carryOutMin <<= step; carryOutMax = (carryOutMax << step) + (pow - 1); steps -= step; } return true; } private static bool AdvanceTopDownUnknown0(ref long carryOutMin, ref long carryOutMax, int steps) { while (steps > 0) { int step = steps > 62 ? 62 : steps; long add = (1L << step) - 1L; if (carryOutMin > ((long.MaxValue - add) >> step) || carryOutMax > ((long.MaxValue - add) >> step)) { carryOutMin = 0; carryOutMax = long.MaxValue; return true; } carryOutMin = (carryOutMin << step) + add; carryOutMax = (carryOutMax << step) + add; steps -= step; } return true; } }