蓝桥杯11 路径之谜

题目链接:https://www.lanqiao.cn/problems/89/learning/

前置知识

网格与编号

1)二维坐标 (r,c) 的含义

r 是行号(row),c 是列号(col)

范围:0 <= r < n 且 0 <= c < n

例如 n=5:合法坐标是 (0..4, 0..4)

越界判断(写条件必须熟):
if (r < 0 || r >= n || c < 0 || c >= n) // 越界

2)编号规则:id = r*n + c(行优先)

这代表每一行有 n 个格子,从左到右编号,下一行接着编号。

(r,c) -> id:id=r×n+c

id -> (r,c):r=id/n,c=id%n

对应 C++:
int id = r * n + c;
int r = id / n;
int c = id % n;

3)四方向移动(右、左、下、上)

常用写法是方向数组(在 DFS 里会一直用):
int dr[4] = {0, 0, 1, -1};
int dc[4] = {1, -1, 0, 0}; // 右 左 下 上

从 (r,c) 走到下一格 (nr,nc):
int nr = r + dr[k];
int nc = c + dc[k];

4)邻居枚举 + 越界过滤(要形成肌肉记忆)

伪代码类似这样:
for (int k = 0; k < 4; k++) {
int nr = r + dr[k], nc = c + dc[k];
if (nr < 0 || nr >= n || nc < 0 || nc >= n) continue; // 越界不要
// (nr, nc) 就是合法邻居
}

回溯/DFS 模板

1)递归函数在表达什么?

我们定义:dfs(r, c) = "我现在站在 (r,c),接下来继续往下走,尝试把路走完。"

递归不是"玄学",就是把"后续怎么走"交给下一层来做。

2)为什么要 vis(访问标记)?

vis[r][c] 表示这个格子是否已经走过。

题目要求每个格子最多进一次

所以每次准备走到 (nr,nc) 之前要判断:
if (vis[nr][nc]) continue;

3)path 为什么要 push / pop?

path 用来记录走过的路径(一般存 id,便于输出)
走进新格子:path.push_back(id)
回来撤销:path.pop_back()

这就是"现场还原"的一部分。

4)回溯的本质:撤销"本分支做过的一切"

你在一个分支里做的修改,只能对这个分支有效。

所以递归返回时必须撤销:
vis[nr][nc] = false
path.pop_back()

(后面会加:row/col 配额也要加回去)

否则会出现一个经典 bug:走过的痕迹污染别的分支,导致明明有解却搜不到。

题目约束建模

建模:走进一个格子 = 消耗该行与该列各 1 次

1)两个数组分别代表什么?
**row[i]:**第 i 行还需要走多少格(还剩多少"名额")
**col[j]:**第 j 列还需要走多少格

可以理解成:每进入一个格子 (r,c),就对 row[r] 和 col[c] 各扣一次。

2)进入新格子时,状态怎么更新?

从 (r,c) 走到 (nr,nc) 时:
row[nr]--;
col[nc]--;
vis[nr][nc] = 1;
path.push_back(nr*n + nc);
dfs(nr, nc);
path.pop_back();
vis[nr][nc] = 0;
row[nr]++; // 回溯时加回
col[nc]++; // 回溯时加回

注意:row/col 的撤销也必须做,它们和 vis/path 一样,都是"分支状态"。

3)什么时候算"合法到终点"?

到 (n-1,n-1) 还不够,还必须满足:

所有 row[i] == 0

所有 col[j] == 0

也就是该走的行/列次数刚好用完。

会写成:
bool ok = true;
for (int i=0;i<n;i++) if (row[i]!=0) ok=false;
for (int j=0;j<n;j++) if (col[j]!=0) ok=false;
if (ok) found = true;

(后面我们会优化:不每次循环扫一遍,而是用一个 rem 计数。)

4)"没配额了"必须直接剪掉(最基础剪枝)

准备走进 (nr,nc) 前,如果:
row[nr] <= 0 或 col[nc] <= 0

说明这一步会把配额用成负数/不允许 → 必死,不走:
if (row[nr] <= 0 || col[nc] <= 0) continue;

剪枝的基础逻辑

A. 不能走的直接排除(最基础)

这是"硬规则",不满足就别递归:

走 (nr,nc) 前检查:

  1. 越界

  2. 已访问 vis[nr][nc]

  3. 配额不足 row[nr] <= 0 || col[nc] <= 0

代码就是:
if (nr < 0 || nr >= n || nc < 0 || nc >= n) continue;
if (vis[nr][nc]) continue;
if (row[nr] <= 0 || col[nc] <= 0) continue;

