hdu 6704 后缀数组+主席树+线段树
题目大意
给一个字符串以及若干询问;询问中描述了一个子串,以及一个数字x,问子串出现的第x次是在原串的哪个位置。题目在此
思路
类似询问某个子串出现次数的题目,能够想到用后缀数组来解。后缀数组是将原串的所有后缀按照字典序排序,那么如果某个子串出现了n次,那么分别将这n个子串作为前缀的后缀串必定在后缀数组中连续出现。我们只需要找到这个区间,对这个区间求后缀起始位置的第K小即可,此处可使用主席树。寻找这个区间的方法有很多种。本人的队友想到了线段树查找,当然也可以使用ST表+二分来查找区间。AC代码是后缀数组+主席树+线段树的策略。
AC代码
#include <bits/stdc++.h>
using namespace std;
const int CHAR_NUM = 128;
const int MAXN = 2e5 + 10;
typedef long long ll;
#define per(i, a, n) for (int i=a;i<n;i++)
#define w(i) T[(i)].w
#define ls(i) T[(i)].ls
#define rs(i) T[(i)].rs
struct node {
int ls, rs; //左儿子,右儿子
ll w; //下面点数
node() { ls = rs = w = 0; }
} T[MAXN * 20];
ll b[MAXN], p[MAXN]; //原始数据,离散化后点值,离散化过程数组
int root[MAXN], sz; //第i棵树根节点,总点数
//插入
void ins(int &i, int l, int r, int x) {
T[++sz] = T[i];
i = sz;
w(i)++;
if (l == r) return;
int m = (l + r) >> 1;
if (x <= m) ins(ls(i), l, m, x);
else ins(rs(i), m + 1, r, x);
}
//查询区间[l, r] 第k小(第1小表示最小)
//query(root[l-1], root[r], 1, N, k);
int query(int i, int j, int l, int r, int k) {
if (l == r) return l;
int t = w(ls(j)) - w(ls(i));
int m = (l + r) >> 1;
if (t >= k) return query(ls(i), ls(j), l, m, k);
else return query(rs(i), rs(j), m + 1, r, k - t);
}
int SA[MAXN], myRank[MAXN], height[MAXN], sum[MAXN], tp[MAXN];
//rank[i] 第i个后缀的排名, SA[i] 排名为i的后缀的位置, Height[i] 排名为i的后缀与排名为(i-1)的后缀的LCP
//sum[i] 基数排序辅助数组, 存储小于i的元素有多少个, tp[i] rank的辅助数组(按第二关键字排序的结果),与SA意义一样
bool cmp(const int *f, int x, int y, int w) {
return f[x] == f[y] && f[x + w] == f[y + w];
}
void get_SA(const char *s, int n, int m) {
//先预处理长度为1的情况
for (int i = 0; i < m; i++) sum[i] = 0;//清0
for (int i = 0; i < n; i++) sum[myRank[i] = s[i]]++;//统计每个字符出现的次数
for (int i = 1; i < m; i++) sum[i] += sum[i - 1];//sum[i]为小于等于i的元素的数目
for (int i = n - 1; i >= 0; i--) SA[--sum[myRank[i]]] = i;//下标从0开始,所以先自减
//SA[i]存储排名第i的后缀下标,SA[--sum[rank[i]]] = i 即下标为i的后缀排名为--sum[rank[i]],这很显然
for (int len = 1; len <= n; len *= 2) {
int p = 0;
//直接用SA数组对第二关键字排序
for (int i = n - len; i < n; i++) tp[p++] = i;//后面i个数没有第二关键字,即第二关键字为空,所以最小
for (int i = 0; i < n; i++) {
if (SA[i] >= len) tp[p++] = SA[i] - len;
}
//tp[i]存储按第二关键字排序第i的下标
//对第二关键字排序的结果再按第一关键字排序,和长度为1的情况类似
for (int i = 0; i < m; i++) sum[i] = 0;
for (int i = 0; i < n; i++) sum[myRank[tp[i]]]++;
for (int i = 1; i < m; i++) sum[i] += sum[i - 1];
for (int i = n - 1; i >= 0; i--) SA[--sum[myRank[tp[i]]]] = tp[i];
//根据SA和rank数组重新计算rank数组
swap(myRank, tp);//交换后tp指向旧的rank数组
p = 1;
myRank[SA[0]] = 0;
for (int i = 1; i < n; i++) {
myRank[SA[i]] = cmp(tp, SA[i - 1], SA[i], len) ? p - 1 : p++;//注意判定rank[i]和rank[i-1]是否相等
}
if (p >= n) break;
m = p;//下次基数排序的最大值
}
//求height
int k = 0;
n--;
for (int i = 0; i <= n; i++) myRank[SA[i]] = i;
for (int i = 0; i < n; i++) {
if (k) k--;
int j = SA[myRank[i] - 1];
while (s[i + k] == s[j + k]) k++;
height[myRank[i]] = k;
}
}
/**
* _ooOoo_
* o8888888o
* 88" . "88
* (| -_- |)
* O\ = /O
* ___/`---'\____
* . ' \\| |// `.
* / \\||| : |||// \
* / _||||| -:- |||||- \
* | | \\\ - /// | |
* | \_| ''\---/'' | |
* \ .-\__ `-` ___/-. /
* ___`. .' /--.--\ `. . __
* ."" '< `.___\_<|>_/___.' >'"".
* | | : `- \`.;`\ _ /`;.`/ - ` : | |
* \ \ `-. \_ __\ /__ _/ .-` / /
* ======`-.____`-.___\_____/___.-`____.-'======
* `=---='
* .............................................
* 佛曰:bug泛滥,我已瘫痪!
*/
char str[MAXN];
/*
* 主席树求区间第 k 小,离线查询
* 先将变量保存至数组 a 中,点编号为 1 - n
* 将变量 tot 初始化为 0
* 然后调用 Init_hash 函数完成建树
* 之后便可以进行询问操作,返回第 k 小的值
* 重复数字会重复结算
*/
const int M = MAXN * 30; // 建树的数组大小,至少 30 倍
int n;
struct SegTree {
int mnn[MAXN << 2];
inline void PushUp(int rt) {
mnn[rt] = min(mnn[rt << 1], mnn[rt << 1 | 1]);
}
inline void Build(int l, int r, int rt) {
if (l == r) {
mnn[rt] = height[l];
return;
}
int mid = (l + r) >> 1;
Build(l, mid, rt << 1);
Build(mid + 1, r, rt << 1 | 1);
PushUp(rt);
}
inline int RQuery(int L, int R, int C, int l, int r, int rt) { //查找区间[L, R]内比C小的第一个
if (L <= l && r <= R) {
if (l == r) return mnn[rt] >= C ? n + 1 : l;
int mid = (l + r) >> 1;
if (mnn[rt << 1] >= C)
if (mnn[rt << 1 | 1] >= C) return n + 1;
else return RQuery(L, R, C, mid + 1, r, rt << 1 | 1);
return RQuery(L, R, C, l, mid, rt << 1);
}
int mid = (l + r) >> 1;
if (L > mid) { return RQuery(L, R, C, mid + 1, r, rt << 1 | 1); }
else { return min(RQuery(L, R, C, l, mid, rt << 1), RQuery(L, R, C, mid + 1, r, rt << 1 | 1)); }
}
inline int LQuery(int L, int R, int C, int l, int r, int rt) { //查找区间[L, R]内比C小的最后一个
if (L <= l && r <= R) {
if (l == r) return mnn[rt] >= C ? 0 : l;
int mid = (l + r) >> 1;
if (mnn[rt << 1 | 1] >= C)
if (mnn[rt << 1] >= C) return 0;
else return LQuery(L, R, C, l, mid, rt << 1);
return LQuery(L, R, C, mid + 1, r, rt << 1 | 1);
}
int mid = (l + r) >> 1;
if (R <= mid) { return LQuery(L, R, C, l, mid, rt << 1); }
else { return max(LQuery(L, R, C, l, mid, rt << 1), LQuery(L, R, C, mid + 1, r, rt << 1 | 1)); }
}
} XyhST;
int tmpl, tmpr, CC;
int index;
void xuyuehao() {
tmpl = 1, tmpr = n + 1;
if (index != 1)
tmpl = XyhST.LQuery(1, index, CC, 1, n, 1);
if (index != n)
tmpr = XyhST.RQuery(index + 1, n, CC, 1, n, 1);
tmpr -= 1;
if(tmpr < tmpl)
tmpr = tmpl;
}
void reset(int r) {
for (int i = 0; i < r + 3; i++) {
SA[i] = myRank[i] = height[i] = sum[i] = tp[i] = 0;
}
}
int main() {
#ifdef ACM_LOCAL
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
auto start_____ = clock();
#endif
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int tt;
cin >> tt;
while (tt--) {
int len, qc;
cin >> len >> qc;
cin >> str;
str[len] = 0;
len++;
n = len;
get_SA(str, len, CHAR_NUM);
n--;
per(i, 1, n + 1) { p[i] = i; }
sort(p + 1, p + 1 + n, [](int i, int j) { return SA[i] < SA[j]; });
per(i, 1, n + 1) { b[p[i]] = i; }
root[0] = 0;
sz = 0;
per(i, 1, n + 1) {
root[i] = root[i - 1];
ins(root[i], 1, n, b[i]);
}
len--;
int l, r, k;
XyhST.Build(1, n, 1);
while (qc--) {
cin >> l >> r >> k;
l--;
r--;
//名次
index = myRank[l];
if (len - SA[index] < r - l + 1)
cout << "-1" << endl;
else {
CC = r - l + 1;
xuyuehao();
if (tmpr - tmpl + 1 < k)
cout << "-1" << endl;
else
cout << query(root[tmpl - 1], root[tmpr], 1, n, k) << endl;
}
}
reset(n + 1);
}
#ifdef ACM_LOCAL
// debug(n);
#endif
#ifdef ACM_LOCAL
auto end_clock_for_debug = clock();
cerr << "Run Time: " << double(end_clock_for_debug - start_____) / CLOCKS_PER_SEC << "s" << endl;
#endif
return 0;
}
总结
比赛的时候灵光乍现,想到用后缀数组来解,跟两个队友debug了两个小时(因为用的板子太多了,简直用尽毕生所学),最后一发入魂简直爽呆。但是感觉自己的数据结构基础还是不够硬,比赛的时候主席树的板子都不会用了,嘤嘤嘤~,回头找找数据结构的题目再多练练。听说题解是后缀自动姬fail树上dfs序建主席树做的,听上去感觉比较复杂,回头会去尝试一下这种解法,但我还是觉得后缀数组的解法更加简单,嘻嘻嘻。