{ Solutions to the Advent Of Code. Copyright (C) 2023-2024 Stefan Müller This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . } unit UHotSprings; {$mode ObjFPC}{$H+} interface uses Classes, SysUtils, Math, Generics.Collections, USolver, UCommon, UMultiIndexEnumerator, UBinomialCoefficients; const COperationalChar = '.'; CDamagedChar = '#'; CWildcardChar = '?'; CPart2Repetition = 5; type TValidationLengths = array of array of Integer; { TDamage } TDamage = record Start, Length, CharsRemaining: Integer; end; TDamages = specialize TList; TBlockCombinationsCache = specialize THashMap; TCombinationsCache = specialize TObjectHashMap; { TBlock } TBlock = class private FPattern: string; FDamages: TDamages; FCombinationsCache: TBlockCombinationsCache; procedure ParseDamages; public constructor Create(const APattern: string; constref ACombinationsCache: TBlockCombinationsCache); destructor Destroy; override; property Pattern: string read FPattern; // List of damages in this block, containing exactly one entry for each sequence of consecutive damage characters in // the block's pattern, ordered such that a damage with lower index is further left. // For example, if Pattern is '??##?#?', then Damages would have 2 entries. property Damages: TDamages read FDamages; property CombinationsCache: TBlockCombinationsCache read FCombinationsCache; end; TBlocks = specialize TObjectList; { TAccumulatedCombinationsMultiIndexStrategy } // Adds accumulated combinations to the enumerable strategy to allow calculation of combinations on the fly, and // therefore early rejection of invalid multi-index configurations. TAccumulatedCombinationsMultiIndexStrategy = class(TEnumerableMultiIndexStrategy) private FAccumulatedCombinations: TInt64Array; protected function CalcCombinations(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer): Int64; virtual; abstract; function UpdateCombinations(const AValidationResult: TIndexValidationResult; constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer): TIndexValidationResult; public function GetCombinations: Int64; end; TConditionRecord = class; { TValidationsToBlockAssignments } // Enumerable strategy that enumerates all valid assignments of ranges of validation numbers to individual blocks in // the form of start and stop indices. TValidationsToBlockAssignments = class(TAccumulatedCombinationsMultiIndexStrategy) private FConditionRecord: TConditionRecord; protected function CalcCombinations(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer): Int64; override; public constructor Create(constref AConditionRecord: TConditionRecord); function GetCardinality: Integer; override; function TryGetStartIndexValue(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer; out AStartIndexValue: Integer): Boolean; override; function ValidateIndexValue(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer): TIndexValidationResult; override; end; { TDamageToValidationAssignments } // Enumerable strategy that enumerates all valid assignments of each damage in the block to a specific validation // number from the validation numbers that have been assigned to the block, as indicated by start and stop indices. TDamageToValidationAssignments = class(TEnumerableMultiIndexStrategy) private FConditionRecord: TConditionRecord; FBlock: TBlock; FValidationStartIndex, FValidationStopIndex: Integer; // Calculates "span", the length of all damages for one validation number combined. function CalcValidationSpan(constref ACurrentIndexArray: TIndexArray; const ALastDamageIndex, AValidationNumber: Integer): Integer; public constructor Create(constref AConditionRecord: TConditionRecord; constref ABlock: TBlock; const AStartValidationIndex, AStopValidationIndex: Integer); function GetCardinality: Integer; override; function TryGetStartIndexValue(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer; out AStartIndexValue: Integer): Boolean; override; function ValidateIndexValue(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer): TIndexValidationResult; override; end; { TValidationPositionInfo } TValidationPositionInfo = record ValidationIndex, MinStart, MaxStart: Integer; end; TValidationPositionInfos = specialize TList; { TValidationPositionOffsets } // Enumerable strategy that enumerates all valid assignments of start positions (positions mean character indices in // the block patterns) of validation numbers that have been assigned to damages in the current block, as indicated by // provided TValidationPositionInfos. TValidationPositionOffsets = class(TAccumulatedCombinationsMultiIndexStrategy) private FConditionRecord: TConditionRecord; FPositionInfos: TValidationPositionInfos; FBlockLength, FValidationStartIndex, FValidationStopIndex: Integer; protected function CalcCombinations(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer): Int64; override; public constructor Create(constref AConditionRecord: TConditionRecord; constref APositionInfos: TValidationPositionInfos; const ABlockLength, AValidationStartIndex, AValidationStopIndex: Integer); function GetCardinality: Integer; override; function TryGetStartIndexValue(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer; out AStartIndexValue: Integer): Boolean; override; function ValidateIndexValue(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer): TIndexValidationResult; override; end; { TConditionRecord } TConditionRecord = class private // List of validation numbers as stated in the problem input. FValidation: TIntegerList; // List of non-empty, maximum-length parts of the pattern without operational springs ("blocks"). FBlocks: TBlocks; // Array 'a' of accumulated validation series lengths. 'a[i, j]' denotes the combined length of consecutive // validation numbers from 'FValidation[i]' to 'FValidation[j - 1]' with a single space in between each pair of // them. FValidationLengths: TValidationLengths; // Array 'a' of minimum indices 'a[i]', such that all remaining validation numbers starting at index 'a[i] - 1' // cannot fit into the remaining blocks starting at 'FBlocks[i]'. FMinIndices: TIndexArray; FCombinationsCache: TCombinationsCache; procedure InitValidationLengths; procedure InitMinIndices; function CalcCombinationsBlockSingleValidation(constref ABlock: TBlock; const AIndex: Integer): Int64; function CalcCombinationsBlockMultiValidations(constref ABlock: TBlock; constref AIndices: TIndexArray; const AStartIndex, AStopIndex: Integer): Int64; function CalcValidationsId(const AStartIndex, AStopIndex: Integer): Int64; public constructor Create(constref ACombinationsCache: TCombinationsCache); destructor Destroy; override; // Adds all non-empty, maximum-length parts of the pattern without operational springs ("blocks"). procedure AddBlocks(const APattern: string); function GenerateBlockAssignments: Int64; function CalcCombinationsBlock(constref ABlock: TBlock; const AStartIndex, AStopIndex: Integer): Int64; function CalcCombinationsWildcardSequence(const ASequenceLength, AStartIndex, AStopIndex: Integer): Int64; property Validation: TIntegerList read FValidation; property Blocks: TBlocks read FBlocks; property ValidationLengths: TValidationLengths read FValidationLengths; property MinIndices: TIndexArray read FMinIndices; end; { THotSprings } THotSprings = class(TSolver) private FCombinationsCache: TCombinationsCache; public constructor Create; destructor Destroy; override; procedure ProcessDataLine(const ALine: string); override; procedure Finish; override; function GetDataFileName: string; override; function GetPuzzleName: string; override; end; implementation { TBlock } procedure TBlock.ParseDamages; var i, len: Integer; damage: TDamage; begin FDamages := TDamages.Create; damage.Length := 0; len := Length(FPattern); for i := 1 to len do // The pattern must only contain damage and wildcard characters here. if FPattern[i] = CDamagedChar then begin if damage.Length = 0 then damage.Start := i; Inc(damage.Length); end else if damage.Length > 0 then begin damage.CharsRemaining := len - damage.Start - damage.Length + 1; FDamages.Add(damage); damage.Length := 0; end; if damage.Length > 0 then begin damage.CharsRemaining := 0; FDamages.Add(damage); end; end; constructor TBlock.Create(const APattern: string; constref ACombinationsCache: TBlockCombinationsCache); begin FPattern := APattern; FCombinationsCache := ACombinationsCache; ParseDamages; end; destructor TBlock.Destroy; begin FDamages.Free; inherited Destroy; end; { TAccumulatedCombinationsMultiIndexStrategy } function TAccumulatedCombinationsMultiIndexStrategy.UpdateCombinations(const AValidationResult: TIndexValidationResult; constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer): TIndexValidationResult; var combinations: Int64; begin Result := AValidationResult; if Result = ivrValid then begin combinations := CalcCombinations(ACurrentIndexArray, ACurrentIndex); if combinations = 0 then Result := ivrSkip else if ACurrentIndex > 0 then FAccumulatedCombinations[ACurrentIndex] := combinations * FAccumulatedCombinations[ACurrentIndex - 1] else begin SetLength(FAccumulatedCombinations, GetCardinality); FAccumulatedCombinations[ACurrentIndex] := combinations; end; end; end; function TAccumulatedCombinationsMultiIndexStrategy.GetCombinations: Int64; begin if FAccumulatedCombinations <> nil then Result := FAccumulatedCombinations[GetCardinality - 1] else Result := 0; end; { TValidationsToBlockAssignments } function TValidationsToBlockAssignments.CalcCombinations(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer): Int64; var block: TBlock; start, stop: Integer; begin // 'ACurrentIndexArray[i] - 1' denotes the index of the last validation number assigned to 'Block[i]', and the index // of the first validation number in 'Validation' assigned to 'Block[i + 1]'. If two consecutive values in // 'ACurrentIndexArray' are the same, then the block in between has no numbers assigned to it. block := FConditionRecord.Blocks[ACurrentIndex]; if ACurrentIndex > 0 then start := ACurrentIndexArray[ACurrentIndex - 1] else start := 0; stop := ACurrentIndexArray[ACurrentIndex] - 1; if block.Damages.Count > 0 then Result := FConditionRecord.CalcCombinationsBlock(block, start, stop) else Result := FConditionRecord.CalcCombinationsWildcardSequence(Length(block.Pattern), start, stop); end; constructor TValidationsToBlockAssignments.Create(constref AConditionRecord: TConditionRecord); begin FConditionRecord := AConditionRecord; end; function TValidationsToBlockAssignments.GetCardinality: Integer; begin Result := FConditionRecord.Blocks.Count; end; function TValidationsToBlockAssignments.TryGetStartIndexValue(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer; out AStartIndexValue: Integer): Boolean; begin Result := True; if ACurrentIndex + 1 = GetCardinality then AStartIndexValue := FConditionRecord.Validation.Count else if ACurrentIndex > 0 then AStartIndexValue := Max(ACurrentIndexArray[ACurrentIndex - 1], FConditionRecord.MinIndices[ACurrentIndex]) else AStartIndexValue := FConditionRecord.MinIndices[ACurrentIndex]; end; function TValidationsToBlockAssignments.ValidateIndexValue(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer): TIndexValidationResult; var start: Integer; begin if ACurrentIndexArray[ACurrentIndex] > FConditionRecord.Validation.Count then Result := ivrBacktrack else begin if ACurrentIndex > 0 then start := ACurrentIndexArray[ACurrentIndex - 1] else start := 0; if FConditionRecord.ValidationLengths[start, ACurrentIndexArray[ACurrentIndex]] <= Length(FConditionRecord.Blocks[ACurrentIndex].Pattern) then Result := ivrValid else Result := ivrBacktrack; end; Result := UpdateCombinations(Result, ACurrentIndexArray, ACurrentIndex); end; { TDamageToValidationAssignments } function TDamageToValidationAssignments.CalcValidationSpan(constref ACurrentIndexArray: TIndexArray; const ALastDamageIndex, AValidationNumber: Integer): Integer; var spanStart: Integer; begin spanStart := ALastDamageIndex; while (spanStart > 0) and (ACurrentIndexArray[spanStart - 1] = AValidationNumber) do Dec(spanStart); Result := FBlock.Damages[ALastDamageIndex].Length; if spanStart < ALastDamageIndex then Inc(Result, FBlock.Damages[ALastDamageIndex].Start - FBlock.Damages[spanStart].Start); end; constructor TDamageToValidationAssignments.Create(constref AConditionRecord: TConditionRecord; constref ABlock: TBlock; const AStartValidationIndex, AStopValidationIndex: Integer); begin FConditionRecord := AConditionRecord; FBlock := ABlock; FValidationStartIndex := AStartValidationIndex; FValidationStopIndex := AStopValidationIndex; end; function TDamageToValidationAssignments.GetCardinality: Integer; begin Result := FBlock.Damages.Count; end; function TDamageToValidationAssignments.TryGetStartIndexValue(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer; out AStartIndexValue: Integer): Boolean; begin Result := True; if ACurrentIndex > 0 then AStartIndexValue := ACurrentIndexArray[ACurrentIndex - 1] else AStartIndexValue := FValidationStartIndex; end; function TDamageToValidationAssignments.ValidateIndexValue(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer): TIndexValidationResult; var i, prev: Integer; begin i := ACurrentIndexArray[ACurrentIndex]; prev := ACurrentIndex - 1; // Checks maximum index value. if i > FValidationStopIndex then Result := ivrBacktrack // Checks if there is enough space after this damage for remaining validation numbers. else if (i < FValidationStopIndex) and (FConditionRecord.ValidationLengths[i + 1, FValidationStopIndex + 1] + 1 > FBlock.Damages[ACurrentIndex].CharsRemaining) then Result := ivrSkip // Checks if there is enough space before this damage for previous validation numbers. else if (FValidationStartIndex < i) and (FConditionRecord.ValidationLengths[FValidationStartIndex, i] + 1 >= FBlock.Damages[ACurrentIndex].Start) then Result := ivrBacktrack // Checks if there is enough space between previous and this damage for skipped validation numbers. else if (ACurrentIndex > 0) and (ACurrentIndexArray[prev] + 1 < i) and (FConditionRecord.ValidationLengths[ACurrentIndexArray[prev] + 1, i] + 2 > FBlock.Damages[ACurrentIndex].Start - FBlock.Damages[prev].Start - FBlock.Damages[prev].Length) then Result := ivrBacktrack // Checks if span is small enough to fit within this validation number. else if FConditionRecord.Validation[i] < CalcValidationSpan(ACurrentIndexArray, ACurrentIndex, i) then Result := ivrSkip else Result := ivrValid; end; { TValidationPositionOffsets } function TValidationPositionOffsets.CalcCombinations(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer): Int64; var space, start, stop: Integer; begin stop := FPositionInfos[ACurrentIndex].ValidationIndex - 1; if ACurrentIndex > 0 then begin space := ACurrentIndexArray[ACurrentIndex] - ACurrentIndexArray[ACurrentIndex - 1] - FConditionRecord.Validation[FPositionInfos[ACurrentIndex - 1].ValidationIndex] - 2; start := FPositionInfos[ACurrentIndex - 1].ValidationIndex + 1; Result := FConditionRecord.CalcCombinationsWildcardSequence(space, start, stop); end else begin // Handles first calculated offset. space := ACurrentIndexArray[0] - 2; Result := FConditionRecord.CalcCombinationsWildcardSequence(space, FValidationStartIndex, stop); end; if (Result > 0) and (ACurrentIndex + 1 = GetCardinality) then begin // Handles last calculated offset. space := FBlockLength - ACurrentIndexArray[ACurrentIndex] - FConditionRecord.Validation[FPositionInfos.Last.ValidationIndex]; start := FPositionInfos.Last.ValidationIndex + 1; Result := Result * FConditionRecord.CalcCombinationsWildcardSequence(space, start, FValidationStopIndex); end; end; constructor TValidationPositionOffsets.Create(constref AConditionRecord: TConditionRecord; constref APositionInfos: TValidationPositionInfos; const ABlockLength, AValidationStartIndex, AValidationStopIndex: Integer); begin FConditionRecord := AConditionRecord; FPositionInfos := APositionInfos; FBlockLength := ABlockLength; FValidationStartIndex := AValidationStartIndex; FValidationStopIndex := AValidationStopIndex; inherited Create; end; function TValidationPositionOffsets.GetCardinality: Integer; begin Result := FPositionInfos.Count; end; function TValidationPositionOffsets.TryGetStartIndexValue(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer; out AStartIndexValue: Integer): Boolean; var info: TValidationPositionInfo; begin info := FPositionInfos[ACurrentIndex]; AStartIndexValue := info.MinStart; // Adjusts start value to avoid overlap of this validation number with the previous one (the one from previous // position info). if ACurrentIndex > 0 then AStartIndexValue := Max(AStartIndexValue, ACurrentIndexArray[ACurrentIndex - 1] + FConditionRecord.Validation[FPositionInfos[ACurrentIndex - 1].ValidationIndex] + 1); Result := True; end; function TValidationPositionOffsets.ValidateIndexValue(constref ACurrentIndexArray: TIndexArray; const ACurrentIndex: Integer): TIndexValidationResult; begin if ACurrentIndexArray[ACurrentIndex] <= FPositionInfos[ACurrentIndex].MaxStart then Result := ivrValid else Result := ivrBacktrack; Result := UpdateCombinations(Result, ACurrentIndexArray, ACurrentIndex); end; { TConditionRecord } procedure TConditionRecord.InitValidationLengths; var i, j: Integer; begin SetLength(FValidationLengths, FValidation.Count + 1, FValidation.Count + 1); for i := 0 to FValidation.Count do begin FValidationLengths[i, i] := 0; for j := i + 1 to FValidation.Count do if FValidationLengths[i, j - 1] <> 0 then FValidationLengths[i, j] := FValidationLengths[i, j - 1] + FValidation[j - 1] + 1 else FValidationLengths[i, j] := FValidationLengths[i, j - 1] + FValidation[j - 1] end; end; procedure TConditionRecord.InitMinIndices; var i, j, patternsLength: Integer; begin SetLength(FMinIndices, FBlocks.Count - 1); patternsLength := Length(FBlocks[FBlocks.Count - 1].Pattern); j := FValidation.Count; for i := FBlocks.Count - 2 downto 0 do begin while (j >= 0) and (FValidationLengths[j, FValidation.Count] <= patternsLength) do Dec(j); FMinIndices[i] := j + 1; patternsLength := patternsLength + 1 + Length(FBlocks[i].Pattern); end; end; function TConditionRecord.CalcCombinationsBlockSingleValidation(constref ABlock: TBlock; const AIndex: Integer): Int64; var len, combinedDamagesLength: Integer; begin len := Length(ABlock.Pattern); if len < FValidation[AIndex] then Result := 0 else if ABlock.Damages.Count = 0 then Result := len - FValidation[AIndex] + 1 else begin combinedDamagesLength := ABlock.Damages.Last.Start + ABlock.Damages.Last.Length - ABlock.Damages.First.Start; if FValidation[AIndex] < combinedDamagesLength then Result := 0 else begin Result := Min(Min(Min( ABlock.Damages.First.Start, FValidation[AIndex] - combinedDamagesLength + 1), len - FValidation[AIndex] + 1), ABlock.Damages.Last.CharsRemaining + 1); end; end; end; function TConditionRecord.CalcCombinationsBlockMultiValidations(constref ABlock: TBlock; constref AIndices: TIndexArray; const AStartIndex, AStopIndex: Integer): Int64; var i, high: Integer; position: TValidationPositionInfo; positions: TValidationPositionInfos; validationPositionOffsets: TValidationPositionOffsets; offsets: TIndexArray; begin positions := TValidationPositionInfos.Create; high := Length(AIndices) - 1; // Initializes first info record. position.ValidationIndex := AIndices[0]; position.MaxStart := ABlock.Damages[0].Start; position.MinStart := 1; for i := 1 to high do if AIndices[i] <> position.ValidationIndex then begin // Finalizes current info record. position.MaxStart := Min(position.MaxStart, ABlock.Damages[i].Start - 1 - FValidation[position.ValidationIndex]); position.MinStart := Max(position.MinStart, ABlock.Damages[i - 1].Start + ABlock.Damages[i - 1].Length - FValidation[position.ValidationIndex]); positions.Add(position); // Initializes next info record. position.ValidationIndex := AIndices[i]; position.MaxStart := ABlock.Damages[i].Start; position.MinStart := position.MinStart + FValidationLengths[AIndices[i - 1], AIndices[i]] + 1; end; // Finalizes last info record. position.MaxStart := Min(position.MaxStart, Length(ABlock.Pattern) + 1 - FValidation[position.ValidationIndex]); position.MinStart := Max(position.MinStart, ABlock.Damages[high].Start + ABlock.Damages[high].Length - FValidation[position.ValidationIndex]); positions.Add(position); Result := 0; validationPositionOffsets := TValidationPositionOffsets.Create(Self, positions, Length(ABlock.Pattern), AStartIndex, AStopIndex); for offsets in validationPositionOffsets do Result := Result + validationPositionOffsets.GetCombinations; validationPositionOffsets.Free; positions.Free; end; function TConditionRecord.CalcValidationsId(const AStartIndex, AStopIndex: Integer): Int64; var i: Integer; begin // Requires 'FValidations[i] < 32' for each 'i' and 'AStopIndex - AStartIndex < 12'. Result := FValidation[AStartIndex]; for i := AStartIndex + 1 to AStopIndex do Result := (Result shl 5) or FValidation[i]; end; constructor TConditionRecord.Create(constref ACombinationsCache: TCombinationsCache); begin FBlocks := TBlocks.Create; FValidation := TIntegerList.Create; FCombinationsCache := ACombinationsCache; end; destructor TConditionRecord.Destroy; begin FBlocks.Free; FValidation.Free; inherited Destroy; end; procedure TConditionRecord.AddBlocks(const APattern: string); var split: TStringArray; part: string; blockCache: TBlockCombinationsCache; begin split := APattern.Split([COperationalChar]); for part in split do if Length(part) > 0 then begin if not FCombinationsCache.TryGetValue(part, blockCache) then begin blockCache := TBlockCombinationsCache.Create; FCombinationsCache.Add(part, blockCache); end; FBlocks.Add(TBlock.Create(part, blockCache)); end; end; function TConditionRecord.GenerateBlockAssignments: Int64; var validationsToBlockAssignments: TValidationsToBlockAssignments; indices: TIndexArray; begin InitValidationLengths; InitMinIndices; Result := 0; validationsToBlockAssignments := TValidationsToBlockAssignments.Create(Self); for indices in validationsToBlockAssignments do Result := Result + validationsToBlockAssignments.GetCombinations; validationsToBlockAssignments.Free; end; function TConditionRecord.CalcCombinationsBlock(constref ABlock: TBlock; const AStartIndex, AStopIndex: Integer): Int64; var validationsId: Int64; indices: TIndexArray; damageToValidationAssignments: TDamageToValidationAssignments; begin // No validation number assigned to this block. if AStartIndex > AStopIndex then begin if ABlock.Damages.Count = 0 then Result := 1 else Result := 0; end // One validation number assigned to this block. else if AStartIndex = AStopIndex then Result := CalcCombinationsBlockSingleValidation(ABlock, AStartIndex) // Multiple validation numbers assigned to this block. Checks cache first. else begin validationsId := CalcValidationsId(AStartIndex, AStopIndex); if not ABlock.CombinationsCache.TryGetValue(validationsId, Result) then begin Result := 0; // Assigns validation numbers to specific damages. damageToValidationAssignments := TDamageToValidationAssignments.Create(Self, ABlock, AStartIndex, AStopIndex); for indices in damageToValidationAssignments do Result := Result + CalcCombinationsBlockMultiValidations(ABlock, indices, AStartIndex, AStopIndex); damageToValidationAssignments.Free; ABlock.CombinationsCache.Add(validationsId, Result); end; end; end; function TConditionRecord.CalcCombinationsWildcardSequence(const ASequenceLength, AStartIndex, AStopIndex: Integer): Int64; var count, freedoms: Integer; begin if AStartIndex < AStopIndex + 1 then begin count := AStopIndex + 1 - AStartIndex; freedoms := ASequenceLength - FValidationLengths[AStartIndex, AStopIndex + 1]; if freedoms >= 0 then Result := BinomialCoefficients.Get(count + freedoms, freedoms) else Result := 0; end else Result := 1; end; { THotSprings } constructor THotSprings.Create; begin FCombinationsCache := TCombinationsCache.Create([doOwnsValues]); end; destructor THotSprings.Destroy; begin FCombinationsCache.Free; inherited Destroy; end; procedure THotSprings.ProcessDataLine(const ALine: string); var conditionRecord1, conditionRecord2: TConditionRecord; mainSplit, split: TStringArray; part, unfolded: string; i: Integer; begin conditionRecord1 := TConditionRecord.Create(FCombinationsCache); conditionRecord2 := TConditionRecord.Create(FCombinationsCache); mainSplit := ALine.Split([' ']); // Adds blocks for part 1. conditionRecord1.AddBlocks(mainSplit[0]); // Adds blocks for part 2. unfolded := mainSplit[0]; for i := 2 to CPart2Repetition do unfolded := unfolded + CWildcardChar + mainSplit[0]; conditionRecord2.AddBlocks(unfolded); // Adds validation numbers. split := mainSplit[1].Split([',']); for part in split do conditionRecord1.Validation.Add(StrToInt(part)); for i := 1 to CPart2Repetition do conditionRecord2.Validation.AddRange(conditionRecord1.Validation); FPart1 := FPart1 + conditionRecord1.GenerateBlockAssignments; FPart2 := FPart2 + conditionRecord2.GenerateBlockAssignments; conditionRecord1.Free; conditionRecord2.Free; end; procedure THotSprings.Finish; begin end; function THotSprings.GetDataFileName: string; begin Result := 'hot_springs.txt'; end; function THotSprings.GetPuzzleName: string; begin Result := 'Day 12: Hot Springs'; end; end.