题解:洛谷 P10264 接竹竿
【题目来源】
洛谷:P10264 [GESP202403 八级] 接竹竿 - 洛谷
【题目描述】
小杨同学想用卡牌玩一种叫做“接竹竿”的游戏。
游戏规则是:每张牌上有一个点数 \(v\),将给定的牌依次放入一列牌的末端。若放入之前这列牌中已有与这张牌点数相
同的牌,则小杨同学会将这张牌和点数相同的牌之间的所有牌全部取出队列(包括这两张牌本身)。
小杨同学现在有一个长度为 \(n\) 的卡牌序列 \(A\),其中每张牌的点数为 \(A_i\)(\(1\le i\le n\))。小杨同学有 \(q\) 次询问。第 \(i\) 次(\(1\le i\le q\))询问时,小杨同学会给出 \(l_i,r_i\) 小杨同学想知道如果用下标在 \([l_i,r_i]\) 的所有卡牌按照下标顺序玩“接竹竿”的游戏,最后队列中剩余的牌数。
【输入】
一行包含一个正整数 \(T\),表示测试数据组数。
对于每组测试数据,第一行包含一个正整数 \(n\),表示卡牌序列 \(A\) 的长度。
第二行包含 \(n\) 个正整数 \(A_1,A_2,\dots,A_n\),表示卡牌的点数 \(A\)。
第三行包含一个正整数 \(q\),表示询问次数。
接下来 \(q\) 行,每行两个正整数 \(l_i,r_i\) 表示一组询问。
【输出】
对于每组数据,输出 \(q\) 行。第 \(i\) 行(\(1\le i\le q\))输出一个非负整数,表示第 \(i\) 次询问的答案。
【输入样例】
1
6
1 2 2 3 1 3
4
1 3
1 6
1 5
5 6
【输出样例】
1
1
0
2
【核心思想】
-
问题分析:给定卡牌序列,规则为放入牌时若队列中已有相同点数的牌,则将这两张牌之间的所有牌(包括自身)取出。有 \(q\) 次区间询问,求区间 \([l, r]\) 内按规则操作后队列剩余牌数。这是一个链表模拟 + 区间查询问题,核心在于预处理每个位置的"配对位置",然后在查询时跳过已配对的区间。
-
算法选择:
- 链表预处理:\(nxt[i]\) 表示与位置 \(i\) 的点数相同的下一个位置(按出现顺序),\(a[x]\) 记录点数 \(x\) 最后一次出现的位置
- 区间跳跃查询:查询时遍历区间,若当前位置 \(i\) 的配对位置 \(nxt[i]\) 在区间内,则跳到 \(nxt[i]\)(跳过中间已消除的牌)
-
关键步骤:
- 预处理链表(遍历 \(i\) 从 \(1\) 到 \(n\)):
- 读入 \(x = A_i\)
- \(nxt[a[x]] = i\)(上一次出现位置指向当前位置)
- \(a[x] = i\)(更新最后出现位置)
- 处理查询(\([st, ed]\)):
- \(ans \leftarrow 0\)
- 遍历 \(i\) 从 \(st\) 到 \(ed\):
- 若 \(nxt[i] < st\) 或 \(nxt[i] > ed\):\(ans \leftarrow ans + 1\)(该牌无配对或配对在区间外,保留)
- 否则:\(i \leftarrow nxt[i]\)(有配对在区间内,两张牌及中间牌全部消除,跳到配对位置)
- 输出 \(ans\)
- 预处理链表(遍历 \(i\) 从 \(1\) 到 \(n\)):
-
时间/空间复杂度:
- 预处理:\(O(n)\),遍历序列一次
- 单次查询:\(O(r-l+1)\),最坏遍历整个区间
- 空间复杂度:\(O(n)\),存储 \(nxt\) 数组
-
链表模拟与区间跳跃的核心思想:
- 配对关系的建立:按顺序处理序列,每次遇到相同点数的牌时,建立前一次出现位置到当前位置的链接。这模拟了"接竹竿"中相同点数牌的配对关系
- 消除的区间特性:若位置 \(i\) 和 \(nxt[i]\) 都在查询区间内,则这两张牌及其中间所有牌会被同时消除。跳到 \(nxt[i]\) 后,中间的牌无需再检查
- 单向链表的局限性:预处理只建立了"前一次出现 → 后一次出现"的链接,但查询时需要判断配对是否在区间内。若配对在区间外,当前牌保留;若在区间内,则消除并跳跃
- 查询优化空间:当前实现单次查询 \(O(n)\),对于大量查询可优化。核心观察是:最终剩余的牌是那些无法形成完整配对的牌,即配对关系在区间边界处"断裂"的牌
- 适用于序列配对消除、区间查询类模拟问题
【算法标签】
普及+ #链表
【代码详解】
#include <bits/stdc++.h>
using namespace std;
// 定义int为unsigned类型,因为第11号测试点需要unsigned才能通过,否则会TLE
#define int unsigned
// 变量声明
unsigned t, n, q; // t: 测试用例数, n: 牌的数量, q: 查询次数
signed main() // 由于使用了#define int unsigned,所以main函数需要改为signed
{
// 读取测试用例数
scanf("%d", &t);
// 处理每个测试用例
while (t--)
{
int a[15] = {}; // a[x]: 记录数字x最后一次出现的位置
int nxt[100005] = {}; // nxt[i]: 记录与位置i的数字相同的下一个位置,0表示没有下一个
// 读取牌的数量
scanf("%d", &n);
// 读取牌的序列
for (int i = 1; i <= n; i++)
{
int x;
scanf("%d", &x);
// 建立链表:当前位置i连接到上一次出现相同数字的位置
nxt[a[x]] = i; // 将上一次出现位置a[x]的nxt指向当前位置i
// 更新a[x]为当前位置i
a[x] = i;
}
// 读取查询次数
scanf("%d", &q);
// 处理每个查询
while (q--)
{
int ans = 0; // 答案:最终留下的牌的数量
int st, ed; // 查询区间[st, ed]
scanf("%d%d", &st, &ed);
// 遍历区间内的每个位置
for (int i = st; i <= ed; i++)
{
// 如果当前位置i没有相同数字在区间内(nxt[i]=0或在区间外)
if (nxt[i] < st || nxt[i] > ed)
{
ans++; // 这张牌会留下来
}
else
{
// 有相同数字在区间内,跳到相同数字的下一个位置
i = nxt[i];
}
}
// 输出当前查询的答案
printf("%d\n", ans);
}
}
return 0;
}
【运行结果】
1
6
1 2 2 3 1 3
4
1 3
1
1 6
1
1 5
0
5 6
2
浙公网安备 33010602011771号