B. 距离下界剪枝(很重要,提速最大)

核心:你不可能瞬移。从当前位置到终点至少要走 dist 步。

1)dist 怎么算?

终点是 (n-1, n-1),曼哈顿距离:
int dist = abs(r - (n-1)) + abs(c - (n-1));

这代表至少还要走 dist 步才可能到终点。

2)rem 是什么?

rem 表示"还剩多少步可以走"。

在这题里非常自然的做法是:

每走进一个格子,就消耗 1 次(同时 row/col 也各减 1)

所有格子总共要走的次数其实是固定的

所以我们维护一个:

rem:剩余还要走的"格子进入次数"(含终点/不含看你定义,但要一致)

通常写法:每走一步到新格子,rem--;回溯 rem++

3)剪枝条件:dist > rem 必死

如果你连"最低步数"都不够,那肯定到不了终点:
if (dist > rem) return; // 或 continue,看你放在哪

直觉:

你还剩 3 步,但离终点最少 5 步 → 怎么走都不可能到。

C. 奇偶性剪枝

这条很像"棋盘黑白格":

每走一步,颜色必然翻转一次

所以从当前位置走到终点,你走的步数奇偶必须跟 dist 同奇偶

等价写法(常见):
if ( (rem - dist) % 2 != 0 ) return;// 奇偶对不上必死

为什么是 rem - dist?

dist 是"最低必须走的步数"

rem 是"你实际还能走的步数"

多出来的步数只能靠"绕路"产生,而绕路必然是一进一出之类的 2 步、4 步... 偶数

所以 rem 必须和 dist 同奇偶,差值必须是偶数

为什么能输出"最小编号序"

1)"最小编号序"到底是什么意思?

路径用 id 序列表示(比如 0,1,6,11,...)

比较两条路径时,从前往后比:第一个不同的位置,谁的 id 更小,谁就更小(字典序)

2)为什么 DFS 先找到的就是最小?

前提:你每一步都先尝试更小的候选格子

做法:

枚举 4 个邻居

把合法邻居按 id = nr*n+nc 排序

按排序后的顺序递归

DFS 的特性是:一条路走到底,先找到的解会立刻返回(found=true)

而你每一层都把选择按 id 从小到大试,所以:

第 1 步选的尽可能小

如果第 1 步相同,就让第 2 步尽可能小

......

最终"第一个找到的完整解"就是字典序最小解

代码演变

先写一个能编译运行的空壳

目的:先把 main 搭好,不要一上来就写 dfs

cpp 复制代码
#include <iostream>
using namespace std;

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

    int n;
    cin >> n;
    cout << "\n";
    return 0;
}

把输入读完整 但先不求解

目的:先确认输入格式,能把数据正确读进来

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

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

    int n;
    cin >> n;

    vector<int> col(n), row(n);
    for (int i = 0; i < n; i++) cin >> col[i]; // 北边:列
    for (int i = 0; i < n; i++) cin >> row[i]; // 西边:行

    // 暂时不求解,先结束
    cout << "\n";
    return 0;
}

补状态变量 先把 dfs 壳子写出来

目的:知道 dfs 需要哪些全局状态。现在 dfs 还不干活

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int n;
vector<int> col, row;
vector<vector<char>> vis;
vector<int> path;
bool found = false;

void dfs(int r, int c) {
    // 先放一个空壳,后面逐步填
    return;
}

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

    cin >> n;
    col.assign(n, 0);
    row.assign(n, 0);
    for (int i = 0; i < n; i++) cin >> col[i];
    for (int i = 0; i < n; i++) cin >> row[i];

    vis.assign(n, vector<char>(n, 0));
    path.clear();
    found = false;

    cout << "\n";
    return 0;

在 dfs 里加走格子的最基础逻辑

目的:先把 DFS 回溯模板写熟 做选择 递归 撤销

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int n;
vector<int> col, row;
vector<vector<char>> vis;
vector<int> path;
bool found = false;

void dfs(int r, int c) {
    if (found) return;

    if (r == n - 1 && c == n - 1) { // 到终点就停
        found = true;
        return;
    }

    const int dr[4] = {0, 0, 1, -1};
    const int dc[4] = {1, -1, 0, 0};

    for (int k = 0; k < 4; k++) {
        int nr = r + dr[k], nc = c + dc[k];
        if (nr < 0 || nr >= n || nc < 0 || nc >= n) continue;
        if (vis[nr][nc]) continue;

        // 做选择
        vis[nr][nc] = 1;
        path.push_back(nr * n + nc);

        dfs(nr, nc);
        if (found) return;

        // 撤销选择
        path.pop_back();
        vis[nr][nc] = 0;
    }
}

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

    cin >> n;
    col.assign(n, 0);
    row.assign(n, 0);
    for (int i = 0; i < n; i++) cin >> col[i];
    for (int i = 0; i < n; i++) cin >> row[i];

    vis.assign(n, vector<char>(n, 0));
    path.clear();
    found = false;

    // 起点加入路径
    vis[0][0] = 1;
    path.push_back(0);

    dfs(0, 0);

    for (int i = 0; i < path.size(); i++) {
        if (i) cout << ' ';
        cout << path[i];
    }
    cout << "\n";
    return 0;
}

