[CSP-J 2024] 接龙

GitHub跳转原题关系图返回列表

沿着 5、15、60、100 分的数据范围逐层优化:先枚举有色边,再压缩接龙人状态,最后用扫描窗口消除平方级边数。

OJ: luogu

题目 ID: P11230

难度:提高

标签:建模动态规划状态压缩滑动窗口枚举

创建: 2026-07-05 21:24

更新: 2026-10-07 10:45

形式化题目

第 pp 个人持有序列 SpS_p。一次操作可以在 SpS_p 中选择两个位置 i<ji<j,满足

2⩽j−i+1⩽k, 2\leqslant j-i+1\leqslant k,

并完成一次从 Sp,iS_{p,i} 到 Sp,jS_{p,j} 的转移。连续两次转移不能选择同一个人。

初始值为 11。每个询问给出 (r,c)(r,c),要求判断能否恰好转移 rr 次后到达 cc。

把每个合法选择看成一条边:

text
S[p][i] -> S[p][j],边的颜色为 p

于是问题等价为:能否从 11 出发走恰好 rr 条边到达 cc,并且相邻两条边颜色不同。

解法路线

设 L=∑liL=\sum l_i,R=max⁡rjR=\max r_j,值域大小为 V=2×105V=2\times 10^5。按每个测试点 55 分计算,可以得到下面的拿分路线。

层次 利用的数据范围 做法 可覆盖测试点 累计分数
第一层 r=1r=1 只枚举第一轮的合法连续子序列 11 55
第二层 n⩽10,L⩽20,R⩽5n\leqslant10,L\leqslant20,R\leqslant5 保留第一层特判,其余询问记忆化搜索 1∼31\sim3 1515
第三层 L⩽2000,R⩽10L\leqslant2000,R\leqslant10 枚举全部有色边,按轮数做 DP 1∼5,7,8,11,12,15∼171\sim5,7,8,11,12,15\sim17 6060
第四层 完整范围 两个接龙人压状态,扫描窗口压边 全部 100100

每一层代码都保留前一层的能力,因此可以作为一份独立提交逐步拿分。测试点 4∼144\sim14 中的特殊性质 A、B、C 不是一条 A 到 B 再到 C 的递进链,而是三个并列提示:

  • A 中 kk 很大,合法结尾接近一个后缀,提示我们批量处理区间;
  • B 中 k⩽5k\leqslant5,每个起点只有常数个结尾,逐一枚举边就不再平方爆炸;
  • C 中每个值至多出现 55 次,按“相同值”寻找下一轮起点时只有常数个候选。

三条支线最后汇合到同一个正解:相同值之间负责连接相邻两轮,同一个人序列中的位置区间负责完成当前轮。

子任务解法:r=1r=1

适用范围

测试点 11 的所有询问都满足 r=1r=1,只需判断哪些值能作为第一轮的结尾。

思路

第一轮没有“与上一轮同一个人”的限制,只要求接龙序列从值 11 开始。因此对每个人的序列枚举所有值为 11 的位置 left,再枚举

left+1⩽right⩽min⁡(lp,left+k−1) left+1\leqslant right\leqslant\min(l_p,left+k-1)

把 S[p][right] 标记为一轮可达。全部询问随后直接查表。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-09-27 21:38
 * update_at: 2026-09-27 21:38
 */
// subtask_05.cpp:测试点 1,所有询问都只有一轮接龙。
#include <bits/stdc++.h>
using namespace std;

const int MAXV = 200005;

bool reachable[MAXV];

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

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

        memset(reachable, 0, sizeof(reachable));

        for (int person = 1; person <= n; person++) {
            int len;
            cin >> len;
            vector<int> seq(len + 1);
            for (int i = 1; i <= len; i++) {
                cin >> seq[i];
            }

            // 第一轮必须从值 1 开始。
            for (int left = 1; left <= len; left++) {
                if (seq[left] != 1) continue;
                int right_end = min(len, left + k - 1);
                for (int right = left + 1; right <= right_end; right++) {
                    reachable[seq[right]] = true;
                }
            }
        }

        for (int i = 1; i <= q; i++) {
            int round, value;
            cin >> round >> value;
            // 本文件只面向保证 round = 1 的测试点。
            cout << (round == 1 && reachable[value] ? 1 : 0) << '\n';
        }
    }

    return 0;
}

复杂度与瓶颈

这一层仍会显式枚举所有第一轮有色边。设边数为 EE,时间复杂度为 O(E+q)O(E+q),空间复杂度为 O(V)O(V);最坏 E=O(L2)E=O(L^2),但测试点 11 的 L⩽2000L\leqslant2000。

当询问轮数增加后,只知道“某个值一轮可达”已经不够,还必须知道上一轮由谁接龙。

