字符串[LOJ6517]

题目描述

(N) 个字符串,每个字符串有一个权值 (v_i)。随后给出 (M) 次询问,每次对一个区间进行检测。令最长的字符串长度为 (L),那么会给出 (g_1,cdots,g_L) 表示每个长度的字符串的「识别值」。

对若干个字符串构成的集合 (P) 进行测试的过程如下:

对字符串 (S) 定义 (f(S)) 表示 (S)(P) 中以其为前缀出现的串的权值和。 那么如果 (S)(P) 中作为前缀出现过,并且 (B*f(S)+A*mathrm{len}(S)ge C),那么则将 (g_{mathrm{len}(S)}) 加入集合。

最后随机选择一个区间 ([x,y](1le xle yle L)),如果 ([x, y] igcap G = ot emptyset),那么测试成功,否则测试失败。输出测试成功的概率并用最简分数表示。

特别地,整数 (k) 表示为 (k/1)

输入格式

第一行四个数 (N, A, B, C)

接下来一行 (N) 个数 (v_1, cdots, v_N)

接下来一行一个数 (M)

接下来 (M) 行,每行两个数 (l,r),表示对第 (l) 到第 (r) 个字符串进行测试。

输出格式

(M) 个数表示答案。

题解

首先对题目中的 (n) 个串建出Trie树。显然如果在某一个 (P) 中有一个串以 (S) 为前缀出现,那么 (S) 一定是Trie树上的一个节点,也就是说Trie树上的每个节点都代表着一个 (S)

考虑暴力做法,可以暴力在Trie树上查询 ([l,r]) 之间的字符串,在Trie树上查询串 (i) 时,对Trie树上经过的每个节点 (S) ,令 (f(S)) 加上 (v_i),然后判断是否满足 (B*f(S)+A*mathrm{len}(S)ge C) ,如果满足就把 (g_{mathrm{len}(S)}) 加入集合 (G)

然后考虑怎么统计答案 显然测试成功的概率=1-测试失败的概率

假设集合 (G) 中从小到大排序有 (b_1,b_2,cdots,b_k)(k) 个元素,那么测试失败的情况数就是 (sumlimits_{i=2}^{k} dfrac{(b_i-b_{i-1})(b_i-b_{i-1}-1)}{2})

因为如果测试失败,则 ([x,y]) 必然满足:对于某一个 (i)(b_i < x le y < b_{i+1})

这样我们就得到了一个 (O(nm)) 的算法

接下来考虑如何优化

发现询问是区间询问 可以用莫队来处理 需要支持每次在 (P) 中加入或删除一个字符串

修改时 (f(S)) 显然是可以在Trie树上维护的 可以证明时间复杂度的上限是 (O(Lsqrt{N}))

考虑如何维护集合 (G)

我们设 (V(x)=dfrac{x(x+1)}{2}),那么如果我们在集合 (G)(b_i)(b_{i+1}) 两个元素中插入一个元素 (y),那么答案就需要减去 (V(b_{i+1}-b_i-1)),然后加上 (V(b_{i+1}-y-1))(V(y-b_i-1))

删除 (G) 中的一个元素时同理

于是就可以想到一个用set维护 (G) 的naive做法 每次删除/插入元素 (y) 时找出它的前驱和后继来修改当前答案

不过这样时间复杂度是 (O(Msqrt{N}log{N})) 的 我们需要找到一个可以 (O(1)) 维护集合 (G) 的数据结构

链表这种数据结构 删除元素或查找前驱后继是 (O(1)) 的 但是如果要在指定位置插入元素就会很慢

所以我们可以换用只删除不插入的回滚莫队 这样插入操作就去和梁非凡共进晚餐了

然后我们就把 (O(log n)) 修改的set成功换成了 (O(1)) 修改的链表 这样就得到了一个上限为 (O((M+L)sqrt{N}))大常数?算法 可以通过此题

如果没有找到正确思路,代码非常非常难写。。。所以在这里放上注释过的代码

#include <bits/stdc++.h>
#define N 100005
using namespace std;
typedef long long ll;

template<typename T>
inline void read(T &nnum) {
	T x = 0, f = 1; char cch = getchar();
	for (; cch > '9' || cch < '0'; cch = getchar()) if (cch == '-') f = -1;
	for (; cch <= '9' && cch >= '0'; cch = getchar()) x = (x << 3) + (x << 1) + (cch ^ '0');
	nnum = x * f;
}

int n, st[N], ed[N], bst[N], bed[N], g[N<<2], len[N<<2], m, L, Q, sz, bcnt; 
char s[N<<2], t[N<<2];
bool vis[N<<2];
int ch[N<<2][26], tot, in[N], rnk[N<<2], pre[N<<2], nxt[N<<2], num[N<<2], top;
ll ans1[N], ans2[N], v[N], cnt[N<<2], nowans, a, b, c;
pair<int, pair<int, int> > changed[N<<2];

void insert(int l, int r) { //建Trie树
	int x = 0;
	for (int i = l; i <= r; i++) {
		if (!ch[x][s[i]-'a']) {
			ch[x][s[i]-'a'] = ++tot;
		}
		x = ch[x][s[i]-'a'];
		len[x] = i - l + 1;
	}
}

inline ll calc(ll x) {
	if (x <= 0) return 0;
	else return x * (x + 1) / 2;
}

