CSP 真题解析:[CSP-J 2019-T4] 加工零件

CSP-J 2019-T4 加工零件

摘要: 本文详细解析了 CSP-J 2019 第四题「加工零件」的解题思路与 C++ 实现。题目本质上是判断无向图中从 1 号点出发,能否恰好走 L 步到达目标点 a。利用「反复横跳不改变路径奇偶性」的图论性质,问题转化为求 1 号点到各点的奇数最短路和偶数最短路。文章通过 BFS 预处理奇偶最短距离,实现 O(1) 在线查询,并梳理了孤立点特判、数组越界、无穷大初始化等常见易错点,附带完整参考代码。

题目描述

凯凯的工厂正在有条不紊地生产一种神奇的零件,神奇的零件的生产过程自然也很神奇。工厂里有 n n n 位工人,工人们从 1 ∼ n 1 \sim n 1∼n 编号。某些工人之间存在双向的零件传送带。保证每两名工人之间最多只存在一条传送带。

如果 x x x 号工人想生产一个被加工到第 L   ( L > 1 ) L\,(L \gt 1) L(L>1) 阶段的零件,则所有 与 x x x 号工人有传送带直接 相连的工人,都需要生产一个被加工到第 L − 1 L - 1 L−1 阶段的零件(但 x x x 号工人自己无需 生产第 L − 1 L - 1 L−1 阶段的零件)。

如果 x x x 号工人想生产一个被加工到第 1 1 1 阶段的零件,则所有 与 x x x 号工人有传送带直接 相连的工人,都需要为 x x x 号工人提供一个原材料。

轩轩是 1 1 1 号工人。现在给出 q q q 张工单,第 i i i 张工单表示编号为 a i a_i ai 的工人想生产一个第 L i L_i Li 阶段的零件。轩轩想知道对于每张工单,他是否需要给别人提供原材料。他知道聪明的你一定可以帮他计算出来!

输入格式

第一行三个正整数 n n n, m m m 和 q q q,分别表示工人的数目、传送带的数目和工单的数目。

接下来 m m m 行,每行两个正整数 u u u 和 v v v,表示编号为 u u u 和 v v v 的工人之间存在一条零件传输带。保证 u ≠ v u \neq v u=v。

接下来 q q q 行,每行两个正整数 a a a 和 L L L,表示编号为 a a a 的工人想生产一个第 L L L 阶段的零件。

输出格式

共 q q q 行,每行一个字符串 Yes 或者 No。如果按照第 i i i 张工单生产,需要编号为 1 的轩轩提供原材料,则在第 i i i 行输出 Yes;否则在第 i i i 行输出 No

输入输出样例 #1

输入 #1

复制代码
3 2 6
1 2
2 3
1 1
2 1
3 1
1 2
2 2
3 2

输出 #1

复制代码
No
Yes
No
Yes
No
Yes

输入输出样例 #2

输入 #2

复制代码
5 5 5
1 2
2 3
3 4
4 5
1 5
1 1
1 2
1 3
1 4
1 5

输出 #2

复制代码
No
Yes
No
Yes
Yes

说明/提示

样例 1 说明

编号为 1 的工人想生产第 1 阶段的零件,需要编号为 2 的工人提供原材料。

编号为 2 的工人想生产第 1 阶段的零件,需要编号为 1 和 3 的工人提供原材料。

编号为 3 的工人想生产第 1 阶段的零件,需要编号为 2 的工人提供原材料。

编号为 1 的工人想生产第 2 阶段的零件,需要编号为 2 的工人生产第 1 阶段的零 件,需要编号为 1 和 3 的工人提供原材料。

编号为 2 的工人想生产第 2 阶段的零件,需要编号为 1 和 3 的工人生产第 1 阶段的零件,他/她们都需要编号为 2 的工人提供原材料。

编号为 3 的工人想生产第 2 阶段的零件,需要编号为 2 的工人生产第 1 阶段的零件,需要编号为 1 和 3 的工人提供原材料。

样例 2 说明

编号为 1 的工人想生产第 1 阶段的零件,需要编号为 2 和 5 的工人提供原材料。

编号为 1 的工人想生产第 2 阶段的零件,需要编号为 2 和 5 的工人生产第 1 阶段的零件,需要编号为 1,3,4 的工人提供原材料。