暴力解法:L⩽20L\leqslant20

适用范围

测试点 2,32,3 满足 n⩽10n\leqslant10、L⩽20L\leqslant20、R⩽5R\leqslant5。规模足够小,可以现场逐一枚举所有合法连续子序列(有色边),边用完即丢,不必保存边表。

代码保留 r=1 的直接查表分支,因此同一份程序也能通过测试点 11,累计获得 1515 分。

思路

搜索状态写成三元组:

(round,value,lastPerson), (round,value,lastPerson),

分别表示已经完成的轮数、当前结尾值、上一轮的接龙人。

要找从当前值出发的出边,扫描所有人的序列、枚举以当前值开头的合法接龙序列即可。从中枚举颜色不等于 lastPerson 的边。走过边 (value,to,p)(value,to,p) 后,进入状态

(round+1,to,p) (round+1,to,p)

当 round == targetRound 时,检查 value == targetValue。相同状态的答案相同,因此可以记忆化。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-09-27 21:38
 * update_at: 2026-10-01 18:42
 */
// subtask_15.cpp:测试点 1 特判;测试点 2、3 逐询问记忆化搜索。
// 不保存边表:搜索到某个状态时,现场扫描所有人的序列,
// 枚举以当前值开头的合法接龙序列(有色边),用完即丢。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MAXV = 200005;   // 值的范围
const int MAXR = 10;       // 测试点 2、3 保证 r <= 5,留富余
const int MAXP = 12;       // 测试点 2、3 保证 n <= 10,留富余

// 一个询问:恰好 r 轮接龙后,最后一个元素是否恰好为 v
struct Query {
    ll r;
    ll v;
};

ll n, k, q;
ll max_value;
ll target_round, target_value;

vector<vector<ll> > seq;    // seq[person] = 第 person 个人的序列,下标从 1 开始用
vector<char> one_round;     // one_round[v] = 1 表示一轮就能以 v 结尾

// memo[round][value][person]:状态 (round, value, person) 的搜索结果
//   0 = 还没搜过,1 = 失败,2 = 成功
// person = 0 表示还没有真正的上一轮接龙人(第 0 轮)。
char memo[MAXR][MAXV][MAXP];

// 从 (round, value, last_person) 出发,判断能否恰好 target_round 轮后以 target_value 结尾。
bool dfs_game(ll round, ll value, ll last_person) {
    if (round == target_round) {
        return value == target_value;
    }

    // 同样的状态搜过一次就够了,直接复用上次的结论。
    if (memo[round][value][last_person] != 0) {
        return memo[round][value][last_person] == 2;
    }

    // 现场枚举所有以 value 开头、接龙人不是 last_person 的合法接龙序列。
    for (ll person = 1; person <= n; person++) {
        if (person == last_person) continue;

        vector<ll>& s = seq[person];
        ll len = s.size() - 1;
        for (ll left = 1; left <= len; left++) {
            if (s[left] != value) continue;

            // 接龙序列长度在 [2, k],结尾 right ∈ [left + 1, left + k - 1]
            ll right_end = min(len, left + k - 1);
            for (ll right = left + 1; right <= right_end; right++) {
                if (dfs_game(round + 1, s[right], person)) {
                    memo[round][value][last_person] = 2;
                    return true;
                }
            }
        }
    }

    memo[round][value][last_person] = 1;
    return false;
}

// 测试点 1(r = 1):枚举所有以 1 开头的合法接龙序列,标记一轮可达的结尾值。
void compute_one_round() {
    one_round.assign(max_value + 1, 0);

    for (ll person = 1; person <= n; person++) {
        vector<ll>& s = seq[person];
        ll len = s.size() - 1;
        for (ll left = 1; left <= len; left++) {
            if (s[left] != 1) continue;

            ll right_end = min(len, left + k - 1);
            for (ll right = left + 1; right <= right_end; right++) {
                one_round[s[right]] = 1;
            }
        }
    }
}

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

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

        seq.assign(n + 1, vector<ll>());
        max_value = 1;
        for (ll person = 1; person <= n; person++) {
            ll len;
            cin >> len;
            seq[person].resize(len + 1);
            for (ll i = 1; i <= len; i++) {
                cin >> seq[person][i];
                max_value = max(max_value, seq[person][i]);
            }
        }

        vector<Query> queries(q + 1);
        for (ll i = 1; i <= q; i++) {
            cin >> queries[i].r >> queries[i].v;
            max_value = max(max_value, queries[i].v);
        }

        compute_one_round();

        for (ll i = 1; i <= q; i++) {
            if (queries[i].r == 1) {
                cout << (one_round[queries[i].v] ? 1 : 0) << '\n';
                continue;
            }

            target_round = queries[i].r;
            target_value = queries[i].v;

            // 目标变了,把上一个询问留下的搜索结果清掉。
            for (ll r = 0; r < target_round; r++) {
                memset(memo[r], 0, sizeof(memo[r]));
            }

            cout << (dfs_game(0, 1, 0) ? 1 : 0) << '\n';
        }
    }

    return 0;
}

