P11230 [CSP-J 2024] 接龙

把“接龙轮数、结尾数字、上一轮选手”压成一个可以快速扫描的 DP。

可达性 DP 状态压缩 线性扫描 大量询问

一、先回答问题

结论

预处理所有轮数,再 O(1) 回答询问

询问很多,不能每次重新模拟。因为 r <= 100,我们按轮数做 DP,每一轮扫描所有人的词库一次。

dp[t][x]:接龙 t 轮后,数字 x 能不能作为结尾,以及最后一轮可能由谁完成。
教学安排

先压状态,再看扫描

这题最容易错在“不能连续选同一个人”。课堂上先讲 dp 的三种值,再用样例演示为什么一个 remain 就能标记后面一段结尾。

一句话模型:从合法起点出发,后面最多 k - 1 个位置都是本轮可选结尾。

二、核心观察

状态含义

dp[t][x] 不是简单真假

-1接龙 t 轮后,不能以数字 x 结尾。
p可以以 x 结尾,但目前只知道最后一轮由第 p 个人完成。
0可以以 x 结尾,而且最后一轮至少能由两个不同的人完成。
转移判断

当前位置能不能当起点

扫描第 p 个人的词库时,当前位置数字是 x。如果上一轮能以 x 结尾,并且上一轮不一定非要由 p 完成,就可以从这里开启新一轮。

dp[t - 1][x] != -1 && dp[t - 1][x] != p

dp[t - 1][x] == 0 时最舒服,说明我们一定能换一个人接上。

扫描顺序要记住:先把当前位置当“前面起点覆盖到的结尾”,再判断它能不能成为新的起点。这样不会把长度为 1 的子序列算进去。

三、互动演示

演示使用题目样例:第 1 人 1 2 3 4 1,第 2 人 1 2 5,第 3 人 5 1 6,且 k = 3

四、自己选答案

1. dp[t][x] = 0 表示什么?

2. 为什么要先处理 remain > 0,再判断当前位置能不能作为起点?

3. 如果只有 1 个人,能完成 2 轮接龙吗?

五、手推结果

轮数 样例中能看到的状态 说明
0 dp[0][1] = 0 还没选人,所以任意人都能从数字 1 开始第一轮。
1 dp[1][2] = 0dp[1][3] = 1dp[1][5] = 2dp[1][6] = 3 数字 2 可以由第 1 人和第 2 人结尾,所以状态变成 0。
2 dp[2][4] = 1 从第一轮结尾 2 出发,第 1 人可以选择 2 3 4,所以询问 (2,4) 输出 1。
3 dp[3][4] = -1 要在第 1 人序列里到 4,能用的前驱 3 只由第 1 人结尾,不能连续再选第 1 人。

六、C++14 参考代码

读代码重点:markReachable 维护三种状态,remain 负责把后面一段位置标为可达。

#include <bits/stdc++.h>
using namespace std;

const int MAXC = 200000;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int T;
    cin >> T;

    while (T--) {
        int n, k, q;
        cin >> n >> k >> q;

        vector<vector<int>> s(n + 1);
        for (int i = 1; i <= n; i++) {
            int len;
            cin >> len;
            s[i].resize(len);
            for (int j = 0; j < len; j++) {
                cin >> s[i][j];
            }
        }

        vector<pair<int, int>> query(q);
        int maxR = 0;
        for (int i = 0; i < q; i++) {
            int r, c;
            cin >> r >> c;
            query[i] = {r, c};
            maxR = max(maxR, r);
        }

        vector<vector<int>> dp(maxR + 1, vector<int>(MAXC + 1, -1));
        dp[0][1] = 0;

        auto markReachable = [&](int round, int value, int person) {
            int &state = dp[round][value];
            if (state == -1) {
                state = person;
            } else if (state != person) {
                state = 0; // 至少两个不同的人都能作为最后一轮选手
            }
        };

        for (int round = 1; round <= maxR; round++) {
            for (int person = 1; person <= n; person++) {
                int remain = 0;

                for (int x : s[person]) {
                    if (remain > 0) {
                        markReachable(round, x, person);
                        remain--;
                    }

                    // 当前 x 能作为本轮起点,后面 k - 1 个位置能作为本轮结尾。
                    if (dp[round - 1][x] != -1 && dp[round - 1][x] != person) {
                        remain = k - 1;
                    }
                }
            }
        }

        for (auto item : query) {
            int r = item.first;
            int c = item.second;
            cout << (dp[r][c] != -1 ? 1 : 0) << '\n';
        }
    }

    return 0;
}

七、易错点收束

错误 1

只存 bool

只知道某个数字可达,不知道最后由谁接,无法判断下一轮能不能换人。

错误 2

把起点算作结尾

题目要求长度至少为 2。当前数字只能影响它后面的位置。

错误 3

每个询问单独做

q 最大是 105。正确做法是先预处理所有轮数,再直接回答。