CCC 2020 J5/S2 Escape Room Solution: BFS on an Implicit Graph



Start NorthStar

CP Weekly Challenge

CCC 2020 J5/S2 Escape Room Solution: BFS on an Implicit Graph

Learn how to solve Escape Room with BFS on an implicit graph, product-to-cells indexing, and visited-value optimization in Python and C++.

CP Weekly Challenge

CCC 2020 J5/S2 Escape Room 题解:把乘积跳跃看成隐式图搜索

把乘积跳跃看成隐式图搜索,用 product-to-cells 索引和 BFS 判断能否逃出房间。

Share this article

Copy the link, open your phone share sheet, scan the QR code for WeChat, or send by email.




Email

This problem looks like a grid movement problem, but it is not about moving up, down, left, or right. Each cell contains a number, and that number tells us which coordinates we are allowed to jump to.

The clean way to solve the problem is to model the grid as an implicit graph. Each cell is a node, and from a cell with value x, we can jump to every cell (a, b) where a * b = x.

What the Problem Is Asking

We are given an M x N grid. Rows are numbered from 1 to M, and columns are numbered from 1 to N.

We start at (1, 1) and want to reach (M, N).

If the current cell contains x, then we may jump to any valid cell (a, b) such that:

a * b = x

If the target cell is outside the grid, it is not a valid jump.

We output yes if the exit can be reached, and no otherwise.

Where Beginners Often Get Stuck

The first trap is treating this like a normal grid traversal. There are no four-direction moves here.

The second trap is factoring the current value again and again. For example, from a cell with value 12, we may think about factor pairs such as (1, 12), (2, 6), (3, 4), and so on.

That works conceptually, but repeated factor checks can become inefficient. A better approach is to reverse the question:

For each possible product p, which cells have row * column = p?

Core Algorithm Idea

We build an index:

product_to_cells[p] = all cells (r, c) where r * c = p

Then, when BFS reaches a cell with value x, all possible next cells are already stored in:

product_to_cells[x]

This avoids recomputing factor pairs during the search.

We also use two visited structures:

visited[r][c]
expanded[x]

visited[r][c] means the cell has already been added to the BFS queue.

expanded[x] means we have already processed all cells in product_to_cells[x]. This matters because many different cells can contain the same value.

Python Solution

from collections import deque


def solve():
    M = int(input())
    N = int(input())

    grid = [[0] * (N + 1)]
    for _ in range(M):
        grid.append([0] + list(map(int, input().split())))

    max_product = M * N

    product_to_cells = [[] for _ in range(max_product + 1)]
    for r in range(1, M + 1):
        for c in range(1, N + 1):
            product_to_cells[r * c].append((r, c))

    visited = [[False] * (N + 1) for _ in range(M + 1)]
    expanded = [False] * (max_product + 1)

    q = deque([(1, 1)])
    visited[1][1] = True

    while q:
        r, c = q.popleft()

        if r == M and c == N:
            print("yes")
            return

        value = grid[r][c]
        if value < 1 or value > max_product:
            continue

        if expanded[value]:
            continue

        expanded[value] = True

        for nr, nc in product_to_cells[value]:
            if not visited[nr][nc]:
                visited[nr][nc] = True
                q.append((nr, nc))

    print("no")


solve()

C++ Solution

#include <iostream>
#include <queue>
#include <utility>
#include <vector>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int M, N;
    cin >> M >> N;

    vector<vector<int>> grid(M + 1, vector<int>(N + 1));
    for (int r = 1; r <= M; r++) {
        for (int c = 1; c <= N; c++) {
            cin >> grid[r][c];
        }
    }

    int maxProduct = M * N;

    vector<vector<pair<int, int>>> productToCells(maxProduct + 1);
    for (int r = 1; r <= M; r++) {
        for (int c = 1; c <= N; c++) {
            productToCells[r * c].push_back({r, c});
        }
    }

    vector<vector<bool>> visited(M + 1, vector<bool>(N + 1, false));
    vector<bool> expanded(maxProduct + 1, false);

    queue<pair<int, int>> q;
    q.push({1, 1});
    visited[1][1] = true;

    while (!q.empty()) {
        auto [r, c] = q.front();
        q.pop();

        if (r == M && c == N) {
            cout << "yes\n";
            return 0;
        }

        int value = grid[r][c];
        if (value < 1 || value > maxProduct) {
            continue;
        }

        if (expanded[value]) {
            continue;
        }

        expanded[value] = true;

        for (auto [nr, nc] : productToCells[value]) {
            if (!visited[nr][nc]) {
                visited[nr][nc] = true;
                q.push({nr, nc});
            }
        }
    }

    cout << "no\n";
    return 0;
}