编号为 1 的工人想生产第 3 阶段的零件,需要编号为 2 和 5 的工人生产第 2 阶段的零件,需要编号为 1,3,4 的工人生产第 1 阶段的零件,需要编号为 2,3,4,5 的工人提供原材料。

编号为 1 的工人想生产第 4 阶段的零件,需要编号为 2 和 5 的工人生产第 3 阶段的零件,需要编号为 1,3,4 的工人生产第 2 阶段的零件,需要编号为 2,3,4,5 的工人生产第 1 阶段的零件,需要全部工人提供原材料。

编号为 1 的工人想生产第 5 阶段的零件,需要编号为 2 和 5 的工人生产第 4 阶段的零件,需要编号为 1,3,4 的工人生产第 3 阶段的零件,需要编号为 2,3,4,5 的工人生产第 2 阶段的零件,需要全部工人生产第 1 阶段的零件,需要全部工人提供原材料。

数据规模与约定

共 20 20 20 个测试点。

对所有测试点保证 1 ≤ u , v , a ≤ n 1 \leq u, v, a \leq n 1≤u,v,a≤n。

测试点 1 ∼ 4 1\sim4 1∼4, 1 ≤ n , m ≤ 1000 1 \leq n, m \leq 1000 1≤n,m≤1000, q = 3 q = 3 q=3, L = 1 L = 1 L=1。

测试点 5 ∼ 8 5\sim8 5∼8, 1 ≤ n , m ≤ 1000 1 \leq n, m \leq 1000 1≤n,m≤1000, q = 3 q = 3 q=3, 1 ≤ L ≤ 10 1 \leq L \leq 10 1≤L≤10。

测试点 9 ∼ 12 9\sim12 9∼12, 1 ≤ n , m , L ≤ 1000 1 \leq n, m, L \leq 1000 1≤n,m,L≤1000, 1 ≤ q ≤ 100 1 \leq q \leq 100 1≤q≤100。

测试点 13 ∼ 16 13\sim16 13∼16, 1 ≤ n , m , L ≤ 1000 1 \leq n, m, L \leq 1000 1≤n,m,L≤1000, 1 ≤ q ≤ 10 5 1 \leq q \leq 10^5 1≤q≤105。

测试点 17 ∼ 20 17\sim20 17∼20, 1 ≤ n , m , q ≤ 10 5 1 \leq n, m, q \leq 10^5 1≤n,m,q≤105, 1 ≤ L ≤ 10 9 1 \leq L \leq 10^9 1≤L≤109。

思路要点

工厂里有 n n n 个工人,工人之间有双向传送带(无向图)。

如果 a a a 号工人要生产一个第 L L L 阶段的零件,跟他在图上有边直接相连 的所有人就要生产第 L − 1 L-1 L−1 阶段的零件;以此类推,一直传导下去。最后第 1 1 1 阶段零件的相邻工人需要提供原材料 (也就是倒数第 0 0 0 阶段)。

题目问:当 a a a 号工人生产第 L L L 阶段零件时, 1 1 1** 号工人(轩轩)是否需要提供原材料?**

关键思路

零件生产的"反向需求传递",本质上就是在图上走一步

  • a a a 需要第 L L L 阶段 → \rightarrow → 找与 a a a 距离为 1 1 1 的人(第 L − 1 L-1 L−1 阶段) → \rightarrow → 找与 a a a 距离为 2 2 2 的人(第 L − 2 L-2 L−2 阶段) → \rightarrow → ... → \rightarrow → 找与 a a a 距离为 L L L 的人(提供原材料)。

  • 因此,问题等价转化为:在无向图中,从 a a a 点出发,能否刚好走 L L L 步到达 1 1 1 号点? (因为是双向传送带,从 a a a 走 L L L 步到 1 1 1,等价于1 1 1 号点出发,能否刚好走 L L L 步到达 a a a。)

  • 无向图有一个重要性质:如果从 1 1 1a a a 有一条长度为 d d d 的路径,那么只要我们在一条边上"反复横跳"(来回走),就可以构造出长度为 d + 2 , d + 4 , d + 6 , ... d+2, d+4, d+6, \dots d+2,d+4,d+6,... 的路径。****注意:但你无法 通过反复横跳把一条偶数长度的路径变成奇数长度!路径长度的奇偶性 在反复横跳中是绝对不变 的(每次来回必定 + 2 +2 +2)。

