接龙 (CSP-J 2024)

接龙 (CSP-J 2024)

第4题难度一般比前三题高一截,所以准备好使用DP、图算法这些基本工具。这个题达到了蓝题的难度,较有挑战。

本题复杂度要求比较高,因为qq10510^5,所以每一次查询必须是O(1)\mathcal{O}(1)或者O(logN)\mathcal{O}(\log N)的,这是一个思考的前提。

以下是一个思考的过程:

  1. 题目问:每个查询都是(rj,cj)(r_j,c_j),表明接龙次数,和最后的数字(开头是11),输出“可能”或者“不可能”即可
  2. 对每个人,考虑生成一个xyx\rightarrow y的映射表(子串长度22kk),表明每个开头数字可以生成哪些结尾数字。然后我们发现,这个方法不行,我们把xyxy映射关系画成一张图,边的数量明显是N2N^2的,这个表内存肯定装不下,所以重想别的办法。
  3. 我们从数据范围入手,接龙次数r<100r \lt 100,其它长度都是10510^5的,包括:nn人数, kk子串长度, qq查询次数, lil_i每人词库长度, SijS_{ij}词编号。rr小是一个提示。
  4. 除了rr小外,题目有比较明显的多层计算的结构,引导我们想到用DP,比如设定状态为 dpijdp_{ij},表示第ii轮的时候,是否能以数字jj结尾,那么对于查询 (r,c)(r,c),答案就是 dprcdp_{rc},因为轮数r<100r \lt 100,而数字最大为21052\cdot 10^5,所以复杂度21072\cdot 10^7,可以符合要求。
  5. 难点1:初始状态dp0,1=1dp_{0,1}=1。朴素的办法,每一步都从dpi1dp_{i-1}中是1的点开始,在图上做扩展。这时我们发现这个图的边特别多,这个扩展的动作会导致整体复杂度变成O(N2)\mathcal{O}(N^2)
  6. 我们观察数据的情况来寻找降低复杂度的办法,有一个条件是li<2105\sum {l_i} \lt 2\cdot 10^5,从这个可以想到(有难度),我们可以从扩展的终点的角度来进行遍历:将每个人的数字aja_j从左到右遍历一遍,当碰到一个dpi1,ajdp_{i-1,a_j}为1的点时,后面k1k-1个数字就是“接龙状态”的数字,设置其dpiajdp_{ia_j}都是11。这样就可以一遍扫描完成dpidp_i的生成。考虑li<2105\sum {l_i} \lt 2\cdot 10^5,整体复杂度是 O(N)\mathcal{O}(N),没有问题。
  7. 难点2:这时我们发现还有一个障碍,接龙的上轮的人不能和这一轮的人是同一个人。朴素的方法,是需要记录每一个数字上一轮都是哪些人,这个肯定就O(N2)\mathcal{O}(N^2)了。
  8. 继续观察,我们发现其实不用记录所有的状态,如果上一轮是一个人xx,那么这一轮就不能是xx,但如果上一轮由超过一个人结尾,那么这一轮就可以是任意人。所以我们可以简化这个状态,dpijdp_{ij}只需要3种状态之一:
    • 1-1表示第ii轮没有人以数字jj结尾
    • 00表示超过一个人可以在第ii轮以数字jj结尾
    • xx (x>0x > 0)表示第ii轮只有xx可以以数字jj结尾。

最后的复杂度基本上就是 O(N)\mathcal{O}(N),每次查询是O(1)\mathcal{O}(1)的。

#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;
}