加上题目的行列配额约束

规则:走到 (nr,nc) 就 row[nr]--、col[nc]--

目标:到终点时所有 row/col 都减到 0

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int n;
vector<int> col, row;
vector<vector<char>> vis;
vector<int> path;
bool found = false;

void dfs(int r, int c) {
    if (found) return;

    if (r == n - 1 && c == n - 1) {
        // 必须所有行列都刚好用完
        for (int i = 0; i < n; i++) {
            if (row[i] != 0 || col[i] != 0) return;
        }
        found = true;
        return;
    }

    const int dr[4] = {0, 0, 1, -1};
    const int dc[4] = {1, -1, 0, 0};

    for (int k = 0; k < 4; k++) {
        int nr = r + dr[k], nc = c + dc[k];
        if (nr < 0 || nr >= n || nc < 0 || nc >= n) continue;
        if (vis[nr][nc]) continue;

        // 如果该行/列已经没配额了,不能走
        if (row[nr] <= 0 || col[nc] <= 0) continue;

        // 做选择:消耗配额
        vis[nr][nc] = 1;
        row[nr]--; col[nc]--;
        path.push_back(nr * n + nc);

        dfs(nr, nc);
        if (found) return;

        // 撤销选择:恢复配额
        path.pop_back();
        row[nr]++; col[nc]++;
        vis[nr][nc] = 0;
    }
}

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

    cin >> n;
    col.assign(n, 0);
    row.assign(n, 0);
    for (int i = 0; i < n; i++) cin >> col[i];
    for (int i = 0; i < n; i++) cin >> row[i];

    // 起点也要消耗一次
    if (row[0] <= 0 || col[0] <= 0) { cout << "\n"; return 0; }
    row[0]--; col[0]--;

    vis.assign(n, vector<char>(n, 0));
    vis[0][0] = 1;
    path.clear();
    path.push_back(0);
    found = false;

    dfs(0, 0);

    for (int i = 0; i < path.size(); i++) {
        if (i) cout << ' ';
        cout << path[i];
    }
    cout << "\n";
    return 0;
}

加输出最小编号序的保证

原题常会要求:若多解,输出编号序列最小的那条

做法:把下一步候选点按 id 排序后依次尝试

这一步本质是暴力:

枚举所有不重复路径,只不过用行列配额做了最基本的合法性过滤

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <utility>
using namespace std;

int n;
vector<int> col, row;
vector<vector<char>> vis;
vector<int> path;
bool found = false;

void dfs(int r, int c) {
    if (found) return;

    if (r == n - 1 && c == n - 1) {
        for (int i = 0; i < n; i++) {
            if (row[i] != 0 || col[i] != 0) return;
        }
        found = true;
        return;
    }

    const int dr[4] = {0, 0, 1, -1};
    const int dc[4] = {1, -1, 0, 0};

    vector<pair<int, pair<int,int>>> nxt; // (id,(nr,nc))
    for (int k = 0; k < 4; k++) {
        int nr = r + dr[k], nc = c + dc[k];
        if (nr < 0 || nr >= n || nc < 0 || nc >= n) continue;
        if (vis[nr][nc]) continue;
        if (row[nr] <= 0 || col[nc] <= 0) continue;
        nxt.push_back({nr * n + nc, {nr, nc}});
    }
    sort(nxt.begin(), nxt.end());

    for (auto &it : nxt) {
        int id = it.first;
        int nr = it.second.first, nc = it.second.second;

        vis[nr][nc] = 1;
        row[nr]--; col[nc]--;
        path.push_back(id);

        dfs(nr, nc);
        if (found) return;

        path.pop_back();
        row[nr]++; col[nc]++;
        vis[nr][nc] = 0;
    }
}

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

    cin >> n;
    col.assign(n, 0);
    row.assign(n, 0);
    for (int i = 0; i < n; i++) cin >> col[i];
    for (int i = 0; i < n; i++) cin >> row[i];

    if (row[0] <= 0 || col[0] <= 0) { cout << "\n"; return 0; }
    row[0]--; col[0]--;

    vis.assign(n, vector<char>(n, 0));
    vis[0][0] = 1;
    path.clear();
    path.push_back(0);
    found = false;

    dfs(0, 0);

    for (int i = 0; i < path.size(); i++) {
        if (i) cout << ' ';
        cout << path[i];
    }
    cout << "\n";
    return 0;
}

