using System; using System.Runtime.CompilerServices; namespace PerfectNumbers.Core.ByDivisor; public readonly struct ColumnBounds(int forcedOnes, int possibleOnes) { public readonly int ForcedOnes = forcedOnes; public readonly int PossibleOnes = possibleOnes; } public readonly struct CarryRange(long min, long max) { public readonly long Min = min; public readonly long Max = max; public static CarryRange Single(long value) => new(value, value); public static readonly CarryRange Zero = Single(0); } public enum ContradictionReason { None, ParityUnreachable, TruncatedLength, TailNotZero, } /// /// 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 BitContradictionSolverWithQMultiplier { private const int PingPongBlockColumns = 64; private const int TailCarryBatchColumns = 1024; private const int TailCarryBatchRepeats = 1; private const int TopDownCacheCapacity = 16; #if DETAILED_LOG public readonly struct TopDownPruneFailure(int column, long carryMin, long carryMax, int unknown) { public readonly int Column = column; public readonly long CarryMin = carryMin; public readonly long CarryMax = carryMax; public readonly int Unknown = unknown; } private readonly struct TopDownCacheValue(bool success, TopDownPruneFailure? failure) { public readonly bool Success = success; public readonly TopDownPruneFailure? Failure = failure; } internal readonly struct BottomUpFailure(int column, long carryMin, long carryMax, int forced, int unknown) { public readonly int Column = column; public readonly long CarryMin = carryMin; public readonly long CarryMax = carryMax; public readonly int Forced = forced; public readonly int Unknown = unknown; } [ThreadStatic] private static TopDownPruneFailure? _lastTopDownFailure; [ThreadStatic] private static BottomUpFailure? _lastBottomUpFailure; [ThreadStatic] private static Dictionary? _topDownCache; internal static TopDownPruneFailure? LastTopDownFailure => _lastTopDownFailure; internal static BottomUpFailure? LastBottomUpFailure => _lastBottomUpFailure; #else [ThreadStatic] private static Dictionary? _topDownCache; #endif [ThreadStatic] private static List? _topDownCacheOrder; private readonly struct TopDownCacheKey(long hash, int length, int maxAllowedA, int pLong) { public readonly long Hash = hash; public readonly int Length = length; public readonly int MaxAllowedA = maxAllowedA; public readonly int PLong = pLong; public bool Equals(TopDownCacheKey other) => Hash == other.Hash && Length == other.Length && MaxAllowedA == other.MaxAllowedA && PLong == other.PLong; public override bool Equals(object? obj) => obj is TopDownCacheKey other && Equals(other); public override int GetHashCode() => HashCode.Combine(Hash, Length, MaxAllowedA, PLong); } private static long ComputeQOffsetsHash(ReadOnlySpan qOneOffsets) { const long fnvOffset = unchecked((long)1469598103934665603); const long fnvPrime = 1099511628211; long hash = fnvOffset; for (int i = 0; i < qOneOffsets.Length; i++) { hash ^= qOneOffsets[i]; hash *= fnvPrime; } return hash; } #if DETAILED_LOG private static bool TryGetTopDownCache(in TopDownCacheKey key, out TopDownCacheValue value) #else private static bool TryGetTopDownCache(in TopDownCacheKey key, out bool value) #endif { var cache = _topDownCache; if (cache == null) { cache = []; _topDownCache = cache; value = default; return false; } return cache.TryGetValue(key, out value); } #if DETAILED_LOG private static void StoreTopDownCache(in TopDownCacheKey key, in TopDownCacheValue value) #else private static void StoreTopDownCache(in TopDownCacheKey key, bool value) #endif { // _topDownCache is always non-null here on EvenPerfectBitScanner execution path. var topDownCache = _topDownCache!; if (topDownCache.ContainsKey(key)) { topDownCache[key] = value; return; } List? topDownCacheOrder = _topDownCacheOrder; if (topDownCacheOrder == null) { topDownCacheOrder = new List(TopDownCacheCapacity + 1); _topDownCacheOrder = topDownCacheOrder; } else if (topDownCacheOrder.Count >= TopDownCacheCapacity) { var evict = topDownCacheOrder[0]; topDownCacheOrder.RemoveAt(0); topDownCache.Remove(evict); } topDownCache[key] = value; topDownCacheOrder.Add(key); } internal static ColumnBounds ComputeColumnBounds( in ReadOnlySpan multiplicand, in ReadOnlySpan multiplier, int columnIndex) { int forced = 0; int 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); } #if DETAILED_LOG internal static bool TryPropagateCarry( CarryRange currentCarry, long forcedOnes, long possibleOnes, int requiredResultBit, out CarryRange nextCarry, out ContradictionReason reason) #else internal static bool TryPropagateCarry( CarryRange currentCarry, int forcedOnes, int possibleOnes, int requiredResultBit, out CarryRange nextCarry) #endif { 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; #if DETAILED_LOG reason = ContradictionReason.ParityUnreachable; #endif return false; } nextCarry = new CarryRange((minSum - requiredResultBit) >> 1, (maxSum - requiredResultBit) >> 1); #if DETAILED_LOG reason = ContradictionReason.None; #endif 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; #if DETAILED_LOG _lastTopDownFailure = null; _lastBottomUpFailure = null; #endif // 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; int pLong = (int)p; int maxAllowedA = pLong - (maxOffset + 1); if (maxAllowedA < 0) { reason = ContradictionReason.TruncatedLength; return true; } long qHash = ComputeQOffsetsHash(qOneOffsets); var cacheKey = new TopDownCacheKey(qHash, qOneOffsetsLength, maxAllowedA, pLong); if (TryGetTopDownCache(in cacheKey, out var cached)) { #if DETAILED_LOG _lastTopDownFailure = cached.Failure; if (!cached.Success) #else if (!cached) #endif { reason = ContradictionReason.ParityUnreachable; #if DETAILED_LOG Console.WriteLine($"[bit-contradiction] parity unreachable in top-down prune (failure={_lastTopDownFailure.HasValue})"); #endif return true; } } else { #if DETAILED_LOG if (!TryTopDownCarryPrune(qOneOffsets, qOneOffsetsLength, pLong, maxAllowedA, out _lastTopDownFailure)) #else if (!TryTopDownCarryPrune(qOneOffsets, qOneOffsetsLength, pLong, maxAllowedA)) #endif { #if DETAILED_LOG StoreTopDownCache(in cacheKey, new TopDownCacheValue(false, _lastTopDownFailure)); Console.WriteLine($"[bit-contradiction] parity unreachable in top-down prune (failure={_lastTopDownFailure.HasValue})"); #else StoreTopDownCache(in cacheKey, false); #endif reason = ContradictionReason.ParityUnreachable; return true; } #if DETAILED_LOG StoreTopDownCache(in cacheKey, new TopDownCacheValue(true, null)); #else StoreTopDownCache(in cacheKey, true); #endif } int lowColumn = 0; int highColumn = pLong - 1; CarryRange carryLow = CarryRange.Zero; CarryRange carryHigh = CarryRange.Zero; int bottomWindowStart = 0; int bottomWindowEnd = 0; int topWindowStart = qOneOffsetsLength; int topWindowEnd = qOneOffsetsLength; bool processTop = true; while (lowColumn <= highColumn) { int remaining = (int)(highColumn - lowColumn + 1); int blockSize = remaining < PingPongBlockColumns ? remaining : PingPongBlockColumns; if (processTop) { #if DETAILED_LOG if (!TryProcessTopDownBlock(qOneOffsets, qOneOffsetsLength, maxAllowedA, highColumn, blockSize, carryHigh, ref topWindowStart, ref topWindowEnd, out carryHigh, out _lastTopDownFailure)) #else if (!TryProcessTopDownBlock(qOneOffsets, qOneOffsetsLength, maxAllowedA, highColumn, blockSize, carryHigh, ref topWindowStart, ref topWindowEnd, out carryHigh)) #endif { reason = ContradictionReason.ParityUnreachable; return true; } highColumn -= blockSize; } else { if (!TryProcessBottomUpBlock(qOneOffsets, maxAllowedA, lowColumn, blockSize, ref carryLow, ref segmentBase, ref bottomWindowStart, ref bottomWindowEnd, 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) { #if DETAILED_LOG Console.WriteLine($"[bit-contradiction] parity unreachable after ping-pong carry merge (low=[{carryLow.Min},{carryLow.Max}] high=[{carryHigh.Min},{carryHigh.Max}])"); #endif reason = ContradictionReason.ParityUnreachable; divides = false; 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 > 30 ? 30 : columnCount; int add = (1 << step) - 1; 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, int maxAllowedA, int startColumn, int columnCount, ref CarryRange carry, ref long segmentBase, ref int windowStartIdx, ref int windowEndIdx, sbyte[] segmentState0, sbyte[] segmentState1, out ContradictionReason reason) { long endColumn = startColumn + columnCount; int qOneOffsetsLength = qOneOffsets.Length; int windowSize = segmentState0.Length; for (int column = startColumn; column < endColumn; column++) { int forced = 0; int unknown = 0; int chosenIndex = -1; int 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; } while (windowEndIdx < qOneOffsetsLength && qOneOffsets[windowEndIdx] <= column) { windowEndIdx++; } int lowerBound = column - maxAllowedA; while (windowStartIdx < windowEndIdx && qOneOffsets[windowStartIdx] < lowerBound) { windowStartIdx++; } for (int offsetIndex = windowStartIdx; offsetIndex < windowEndIdx; offsetIndex++) { int offset = qOneOffsets[offsetIndex]; int 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; } } else { 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) { #if DETAILED_LOG _lastBottomUpFailure = new BottomUpFailure(column, carryMin, carryMax, forced, unknown); #endif 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) { #if DETAILED_LOG _lastBottomUpFailure = new BottomUpFailure(column, carry.Min, carry.Max, forced, unknown); #endif reason = ContradictionReason.ParityUnreachable; return false; } long nextCarry = (minSum - requiredParity) >> 1; carry = CarryRange.Single(nextCarry); } else { if ((minSum & 1) != requiredParity && unknown == 0) { #if DETAILED_LOG _lastBottomUpFailure = new BottomUpFailure(column, carry.Min, carry.Max, forced, unknown); #endif reason = ContradictionReason.ParityUnreachable; return false; } #if DETAILED_LOG if (!TryPropagateCarry( carry, forced, forced + unknown, requiredParity, out var next, out var propagateReason)) #else if (!TryPropagateCarry( carry, forced, forced + unknown, requiredParity, out var next)) #endif { #if DETAILED_LOG _lastBottomUpFailure = new BottomUpFailure(column, carry.Min, carry.Max, forced, unknown); reason = propagateReason; #else reason = ContradictionReason.ParityUnreachable; #endif return false; } carry = next; } } reason = ContradictionReason.None; return true; } #if DETAILED_LOG private static bool TryProcessTopDownBlock( ReadOnlySpan qOneOffsets, int qOneOffsetsLength, int maxAllowedA, int startHighColumn, int columnCount, CarryRange carryOut, ref int windowStart, ref int windowEnd, out CarryRange nextCarryOut, out TopDownPruneFailure? failure) #else private static bool TryProcessTopDownBlock( ReadOnlySpan qOneOffsets, int qOneOffsetsLength, int maxAllowedA, int startHighColumn, int columnCount, CarryRange carryOut, ref int windowStart, ref int windowEnd, out CarryRange nextCarryOut) #endif { #if DETAILED_LOG failure = null; #endif long carryOutMin = carryOut.Min, carryOutMax = carryOut.Max; int value; int endColumn = startHighColumn - columnCount + 1; int 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) { int lastEnd = windowEnd > 0 ? qOneOffsets[windowEnd - 1] : int.MinValue; int lastStart = windowStart > 0 ? qOneOffsets[windowStart - 1] + maxAllowedA : int.MinValue; int 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)) { #if DETAILED_LOG failure = new TopDownPruneFailure(column, carryOutMin, carryOutMax, value); #endif nextCarryOut = default; return false; } column = chunkEnd - 1; continue; } } else if (value == 1) { int lastEnd = windowEnd > 0 ? qOneOffsets[windowEnd - 1] : int.MinValue; int lastStart = windowStart > 0 ? qOneOffsets[windowStart - 1] + maxAllowedA : int.MinValue; int 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)) { #if DETAILED_LOG failure = new TopDownPruneFailure(column, carryOutMin, carryOutMax, value); #endif 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) { #if DETAILED_LOG failure = new TopDownPruneFailure(column, carryOutMin, carryOutMax, value); #endif nextCarryOut = default; return false; } column--; } nextCarryOut = new CarryRange(carryOutMin, carryOutMax); return true; } private static long AlignUpToParity(long value, int parity) => (value & 1L) == parity ? value : value + 1L; private static long AlignDownToParity(long value, int parity) => (value & 1L) == parity ? value : value - 1L; #if DETAILED_LOG private static bool TryTopDownCarryPrune( ReadOnlySpan qOneOffsets, int qOneOffsetsLength, int pLong, int maxAllowedA, out TopDownPruneFailure? failure) #else private static bool TryTopDownCarryPrune( ReadOnlySpan qOneOffsets, int qOneOffsetsLength, int pLong, int maxAllowedA) #endif { #if DETAILED_LOG failure = null; #endif long carryOutMin = 0, carryOutMax = 0; int value; int windowStart = qOneOffsetsLength; int windowEnd = qOneOffsetsLength; int 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) { int lastEnd = windowEnd > 0 ? qOneOffsets[windowEnd - 1] : int.MinValue; int lastStart = windowStart > 0 ? qOneOffsets[windowStart - 1] + maxAllowedA : int.MinValue; int 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)) { #if DETAILED_LOG failure = new TopDownPruneFailure(column, carryOutMin, carryOutMax, value); #endif return false; } column = chunkEnd - 1; continue; } } else if (value == 1) { int lastEnd = windowEnd > 0 ? qOneOffsets[windowEnd - 1] : int.MinValue; int lastStart = windowStart > 0 ? qOneOffsets[windowStart - 1] + maxAllowedA : int.MinValue; int 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)) { #if DETAILED_LOG failure = new TopDownPruneFailure(column, carryOutMin, carryOutMax, value); #endif 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) { #if DETAILED_LOG failure = new TopDownPruneFailure(column, carryOutMin, carryOutMax, value); #endif return false; } column--; } if (carryOutMin > 0) { #if DETAILED_LOG failure = new TopDownPruneFailure(0, carryOutMin, carryOutMax, 0); #endif return false; } return true; } private static bool AdvanceTopDownUnknown1(ref long carryOutMin, ref long carryOutMax, int steps) { while (steps > 0) { int step = steps > 30 ? 30 : steps; int pow = 1 << step; if (carryOutMin > (long.MaxValue >> step) || carryOutMax > ((long.MaxValue - (pow - 1)) >> step)) { carryOutMin = 0; carryOutMax = int.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 > 30 ? 30 : steps; int add = (1 << step) - 1; if (carryOutMin > ((int.MaxValue - add) >> step) || carryOutMax > ((int.MaxValue - add) >> step)) { carryOutMin = 0; carryOutMax = int.MaxValue; return true; } carryOutMin = (carryOutMin << step) + add; carryOutMax = (carryOutMax << step) + add; steps -= step; } return true; } }