
CP Weekly Challenge
CCC 2024 J5 Harvest Waterloo Solution: Flood Fill and Grid BFS
Learn how to solve CCC 2024 J5 Harvest Waterloo with flood fill, BFS, connected components, and grid accumulation in Python and C++.
CP Weekly Challenge
CCC 2024 J5 Harvest Waterloo 题解:用 Flood Fill 收集可到达南瓜
用 flood fill 从起点遍历整片可到达南瓜地,并累计 S、M、L 南瓜的总价值。
This problem is set in a pumpkin patch, but algorithmically it is a classic flood fill problem. We are not looking for one path to one destination. We are looking for the entire connected region that the farmer can reach from the starting cell.
Once we visit every reachable pumpkin cell, we add up the values of those pumpkins.
What the Problem Is Asking
We are given an R x C grid. Each cell contains one of four characters:
S = small pumpkin, value 1
M = medium pumpkin, value 5
L = large pumpkin, value 10
* = bale of hay, blocked
The farmer starts at row A and column B. The top-left corner is (0, 0), so the input coordinates are already zero-indexed.
The farmer may move up, down, left, or right. The farmer cannot move diagonally, move outside the grid, or move through a bale of hay.
The output is the total value of all pumpkins reachable from the starting position.
Where Beginners Often Get Stuck
The first common mistake is thinking there must be a target cell. There is no exit to reach in this problem. We need the total value of the whole reachable component.
The second common mistake is not using visited. Without it, neighboring cells can keep adding each other back into the queue.
The third common mistake is indexing. The starting row and column are already zero-indexed, so we should not subtract one.
Core Algorithm Idea
Treat the grid as a graph:
each non-* cell = node
4-direction adjacency = edge
Starting from (A, B), we run BFS to visit every node in the same connected component. Whenever we process a cell, we add its pumpkin value to the answer.
BFS and DFS both work. BFS with a queue is often safer in Python because it avoids recursion-depth problems on large grids.
Python Solution
from collections import deque
R = int(input())
C = int(input())
patch = [list(input().strip()) for _ in range(R)]
A = int(input())
B = int(input())
value = {
"S": 1,
"M": 5,
"L": 10,
}
directions = [
(1, 0),
(-1, 0),
(0, 1),
(0, -1),
]
visited = [[False] * C for _ in range(R)]
q = deque()
visited[A][B] = True
q.append((A, B))
total = 0
while q:
r, c = q.popleft()
total += value[patch[r][c]]
for dr, dc in directions:
nr = r + dr
nc = c + dc
if nr < 0 or nr >= R or nc < 0 or nc >= C:
continue
if patch[nr][nc] == "*":
continue
if visited[nr][nc]:
continue
visited[nr][nc] = True
q.append((nr, nc))
print(total)
C++ Solution
#include <iostream>
#include <queue>
#include <string>
#include <utility>
#include <vector>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int R, C;
cin >> R >> C;
vector<string> patch(R);
for (int r = 0; r < R; r++) {
cin >> patch[r];
}
int A, B;
cin >> A >> B;
vector<vector<bool>> visited(R, vector<bool>(C, false));
queue<pair<int, int>> q;
visited[A][B] = true;
q.push({A, B});
int total = 0;
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 (patch[r][c] == 'S') total += 1;
else if (patch[r][c] == 'M') total += 5;
else if (patch[r][c] == 'L') total += 10;
for (int k = 0; k < 4; k++) {
int nr = r + dr[k];
int nc = c + dc[k];
if (nr < 0 || nr >= R || nc < 0 || nc >= C) {
continue;
}
if (patch[nr][nc] == '*') {
continue;
}
if (visited[nr][nc]) {
continue;
}
visited[nr][nc] = true;
q.push({nr, nc});
}
}
cout << total << "\n";
return 0;
}
Complexity Analysis
Each grid cell is added to the queue at most once. For each cell, we check four neighbors.
The time complexity is:
O(RC)
The memory complexity is also:
O(RC)
This fits the official bound R * C <= 100000.
Common Mistakes
- Solving it as a shortest-path problem instead of a connected-component sum.
- Forgetting to use
visited. - Marking cells as visited too late, which can allow duplicate queue entries.
- Subtracting 1 from the starting coordinates even though they are already zero-indexed.
- Allowing diagonal movement.
- Walking through
*hay bales. - Using recursive DFS in Python on a large grid and hitting recursion limits.
Review Questions
- Why is this problem a flood fill problem?
- Why should a cell be marked visited when it is added to the queue?
- Why is every reachable cell counted exactly once?
- Why are pumpkins in other separated regions not included?
- Why is BFS usually safer than recursive DFS in Python for this problem?
这道题的场景很可爱:一个农夫站在南瓜地里,要把所有能走到的南瓜都收走。但从算法角度看,它不是“找一条路”,而是“找一整片连通区域”。
所以 Harvest Waterloo 的核心不是最短路,也不是 DP,而是 flood fill:从起点开始,把所有能通过四方向走到的格子都搜出来,同时把 S、M、L 的价值加总。
题目要我们做什么
输入给出一个 R x C 的南瓜地。每个格子是下面四种字符之一:
S = small pumpkin, value 1
M = medium pumpkin, value 5
L = large pumpkin, value 10
* = bale of hay, blocked
农夫从坐标 (A, B) 开始。注意这道题的坐标是从 0 开始编号:
top-left = (0, 0)
每一步可以向上、下、左、右移动,但不能:
- 走出南瓜地边界。
- 走到
*干草堆上。 - 斜着走。
我们要输出农夫能收获到的所有南瓜总价值。
初学者容易卡在哪里
第一个误区是把它当成“有没有终点”的题。
很多 grid search 题会问能不能从起点走到终点,或者最短要走几步。但这道题没有指定终点。它问的是:
从起点所在的这一整片可到达区域里,
所有南瓜加起来值多少钱?
第二个误区是忘记 visited。如果不标记已经访问过的格子,两个相邻格子可能互相把对方重新加入 queue,程序会反复绕圈。
第三个误区是坐标。输入的 A 和 B 已经是 0-indexed,不需要再减 1。
核心算法概念
这道题是典型的 flood fill。
我们可以把 grid 看成一张图:
每个非 * 格子 = 一个 node
四方向相邻 = edge
从起点 (A, B) 出发,所有能到达的格子组成一个 connected component。
我们要做的事情就是:
遍历这个 connected component,
把每个格子的南瓜价值加到 total。
BFS 和 DFS 都可以完成 flood fill。这里建议用 BFS,因为 Python 里递归 DFS 可能遇到 recursion limit,而 deque 写 BFS 很稳。
状态定义
我们需要这些数据:
patch[r][c] = 第 r 行第 c 列的字符
visited[r][c] = 这个格子是否已经加入过 queue
total = 当前已收获南瓜总价值
南瓜价值可以用 dictionary / map 表示:
S -> 1
M -> 5
L -> 10
四个方向可以写成:
directions = [(1,0), (-1,0), (0,1), (0,-1)]
算法步骤
- 读入
R和C。 - 读入
R行南瓜地。 - 读入起点
(A, B)。 - 建立 BFS queue,并把起点加入 queue。
- 标记起点
visited[A][B] = True。 - 当 queue 不为空:
- 弹出一个格子
(r, c)。 - 根据
patch[r][c]把南瓜价值加入total。 - 枚举四个邻居
(nr, nc)。 - 如果邻居在边界内、不是
*、没有访问过,就标记 visited 并加入 queue。
- BFS 结束后,输出
total。
这里有一个很重要的小习惯:
在“入队时”标记 visited,而不是出队时才标记。
这样可以避免同一个格子在被真正处理前,被多个方向重复加入 queue。
Python 解法
from collections import deque
R = int(input())
C = int(input())
patch = [list(input().strip()) for _ in range(R)]
A = int(input())
B = int(input())
value = {
"S": 1,
"M": 5,
"L": 10,
}
directions = [
(1, 0),
(-1, 0),
(0, 1),
(0, -1),
]
visited = [[False] * C for _ in range(R)]
q = deque()
visited[A][B] = True
q.append((A, B))
total = 0
while q:
r, c = q.popleft()
total += value[patch[r][c]]
for dr, dc in directions:
nr = r + dr
nc = c + dc
if nr < 0 or nr >= R or nc < 0 or nc >= C:
continue
if patch[nr][nc] == "*":
continue
if visited[nr][nc]:
continue
visited[nr][nc] = True
q.append((nr, nc))
print(total)
在这份写法里,* 永远不会进入 queue,所以弹出格子时可以直接:
total += value[patch[r][c]]
不用额外判断当前格子是不是南瓜。
C++ 解法
#include <iostream>
#include <queue>
#include <string>
#include <utility>
#include <vector>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int R, C;
cin >> R >> C;
vector<string> patch(R);
for (int r = 0; r < R; r++) {
cin >> patch[r];
}
int A, B;
cin >> A >> B;
vector<vector<bool>> visited(R, vector<bool>(C, false));
queue<pair<int, int>> q;
visited[A][B] = true;
q.push({A, B});
int total = 0;
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 (patch[r][c] == 'S') total += 1;
else if (patch[r][c] == 'M') total += 5;
else if (patch[r][c] == 'L') total += 10;
for (int k = 0; k < 4; k++) {
int nr = r + dr[k];
int nc = c + dc[k];
if (nr < 0 || nr >= R || nc < 0 || nc >= C) {
continue;
}
if (patch[nr][nc] == '*') {
continue;
}
if (visited[nr][nc]) {
continue;
}
visited[nr][nc] = true;
q.push({nr, nc});
}
}
cout << total << "\n";
return 0;
}
C++ 里也保持同样的结构:
queue<pair<int, int>>做 BFS。visited防止重复处理。dr/dc枚举四个方向。- 每访问一个南瓜格,就把价值加到
total。
复杂度分析
每个格子最多进入 queue 一次。每次处理一个格子时,只检查四个方向。
所以时间复杂度是:
O(RC)
空间复杂度主要来自 grid、visited 和 queue:
O(RC)
这也正好适合官方最大数据范围 R * C <= 100000。
常见错误
- 把这题当成找终点或最短路,而不是求整个 reachable component 的总价值。
- 忘记
visited,导致同一个格子反复进队。 - 在出队时才标记 visited,造成同一格子可能被重复加入 queue。
- 把输入坐标
(A, B)当成 1-indexed,又错误地减 1。 - 允许斜方向移动。
- 忘记
*是障碍,不能走进 queue。 - 在 Python 中用递归 DFS 处理大 grid,可能触发 recursion limit。
复习问题
- 为什么 Harvest Waterloo 是 flood fill,而不是最短路?
visited[r][c]应该在什么时候标记?为什么?- 为什么每个格子最多处理一次?
- 如果起点所在区域被
*隔开,其他区域的南瓜为什么不能计入答案? - 这道题用 DFS 可以吗?Python 里为什么 BFS 更稳?
Questions or feedback?
Have a question about this article or want to suggest an improvement? Send us a private message.
有问题或建议?
如果你对本文有疑问,或希望提出改进建议,请给我们发送私密留言。
Related Learning
Continue exploring related learning paths.
These related pages help students and parents move from interest to the right next step.