字符串[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](1\le x\le y\le L)\),如果 \([x, y] \bigcap G =\not \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\) 个元素,那么测试失败的情况数就是 \(\sum\limits_{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(L\sqrt{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(M\sqrt{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\n", ans1[i], ans2[i]);
		}
	}
	return 0;
}
posted @ 2020-08-02 22:11  AK_DREAM  阅读(115)  评论(0编辑  收藏  举报