至此,我们可以总结下思路:要判断能否从 1 1 1 走 L L L 步到达 a a a,只需要满足两个条件:

  • 奇偶性相同 : L L L 的奇偶性,必须与从 1 1 1 到 a a a 的某条路径的奇偶性相同。

  • 长度足够大 : L L L 必须 ≥ \ge ≥ 从 1 1 1 到 a a a 该奇偶性下的最短路径长度(多了的步数我们可以通过反复横跳消耗掉)。

所以,我们只需要针对每个点 i i i,求出:

  • ev[i]:从 1 1 1 号点到 i i i 号点的偶数最短路径长度

  • od[i]:从 1 1 1 号点到 i i i 号点的奇数最短路径长度

查询时:

  • 如果 L L L 是奇数:只要 L ≥ o d a L \ge oda L≥oda,输出 Yes,否则 No

  • 如果 L L L 是偶数:只要 L ≥ e v a L \ge eva L≥eva,输出 Yes,否则 No

解题步骤

我们以 样例 1 为例,模拟代码的图构建与 BFS 执行过程:

输入:

n = 3 , m = 2 , q = 6 n=3, m=2, q=6 n=3,m=2,q=6

边: ( 1 , 2 ) , ( 2 , 3 ) (1,2), (2,3) (1,2),(2,3)

  1. 图的初始化与建边 (链式前向星)

代码中使用 head, to, nxt 数组模拟邻接表:

  • 插入边 1 ↔ 2 1 \leftrightarrow 2 1↔2 和 2 ↔ 3 2 \leftrightarrow 3 2↔3 后,图结构如下:

    1 1 1 的邻居: 2 2 2

    2 2 2 的邻居: 3 , 1 3, 1 3,1

    3 3 3 的邻居: 2 2 2

  1. BFS 初始化:设置距离数组起点值

定义 ev[maxn] (偶数最短路) 和 od[maxn] (奇数最短路),初始全部设为无穷大 0x3f3f3f3f

  • 起点为 1 1 1 号点:走 0 0 0 步(偶数)到自己,所以初始化 ev[1] = 0

  • 将起点 1 1 1 压入队列 q1.push(1)

  1. BFS 广度优先搜索更新奇偶最短路径

    我们通过 ud(x, y, v) 函数进行松弛操作(用当前的步数 + 1 +1 +1 去更新目标点对应奇偶性的最短路):

    当前边起点 u 当前边终点 v 松弛逻辑 (当前长度 + 1) 更新后的距离 是否入队
    u = 1 v = 2 ev1+1=1 (奇数) 去更新 od2 od2 = 1 push(2)
    u = 2 v = 3 od2+1=2 (偶数) 去更新 ev3 ev3 = 2 push(3)
    v = 1 od2+1=2 (偶数) 去更新 ev1 ev1已经是0,不更新
    u = 3 v = 2 ev3+1=3 (奇数) 去更新 od2 od2已经是1,不更新
    此时队列为空,BFS 结束。

    最终得到的距离表: 1 1 1 号点:ev[1] = 0, od[1] = ∞ (注意:在图中如果 1 连了 2,其实走 2 步可以回到 1,这里因为连了边,实际循环里如果有边触发会算出 ev[1]=0, od[1]=∞;由于 1 到 2 有边,真正跑完样例 1 最终会算出 ev[1]=0, od[1]=∞, ev[2]=2, od[2]=1, ev[3]=2, od[3]=3,具体看反复横跳更新)。

  2. 处理 O ( 1 ) O(1) O(1) 判定询问

    • 以工单 a = 1, L = 1 为例:

      • L = 1 L=1 L=1 为奇数,我们查看 od[1]

      • 只有当 1 1 1 有连边时, 1 → 2 → 1 1 \rightarrow 2 \rightarrow 1 1→2→1 长度为 2 (偶数),而在样例1中无法 1 步走回 1。

      • 此时 L < o d 1 L < od1 L<od1(或 o d 1 od1 od1 为无穷大),输出 No

    • 以工单 a = 2, L = 1 为例:

      • L = 1 L=1 L=1 为奇数,查看 od[2]

      • 我们查表发现 od[2] = 1

      • 满足 l >= od[a] 且奇偶性相同,调用 jg() 函数输出 Yes

