[NOI2017] 蚯蚓排队

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

按被询问到的每个 k 分开离线模拟,只维护该 k 的向后串计数;合并分裂时只更新边界附近 k-1 个起点。

OJ: luogu

题目 ID: P3823

难度:NOI/NOI+/CTSC

标签:字符串哈希链表离线计数建模

日期: 2026-06-21 14:25

题意

nn 只蚯蚓,每只蚯蚓有一个长度数字 161 \dots 6

初始时每只蚯蚓单独成队。之后有三种操作:

  1. ii 所在队伍和 jj 所在队伍接起来,让 jj 紧挨在 ii 后面
  2. ii 和它后面的那只蚯蚓断开
  3. 给一个数字串 ss 和一个 kk,设 f(t)f(t) 表示当前所有蚯蚓里,向后看最近 kk 只蚯蚓恰好形成字符串 tt 的起点数量,询问 ss 的所有长度为 kk 的连续子串 ttf(t)f(t) 乘积

要求输出每个第三类操作的答案。

思路

先看一个可以直接验证想法的朴素解:

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

const int MOD = 998244353;
const int MAXN = 205;

int n, m;
int worm_len[MAXN];
int pre_node[MAXN], nxt_node[MAXN];

string forward_string(int u, int k) {
    string res = "";
    while (u != 0 && k > 0) {
        res.push_back(char('0' + worm_len[u]));
        u = nxt_node[u];
        k--;
    }
    if (k > 0) {
        return "";
    }
    return res;
}

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

    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> worm_len[i];
        pre_node[i] = nxt_node[i] = 0;
    }

    for (int opi = 1; opi <= m; opi++) {
        int type;
        cin >> type;
        if (type == 1) {
            int i, j;
            cin >> i >> j;
            nxt_node[i] = j;
            pre_node[j] = i;
        }
        else if (type == 2) {
            int i;
            cin >> i;
            int x = nxt_node[i];
            nxt_node[i] = 0;
            if (x != 0) {
                pre_node[x] = 0;
            }
        }
        else {
            string s;
            int k;
            cin >> s >> k;

            map<string, int> cnt;
            for (int i = 1; i <= n; i++) {
                string t = forward_string(i, k);
                if (!t.empty()) {
                    cnt[t]++;
                }
            }

            long long answer = 1;
            for (int i = 0; i + k <= (int)s.size(); i++) {
                string t = s.substr(i, k);
                answer = answer * cnt[t] % MOD;
            }
            cout << answer << '\n';
        }
    }

    return 0;
}

如果每次询问都暴力扫所有蚯蚓,重新统计所有长度为 kk 的向后串,复杂度一定爆炸。

这题真正关键的地方有两个:

  1. k50k \leqslant 50 很小
  2. 一次合并或分裂,真正受影响的起点只会出现在断点附近

我们先固定某一个 kk 来看。

对于这个固定的 kk,每只蚯蚓最多只会贡献一个"向后 kk 串"。
于是我们只需要维护每个长度为 kk 的字符串当前出现了多少次。

再看修改操作。

假设把一条队伍的尾巴 ii 和另一条队伍的头 jj 接起来。
队伍内部原来已经存在的长度 kk 串都不会变,只有左边队尾附近最多 k1k-1 个起点,原来因为后面不够长没有形成完整长度 kk 串,现在可能跨过新边界形成新的长度 kk 串。

分裂也是同理,只会让边界左侧最多 k1k-1 个起点失去原来跨边界的长度 kk 串。

所以:

  • 合并时,只需要给边界附近新出现的长度 kk 串计数 +1+1
  • 分裂时,只需要把边界附近消失的长度 kk 串计数 1-1

接下来还有一个重要观察:

不同询问的 kk 彼此独立。
固定 k=3k=3 时维护的是所有长度为 33 的串计数;固定 k=5k=5 时维护的是长度为 55 的串计数,它们互不干扰。

因此可以把所有询问按 kk 离线分组:

  1. 先读入全部操作
  2. 对每个真正出现过的 kk
  3. 从初始状态重新模拟一遍所有操作
  4. 只在这一遍里维护这个 kk 的计数
  5. 遇到同样 kk 的询问就直接回答

固定某个 kk 时:

  • 合并/分裂的更新量是 O(k)O(k)
  • 询问只要对 ss 做长度 kk 的滑动窗口,用哈希表查每个窗口当前出现次数即可

代码里用双向链表维护队伍关系,用一个精确的 3-bit 打包键来表示长度 kk 的数字串,避免哈希冲突。