加剪枝 奇偶 + 行列上界

这一步就是最终版,多了 rem/usedR/usedC 和剪枝判断

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

int n, rem;
vector<int> col, row, usedR, usedC;
vector<vector<char>> vis;
vector<int> path;
bool found = false;

void dfs(int r, int c) {
    if (found) return;

    int dist = abs(r - (n - 1)) + abs(c - (n - 1));
    if (dist > rem) return;
    if (((rem - dist) & 1) != 0) return;

    for (int i = 0; i < n; i++) {
        if (row[i] < 0 || col[i] < 0) return;
        if (row[i] > n - usedR[i]) return;
        if (col[i] > n - usedC[i]) return;
    }

    if (r == n - 1 && c == n - 1) {
        if (rem == 0) found = true;
        return;
    }

    const int dr[4] = {0, 0, 1, -1};
    const int dc[4] = {1, -1, 0, 0};
    vector<pair<int, pair<int,int>>> nxt;

    for (int k = 0; k < 4; k++) {
        int nr = r + dr[k], nc = c + dc[k];
        if (nr < 0 || nr >= n || nc < 0 || nc >= n) continue;
        if (vis[nr][nc]) continue;
        if (row[nr] <= 0 || col[nc] <= 0) continue;
        nxt.push_back({nr * n + nc, {nr, nc}});
    }
    sort(nxt.begin(), nxt.end());

    for (auto &it : nxt) {
        int nr = it.second.first, nc = it.second.second, id = it.first;

        vis[nr][nc] = 1;
        row[nr]--; col[nc]--;
        usedR[nr]++; usedC[nc]++;
        path.push_back(id);
        rem--;

        dfs(nr, nc);
        if (found) return;

        rem++;
        path.pop_back();
        usedR[nr]--; usedC[nc]--;
        row[nr]++; col[nc]++;
        vis[nr][nc] = 0;
    }
}

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

    cin >> n;
    if (n <= 0) { cout << "\n"; return 0; }

    col.assign(n, 0); row.assign(n, 0);
    for (int i = 0; i < n; i++) cin >> col[i];
    for (int i = 0; i < n; i++) cin >> row[i];

    int sumC = 0, sumR = 0;
    for (int x : col) sumC += x;
    for (int x : row) sumR += x;
    if (sumC != sumR || row[0] <= 0 || col[0] <= 0) { cout << "\n"; return 0; }

    vis.assign(n, vector<char>(n, 0));
    usedR.assign(n, 0); usedC.assign(n, 0);
    path.clear();

    vis[0][0] = 1;
    row[0]--; col[0]--;
    usedR[0] = usedC[0] = 1;
    path.push_back(0);
    rem = sumR - 1;

    dfs(0, 0);

    for (int i = 0; i < path.size(); i++) {
        if (i) cout << ' ';
        cout << path[i];
    }
    cout << "\n";
    return 0;
}
相关推荐
羑悻的小杀马特1 小时前
深入C++与Redis的异构之美:用redis-plus-plus优雅操控键值宇宙之通用命令版!
c++·redis
MSTcheng.2 小时前
【C++】菱形继承为何会引发二义性?虚继承如何破解?
开发语言·c++
Lion Long2 小时前
C++20 异步编程:用future、promise 还是协程?
开发语言·c++·stl·c++20
渡我白衣2 小时前
计算机组成原理(3):计算机软件
java·c语言·开发语言·jvm·c++·人工智能·python
qq_310658512 小时前
mediasoup源码走读(三)Node.js 控制面
c++·音视频
小龙报2 小时前
【C语言初阶】动态内存分配实战指南:C 语言 4 大函数使用 + 经典笔试题 + 柔性数组优势与内存区域
android·c语言·开发语言·数据结构·c++·算法·visual studio
小龙报2 小时前
【算法通关指南:算法基础篇(三)】一维差分专题:1.【模板】差分 2.海底高铁
android·c语言·数据结构·c++·算法·leetcode·visual studio
小李小李快乐不已2 小时前
图论理论基础(5)
数据结构·c++·算法·机器学习·动态规划·图论
承渊政道2 小时前
C++学习之旅【C++基础知识介绍】
c语言·c++·学习·程序人生