Complexity Analysis

Building the product index processes every cell once, so it takes:

O(MN)

During BFS, each cell is added to the queue at most once. Each product list is expanded at most once, and all product lists together contain exactly MN cell coordinates.

The total time complexity is:

O(MN)

The memory complexity is:

O(MN)

Common Mistakes

  1. Treating the problem as four-direction grid movement.
  2. Forgetting that coordinates are 1-indexed.
  3. Recomputing factor pairs every time a cell is visited.
  4. Not using expanded[value], which causes repeated scans of the same destination list.
  5. Forgetting to skip values larger than M * N.
  6. Looking for a shortest path when the problem only asks whether the exit is reachable.

Review Questions

  1. Why is Escape Room an implicit graph problem?
  2. What does product_to_cells[x] store?
  3. Why do we need both visited[r][c] and expanded[x]?
  4. Why can a value larger than M * N lead nowhere?
  5. Would DFS also work for this problem? Why?

这道题看起来像在一个房间 grid 里移动,但真正难的地方不是上下左右走格子,而是理解“格子里的数字”其实在告诉你下一批可能到达的位置。

如果你把每个格子当成一个 graph node,那么这道题就是一个 reachability problem:从左上角 (1, 1) 出发,能不能最终到达右下角 (M, N)。关键是这些边不需要提前全部画出来,我们可以在 BFS 过程中按需要展开。

题目要我们做什么

房间是一个 M x N 的 grid。每个格子 (r, c) 里有一个正整数。

你从左上角 (1, 1) 开始,目标是到达右下角 (M, N)

移动规则比较特别:

如果当前格子的数字是 x,
你可以跳到任何满足 a * b = x 的格子 (a, b)。

例如,一个格子的数字是 6,理论上可以跳到:

(1, 6), (2, 3), (3, 2), (6, 1)

但只有在这些 row 和 column 都在 grid 范围内时,才是真正可跳的位置。

最后输出:

yes

如果可以逃出;否则输出:

no

初学者容易卡在哪里

很多同学第一眼会把它当成普通 grid problem,然后开始想:

从这个格子往上、下、左、右走?

但这题不是四方向移动。你能跳到哪里,只由当前格子的 value 决定。

第二个常见误区是:每到一个格子,就现场分解当前数字 x,枚举所有 factor pair。

例如:

x = 12
factor pairs: (1,12), (2,6), (3,4), ...

这个做法可以写出来,但如果很多格子反复出现同一个 value,我们会重复做大量工作。

这道题真正要练的是一个更竞赛化的思维:

不要问“这个数字有哪些因子”。
先反过来建索引:哪些格子的 row * col 等于这个数字?

核心算法概念

这道题的核心是 implicit graph search。

我们可以把每个格子看成一个节点:

node = (r, c)

如果格子 (r, c) 的值是 x,那么它有 directed edges 指向所有满足:

a * b = x

的格子 (a, b)

因为我们只需要知道“能不能到达终点”,不需要最短路径长度,所以 BFS 或 DFS 都可以。这里使用 BFS,写法稳定,也方便控制 visited。

数据结构设计

最重要的数据结构是:

product_to_cells[p] = 所有满足 r * c = p 的格子

例如在一个 3 x 4 grid 里:

product_to_cells[3] = [(1, 3), (3, 1)]
product_to_cells[4] = [(1, 4), (2, 2)]
product_to_cells[12] = [(3, 4)]

这样,当我们站在一个 value 为 x 的格子时,不用现场找因子,直接查:

product_to_cells[x]

就能得到所有合法目的地。

还需要两个 visited 结构:

visited[r][c] = 这个格子是否已经进入过 BFS queue
expanded[x] = value x 对应的目的地列表是否已经展开过

visited 防止同一个 cell 反复进队。

expanded 是这题的关键优化。如果多个已访问格子的 value 都是 12,我们只需要展开一次 product_to_cells[12]。第二次遇到 value 12 时,直接跳过,因为所有能由 12 到达的位置已经处理过了。

算法步骤

  1. 读入 M, N 和整个 grid。
  2. 建立 product_to_cells
  • 对每个合法坐标 (r, c),计算 p = r * c
  • (r, c) 放进 product_to_cells[p]
  1. (1, 1) 开始 BFS。
  2. 每次弹出当前格子 (r, c)
  • 如果它是 (M, N),输出 yes
  • 读取当前格子的值 x = grid[r][c]
  • 如果 x 不在 1..M*N 之间,它不可能对应任何合法格子,跳过。
  • 如果 x 已经展开过,跳过。
  • 否则遍历 product_to_cells[x],把没访问过的格子加入 BFS queue。
  1. 如果 BFS 结束仍然没有到达 (M, N),输出 no