inline ll V(int x) {
	if (!cnt[x]) return 0;
	return 1ll * b * cnt[x] + 1ll * a * len[x]; 
}

void del(int l, int r, int id) { //删除操作,修改点权+修改链表+修改答案
	int x = 0;
	for (int i = l; i <= r; i++) {
		x = ch[x][s[i]-'a'];
		cnt[x] -= v[id];
		if (V(x) < c && vis[x]) { //若当前节点不满足条件且现在在集合G里
			if (!(--num[g[len[x]]])) { //将g[len_S]的出现次数-1
				int y = g[len[x]], _pre = pre[y], _nxt = nxt[y];
				nowans -= calc(y - _pre - 1) + calc(_nxt - y - 1);
				nowans += calc(_nxt - _pre - 1); //修改当前答案
				changed[++top] = make_pair(_pre, make_pair(pre[_pre], nxt[_pre])); 
				changed[++top] = make_pair(_nxt, make_pair(pre[_nxt], nxt[_nxt]));
                                //记录一下哪些链表元素被修改了 回滚时暴力修改回去
				nxt[_pre] = _nxt; pre[_nxt] = _pre;
			}
			vis[x] = 0;
		}
	}
}

void add(int l, int r, int id) { //增加操作,仅修改点权,不修改链表和答案
	int x = 0;
	for (int i = l; i <= r; i++) {
		x = ch[x][s[i]-'a'];
		cnt[x] += v[id];
		if (V(x) >= c && !vis[x]) { //若当前节点已满足条件且不在集合G里
			num[g[len[x]]]++;
			vis[x] = 1;
		}
	}
}

struct node{
	int l, r, id;
	bool operator < (const node bb) const {
		return in[l] != in[bb.l] ? l < bb.l : r > bb.r; 
                //由于是只删不增的回滚莫队 所以右端点从大到小排序
	}
} q[N];

int main() {
	read(n); read(a); read(b); read(c);
	sz = sqrt(n); bcnt = 1;
	for (int i = 1; i <= n; i++) {
		if (i % sz == 1) bst[bcnt] = i;
		in[i] = bcnt;
		if (i % sz == 0) bed[bcnt] = i, bcnt++;
                //表示一个块的起始和结束位置
	}
	if (n % sz == 0) bcnt--;
	bed[bcnt] = n;
	for (int i = 1; i <= n; i++) {
		read(v[i]);
	}
	for (int i = 1, l; i <= n; i++) {
		scanf("%s", t + 1); l = strlen(t + 1);
		L = max(L, l);
		st[i] = m + 1;
		for (int j = 1; j <= l; j++) {
			s[++m] = t[j];
		}
		ed[i] = m; 
	}
	for (int i = 1; i <= L; i++) {
		read(g[i]);
	}
	read(Q);
	for (int i = 1; i <= Q; i++) {
		read(q[i].l); read(q[i].r); 
		q[i].id = i;
	}
	sort(q + 1, q + Q + 1);
	
	for (int i = 1; i <= n; i++) {
		insert(st[i], ed[i]);
	}
	num[L+1] = 114514; //哼哼啊啊啊啊
	int now = 1, nl = 0, nr = 0;
	for (int i = 1; i <= bcnt; i++) {
		nowans = 0;
		for (int j = bst[i]; j <= n; j++) {
			add(st[j], ed[j], j); //将 当前块的起始位置 ~ N 的所有字符串加入集合P
		}
		int lst = 0;
		for (int j = 1; j <= L + 1; j++) {
			if (num[j]) { //预处理出初始链表和初始答案
				pre[j] = lst; nxt[lst] = j; 
				nowans += calc(j - lst - 1);
				lst = j;
			} 
		} 
		nr = n; //当前右端点从N开始向左移动
		while (in[q[now].l] == i) {
			while (nr > q[now].r) del(st[nr], ed[nr], nr), nr--;
			top = 0; //右端点的修改不用回滚!
                        ll tmp = nowans; //记录当前答案 方便查询完这次之后直接改回去
			nl = bst[i];
			while (nl < q[now].l) del(st[nl], ed[nl], nl), nl++; 
                        //删除时 点权,链表,答案可以一起修改 但是撤销时最好要一个一个撤销比较好写
			ans1[q[now].id] = nowans;
			ans2[q[now].id] = 1ll * L * (L+1) / 2; 
			for (int j = q[now].l - 1; j >= bst[i]; j--) add(st[j], ed[j], j); //撤销对Trie树上点权的修改
			while (top) { //撤销对链表的修改
				pre[changed[top].first] = changed[top].second.first;
				nxt[changed[top].first] = changed[top].second.second;
				top--;
			}
			nowans = tmp; //撤销对答案的修改
			now++;
		}
		while (nr >= bst[i]) { //把剩余字符串全部删掉 相当于让P集合回到空集
			del(st[nr], ed[nr], nr);
			nr--;
		}
	}
	for (int i = 1; i <= Q; i++) {
		ans1[i] = ans2[i] - ans1[i];
		if (ans1[i] == 0) puts("0/1");
		else if (ans1[i] == ans2[i]) puts("1/1");
		else {
			ll gcd = __gcd(ans1[i], ans2[i]);
			ans1[i] /= gcd; ans2[i] /= gcd;
			printf("%lld/%lld
", ans1[i], ans2[i]);
		}
	}
	return 0;
}
原文地址:https://www.cnblogs.com/ak-dream/p/AK_DREAM97.html