题解:洛谷 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

【核心思想】

  1. 问题分析:给定卡牌序列,规则为放入牌时若队列中已有相同点数的牌,则将这两张牌之间的所有牌(包括自身)取出。有 \(q\) 次区间询问,求区间 \([l, r]\) 内按规则操作后队列剩余牌数。这是一个链表模拟 + 区间查询问题,核心在于预处理每个位置的"配对位置",然后在查询时跳过已配对的区间。

  2. 算法选择

    • 链表预处理\(nxt[i]\) 表示与位置 \(i\) 的点数相同的下一个位置(按出现顺序),\(a[x]\) 记录点数 \(x\) 最后一次出现的位置
    • 区间跳跃查询:查询时遍历区间,若当前位置 \(i\) 的配对位置 \(nxt[i]\) 在区间内,则跳到 \(nxt[i]\)(跳过中间已消除的牌)
  3. 关键步骤

    • 预处理链表(遍历 \(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\)
  4. 时间/空间复杂度

    • 预处理:\(O(n)\),遍历序列一次
    • 单次查询:\(O(r-l+1)\),最坏遍历整个区间
    • 空间复杂度:\(O(n)\),存储 \(nxt\) 数组
  5. 链表模拟与区间跳跃的核心思想

    • 配对关系的建立:按顺序处理序列,每次遇到相同点数的牌时,建立前一次出现位置到当前位置的链接。这模拟了"接竹竿"中相同点数牌的配对关系
    • 消除的区间特性:若位置 \(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
posted @ 2026-07-17 16:02  团爸讲算法  阅读(9)  评论(0)    收藏  举报