Opening the reading…
Opening the reading…
GRAPHS › DFS AND BFS ON GRAPHS
Open — the attempt gate is not wired up yet
This editorial is meant to unlock after you have run the problem at least once, with the worked solution behind one further deliberate click. That needs per-learner unlock state nothing stores today, so for now the whole article is open.
Try it yourself first →For a fixed day, the first day cells[i] are water and every later cell is still land. A crossing exists exactly when some land cell in the top row can reach some land cell in the bottom row through four-directional moves. BFS can answer that question by starting from every available top-row cell and expanding through unvisited land.
The important extra fact is that the answer changes only once. If flooding has already made crossing impossible, every later day keeps those water cells and adds more, so a path cannot reappear. The results therefore look like true, true, ..., true, false, false, ..., false, which is the shape binary search needs.
| MEASURE | BOUND | WHY |
|---|---|---|
| Time | O(n log n) | Each feasibility check marks and processes each cell at most once, including the work needed to build the flooded grid. Binary search performs O(log n) such checks, so the total is O(n log n). |
| Space | O(n) extra | The water grid, visited grid, queue, and direction storage together use O(n) working memory. The returned value is a single integer and is not part of the bound; even a grid shape that creates a queue containing a wide frontier stays within O(n). |
#include <queue>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int latestDayToCross(int row, int col, vector<vector<int>>& cells) {
int n = row * col;
int lo = 0;
int hi = n;
while (lo < hi) {
int mid = (lo + hi + 1) / 2;
if (canCross(row, col, cells, mid)) {
lo = mid;
} else {
hi = mid - 1;
}
}
return lo;
}
private:
bool canCross(int row, int col, vector<vector<int>>& cells, int day) {
vector<vector<bool>> water(row, vector<bool>(col, false));
for (int i = 0; i < day; ++i) {
int r = cells[i][0] - 1;
int c = cells[i][1] - 1;
water[r][c] = true;
}
vector<vector<bool>> visited(row, vector<bool>(col, false));
queue<pair<int, int>> q;
for (int c = 0; c < col; ++c) {
if (!water[0][c]) {
q.push({0, c});
visited[0][c] = true;
}
}
int dr[4] = {-1, 1, 0, 0};
int dc[4] = {0, 0, -1, 1};
while (!q.empty()) {
auto [r, c] = q.front();
q.pop();
if (r == row - 1) {
return true;
}
for (int k = 0; k < 4; ++k) {
int nr = r + dr[k];
int nc = c + dc[k];
if (nr >= 0 && nr < row && nc >= 0 && nc < col &&
!water[nr][nc] && !visited[nr][nc]) {
visited[nr][nc] = true;
q.push({nr, nc});
}
}
}
return false;
}
};The upper-middle expression is the small detail that makes this a maximum search rather than an ordinary membership search. When mid is feasible, keeping mid in the range is necessary because it might be the answer, so lo becomes mid. When it is not feasible, mid and every later day are discarded. The BFS marks a cell when it is enqueued, preventing several neighboring cells from adding it again.
You can avoid rebuilding and searching many grids by reversing time. Start with every cell treated as water, then add cells back in reverse order. When cells[i] is added, the active land cells are exactly those that remain after the first i days. A disjoint-set structure joins adjacent active cells and also joins top-row cells to a virtual top node and bottom-row cells to a virtual bottom node.
#include <numeric>
#include <vector>
using namespace std;
class Solution {
vector<int> parent;
vector<int> size;
int find(int x) {
if (parent[x] == x) {
return x;
}
return parent[x] = find(parent[x]);
}
void unite(int a, int b) {
a = find(a);
b = find(b);
if (a == b) {
return;
}
if (size[a] < size[b]) {
swap(a, b);
}
parent[b] = a;
size[a] += size[b];
}
public:
int latestDayToCross(int row, int col, vector<vector<int>>& cells) {
int n = row * col;
int top = n;
int bottom = n + 1;
parent.resize(n + 2);
size.assign(n + 2, 1);
iota(parent.begin(), parent.end(), 0);
vector<vector<bool>> land(row, vector<bool>(col, false));
int dr[4] = {-1, 1, 0, 0};
int dc[4] = {0, 0, -1, 1};
for (int i = n - 1; i >= 0; --i) {
int r = cells[i][0] - 1;
int c = cells[i][1] - 1;
land[r][c] = true;
int id = r * col + c;
if (r == 0) {
unite(id, top);
}
if (r == row - 1) {
unite(id, bottom);
}
for (int k = 0; k < 4; ++k) {
int nr = r + dr[k];
int nc = c + dc[k];
if (nr >= 0 && nr < row && nc >= 0 && nc < col && land[nr][nc]) {
unite(id, nr * col + nc);
}
}
if (find(top) == find(bottom)) {
return i;
}
}
return 0;
}
};This reverse-time method is an optimisation, not merely a rearrangement. Each cell is activated once and each neighboring relationship is considered a constant number of times, giving O(n alpha(n)) time and O(n) extra space, where alpha is the inverse Ackermann function. The binary-search BFS version is often easier to derive and debug; the disjoint-set version is preferable when the tighter time bound matters.