蓝桥杯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 

#include <iostream>
using namespace std;

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

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

把输入读完整 但先不求解

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

#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 还不干活

#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 回溯模板写熟 做选择 递归 撤销

#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

#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 排序后依次尝试

这一步本质是暴力:
枚举所有不重复路径,只不过用行列配额做了最基本的合法性过滤

#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 和剪枝判断

#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;
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值