用C语言手把手教你搞定PTA数据结构里的‘列车厢调度’题(附完整代码和避坑指南)
当你第一次看到PTA平台上的"列车厢调度"问题时,可能会被那些轨道示意图和操作规则搞得一头雾水。别担心,这很正常——很多同学都在这个题目上栽过跟头。本文将从一个"过来人"的角度,带你一步步理解题目本质,掌握用栈模拟调度的核心思路,最终用C语言实现完整解法。我们不仅会提供可直接提交的代码,还会重点讲解那些容易踩坑的细节。
1. 彻底理解题目:从ASCII图到实际问题
首先让我们抛开所有代码,专注于理解题目本身。题目描述中那个看似简单的ASCII图其实包含了所有关键信息:
1 ====== <--移动方向 / 3 ===== \ \ 2 ====== -->移动方向这张图展示了三条平行轨道(1、2、3号)以及它们的连接方式。初始状态下,所有车厢都停在1号轨道上,我们的目标是通过有限的操作,将它们按照指定顺序转移到2号轨道。理解以下几点至关重要:
操作限制:
- 每次只能移动一节车厢
- 1号轨道的车厢有两种选择:
- 直接通过1-3连接道进入3号轨道(操作记为"1->3")
- 通过两条连接道直接进入2号轨道(操作记为"1->2")
- 3号轨道的车厢只能通过2-3连接道进入2号轨道(操作记为"3->2")
- 一旦车厢进入2号轨道,就不能再移动
输入输出规则:
- 输入是两行大写字母字符串,第一行表示1号轨道的初始顺序(从左到右),第二行表示2号轨道的目标顺序
- 需要输出最短操作序列,或无法完成时的特定提示
关键理解点:第二行的目标顺序是指车厢进入2号轨道的顺序。例如样例1中的"CBA"表示C最先进入(位于最右侧),A最后进入(位于最左侧)。
2. 问题抽象与算法选择
将实际问题转化为数据结构问题,是解题的关键一步。通过分析,我们可以得出以下对应关系:
- 1号轨道:输入序列,只能从头部取出元素(先进后出)
- 3号轨道:典型的栈结构(后进先出)
- 2号轨道:目标序列,一旦进入就不能再移动
因此,这个问题本质上是要验证:能否通过栈的缓冲作用,将输入序列转换为指定的输出序列——这正是栈的经典应用场景之一。
2.1 算法思路详解
我们采用以下步骤来解决这个问题:
- 初始化一个空栈和两个指针(分别指向输入序列和目标序列的当前位置)
- 遍历输入序列中的每个车厢:
- 如果当前车厢与目标序列的当前需求匹配,直接执行"1->2"操作
- 否则,将车厢压入栈中(执行"1->3"操作)
- 检查栈顶车厢是否与后续目标需求匹配:
- 如果匹配,弹出栈顶(执行"3->2"操作)
- 否则,继续处理输入序列
- 最终检查:
- 如果所有车厢都能按需求进入2号轨道,输出操作序列
- 否则,输出无法完成的提示
关键点:在每次直接匹配后,都要检查栈中是否还有可以满足当前需求的车厢(这就是样例ABCDE DCBAE的考察重点)。
3. 代码实现与逐行解析
下面给出完整的C语言实现,我们将分段解析关键代码逻辑。
3.1 基础定义与输入处理
#include <stdio.h> #include <string.h> #define MAX_LEN 80 int main() { char stack[MAX_LEN]; // 3号轨道栈 int top = -1; // 栈顶指针 char train[MAX_LEN]; // 1号轨道初始序列 char fin[MAX_LEN]; // 2号轨道目标序列 int ops[MAX_LEN * 2]; // 存储操作序列(1:1->2, 2:1->3, 3:3->2) int op_count = 0; // 操作计数 int possible = 1; // 是否可能完成 // 读取输入 scanf("%s", train); scanf("%s", fin); int len = strlen(train);这部分代码定义了必要的变量:
stack数组模拟3号轨道,top是栈顶指针train和fin分别存储初始序列和目标序列ops数组记录操作序列,op_count记录操作数量possible标志位表示是否可能完成调度
3.2 核心调度逻辑
int i = 0, j = 0; // i遍历train,j遍历fin while (i < len && possible) { if (train[i] == fin[j]) { // 直接匹配,执行1->2操作 ops[op_count++] = 1; i++; j++; // 检查栈中是否还有可匹配的车厢 while (top >= 0 && stack[top] == fin[j]) { ops[op_count++] = 3; top--; j++; } } else { // 不匹配,压入栈中(1->3操作) if (top < MAX_LEN - 1) { stack[++top] = train[i]; ops[op_count++] = 2; i++; } else { possible = 0; // 栈溢出,不可能完成 } } }这段代码实现了核心调度算法:
- 比较当前车厢(
train[i])与目标需求(fin[j]) - 如果匹配,执行"1->2"操作,并检查栈中是否有连续匹配
- 如果不匹配,执行"1->3"操作将车厢压入栈中
3.3 最终检查与输出
// 处理栈中剩余车厢 while (j < len && top >= 0 && possible) { if (stack[top] == fin[j]) { ops[op_count++] = 3; top--; j++; } else { possible = 0; } } // 输出结果 if (!possible || j != len) { printf("Are you kidding me?"); } else { for (int k = 0; k < op_count; k++) { switch (ops[k]) { case 1: printf("1->2\n"); break; case 2: printf("1->3\n"); break; case 3: printf("3->2\n"); break; } } } return 0; }最后这部分代码:
- 检查栈中是否还有可以满足目标需求的车厢
- 根据是否完成所有需求,输出相应结果
- 如果成功,按照记录的操作序列输出每一步操作
4. 关键测试用例与避坑指南
在实际解题过程中,有几个容易出错的地方需要特别注意:
4.1 易错点分析
目标序列的理解:
- 错误理解:认为第二行输入表示2号轨道的最终物理顺序
- 正确理解:第二行输入表示车厢进入2号轨道的顺序(即出站顺序)
栈的连续弹出:
- 在直接匹配后("1->2"操作),必须检查栈顶是否还能继续满足后续需求
- 这就是样例ABCDE DCBAE的考察重点:在D匹配后,栈中有CBA可以连续弹出
操作序列的最短性:
- 题目要求输出最短操作序列
- 这意味着只要"1->2"操作可行,就不应该选择"1->3"+"3->2"的组合
4.2 重要测试用例
除了题目提供的样例,以下测试用例能帮助你验证代码的健壮性:
| 测试用例 | 预期输出 | 检查重点 |
|---|---|---|
| ABC ACB | 1->2 1->3 3->2 | 基本功能 |
| ABCD DCBA | 1->3 1->3 1->3 1->2 3->2 3->2 3->2 | 完全逆序 |
| ABCDE DECBA | Are you kidding me? | 不可能情况 |
| ABCDEF FEDCBA | 1->3 1->3 1->3 1->3 1->3 1->2 3->2 3->2 3->2 3->2 3->2 | 长序列处理 |
5. 算法优化与扩展思考
虽然上述解决方案已经能够正确解决问题,但仍有优化空间:
空间优化:
- 当前使用了三个数组(stack, train, fin),实际上可以只保留stack
- 输入序列可以边读取边处理,不需要完整存储
时间效率:
- 当前算法时间复杂度为O(n),已经是最优
- 可以通过减少循环内的条件判断来优化常数因子
扩展问题:
- 如果允许更多的轨道,问题会如何变化?
- 如果车厢可以重复,算法需要如何调整?
// 优化后的输入处理示例 int ch; int i = 0; while ((ch = getchar()) != '\n' && ch != EOF) { train[i++] = ch; } train[i] = '\0';通过本文的详细讲解,你应该已经掌握了列车厢调度问题的核心解法。记住,理解题目是第一步,将实际问题抽象为数据结构问题是关键,而代码实现只是这个过程的自然结果。现在,你可以自信地去PTA上提交这个问题的解决方案了!