本题易错点

  • 坑一:孤立点特判

    要点提醒 :如果 1 1 1 号点没有连出任何一条边,那么任何人想生产零件, 1 1 1 号点都绝对无法提供原材料!所以必须加 if(!head[1]) { printf("No\n"); continue; }

  • 坑二:数组开二倍空间

    要点提醒: 链式前向星存无向图,一条无向边相当于两条有向边。 tonxt 数组必须开到 maxn * 2,否则会引发数组越界(RE)。

  • 坑三:距离数组初始化为无穷大 0x3f

    要点提醒: 用 BFS 不断更新找最短路径,需要给每个点的初识路径长度设为一个极大值。每个字节被设为 0x3f,整型变量实际上变成了 0x3f3f3f3f(十进制约 10 亿),这个值足够大(大于题目最大的边长),同时两个 0x3f3f3f3f 相加不会发生 int 溢出变成负数。

参考代码

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

int n, m, q;
int k, to[maxn * 2], head[maxn], nxt[maxn * 2]; // 链式前向星存图数组,无向图开两倍空间
int ev[maxn], od[maxn]; // ev: 偶数最短路;od: 奇数最短路
queue<int> q1; // BFS 队列

void adde(int u, int v){ // 建边: u -> v
    to[++k] = v;
    nxt[k]  = head[u];
    head[u] = k;
}

// 最短路松弛优化函数:用 x + 1 的长度去尝试更新目标值 y
void ud(int x, int &y, int v){
    if(x + 1 < y){
        y = x + 1;
        q1.push(v); // 只有最短路被更新了,才需要入队继续推导
    }
}

// 求解从结点 1 出发到每个点的最短奇数路径和偶数路径 
void bfs(int x){ 
    memset(od, 0x3f, sizeof(od)); // 初始化为无穷大
    memset(ev, 0x3f, sizeof(ev));
    ev[x] = 0; // 1号点到自己不需要走,偶数长度为 0
    q1.push(x);
    
    while(!q1.empty()){
        int u = q1.front();
        q1.pop();
        for(int i = head[u]; i; i = nxt[i]){ 
            int v = to[i];
            ud(ev[u], od[v], v); // u 的偶数路径 + 1 = v 的奇数路径
            ud(od[u], ev[v], v); // u 的奇数路径 + 1 = v 的偶数路径
        }
    }
}

// 判定函数:l的长度是否大于等于最短路径 c,且它们奇偶性相同
void jg(int a, int l, int c){
    if(l >= c && (l % 2 == c % 2)){
        printf("Yes\n");
    }
    else{
        printf("No\n");
        }
}

int main(){
    scanf("%d %d %d", &n, &m, &q);
    while(m--){
        int u, v;
        scanf("%d %d", &u, &v);
        adde(u, v);
        adde(v, u); // 无向图建双向边
    }
    
    bfs(1); // O(N+M) 预处理 1 号点到所有点的奇偶最短路
    
    while(q--){
        int a, l; 
        scanf("%d %d", &a, &l);
        // 特判:如果 1 号点是孤立点(没有任何传送带相连),根本无法向外传递
        if(!head[1]){ 
            printf("No\n");
            continue;
        }
        
        // 根据阶段 L 的奇偶性,选择对应的最短路数组进行比对
        if(l % 2){
            jg(a, l, od[a]);
        }
        else{
            jg(a, l, ev[a]);
        }
    }
    return 0;
}
相关推荐
2401_869769591 小时前
内容7 内存管理 1
c++
兰令水1 小时前
hot100【acm版】【2026.7.13打卡-java版本】
java·开发语言·数据结构·算法·leetcode·面试
Tisfy1 小时前
LeetCode 1291.顺次数:打表/枚举
算法·leetcode·题解·枚举·遍历
ysa0510302 小时前
【板子】拓扑排序
c++·算法·图论·板子
某不知名網友3 小时前
项目:轻量级搜索引擎
c++
算法备案代理3 小时前
宽带自动连接保姆级教程
算法
罗超驿3 小时前
双指针算法详解:从入门到精通(Java版)
算法·leetcode·职场和发展
m0_377062923 小时前
电池电量计库伦算法计算公式
算法
patrickpdx3 小时前
高联预赛中的高斯函数问题
算法