NOI2015.品酒大会(后缀数组)

NOI2015:品酒大会——后缀数组解题策略
该博客探讨了NOI2015中的一道问题,涉及字符串相似性及权值计算。题目要求统计不同相似度下,两字符的权值乘积及最大值。博主首先介绍了50分的暴力解法,通过预处理字符串和Height数组,但可能因数据复杂度导致超时。随后,博主提出了一种优化方案,通过对Height数组排序,利用并查集维护集合贡献,降低时间复杂度至O(nlogn)。

给出一个 长度为 n 的字符串,每一位有一个权值 val。定义两个位字符为 r 相似,是指分别从这两个字符开始,到后面的 r 个字符都相等。两个 r 相似的字符还有一个权值为这两个字符权值的乘积。问对于 r = 0, 1, 2, … , n - 1,统计出有多少种方法可以选出 2 个“相似”的字符,并回答选择 2 个”r 相似”的字符可以得到的权值的最大值。 

首先说一个暴力的做法,可以得到 50 分:

先预处理出 1 - n 累加的结果。对于这个字符串求出 Height 数组,然后枚举 r ,按 r Height 分组,然后统计方案数:假设组内有 x 个后缀,那么贡献的方案数就是 1 + 2 + ... + (x - 1),统计最大值:维护最大值和次大值,最小值和次小值,更新答案。

这种方法的复杂度主要看数据,如果数据很乱,导致 Height数组的最大值很小,那么这种方法就可以过,否则就会 TLE

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int MAX_N = 300005;
const long long INF = 9223372036854775807;

typedef long long LL;

int n, a[MAX_N], sa[MAX_N], r[MAX_N], h[MAX_N], id1, id2;
LL val[MAX_N], Mx, Mxx, Mn, Mnn;
int ws[MAX_N], wv[MAX_N], wa[MAX_N], wb[MAX_N];
LL add[MAX_N], ans, ret;

void da(int *a, int *sa, int n, int m) {
	int *x = wa, *y = wb;
	for (int i = 0; i < m; i ++) ws[i] = 0;
	for (int i = 0; i < n; i ++) ws[x[i] = a[i]] ++;
	for (int i = 1; i < m; i ++) ws[i] += ws[i - 1];
	for (int i = n - 1; i >= 0; i --) sa[-- ws[x[i]]] = i;
	for (int k = 1; k <= n; k <<= 1) {
		int p = 0;
		for (int i = n - k; i < n; i ++) y[p ++] = i;
		for (int i = 0; i < n; i ++) if (sa[i] >= k) y[p ++] = sa[i] - k;
		for (int i = 0; i < n; i ++) wv[i] = x[y[i]];
		for (int i = 0; i < m; i ++) ws[i] = 0;
		for (int i = 0; i < n; i ++) ws[wv[i]] ++;
		for (int i = 1; i < m; i ++) ws[i] += ws[i - 1];
		for (int i = n - 1; i >= 0; i --) sa[-- ws[wv[i]]] = y[i];
		swap(x, y); p = 1; x[sa[0]] = 0;
		for (int i = 1; i < n; i ++) x[sa[i]] = (y[sa[i - 1]] == y[sa[i]]) && (y[sa[i - 1] + k] == y[sa[i] + k]) ? p - 1 : p ++;
		if (p >= n) break; m = p;
	}
}
void calc() {
	for (int i = 1; i <= n; i ++) r[sa[i]] = i;
	int k = 0, j;
	for (int i = 0; i < n; h[r[i ++]] = k)
		for (k ? k -- : 0, j = sa[r[i] - 1]; a[i + k] == a[j + k]; k ++);
}
void work(int x) {
	int sum = 0, j;
	for (int i = 2; i <= n; i = j + 1) {
		for (; h[i] < x && i <= n; i ++);
		for (j = i; h[j] >= x; j ++);
		if (i == j) continue;
		sum = 0; Mx = -INF; Mn = Mnn = INF;
		ans += add[j - i];
		for (int k = i - 1; k < j; k ++) {
			if (val[sa[k]] > Mx) Mx = val[sa[k]], id1 = k;
			if (val[sa[k]] < Mn) Mn = val[sa[k]], id2 = k;
		}
		Mxx = -INF, Mnn = INF;
		for (int k = i - 1; k < j; k ++) {
			if (val[sa[k]] > Mxx && id1 != k) Mxx = val[sa[k]];
			if (val[sa[k]] < Mnn && id2 != k) Mnn = val[sa[k]];
		}
		ret = max(ret, max(Mxx * Mx, Mn * Mnn));
	}
}
void init() {
	scanf("%d", &n); getchar();
	for (int i = 0; i < n; i ++) {
		char c; scanf("%c", &c);
		a[i] = (int)c;
	}
	a[n] = 0; 
	Mx = Mxx = -INF; Mn = Mnn = INF;
	for (int i = 0; i < n; i ++) {
		scanf("%lld", &val[i]);
		if (Mx < val[i]) Mx = val[i], id1 = i;
		if (Mn > val[i]) Mn = val[i], id2 = i;
	}
	da(a, sa, n + 1, 128); calc();
	for (int i = 1; i <= n; i ++) printf("%d ", sa[i]); printf("\n");
	for (int i = 1; i <= n; i ++) printf("%d ", h[i]); printf("\n");
}
void doit() {
	int mxh = -1;
	for (int i = 1; i <= n; i ++) mxh = max(mxh, h[i]);
	for (int i = 1; i <= n; i ++) add[i] = add[i - 1] + i;
	for (int i = 0; i < n; i ++) {
		if (Mxx < val[i] && i != id1) Mxx = val[i];
		if (Mnn > val[i] && i != id2) Mnn = val[i];
	}
	printf("%lld %lld\n", add[n - 1], max(Mx * Mxx, Mn * Mnn));
	for (int i = 1; i < n; i ++) {
		if (i <= mxh) {
			ans = 0; ret = -INF;
			Mx = Mxx = -INF; Mn = Mnn = INF;
			work(i);
			printf("%lld %lld\n", ans, ret == -INF ? 0 : ret);
		} else printf("0 0\n");
	}
}
int main() {
	init();
	doit();
	return 0;
}
满分算法:

Height 数组,然后按从大到小的顺序排序,因为可以发现 Height 中的大值不会影响小值对答案的贡献。每次更新答案,将当前两个字符串合并,即用并查集维护一下,他们对于方案数的贡献就是这两个后缀所在的集合个数的乘积,同时维护一下最大最小值就行了。时间复杂度 O( nlogn )

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int MAX_N = 300005;

typedef long long LL;

int n, a[MAX_N], sa[MAX_N], r[MAX_N], h[MAX_N];
int ws[MAX_N], wv[MAX_N], wa[MAX_N], wb[MAX_N];
int f[MAX_N], sz[MAX_N];
LL ans[MAX_N], sum[MAX_N], mx[MAX_N], mn[MAX_N], val[MAX_N];
char c;
struct node {
	int h, x, y;
}g[MAX_N];

inline bool cmp(node a, node b) { return a.h > b.h; }
int find(int x) { return x == f[x] ? x : (f[x] = find(f[x])); }
void uniont(int x, int y) { 
	f[y] = x; sz[x] += sz[y];
	mx[x] = max(mx[x], mx[y]);
	mn[x] = min(mn[x], mn[y]);
}
void da(int *a, int *sa, int n, int m) {
	int *x = wa, *y = wb;
	for (int i = 0; i < m; i ++) ws[i] = 0;
	for (int i = 0; i < n; i ++) ws[x[i] = a[i]] ++;
	for (int i = 1; i < m; i ++) ws[i] += ws[i - 1];
	for (int i = n - 1; i >= 0; i --) sa[-- ws[x[i]]] = i;
	for (int k = 1; k <= n; k <<= 1) {
		int p = 0;
		for (int i = n - k; i < n; i ++) y[p ++] = i;
		for (int i = 0; i < n; i ++) if (sa[i] >= k) y[p ++] = sa[i] - k;
		for (int i = 0; i < n; i ++) wv[i] = x[y[i]];
		for (int i = 0; i < m; i ++) ws[i] = 0;
		for (int i = 0; i < n; i ++) ws[wv[i]] ++;
		for (int i = 1; i < m; i ++) ws[i] += ws[i - 1];
		for (int i = n - 1; i >= 0; i --) sa[-- ws[wv[i]]] = y[i];
		swap(x, y); p = 1; x[sa[0]] = 0;
		for (int i = 1; i < n; i ++) x[sa[i]] = (y[sa[i - 1]] == y[sa[i]]) && (y[sa[i - 1] + k] == y[sa[i] + k]) ? p - 1 : p ++;
		if (p >= n) break; m = p;
	}
}
void calc() {
	for (int i = 1; i <= n; i ++) r[sa[i]] = i;
	int k = 0, j;
	for (int i = 0; i < n; h[r[i ++]] = k)
		for (k ? k -- : 0, j = sa[r[i] - 1]; a[i + k] == a[j + k]; k ++);
}
void init() {
	scanf("%d", &n); getchar();
	for (int i = 0; i < n; i ++) {
		char c; scanf("%c", &c);
		a[i] = (int)c;
	}
	a[n] = 0; 
	for (int i = 1; i <= n; i ++) scanf("%lld", &val[i]);
	da(a, sa, n + 1, 128); calc();
	for (int i = 1; i <= n; i ++) 
		f[i] = i, mx[r[i - 1]] = val[i], mn[r[i - 1]] = val[i], sz[i] = 1;
}
void doit() {
	for (int i = 2; i <= n; i ++) g[i - 1].h = h[i], g[i - 1].x = i, g[i - 1].y = i - 1;
	sort(g + 1, g + n, cmp);
	memset(sum, 128, sizeof(sum));
	for (int i = g[1].h, j = 1; i >= 0; i --) {
		ans[i] = ans[i + 1], sum[i] = sum[i + 1];
		for (; j < n && g[j].h == i; j ++) {
			int x = find(g[j].x), y = find(g[j].y);
			sum[i] = max(sum[i], 1ll * mx[x] * mx[y]);
			sum[i] = max(sum[i], 1ll * mn[x] * mn[y]);
			ans[i] += 1ll * sz[x] * sz[y];
			uniont(x,y);
		}
	}
	for (int i = 0; i < n; i ++) 
		printf("%lld %lld\n", ans[i], ans[i] == 0 ? 0 : sum[i]);
}
int main() {
	init();
	doit();
	return 0;
}