代码

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

const int MOD = 998244353;
const int MAXN = 200005;
const int MAXK = 55;
const int HASH_SIZE = 1 << 20;

struct Key {
    unsigned long long x0, x1, x2;

    bool operator==(const Key &other) const {
        return x0 == other.x0 && x1 == other.x1 && x2 == other.x2;
    }
};

struct HashNode {
    Key key;
    int value;
    int next;
};

struct HashTable {
    int head[HASH_SIZE];
    vector<HashNode> nodes;
    vector<int> free_list;

    void init() {
        memset(head, -1, sizeof(head));
        nodes.clear();
        free_list.clear();
        nodes.reserve(400000);
        free_list.reserve(200000);
    }

    unsigned int hash_key(const Key &key) const {
        unsigned long long h = key.x0 * 1000003ULL;
        h ^= key.x1 * 1000033ULL;
        h ^= key.x2 * 1000211ULL;
        h ^= h >> 32;
        return (unsigned int)(h & (HASH_SIZE - 1));
    }

    int get(const Key &key) const {
        unsigned int bucket = hash_key(key);
        for (int i = head[bucket]; i != -1; i = nodes[i].next) {
            if (nodes[i].key == key) {
                return nodes[i].value;
            }
        }
        return 0;
    }

    void add(const Key &key, int delta) {
        unsigned int bucket = hash_key(key);
        int prev = -1;
        for (int i = head[bucket]; i != -1; i = nodes[i].next) {
            if (nodes[i].key == key) {
                nodes[i].value += delta;
                if (nodes[i].value == 0) {
                    if (prev == -1) {
                        head[bucket] = nodes[i].next;
                    }
                    else {
                        nodes[prev].next = nodes[i].next;
                    }
                    free_list.push_back(i);
                }
                return;
            }
            prev = i;
        }

        if (delta <= 0) {
            return;
        }

        int id;
        if (!free_list.empty()) {
            id = free_list.back();
            free_list.pop_back();
            nodes[id].key = key;
            nodes[id].value = delta;
            nodes[id].next = head[bucket];
        }
        else {
            id = (int)nodes.size();
            nodes.push_back({key, delta, head[bucket]});
        }
        head[bucket] = id;
    }
};

struct Operation {
    int type;
    int a, b;
    int k;
    int id;
    string s;
};

int n, m;
int worm_len[MAXN];
vector<Operation> ops;
vector<int> query_k_list;
vector<int> query_answer;
bool need_k[MAXK];

int pre_node[MAXN], nxt_node[MAXN];
int left_digits[MAXK], right_digits[MAXK], concat_digits[MAXK * 2];

unsigned long long mask0, mask1, mask2;

void clear_key(Key &key) {
    key.x0 = key.x1 = key.x2 = 0;
}

unsigned long long build_mask(int bits) {
    if (bits <= 0) {
        return 0;
    }
    if (bits >= 64) {
        return ~0ULL;
    }
    return (1ULL << bits) - 1;
}

void set_key_mask(int k) {
    int total_bits = 3 * k;
    mask0 = build_mask(min(total_bits, 64));
    mask1 = build_mask(min(max(total_bits - 64, 0), 64));
    mask2 = build_mask(min(max(total_bits - 128, 0), 64));
}

void append_digit(Key &key, int digit) {
    unsigned long long nx2 = (key.x2 << 3) | (key.x1 >> 61);
    unsigned long long nx1 = (key.x1 << 3) | (key.x0 >> 61);
    unsigned long long nx0 = (key.x0 << 3) | (unsigned long long)digit;
    key.x0 = nx0 & mask0;
    key.x1 = nx1 & mask1;
    key.x2 = nx2 & mask2;
}

int get_left_digits(int u, int limit, int out[]) {
    int tmp[MAXK];
    int cnt = 0;
    while (u != 0 && cnt < limit) {
        tmp[cnt++] = worm_len[u];
        u = pre_node[u];
    }
    for (int i = 0; i < cnt; i++) {
        out[i] = tmp[cnt - 1 - i];
    }
    return cnt;
}

int get_right_digits(int u, int limit, int out[]) {
    int cnt = 0;
    while (u != 0 && cnt < limit) {
        out[cnt++] = worm_len[u];
        u = nxt_node[u];
    }
    return cnt;
}