复杂度与瓶颈

设全部有色边的条数为

E=∑p=1n∑i=1lpmin⁡(k−1,lp−i) E=\sum_{p=1}^{n}\sum_{i=1}^{l_p}\min(k-1,l_p-i)

最坏情况下 E=O(L2)E=O(L^2)。不保存边表后,每个搜索状态现场花 O(L)O(L) 找出全部出边,转移代价为 O(L+Ev)O(L+E_v)(EvE_v 为从 vv 出发的边数),对 L⩽20L\leqslant20 的规模没有影响。记忆化虽然避免了重复搜索状态,但每个询问仍要单独搜索;当 qq、LL 或 RR 增大时都会失效。

下一层先解决“每个询问重复搜索”的问题。

子任务解法:L⩽2000,R⩽10L\leqslant2000,R\leqslant10

适用范围

所有满足 L⩽2000L\leqslant2000、R⩽10R\leqslant10 的测试点都没有必要分别利用 A、B、C。直接按最坏 O(L2)O(L^2) 双重循环逐一枚举有色边,也只有约 2×1062\times10^6 条边要检查,不必把它们保存下来。

因此这一做法能统一覆盖测试点

1∼5,7,8,11,12,15∼17 1\sim5,\quad 7,8,\quad 11,12,\quad 15\sim17

共 1212 个测试点,即 6060 分。

思路

先读完所有询问,求出最大轮数 RR,再统一预处理第 11 到第 RR 轮的可达状态。这样每个询问只需查表,不再重复搜索。

若朴素保存状态 reachable[value][person],会多出很大的一维。注意下一轮选择人 pp 时,我们只问一件事:

到达当前值的方案中,是否存在一个上一轮接龙人不等于 pp?

因此,每个值只需保留最多两个不同的接龙人:

  • 没有代表人:这个值不可达;
  • 只有代表人 xx:下一轮不能选 xx,其他人都能选;
  • 有两个不同代表人 x,yx,y:无论下一轮选择谁,x,yx,y 中至少有一个人与他不同。

第 00 轮令值 11 的代表人为特殊编号 00,表示还没有真正选择接龙人。

每一轮按人扫描他的序列,用双重循环现场枚举这个人的全部有色边,边用完即丢,不保存边表。如果边为 (u,v,p)(u,v,p),且 uu 的两个代表人中存在一个不等于 pp,就把 pp 加入 vv 的下一轮代表人集合。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-09-27 21:29
 * update_at: 2026-10-01 15:37
 */
// subtask_60.cpp:按人扫描序列,双重循环枚举所有合法连续子序列(最坏 O(L^2)),
// 不显式存边,适合 sum(l) <= 2000、r <= 10 的测试点。
#include <bits/stdc++.h>
using namespace std;

const int MAXV = 200005;   // 值的范围
const int MAXR = 105;      // 询问中的最大轮数不超过 100

// 一个询问:恰好 r 轮接龙后,最后一个元素是否恰好为 v
struct Query {
    int r;
    int v;
};

int n, k, q;
int max_round;              // 所有询问中的最大轮数

vector<vector<int> > seq;   // seq[person] = 第 person 个人的序列,下标从 1 开始用
vector<Query> queries;      // 全部询问

// reachable[r][v]:第 r 轮结束时值 v 是否可达
bool reachable[MAXR][MAXV];

// ---------- 轮次状态 ----------
// last1[v]:值 v 在上一轮的第一个生产者
// last2[v]:值 v 在上一轮的第二个生产者(同一轮中另一个不同的人)
// -1 不可达,0 第 0 轮(初始状态,任何人都可用)
int last1[MAXV], last2[MAXV];
int next1[MAXV], next2[MAXV];

// 读入一组数据,并求出询问中的最大轮数。
void read_input() {
    cin >> n >> k >> q;

    seq.assign(n + 1, vector<int>());
    for (int person = 1; person <= n; person++) {
        int len;
        cin >> len;
        seq[person].resize(len + 1);   // 下标 0 不用,方便和题面下标对应
        for (int i = 1; i <= len; i++) {
            cin >> seq[person][i];
        }
    }

    queries.resize(q + 1);
    max_round = 0;
    for (int i = 1; i <= q; i++) {
        cin >> queries[i].r >> queries[i].v;
        max_round = max(max_round, queries[i].r);
    }
}

