洛谷 P1160 队列安排 —— 双向链表模拟详解
洛谷 P1160 队列安排 —— 双向链表模拟详解
1. 题目背景与描述
一个学校里老师要将班上 个同学排成一列,同学被编号为 。老师采取如下的方法:
- 先将 号同学安排进队列,此时队列中只有他一个人;
- 号同学依次入列,编号为 的同学入列方式为:老师指定编号为 的同学站在编号为 中某位同学(即之前已经入列的同学)的左边或右边;
- 从队列中去掉 个同学,其他同学位置顺序不变。
在所有同学按照上述方法队列排列完毕后,老师想知道从左到右所有同学的编号。
输入格式:第一行一个整数 。第 行,第 行包含两个整数 ,其中 为小于 的正整数, 为 或者 。若 为 ,则表示将 号同学插入到 号同学的左边, 为 则表示插入到右边。第 行为一个整数 ,表示去掉的同学数目。接下来 行,每行一个正整数 ,表示将 号同学从队列中移去,如果 号同学已经不在队列中则忽略这一条指令。
输出格式:一行,包含最多 个空格隔开的整数,表示了队列从左到右所有同学的编号。
输入输出样例 #1:
1 | |
数据范围:对于 的数据,。
2. 问题剖析与解题思路
本题的核心操作是在任意已知位置(某个已入列同学的左边或右边)插入新节点,以及按编号删除任意节点。数据规模达到 ,要求我们必须在 时间内完成单次插入和删除操作。
2.1 为什么不能用数组/vector?
- 若使用
std::vector,在中间插入或删除元素需要 的时间搬移后续元素。 - 最坏情况下 ,总操作次数约 ,时间复杂度将达到 ,无法通过。
2.2 为什么选择双向链表?
- 插入操作:已知插入位置(某个节点的左侧或右侧),双向链表可以在 时间内完成插入。
- 删除操作:已知待删除节点的编号,若能 定位到该节点,则删除也是 。
- 最终遍历输出:沿着链表从头到尾走一遍即可,。
2.3 数据结构设计
由于 最大为 ,我们采用数组模拟双向链表:
l[i]:编号为 的同学左边的同学编号(前驱)。r[i]:编号为 的同学右边的同学编号(后继)。vis[i]:标记编号为 的同学是否还在队列中。
这样做的好处是:通过编号可以直接 访问到该节点的前驱和后继,无需像指针链表那样遍历查找。
3. 核心难点与细节处理
3.1 如何初始化?
号同学最先入列,此时队列只有他一个人,所以:
1 | |
3.2 插入操作的实现——关键点
对于第 个同学(),给定目标同学 和方向 :
- :将 插入到 的左边。
- :将 插入到 的右边。
我们以 (插入到 左边) 为例,详细拆解指针变化:
假设当前队列中有一段:... L <-> k <-> R ...( 是 左边的同学, 是 右边的同学)。
将 插入到 的左边后,队列变为:... L <-> i <-> k <-> R ...
需要修改四条指针:
l[i] = L( 的左邻居是 )r[i] = k( 的右邻居是 )- 如果 ,则
r[L] = i( 的右邻居改为 ) l[k] = i( 的左邻居改为 )
注意:如果 本来就是队首(即 ),那么插入后 成为新的队首,此时不需要修改 r[0],但需要记录新的队首编号。当然,我们也可以统一用 作为哨兵节点来处理边界情况。
同理,(插入到 右边) 的指针变化为:
l[i] = kr[i] = R( 是 右边的同学)r[k] = i- 如果 ,则
l[R] = i
3.3 删除操作的实现
给定编号 ,如果 已经不在队列中(vis[x] == false),则直接忽略。
否则,设 的左邻居为 ,右邻居为 。删除 后,需要将 和 直接相连:
- 如果 ,则
r[L] = R - 如果 ,则
l[R] = L - 标记
vis[x] = false
注意:如果 是队首(),删除后 成为新的队首;如果 是队尾(),删除后 成为新的队尾。这两种情况在上述逻辑中都能正确处理。
3.4 如何确定队首?
插入和删除操作都可能改变队首。我们可以在每次插入 且 是队首时更新队首为 ;在每次删除时,如果删除的是队首则更新队首为 r[x]。
但更简单的方式是:最后输出时从 开始沿着 指针找,直到找到某个节点其左邻居为 ,那就是队首。或者,我们在初始化时用变量 head 记录队首,每次操作后维护它。
推荐维护 head 变量,避免最后遍历查找。
3.5 关于重复删除指令
题目明确指出:如果 号同学已经不在队列中则忽略这一条指令。因此每次删除前必须检查 vis[x]。
4. 完整代码实现
1 | |
5. 复杂度分析
- 时间复杂度:每个同学至多入队一次、出队一次,每次插入和删除操作均只修改常数条指针,故总时间复杂度为 。
- 空间复杂度:使用三个大小为 的数组,。
6. 知识拓展:为什么不用 std::list?
C++ STL 中的 std::list 是双向链表,理论上也能完成本题。但需要注意:
std::list的迭代器在插入和删除后不会失效(除了被删除的迭代器),这本来是优点。- 但本题要求按编号 快速定位到节点。如果用
std::list,需要额外维护一个数组pos[i]存储编号为 的同学对应的迭代器,代码复杂度反而更高。 - 数组模拟双向链表在算法竞赛中更常见:代码简洁、常数小、调试方便。
7. 常见错误与避坑指南
| 错误类型 | 错误写法 | 正确写法 |
|---|---|---|
| 插入时忘记处理边界 | 只修改 l[i]、r[i] 和 l[k],忘记修改 L 的 r 指针 |
必须同时修改 L 的 r 和 k 的 l |
| 删除时忘记更新队首 | 删除队首后 head 仍指向已删除节点 |
删除时判断 if (x == head) head = R |
| 重复删除未检查 | 直接删除,导致指针错乱 | 每次删除前 if (!vis[x]) continue |
| 插入顺序错误 | 先修改 l[k] 再获取 L |
先获取 L 或 R 再修改,避免信息丢失 |
8. 总结
本题是双向链表的经典应用题,核心考察点在于:
- 识别数据结构的适用场景——频繁的中间插入和删除 链表。
- 数组模拟链表的实现技巧——用编号直接索引前驱和后继, 定位。
- 边界条件的细致处理——队首、队尾、空队列等情况。
掌握了这些,遇到类似的“动态维护序列”问题(如插队、排队、LRU 缓存等)时,就能游刃有余了。