Day 15 is all about the negative space. A fun puzzle, which tripped me with with an unexpected gotcha. Solution spoilers, C++ and some light commentary contained within.
In the problem we are dealing with Sensors. Each Sensor can detect the closest Beacon. I define a struct to store them:
USTRUCT(BlueprintType)
struct FSensorZone
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "D15")
FVector2D Location;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "D15")
FVector2D Beacon;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "D15")
int32 Size;
};In a new actor I create a static member function to load Sensor & Beacon pairs from the input strings:
FSensorZone AD15Actor::SensorZoneFromString(const FString& Line)
{
TArray<FString> SplitString;
Line.ParseIntoArray(SplitString, TEXT("="), true);
ensure(SplitString.Num()==5);
FSensorZone Result;
Result.Location.X = FCString::Atoi(*SplitString[1]);
Result.Location.Y = FCString::Atoi(*SplitString[2]);
Result.Beacon.X = FCString::Atoi(*SplitString[3]);
Result.Beacon.Y = FCString::Atoi(*SplitString[4]);
Result.Size = ManhattenDistance(Result.Location,Result.Beacon );
return Result;
}
int AD15Actor::ManhattenDistance(FVector2d From, FVector2d To)
{
return FMath::Abs(From.X-To.X) + FMath::Abs(From.Y-To.Y);
}I wrote some unit tests to make sure the loading works. The look like this, but more of them:
Zone = AD15Actor::SensorZoneFromString(TEXT("Sensor at x=9, y=16: closest beacon is at x=10, y=16"));
TestEqual(TEXT("the sensor at X=9"), (int)Zone.Location.X, 9);
TestEqual(TEXT("the sensor at Y=16"), (int)Zone.Location.Y, 16);
TestEqual(TEXT("the closest beacon at X=10"), (int)Zone.Beacon.X, 10);
TestEqual(TEXT("the closest beacon at Y=16"), (int)Zone.Beacon.Y, 16);Part one asks us to consider just one row only, and count the spaces that are “safe” (closer to a sensor than its closest beacon). Since the problem asked for just one row I implemented a helper function to check if a Sensor Zone intersects it. I also returned the start and end of the column for that row so I could use it later to determine bounds and jump spans. I used half-open ranges.
//static
bool AD15Actor::IntersectsRow(const FSensorZone& Zone, int Row, int& ColumnStart, int &ColumnEnd)
{
int MinY = Zone.Location.Y-Zone.Size;
int MaxY = Zone.Location.Y+Zone.Size + 1;
bool Result = (Row >= MinY && Row < MaxY );
if (Result)
{
int DY = FMath::Abs(Row - Zone.Location.Y );
int DX = Zone.Size - DY;
ColumnStart = Zone.Location.X - DX;
ColumnEnd = Zone.Location.X + DX + 1;
}
return Result;
}I add some tests for this as well:
Zone = AD15Actor::SensorZoneFromString(TEXT("Sensor at x=8, y=7: closest beacon is at x=2, y=10"));
TestEqual(TEXT("the sensor at X=8"), (int)Zone.Location.X, 8);
TestEqual(TEXT("the sensor at Y=7"), (int)Zone.Location.Y, 7);
int Start, End;
TestFalse( TEXT("Above"), AD15Actor::IntersectsRow(Zone,-3, Start, End));
TestTrue( TEXT("Top"),AD15Actor::IntersectsRow(Zone,-2, Start, End));
TestEqual( TEXT("Start=8"), Start, 8);
TestEqual( TEXT("Width=1"), End-Start, 1);
TestTrue( TEXT("Bottom"), AD15Actor::IntersectsRow(Zone,16, Start, End));
TestEqual( TEXT("Start=8"), Start, 8);
TestEqual( TEXT("Width=1"), End-Start, 1);
TestFalse( TEXT("Below"), AD15Actor::IntersectsRow(Zone,-3, Start, End));The IntersectsRow function will be used to determine the first and last column in the row that need checking. I will need a function to check if a space is “Safe”. A space is safe it is within ANY sensors safe zone:
bool AD15Actor::SafeSpace(int Row, int Column) const
{
for (auto Zone : Sensors)
{
int Start, End;
if (IntersectsRow(Zone, Row, Start, End))
{
if (Column>=Start && Column<End) return true;
}
}
return false;
}I will also need a function that tells me if a space already has a beacon on it:
bool AD15Actor::BeaconLocation(int Row, int Column) const
{
FVector2D P(Column,Row);
for (auto Zone : Sensors)
{
if (Zone.Beacon==P) return true;
}
return false;
}With these three helper-functions: IntersectsRow, SafeSpace and BeaconLocation I can write my Part 1 Solution Finder, CountSafeInRow:
int AD15Actor::CountSafeInRow(int Row) const
{
// find the start and end columns occupied on this Row
int MinC=INT_MAX, MaxC=-INT_MAX;
for (auto Zone : Sensors)
{
int Start, End;
if (IntersectsRow(Zone, Row, Start, End))
{
if (Start<MinC) MinC = Start;
if (End>MaxC) MaxC = End;
}
}
// check every square 1 at a time from start to end on this row
int Total = 0;
for (int Column = MinC; Column!=MaxC; ++Column)
{
if (BeaconLocation(Row, Column)) continue;
if (SafeSpace(Row, Column)) Total++;
}
return Total;
}This did not work first time. I didn’t have the BeaconLocation check, that came after figuring out why it was off-by-one against the puzzle example.
Part 2 turns this on its head. I read the problem and then went to eat. Then I read it again. I need to find the one unsafe (not in any of the sensors ranges) in a massive 4 million x 4 million square grid. Every square needs checking against 32 beacons.
I’ve been working in rows so far so I decide to go with span-skipping. Once I have a Sensor and I know it overlaps a Row, then I know the start and end and I don’t need to check inside them. I will be checking every Row, but I cant think of a quick way to know I can skip a row so I just hope skipping columns will be fast enough, and so I write it out and after “some debugging” I end up with this:
int64 AD15Actor::LocateBeacon(int Lowest, int Largest) const
{
int Row = Lowest;
int Column = Lowest;
do
{
bool foundSafe = true;
for (auto Zone : Sensors)
{
int Start, End;
if (IntersectsRow(Zone, Row, Start, End))
{
if (Column>=Start && Column<End)
{
foundSafe = false;
Column = End;
break;
}
}
}
if (foundSafe && !BeaconLocation(Row, Column))
{
ensure(!SafeSpace(Row,Column));
int64 Result = Column;
Result *= 4000000;
Result += Row;
UE_LOG(LogTemp, Log, TEXT("D15 64bit Check: %lld"), Result);
return Result;
}
if (Column>Largest)
{
Column = Lowest;
Row ++;
}
}
while (Row <= Largest);
return 0;
}This algorithm is good and fast enough to not worry about. You will note however that the function returns an int64, and that I have an extra logging using %lld formatting. This is because I spent at least an hour scratching my head about why my solver matched the example input but didn’t get the right part 2 answer. I should really have anticipated that multiply by 4 million overflows my poor lowly integers, but I went looking for fence post errors and problems with rounding to int from float – the Vector class in unreal was switched to double in 5.0, which holds bigger ints (exactly) than a 32 bit int.
Right well that’s that for now. I hope you enjoyed reading this. Until tomorrow!