// 判断值 v 是否可以作为本轮 person 的开头:
// 上轮必须有人以 v 结尾,且本轮的人不能与上轮第一生产者相同
// 除非上轮还有另一个不同的人也以 v 结尾
bool can_start(int v, int person) {
    if (last1[v] == -1) return false;
    if (last1[v] == 0) return true;
    if (last1[v] != person) return true;
    return last2[v] != -1;
}

// 记录“本轮可以由 person 接到 value”。
// 每个值只保留两个不同的人,已经足够判断下一轮能否换人。
void add_next_state(int value, int person) {
    if (next1[value] == -1) {
        next1[value] = person;
    } else if (next1[value] != person && next2[value] == -1) {
        next2[value] = person;
    }
}

// 固定本轮接龙人,双重循环枚举他的所有合法接龙序列 seq[person][left..right]。
void scan_person(int person, int round) {
    vector<int>& s = seq[person];
    int len = (int)s.size() - 1;

    for (int left = 1; left <= len; left++) {
        int from_value = s[left];
        if (!can_start(from_value, person)) continue;

        // 接龙序列长度必须在 [2, k],所以结尾 right ∈ [left + 1, left + k - 1]
        int right_end = min(len, left + k - 1);
        for (int right = left + 1; right <= right_end; right++) {
            int to_value = s[right];
            add_next_state(to_value, person);
            reachable[round][to_value] = true;
        }
    }
}

// 从上一轮状态计算指定轮的全部可达状态。
void transfer_one_round(int round) {
    memset(next1, -1, sizeof(next1));
    memset(next2, -1, sizeof(next2));

    for (int person = 1; person <= n; person++) {
        scan_person(person, round);
    }

    memcpy(last1, next1, sizeof(last1));
    memcpy(last2, next2, sizeof(last2));
}

void preprocess_answers() {
    memset(last1, -1, sizeof(last1));
    memset(last2, -1, sizeof(last2));
    memset(reachable, 0, sizeof(reachable));

    // 第 0 轮从值 1 开始,0 表示还没有真正的上一轮接龙人。
    last1[1] = 0;

    for (int round = 1; round <= max_round; round++) {
        transfer_one_round(round);
    }
}

void print_answers() {
    for (int i = 1; i <= q; i++) {
        cout << (reachable[queries[i].r][queries[i].v] ? 1 : 0) << '\n';
    }
}

void solve() {
    read_input();
    preprocess_answers();
    print_answers();
}

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

    int T;
    cin >> T;
    while (T--) {
        solve();
    }

    return 0;
}

复杂度与瓶颈

每轮枚举边的复杂度为 O(E+V)O(E+V),轮次 DP 总计 O(R(E+V))O(R(E+V)),回答询问为 O(q)O(q);边不落地保存,空间复杂度为 O(RV+V+L)O(RV+V+L),最坏 E=O(L2)E=O(L^2)。

这一层已经消除了查询数 qq 带来的重复计算,也消除了“上一轮接龙人”这一大维状态。剩下的唯一瓶颈是 EE:完整数据中,一个长度为 2×1052\times10^5 的序列可能产生平方级别的边,逐一枚举根本做不完。

性质 B 恰好暴露了这个瓶颈。当 k⩽5k\leqslant5 时,E=O(kL)=O(L)E=O(kL)=O(L),逐一枚举边重新变得可行;但要统一通过所有数据,仍需让每轮转移不依赖 EE。

正解

关键观察一:两个接龙人已经足够

设 Pt(v)P_t(v) 为恰好完成 tt 轮并以 vv 结尾时,最后一轮所有可能的接龙人集合。

下一轮若准备选择人 pp,只需判断

∃x∈Pt(v),x≠p \exists x\in P_t(v),\quad x\neq p

这个判断只关心集合是空、只有 pp,还是存在另一个人。因此保存任意两个不同的代表人,就能得到与完整集合完全相同的判断结果。

代码使用:

  • last1[v]、last2[v]:上一轮到达 vv 的两个不同代表人;
  • next1[v]、next2[v]:本轮到达 vv 的两个不同代表人;
  • -1:不存在;
  • 0:第 00 轮的虚拟接龙人。

这一观察继承自 6060 分做法,没有改变。

关键观察二:一批边其实是一个位置区间

固定接龙人 pp,从左到右扫描 SpS_p。如果位置 ii 的值能作为本轮起点,那么它能够到达的结尾位置恰好为

[i+1,min⁡(lp,i+k−1)] [i+1,\min(l_p,i+k-1)]

逐一枚举有色边会逐个检查区间中的所有位置。其实只需维护当前被至少一个合法起点覆盖的区间。