void update_cross_boundary(int left_tail, int right_head, int k, int delta, HashTable &counter) {
    if (k == 1 || left_tail == 0 || right_head == 0) {
        return;
    }

    int len_left = get_left_digits(left_tail, k - 1, left_digits);
    int len_right = get_right_digits(right_head, k - 1, right_digits);
    int total = len_left + len_right;
    if (total < k) {
        return;
    }

    for (int i = 0; i < len_left; i++) {
        concat_digits[i] = left_digits[i];
    }
    for (int i = 0; i < len_right; i++) {
        concat_digits[len_left + i] = right_digits[i];
    }

    int start_count = min(len_left, total - k + 1);
    if (start_count <= 0) {
        return;
    }

    Key key;
    clear_key(key);
    for (int i = 0; i < k; i++) {
        append_digit(key, concat_digits[i]);
    }
    counter.add(key, delta);

    for (int start = 1; start < start_count; start++) {
        append_digit(key, concat_digits[start + k - 1]);
        counter.add(key, delta);
    }
}

int query_string_product(const string &s, int k, const HashTable &counter) {
    for (int i = 0; i < (int)s.size(); i++) {
        if (s[i] < '1' || s[i] > '6') {
            return 0;
        }
    }

    Key key;
    clear_key(key);
    for (int i = 0; i < k; i++) {
        append_digit(key, s[i] - '0');
    }

    long long answer = counter.get(key);
    if (answer == 0) {
        return 0;
    }

    for (int i = k; i < (int)s.size(); i++) {
        append_digit(key, s[i] - '0');
        int ways = counter.get(key);
        if (ways == 0) {
            return 0;
        }
        answer = answer * ways % MOD;
    }
    return (int)answer;
}

void simulate_one_k(int k) {
    set_key_mask(k);

    for (int i = 1; i <= n; i++) {
        pre_node[i] = 0;
        nxt_node[i] = 0;
    }

    HashTable counter;
    counter.init();

    if (k == 1) {
        for (int i = 1; i <= n; i++) {
            Key key;
            clear_key(key);
            append_digit(key, worm_len[i]);
            counter.add(key, 1);
        }
    }

    for (int idx = 0; idx < (int)ops.size(); idx++) {
        Operation &op = ops[idx];
        if (op.type == 1) {
            update_cross_boundary(op.a, op.b, k, 1, counter);
            nxt_node[op.a] = op.b;
            pre_node[op.b] = op.a;
        }
        else if (op.type == 2) {
            int right_head = nxt_node[op.a];
            update_cross_boundary(op.a, right_head, k, -1, counter);
            if (right_head != 0) {
                pre_node[right_head] = 0;
            }
            nxt_node[op.a] = 0;
        }
        else if (op.k == k) {
            query_answer[op.id] = query_string_product(op.s, k, counter);
        }
    }
}

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

    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> worm_len[i];
    }

    ops.reserve(m);
    int query_cnt = 0;
    for (int i = 1; i <= m; i++) {
        Operation op;
        cin >> op.type;
        op.id = -1;
        op.a = op.b = op.k = 0;
        op.s.clear();

        if (op.type == 1) {
            cin >> op.a >> op.b;
        }
        else if (op.type == 2) {
            cin >> op.a;
        }
        else {
            cin >> op.s >> op.k;
            op.id = query_cnt++;
            if (!need_k[op.k]) {
                need_k[op.k] = true;
                query_k_list.push_back(op.k);
            }
        }
        ops.push_back(op);
    }

    query_answer.assign(query_cnt, 0);
    for (int i = 0; i < (int)query_k_list.size(); i++) {
        simulate_one_k(query_k_list[i]);
    }

    for (int i = 0; i < query_cnt; i++) {
        cout << query_answer[i] << '\n';
    }

    return 0;
}

复杂度

设被询问到的不同 kk 个数为 DD

对某个固定的 kk

  • 每次合并/分裂只更新边界附近 O(k)O(k) 个起点
  • 每次询问扫描一次字符串 ss

所以总复杂度可以看成:

  • O((合并数+分裂数)×sum(k))O((\text{合并数} + \text{分裂数}) \times \text{sum}(k))
  • 再加上所有询问字符串总长度 O(sums)O(\text{sum} |s|)

其中 sum(k)\text{sum}(k) 只对真正出现过的 kk 求和,最大也就是 1+2++50=12751+2+\dots+50=1275

总结

这题最难的不是链表,而是看出"固定一个 kk 单独维护"这个离线思路。

一旦按 kk 拆开,问题就变成:

  • 用链表表示队伍
  • 用边界局部更新维护长度 kk 串计数
  • 用滑动窗口回答查询

核心就是"修改只影响边界附近,且不同 kk 可以分治处理"。

总结