接龙 (CSP-J 2024)
第4题难度一般比前三题高一截,所以准备好使用DP、图算法这些基本工具。这个题达到了蓝题的难度,较有挑战。
本题复杂度要求比较高,因为是,所以每一次查询必须是或者的,这是一个思考的前提。
以下是一个思考的过程:
- 题目问:每个查询都是,表明接龙次数,和最后的数字(开头是),输出“可能”或者“不可能”即可
- 对每个人,考虑生成一个的映射表(子串长度到),表明每个开头数字可以生成哪些结尾数字。然后我们发现,这个方法不行,我们把映射关系画成一张图,边的数量明显是的,这个表内存肯定装不下,所以重想别的办法。
- 我们从数据范围入手,接龙次数,其它长度都是的,包括:人数, 子串长度, 查询次数, 每人词库长度, 词编号。小是一个提示。
- 除了小外,题目有比较明显的多层计算的结构,引导我们想到用DP,比如设定状态为 ,表示第轮的时候,是否能以数字结尾,那么对于查询 ,答案就是 ,因为轮数,而数字最大为,所以复杂度,可以符合要求。
- 难点1:初始状态。朴素的办法,每一步都从中是1的点开始,在图上做扩展。这时我们发现这个图的边特别多,这个扩展的动作会导致整体复杂度变成。
- 我们观察数据的情况来寻找降低复杂度的办法,有一个条件是,从这个可以想到(有难度),我们可以从扩展的终点的角度来进行遍历:将每个人的数字从左到右遍历一遍,当碰到一个为1的点时,后面个数字就是“接龙状态”的数字,设置其都是。这样就可以一遍扫描完成的生成。考虑,整体复杂度是 ,没有问题。
- 难点2:这时我们发现还有一个障碍,接龙的上轮的人不能和这一轮的人是同一个人。朴素的方法,是需要记录每一个数字上一轮都是哪些人,这个肯定就了。
- 继续观察,我们发现其实不用记录所有的状态,如果上一轮是一个人,那么这一轮就不能是,但如果上一轮由超过一个人结尾,那么这一轮就可以是任意人。所以我们可以简化这个状态,只需要3种状态之一:
- 表示第轮没有人以数字结尾
- 表示超过一个人可以在第轮以数字结尾
- ()表示第轮只有可以以数字结尾。
最后的复杂度基本上就是 ,每次查询是的。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int t,n,k,q;
int r,c;
vector<int> a[100005];
int l[100005];
int dp[105][200005];
int main() {
scanf("%d",&t);
while(t--) {
scanf("%d %d %d", &n, &k, &q);
for (int i = 1; i <= n; i++) {
scanf("%d", &l[i]);
a[i].clear();
for (int j = 0; j < l[i]; j++) {
int x;
scanf("%d", &x);
a[i].push_back(x);
}
}
// DP precalc
memset(dp, -1, sizeof(dp));
dp[0][1] = 0;
for (r = 1; r <= 100; r++) { // precalc all rounds
for (int i = 1; i <= n; i++) { // precalc all people
int cnt = 0;
for (int j = 0; j < a[i].size(); j++) { // sum l[i] < 2e5, so complexity ok
int x = a[i][j]; // current number
if (cnt > 0) { // current number is an ending number
if (dp[r][x] == -1) // is first person ending in this?
dp[r][x] = i; // mark this person as the only person ending in x
else if (dp[r][x] != i) // more than 1 people ends in x
dp[r][x] = 0;
}
cnt = cnt > 0 ? cnt-1 : 0;
if (dp[r-1][x] != -1 && dp[r-1][x] != i)
cnt = k-1; // enter "接龙状态"
}
}
}
// queries
while (q--) {
scanf("%d %d", &r, &c);
printf("%d\n", dp[r][c] != -1);
}
}
return 0;
}