扫描到位置 pos 时:

  1. 用上一轮的两个代表人判断 S[p][pos] 能否由人 pp 作为本轮起点;
  2. 若能,就把新区间 [pos+1,min⁡(lp,pos+k−1)][pos+1,\min(l_p,pos+k-1)] 合并进当前覆盖范围;
  3. 若 pos 在覆盖范围中,它就是本轮的合法结尾,把人 pp 加入 S[p][pos] 的下一轮代表人集合。

为什么只维护一个当前区间就够?为什么合并后一定是连续的?

按整数数轴来看:新起点 pospos 产生的覆盖区间左端点恰好是 pos+1pos + 1。旧区间的右端点只要满足 range_end >= pos,合并就绝对不会断开:

text
情况 1:range_end > pos(有重叠,无缝连通)
旧区间覆盖:  [-----------------] (到 pos+1 或更远)
新区间覆盖:              [pos+1 --------------->
合并结果:    [--------------------------------->

情况 2:range_end == pos(刚好相邻,无缝拼接)
旧区间覆盖:  [--------- pos]
新区间覆盖:                 [pos+1 ------------->
合并结果:    [----------------------------------->
注:pos 与 pos+1 是两个相邻整数,中间没有空隙!

情况 3:range_end < pos(真正断开)
旧区间覆盖:  [--- range_end]  (已在 pos 之前结束)
                      ... 空隙 ...
新区间覆盖:                           [pos+1 --->
处理方式:    旧区间已在身后过期,直接舍弃,以 [pos+1, new_end] 作为全新区间。

因此:

  • 只要 range_end >= pos,旧区间和新区间要么重叠、要么紧紧贴在一起,合并后必定是连续的一整段闭区间;
  • 只有 range_end < pos 时才会出现断层,而此时旧区间已经完全落后于当前扫描位置,直接重开即可。

这一步把每轮的 O(E)O(E) 降为扫描所有序列一次的 O(L)O(L)。

完整转移

先读入全部询问并求出 RR,然后:

  1. 初始化 last1[1] = 0,其余值不可达;
  2. 枚举轮数 t=1,2,…,Rt=1,2,\ldots,R;
  3. 清空本轮的 next1、next2;
  4. 依次扫描每个人的完整序列,用覆盖区间批量完成本轮转移;
  5. 记录第 tt 轮的所有可达结尾值;
  6. 把 next 数组变成下一轮的 last 数组。

于是每个询问 (r,c)(r,c) 都能 O(1)O(1) 回答。

样例中的第一轮

样例的三个序列为 [1,2,3,4,1][1,2,3,4,1]、[1,2,5][1,2,5]、[5,1,6][5,1,6],且 k=3k=3。第 00 轮只有状态 P0(1)={0}P_0(1)=\{0\}。

下面的表格展示“合法起点覆盖一段结尾位置”的过程。

接龙人 合法起点 覆盖的结尾位置 产生的 (结尾值,接龙人)(结尾值,接龙人)
11 第 11 位的 11 第 2∼32\sim3 位 (2,1),(3,1)(2,1),(3,1)
22 第 11 位的 11 第 2∼32\sim3 位 (2,2),(5,2)(2,2),(5,2)
33 第 22 位的 11 第 33 位 (6,3)(6,3)

所以第一轮结束后:

结尾值 vv 保存的代表人
22 {1,2}\{1,2\}
33 {1}\{1\}
55 {2}\{2\}
66 {3}\{3\}

值 22 保存两个代表人后,下一轮无论选择人 11 还是人 22,都能从另一个代表人的方案接过来;而值 33 只有代表人 11,所以下一轮不能继续选择人 11。

正确性说明

对轮数 tt 归纳。

归纳假设:last1[v]、last2[v] 足以正确判断是否存在恰好 t−1t-1 轮到达 vv、且最后一轮接龙人不等于任意给定人 pp 的方案。

固定本轮接龙人 pp。扫描 SpS_p 时,一个位置 ii 被判为合法起点,当且仅当上一轮能到达 Sp,iS_{p,i},并且存在一个代表人不等于 pp。根据归纳假设,这与游戏的换人规则完全等价。

每个合法起点 ii 覆盖且只覆盖 i+1i+1 到 min⁡(lp,i+k−1)\min(l_p,i+k-1),正好对应长度在 [2,k][2,k] 内的全部连续子序列。因此扫描得到的结尾位置不多不少。把当前人 pp 记入结尾值的代表集合后,就得到本轮所有合法方案。

若某个结尾值有至少两个不同接龙人,保留任意两个后,对任意下一轮接龙人仍至少有一个代表人与他不同;若只有一个人,则必须保留这个人。故压缩不会改变下一轮的任何可行性判断,归纳成立。

初始状态 last1[1] = 0 正确表示游戏从值 11 开始且尚无上一轮接龙人,所以算法对所有轮数均正确。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-05 21:47
 * update_at: 2026-10-01 15:36
 */
// main.cpp:按人扫描序列做轮次 DP,用覆盖区间批量标记可达值。
#include <bits/stdc++.h>
using namespace std;

const int MAXV = 200005;   // 值的范围
const int MAXR = 105;      // 询问中的最大轮数不超过 100

// 一个询问:恰好 r 轮接龙后,最后一个元素是否恰好为 v
struct Query {
    int r;
    int v;
};

int n, k, q;
int max_round;              // 所有询问中的最大轮数

vector<vector<int> > seq;   // seq[person] = 第 person 个人的序列,下标从 1 开始用
vector<Query> queries;      // 全部询问

// reachable[r][v]:第 r 轮结束时值 v 是否可达
bool reachable[MAXR][MAXV];

// ---------- 轮次状态 ----------
// last1[v]:值 v 在上一轮的第一个生产者
// last2[v]:值 v 在上一轮的第二个生产者(同一轮中另一个不同的人)
// -1 不可达,0 第 0 轮(初始状态,任何人都可用)
int last1[MAXV], last2[MAXV];
int next1[MAXV], next2[MAXV];

// 读入一组数据,并求出询问中的最大轮数。
void read_input() {
    cin >> n >> k >> q;

    seq.assign(n + 1, vector<int>());
    for (int person = 1; person <= n; person++) {
        int len;
        cin >> len;
        seq[person].resize(len + 1);   // 下标 0 不用,方便和题面下标对应
        for (int i = 1; i <= len; i++) {
            cin >> seq[person][i];
        }
    }

    queries.resize(q + 1);
    max_round = 0;
    for (int i = 1; i <= q; i++) {
        cin >> queries[i].r >> queries[i].v;
        max_round = max(max_round, queries[i].r);
    }
}

// 判断值 v 是否可以作为本轮 person 的开头:
// 上轮必须有人以 v 结尾,且本轮的人不能与上轮第一生产者相同
// 除非上轮还有另一个不同的人也以 v 结尾
bool can_start(int v, int person) {
    if (last1[v] == -1) return false;
    if (last1[v] == 0) return true;
    if (last1[v] != person) return true;
    return last2[v] != -1;
}

// 记录“本轮可以由 person 接到 value”。
// 每个值只保留两个不同的人,已经足够判断下一轮能否换人。
void add_next_state(int value, int person) {
    if (next1[value] == -1) {
        next1[value] = person;
    } else if (next1[value] != person && next2[value] == -1) {
        next2[value] = person;
    }
}

// 固定本轮接龙人,扫描他的整个序列。
void scan_person(int person, int round) {
    vector<int>& s = seq[person];
    int len = (int)s.size() - 1;

    int range_start = 1;
    int range_end = 0;

    for (int pos = 1; pos <= len; pos++) {
        int value = s[pos];

        // 合法起点 pos 能覆盖后面的 [pos + 1, pos + k - 1]。
        if (can_start(value, person)) {
            int new_end = min(len, pos + k - 1);
            if (range_end < pos) {
                range_start = pos + 1;
                range_end = new_end;
            } else {
                range_end = max(range_end, new_end);
            }
        }

        // pos 被某个更早的合法起点覆盖,因此能作为本轮结尾。
        if (pos >= range_start && pos <= range_end) {
            add_next_state(value, person);
            reachable[round][value] = true;
        }
    }
}

// 从上一轮状态计算指定轮的全部可达状态。
void transfer_one_round(int round) {
    memset(next1, -1, sizeof(next1));
    memset(next2, -1, sizeof(next2));

    for (int person = 1; person <= n; person++) {
        scan_person(person, round);
    }

    memcpy(last1, next1, sizeof(last1));
    memcpy(last2, next2, sizeof(last2));
}

void preprocess_answers() {
    memset(last1, -1, sizeof(last1));
    memset(last2, -1, sizeof(last2));
    memset(reachable, 0, sizeof(reachable));

    // 第 0 轮从值 1 开始,0 表示还没有真正的上一轮接龙人。
    last1[1] = 0;

    for (int round = 1; round <= max_round; round++) {
        transfer_one_round(round);
    }
}

void print_answers() {
    for (int i = 1; i <= q; i++) {
        cout << (reachable[queries[i].r][queries[i].v] ? 1 : 0) << '\n';
    }
}

void solve() {
    read_input();
    preprocess_answers();
    print_answers();
}

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

    int T;
    cin >> T;
    while (T--) {
        solve();
    }

    return 0;
}

两个代表人还可以合并成一个整数编码:last[v] = p 表示上一轮只有人 pp 到达 vv,下一轮只要换人就能从 vv 开头;last[v] = 0 表示上一轮至少有两个不同的人到达 vv,谁都能接着开头;-1 表示不可达。第 00 轮的初始状态 last[1] = 0 正好复用“谁都能开头”的含义,不需要额外特判。下面是这种写法的完整代码:

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-01 15:46
 * update_at: 2026-10-01 15:46
 */
// main_no_last2.cpp:main.cpp 的简化版,只用一个 last[v] 记录上一轮接龙人,
// 不再分开保存 last1 / last2。扫描窗口做法不变。
//
// last[v] 的编码:
//   -1     :值 v 不可达,谁都不能拿它开头;
//    0     :上一轮至少有两个不同的人到达 v(或第 0 轮的初始值 1),
//            下一轮任何人都可以拿它开头;
//    p (>=1):上一轮只有人 p 到达 v,下一轮只有 p 不能拿它开头。
#include <bits/stdc++.h>
using namespace std;

const int MAXV = 200005;   // 值的范围
const int MAXR = 105;      // 询问中的最大轮数不超过 100

// 一个询问:恰好 r 轮接龙后,最后一个元素是否恰好为 v
struct Query {
    int r;
    int v;
};

int n, k, q;
int max_round;              // 所有询问中的最大轮数

vector<vector<int> > seq;   // seq[person] = 第 person 个人的序列,下标从 1 开始用
vector<Query> queries;      // 全部询问

// reachable[r][v]:第 r 轮结束时值 v 是否可达
bool reachable[MAXR][MAXV];

// ---------- 轮次状态 ----------
int last[MAXV];             // 上一轮状态,编码见文件头
int next_last[MAXV];          // 本轮状态,扫描完再整体复制到 last

// 读入一组数据,并求出询问中的最大轮数。
void read_input() {
    cin >> n >> k >> q;

    seq.assign(n + 1, vector<int>());
    for (int person = 1; person <= n; person++) {
        int len;
        cin >> len;
        seq[person].resize(len + 1);   // 下标 0 不用,方便和题面下标对应
        for (int i = 1; i <= len; i++) {
            cin >> seq[person][i];
        }
    }

    queries.resize(q + 1);
    max_round = 0;
    for (int i = 1; i <= q; i++) {
        cin >> queries[i].r >> queries[i].v;
        max_round = max(max_round, queries[i].r);
    }
}

// 判断值 v 是否可以作为本轮 person 的开头。
bool can_start(int v, int person) {
    if (last[v] == -1) return false;   // 不可达
    if (last[v] == 0) return true;     // 至少两个不同的人(或初始状态)
    return last[v] != person;          // 唯一生产者不是 person 才行
}

// 记录“本轮可以由 person 接到 value”。
// 只有一个生产者时记下他,出现第二个不同的人就改记 0。
void add_next_laststate(int value, int person) {
    if (next_last[value] == -1) {
        next_last[value] = person;
    } else if (next_last[value] != person) {
        next_last[value] = 0;
    }
}

// 固定本轮接龙人,扫描他的整个序列。
void scan_person(int person, int round) {
    vector<int>& s = seq[person];
    int len = (int)s.size() - 1;

    int range_start = 1;
    int range_end = 0;

    for (int pos = 1; pos <= len; pos++) {
        int value = s[pos];

        // 合法起点 pos 能覆盖后面的 [pos + 1, pos + k - 1]。
        if (can_start(value, person)) {
            int new_end = min(len, pos + k - 1);
            if (range_end < pos) {
                range_start = pos + 1;
                range_end = new_end;
            } else {
                range_end = max(range_end, new_end);
            }
        }

        // pos 被某个更早的合法起点覆盖,因此能作为本轮结尾。
        if (pos >= range_start && pos <= range_end) {
            add_next_laststate(value, person);
            reachable[round][value] = true;
        }
    }
}

// 从上一轮状态计算指定轮的全部可达状态。
void transfer_one_round(int round) {
    memset(next_last, -1, sizeof(next_last));

    for (int person = 1; person <= n; person++) {
        scan_person(person, round);
    }

    memcpy(last, next_last, sizeof(last));
}

void preprocess_answers() {
    memset(last, -1, sizeof(last));
    memset(reachable, 0, sizeof(reachable));

    // 第 0 轮从值 1 开始,且还没有真正的上一轮接龙人,
    // 任何人开头都可以,正好用 0 表示。
    last[1] = 0;

    for (int round = 1; round <= max_round; round++) {
        transfer_one_round(round);
    }
}

void print_answers() {
    for (int i = 1; i <= q; i++) {
        cout << (reachable[queries[i].r][queries[i].v] ? 1 : 0) << '\n';
    }
}

void solve() {
    read_input();
    preprocess_answers();
    print_answers();
}

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

    int T;
    cin >> T;
    while (T--) {
        solve();
    }

    return 0;
}

同一算法的 Python 版本见下。生产者编码与 main_no_last2.cpp 相同(ANY = 0 表示至少两个不同的人,谁都能接着开头),区别在于:用字典代替值域数组,只记录出现过的值;询问按轮数分桶,算完第 tt 轮就地回答;某一轮没有任何值可达时,后面的轮数直接跳过。

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-28 16:40
# update_at: 2026-09-28 16:40

import sys
from collections import defaultdict
from collections.abc import Iterator

ANY = 0  # 生产者编码:0 = 至少两个人可以接(第 0 轮的值 1 也记成它)

# 类型别名(Python 3.12+ 的 type 语句),相当于 C++ 的 using / typedef
type PrevMap = dict[int, int]    # 上一轮/本轮的"值 -> 生产者编码"
type Seqs = list[list[int]]      # 每个人的序列


def reachable_values(seq: list[int], person: int, k: int, prev: PrevMap) -> Iterator[int]:
    """依次产出本轮这个人能收尾的值(同一个值的多次出现会重复产出)。

    合法起点 pos 覆盖结尾位置 [pos+1, pos+k-1]。起点从左往右扫,pos+k-1 单调递增,
    所以"覆盖到哪"只需一个 deadline,不必记录整段区间。
    """
    deadline = 0
    for pos, value in enumerate(seq, 1):
        covered = pos <= deadline                      # 更早的合法起点已经覆盖到 pos
        if covered:
            yield value
        can_start = prev.get(value, person) != person  # 上一轮到过 value,且不是只有自己
        if can_start:
            deadline = pos + k - 1                     # 从 pos 出发能延伸到的最右位置


def advance(prev: PrevMap, seqs: Seqs, k: int) -> PrevMap:
    """由第 r-1 轮可达状态推出第 r 轮状态:合并所有人的可达值。"""
    nxt: PrevMap = {}
    for person, seq in enumerate(seqs, 1):
        for value in reachable_values(seq, person, k, prev):
            # 登记生产者:本轮首次出现 value、或唯一生产者还是自己 → person,否则 ANY
            nxt[value] = person if nxt.setdefault(value, person) == person else ANY
    return nxt


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    out: list[str] = []
    T = next(data)

    for _ in range(T):
        n, k, q = next(data), next(data), next(data)

        seqs: Seqs = []
        for _ in range(n):
            length = next(data)  # 题面的 l_i
            seqs.append([next(data) for _ in range(length)])

        # 询问按轮数分桶,桶里存 (原始下标, 结尾值),这样结果能按输入顺序输出。
        by_round = defaultdict(list)
        for i in range(q):
            r, c = next(data), next(data)
            by_round[r].append((i, c))

        ans = [0] * q
        prev: PrevMap = {1: ANY}  # 第 0 轮只有值 1,且没有上一轮的接龙人
        max_round = max(by_round)        # max(dict) 迭代的是 key,最大 key 就是最大轮数
        for rnd in range(1, max_round + 1):
            prev = advance(prev, seqs, k)
            for i, c in by_round[rnd]:
                ans[i] = 1 if c in prev else 0
            if not prev:  # 这一轮一个值都到不了,后面永远都到不了
                break

        out += map(str, ans)

    print('\n'.join(out))


if __name__ == "__main__":
    solve()

复杂度

每一轮扫描全部 LL 个序列元素,并清空、复制值域状态数组,时间复杂度为

O(R(L+V)+q) O(R(L+V)+q)

保存所有轮数的询问答案需要 O(RV)O(RV) 空间,滚动的代表人状态与所有人的序列存储需要 O(V+L)O(V+L) 空间,因此总空间复杂度为

O(RV+V+L) O(RV+V+L)

在完整范围 R⩽100R\leqslant100、L,V⩽2×105L,V\leqslant2\times10^5 下可以通过。

总结

这道题的数据层次对应两个独立瓶颈:

  1. 搜索会对不同询问重复计算,于是改成按轮数预处理;
  2. 记录全部上一轮接龙人太大,于是每个值只留两个不同代表人;
  3. 显式连续子序列边可能达到平方级,于是把一个起点的全部出边看成位置区间,用一次线性扫描批量转移。

因此,1515 分搜索、6060 分逐边枚举 DP 和 100100 分扫描 DP 并不是互不相关的三套算法。后一层始终保留前一层的有色边模型,只针对刚刚暴露出的瓶颈做一次压缩;这才是各数据范围之间真正的递进关系。