### 关于NOI金牌导航中的字符串算法与后缀数组 在处理涉及多个字符串连接并构建其后缀数组的问题时,一种有效的方法是在这些字符串之间插入不同的分隔符[^1]。此方法确保了即使原始字符串内部存在重复部分,在计算后缀数组过程中仍能区分来自不同源字符串的部分。 对于具体实现而言,当面对需要高效查询和匹配的任务时,除了基本的后缀数组外,还可以考虑使用更高级的数据结构如广义SAM(Suffix Automaton),这有助于统计本质不同的子串数量以及其他复杂的模式识别操作[^3]。然而值得注意的是,并不是所有情况下都适合采用最复杂的数据结构;有时简单的基于后缀数组的操作已经能够满足需求,尤其是在时间复杂度要求较为严格的情况下——例如通过二分法配合后缀分组来解决问题可以达到\(O(n\log n)\)的时间效率。 针对特定应用场景下的性能优化也至关重要。某些场景下,尽管理论上可行的做法可能因为实际输入特性而表现不佳,比如高度随机化的Height数组可能导致较高复杂度算法超时(TLE),此时就需要根据具体情况调整策略以适应数据特点[^5]。 ```cpp // 示例代码:简单展示如何创建两个字符串之间的后缀数组 #include <iostream> #include <vector> #include <string> using namespace std; void build_suffix_array(const string &text, vector<int> &suffixArray){ int n = text.size(); suffixArray.resize(n); // 构造后缀数组逻辑... } int main(){ string s1="abc",s2="def"; char separator='#'; // 假设'#'不在任何给定字符串内出现 string combinedString=s1+separator+s2; vector<int> sa; build_suffix_array(combinedString,sa); cout << "Combined String with Separator: " << combinedString << endl; } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值