After a few of the really tough ones, this seems to me like it might be a relatively simple breadth first search. Not even a proper pathfinding algorithm, as the longest path is the one that should be taken. So for this problem, just a simple BFS, and take the last/longest result.
I know this will probably bite me in the butt for part 2, but hey, why not.
struct Path {
set<int, int> steps;
pair<int, int> current;
};
//---------
while (!routes.empty())
{
auto trail = routes.front(); routes.pop();
trail.steps.insert(pair<int, int>(trail.x, trail.y));
//printTrail(grounds, trail.steps);
if (trail.x == lastX && trail.y == lastY)
{
maxLen = max(maxLen, trail.steps.size());
cout << "Path finished: " << trail.steps.size();
finished++;
continue;
}
// North
if ((grounds[trail.x-1][trail.y] == '.' || grounds[trail.x-1][trail.y] == '^')
&& trail.steps.find(pair<int, int>(trail.x-1, trail.y)) == trail.steps.end())
{
Path nextTrail { set<pair<int, int>>(trail.steps), trail.x-1, trail.y };
if (grounds[trail.x-1][trail.y] == '^')
{
nextTrail.steps.insert(pair<int, int>(nextTrail.x, nextTrail.y));
nextTrail.x--;
}
routes.push(nextTrail);
}
/// Copy for South, East, & West...
}
For the sample it's fine, but for the full input it's taking a looong time to run.
I'm thinking it's because it's retreading a lot of ground - even in the sample there's some fairly long paths without any intersection points. It's going to be doing a lot of spinning, re-checking stuff it's already checked, and in the process copying a lot of memory as well.
Really, it's not the whole paths that matter, it's just the branching/intersection points that matter. If I update this traversal to instead only look for where there's more than one non-#
character, I can make a graph of these points, and just do a breadth-first search upon that. I'll also be able to tell how long the path is between those points, and even take into account the one-way pieces!
One thing I've noticed is that these one-ways only occur adjacent to these path intersections, so that makes the checks a bit easier. I've adapted the above code to look for the path segments and record those, then do a BFS on the segments found.
struct Path {
vector<Coord> steps;
Coord start;
int x, y;
};
struct Node { int x, y, length; set<Coord> seen; };
//-----
// Proper Breadth First Search
queue<Node> routeQueue;
routeQueue.push(Node{0, 1, 0});
while (!routeQueue.empty())
{
auto route = routeQueue.front(); routeQueue.pop();
route.seen.insert(Coord(route.x, route.y));
if (route.x == grounds.size() - 1)
{
maxLen = max(maxLen, route.length);
finished++;
continue;
}
for (auto m : segments[Coord{route.x, route.y}])
{
if (route.seen.find(Coord(m.x, m.y)) != route.seen.end()) continue;
routeQueue.push(Node{m.x, m.y, route.length + m.length });
}
}
This way the paths segments are only traversed once (per direction), and only the portions with any choice of direction matter. It still runs a little slow, but takes less than 30 seconds!
Ahahaha, great! I barely have to make any modifications to my code. Basically, just ignore any slop checks. In fact, I just need to remove any of the slope handling code, and it should work just fine! I think this is the first time I've removed more code than I added for a part 2!
... except this is running for an exceptionally long time. It turns out that now there's too many paths to check, and it's taking roughly twice as long to check each sucessive length of path. It will take a full day to get it the way I'm doing it. I need to find a way to prune out some of the paths.... so much for this being "just remove some code".
I imagine a good chunk of paths can't even get to the end, but still have a lot of search space. For example
a - b - c
| | |
d - e - f
| | |
g - h - i
Here, a path going from a-b-e-h; the next path going to 'g' is never going to reach 'i' as well. So basically, any nodes that are on the peremeter (only having three connections) should only be able to move "forward". If it tries to move "backwards" (towards the start), it will only cut off it's paths.
// Prune paths that cut off unwalked route segments from reaching the end
set<Coord> seenPeremeter;
queue<Coord> peremeterQueue;
peremeterQueue.push(Coord(0,1));
while (!peremeterQueue.empty())
{
Coord curNode = peremeterQueue.front(); peremeterQueue.pop();
seenPeremeter.insert(curNode);
for (int i = 0; i < segments[curNode].size(); i++)
{
auto m = segments[curNode][i];
if (getPathCount(grounds, m.x, m.y) != 3) continue;
if (seenPeremeter.find(Coord(m.x, m.y)) != seenPeremeter.end())
{
segments[curNode].erase(segments[curNode].begin() + i);
i--;
} else {
peremeterQueue.push(Coord(m.x, m.y));
}
}
}
I left that running for an hour... then it it hit another wall, though further in.
I got frustrated, killed the process, then tried something new: Used a bitmask for the visited list, rather than a vector. Surprisingly, it finishes in a few seconds!
It also gave a wrong answer because a bitmask was being assigned from a 32 bit constant. Fun fact - unless explicitly marked, a constant 1
will be a 32-bit integer. I just happened to have 36 junctions I needed to mask out. So 4 had corrupted masks. Whoops! It took me 2 hours to track down that one issue.