Python 解法

from collections import deque


def solve():
    M = int(input())
    N = int(input())

    grid = [[0] * (N + 1)]
    for _ in range(M):
        grid.append([0] + list(map(int, input().split())))

    max_product = M * N

    product_to_cells = [[] for _ in range(max_product + 1)]
    for r in range(1, M + 1):
        for c in range(1, N + 1):
            product_to_cells[r * c].append((r, c))

    visited = [[False] * (N + 1) for _ in range(M + 1)]
    expanded = [False] * (max_product + 1)

    q = deque()
    q.append((1, 1))
    visited[1][1] = True

    while q:
        r, c = q.popleft()

        if r == M and c == N:
            print("yes")
            return

        value = grid[r][c]

        if value < 1 or value > max_product:
            continue

        if expanded[value]:
            continue

        expanded[value] = True

        for nr, nc in product_to_cells[value]:
            if not visited[nr][nc]:
                visited[nr][nc] = True
                q.append((nr, nc))

    print("no")


solve()

这份代码里有一个细节很重要:

expanded[value] = True

它不是标记一个格子,而是标记“这个乘积 value 对应的所有目的地已经展开过”。这是避免重复扫描的关键。

C++ 解法

#include <iostream>
#include <queue>
#include <utility>
#include <vector>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int M, N;
    cin >> M >> N;

    vector<vector<int>> grid(M + 1, vector<int>(N + 1));
    for (int r = 1; r <= M; r++) {
        for (int c = 1; c <= N; c++) {
            cin >> grid[r][c];
        }
    }

    int maxProduct = M * N;

    vector<vector<pair<int, int>>> productToCells(maxProduct + 1);
    for (int r = 1; r <= M; r++) {
        for (int c = 1; c <= N; c++) {
            productToCells[r * c].push_back({r, c});
        }
    }

    vector<vector<bool>> visited(M + 1, vector<bool>(N + 1, false));
    vector<bool> expanded(maxProduct + 1, false);

    queue<pair<int, int>> q;
    q.push({1, 1});
    visited[1][1] = true;

    while (!q.empty()) {
        auto [r, c] = q.front();
        q.pop();

        if (r == M && c == N) {
            cout << "yes\n";
            return 0;
        }

        int value = grid[r][c];

        if (value < 1 || value > maxProduct) {
            continue;
        }

        if (expanded[value]) {
            continue;
        }

        expanded[value] = true;

        for (auto [nr, nc] : productToCells[value]) {
            if (!visited[nr][nc]) {
                visited[nr][nc] = true;
                q.push({nr, nc});
            }
        }
    }

    cout << "no\n";
    return 0;
}

C++ 版本和 Python 版本的逻辑完全一致:

  • productToCells 是乘积到坐标列表的索引。
  • visited 标记已经进过 queue 的格子。
  • expanded 标记某个 value 是否已经展开过。

复杂度分析

建立 product_to_cells 时,每个格子处理一次:

O(MN)

BFS 中,每个格子最多进队一次。每个 product 的目的地列表最多展开一次。所有目的地列表加起来一共也是 MN 个坐标。

所以总时间复杂度是:

O(MN)

空间复杂度主要来自 grid、visited 和 product_to_cells:

O(MN)

常见错误

  1. 把题目误解成上下左右四方向移动。
  2. 忘记坐标是从 1 开始编号,不是从 0 开始。
  3. 每次都重新枚举因子,导致重复工作太多。
  4. 只用 visited cell,但没有用 expanded value,同一个 value 会被反复展开。
  5. 没有处理 value > M * N 的情况,结果访问 product 索引时越界。
  6. 以为一定要找最短路径。其实这题只问能不能到达。

复习问题

  1. 为什么这道题可以看成 graph reachability?
  2. product_to_cells[x] 存的是什么?
  3. 为什么 expanded[value] 比只用 visited[r][c] 更快?
  4. 如果某个格子的 value 大于 M * N,为什么它不能跳到任何合法格子?
  5. BFS 和 DFS 在这题里都可以吗?为什么?

Questions or feedback?

Have a question about this article or want to suggest an improvement? Send us a private message.

Send Feedback

有问题或建议?

如果你对本文有疑问,或希望提出改进建议,请给我们发送私密留言。

发送反馈

Back to CP Weekly Challenge
返回 CP Weekly Challenge

滚动至顶部