按被询问到的每个 k 分开离线模拟,只维护该 k 的向后串计数;合并分裂时只更新边界附近 k-1 个起点。
OJ: luogu
题目 ID: P3823
难度:NOI/NOI+/CTSC
标签:字符串哈希链表离线计数建模
日期: 2026-06-21 14:25
题意
有
初始时每只蚯蚓单独成队。之后有三种操作:
- 把
所在队伍和 所在队伍接起来,让 紧挨在 后面 - 把
和它后面的那只蚯蚓断开 - 给一个数字串
和一个 ,设 表示当前所有蚯蚓里,向后看最近 只蚯蚓恰好形成字符串 的起点数量,询问 的所有长度为 的连续子串 的 乘积
要求输出每个第三类操作的答案。
思路
先看一个可以直接验证想法的朴素解:
#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;
}如果每次询问都暴力扫所有蚯蚓,重新统计所有长度为
这题真正关键的地方有两个:
很小 - 一次合并或分裂,真正受影响的起点只会出现在断点附近
我们先固定某一个
对于这个固定的
于是我们只需要维护每个长度为
再看修改操作。
假设把一条队伍的尾巴
队伍内部原来已经存在的长度
分裂也是同理,只会让边界左侧最多
所以:
- 合并时,只需要给边界附近新出现的长度
串计数 - 分裂时,只需要把边界附近消失的长度
串计数
接下来还有一个重要观察:
不同询问的
固定
因此可以把所有询问按
- 先读入全部操作
- 对每个真正出现过的
- 从初始状态重新模拟一遍所有操作
- 只在这一遍里维护这个
的计数 - 遇到同样
的询问就直接回答
固定某个
- 合并/分裂的更新量是
- 询问只要对
做长度 的滑动窗口,用哈希表查每个窗口当前出现次数即可
代码里用双向链表维护队伍关系,用一个精确的 3-bit 打包键来表示长度
代码
#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;
}复杂度
设被询问到的不同
对某个固定的
- 每次合并/分裂只更新边界附近
个起点 - 每次询问扫描一次字符串
所以总复杂度可以看成:
- 再加上所有询问字符串总长度
其中
总结
这题最难的不是链表,而是看出"固定一个
一旦按
- 用链表表示队伍
- 用边界局部更新维护长度
串计数 - 用滑动窗口回答查询
核心就是"修改只影响边界附近,且不同