K-th occurrence (后缀自动机上合并权值线段树+树上倍增)

 

You are given a string SSS consisting of only lowercase english letters and some queries.

For each query (l,r,k)(l,r,k)(l,r,k), please output the starting position of the k-th occurence of the substring SlSl+1⋯SrS_lS_{l+1}\cdots S_rSl​Sl+1​⋯Sr​ in SSS.

Input

The first line contains an integer T(1≤T≤20)T(1≤T≤20)T(1≤T≤20), denoting the number of test cases.

The first line of each test case contains two integer N(1≤N≤105),Q(1≤Q≤105)N(1≤N≤10^5),Q(1≤Q≤10^5)N(1≤N≤105),Q(1≤Q≤105), denoting the length of SSS and the number of queries.

The second line of each test case contains a string S(∣S∣=N)S(|S|=N)S(∣S∣=N) consisting of only lowercase english letters. Then QQQ lines follow, each line contains three integer l,r(1≤l≤r≤N)l,r(1≤l≤r≤N)l,r(1≤l≤r≤N) and k(1≤k≤N)k(1≤k≤N)k(1≤k≤N), denoting a query. There are at most 5 testcases which NNN is greater than 10310^3103.

Output

For each query, output the starting position of the k-th occurence of the given substring. If such position don’t exists, output -1 instead.

Sample Input

2

12 6

aaabaabaaaab

3 3 4

2 3 2

7 8 3

3 4 2

1 4 2

8 12 1

1 1

a

1 1 1

Sample Output

5

2

-1

6

9

8

1

hdu6704

题目大意

给出一个字符串S(∣S∣≤105)S(|S|\leq 10^5)S(∣S∣≤105),然后有Q(Q≤105)Q(Q\leq 10^5)Q(Q≤105)个询问,每个询问包含三个正整数L,R,K(1≤L≤R≤∣S∣,1≤K≤∣S∣)L,R,K(1\leq L\leq R\leq |S|,1\leq K\leq |S|)L,R,K(1≤L≤R≤∣S∣,1≤K≤∣S∣),代表询问SLSL+1⋯SRS_LS_{L+1}\cdots S_RSL​SL+1​⋯SR​这个SSS的子串在SSS中第KKK次出现的位置是哪里,如果SSS的这个子串出现次数小于KKK,就输出-1。

题目总览

首先,这道题要求S的某个子串的出现位置,毫无疑问就是求这个子串在后缀自动机上的endpos。而endpos是对应于自动机上的状态节点的,因此我们首先要找到S所求的子串对应的状态节点。接着,我们要求所有状态节点的endpos集合,并支持查询第KKK小,这样就能建立子串-状态节点-endpos集合的关系,从而能求出相应子串的第KKK次出现的位置了。

求子串对应的状态节点

首先,我们定义u[i]u[i]u[i]为SSS从1到iii的子串对应于自动机上的状态节点编号。在构造自动机的过程中,我们是从自动机的初始状态出发的,因此u数组可以边构造边更新。

for (int i = 0; i < N; ++i) {
//自动机插入一个字符
sam.insert(S[i] - 'a');
//等于该字符插入时的状态,编号从1开始
u[i + 1] = sam.cur;
}

现在,我们知道了从1开始的所有子串对应的状态节点了,那么怎样求从l到r对应的状态节点呢?显然l到r是1到r的后缀,那么根据后缀自动机的性质,l到r要么是1到r对应的节点,要么是其后缀连接树上的父节点。设p为l到r的对应状态,那么p满足maxlen(link(p))<(r−l+1)≤maxlen(p)maxlen(link(p))<(r-l+1)\leq maxlen(p)maxlen(link(p))<(r−l+1)≤maxlen(p),即1到r的满足(r−l+1)≤maxlen(p)(r-l+1)\leq maxlen(p)(r−l+1)≤maxlen(p)的第一个父节点(或它本身)。这样的话,我们只要根据后缀连接边来建一棵树,然后不断往父节点跳知道满足条件即可。但是,这样的话时间复杂度就是O(∣S∣)O(|S|)O(∣S∣),而对应于QQQ个询问显然会超时。不过对于树上跳,我们可以采用树上倍增的方式进行优化。

Depth[i]Depth[i]Depth[i]表示第i个节点的深度

Father[i][j]Father[i][j]Father[i][j]表示第i个节点的第2j2^j2j级祖先的编号

Log[i]Log[i]Log[i]表示log2ilog_2ilog2​i向上取整

EndPosToTree[i]EndPosToTree[i]EndPosToTree[i]表示状态节点i对应于线段树上相应的根的编号

class {
int Depth[2 * MAXN];
int Father[2 * MAXN][22];
int Log2[2 * MAXN];
//树上DFS求Depth和Father数组
void DFS(int root, int father = 0) {
Depth[root] = Depth[father] + 1;
Father[root][0] = father;
//一个节点的2^i祖先等于他的2^i-1祖先的2^i-1祖先
for (int i = 1; i <= Log2[Depth[root]]; ++i) {
Father[root][i] = Father[Father[root][i - 1]][i - 1];
}
for (const auto& Next : Edges[root]) {
if (Next != father) {
DFS(Next, root);
}
}
}
public:
//记录树结构
vector<int> Edges[2 * MAXN];
//初始化log2,优化时间
void InitLog2() {
for (int i = 1; i < 2 * MAXN; ++i) {
Log2[i] = Log2[i - 1] + (1 << Log2[i - 1] == i);
}
}
void AddEdge(const int& x, const int& y) {
this->Edges[x].emplace_back(y);
this->Edges[y].emplace_back(x);
}
//树上倍增
int getAncestor(int Point, bool (*Condition)(const int&))const {
//从最高级别的祖先起跳
for (int k = Log2[Depth[Point]]; k >= 0; --k) {
//满足条件就跳
if (Condition(Father[Point][k])) {
Point = Father[Point][k];
}
}
return Point;
}
void Clear() {
memset(Depth, 0x0, sizeof(Depth));
memset(Father, 0x0, sizeof(Father));
for (int i = 0; i <= 2 * N + 1; ++i) {
Edges[i].clear();
}
}
void Init() {
Clear();
for (int i = 1; i < sam.size; ++i) {
const int& Link = sam.node[i].link;
MultiplyOnTree.AddEdge(i, Link);
}
DFS(0);
}
}MultiplyOnTree;

求状态节点的endpos集合并维护第k小

根据后缀自动机的性质,一个节点的endpos集合等于其所有后缀连接树上子节点的endpos集合的交集,即父节点是子节点的后缀。而叶节点的endpos集合就是自动机构造时插入的字符的位置,大小为1。但是在寻找第k小时,如果O(N)O(N)O(N)遍历,对于QQQ个询问显然后超时。不过我们可以用合并权值线段树的方法来维护第k小。

Tree[i]Tree[i]Tree[i]表示线段树上的i个节点所管辖的范围有多少个数

LeftSon[i],RightSon[i]LeftSon[i],RightSon[i]LeftSon[i],RightSon[i]表示第i个节点的左右儿子的编号

Radix,OrderRadix,OrderRadix,Order用于计数排序

class SegmentTree {
friend class SAM;
int Tot;
int
Tree[50 * MAXN],
LeftSon[50 * MAXN],
RightSon[50 * MAXN],
Radix[MAXN],
Order[2 * MAXN];
public:
void Clear() {
Tot = 0;
memset(Tree, 0x0, sizeof(Tree));
memset(Radix, 0x0, sizeof(Radix));
}
SegmentTree() = default;
//合并两棵权值线段树
int Merge(int Left, int Right) {
if (!Left || !Right) {
return Left | Right;
}
int NewRoot = ++Tot;
LeftSon[NewRoot] = Merge(LeftSon[Left], LeftSon[Right]);
RightSon[NewRoot] = Merge(RightSon[Left], RightSon[Right]);
this->Tree[NewRoot] = this->Tree[LeftSon[NewRoot]] + this->Tree[RightSon[NewRoot]];
return NewRoot;
}
//单点更新
void Update(int Loc, int& root, int Left = 1, int Right = N) {
//如果是新加入的根节点,就为它分配一个新节点编号
if (!root) {
root = ++Tot;
Tree[root] = LeftSon[root] = RightSon[root] = 0;
}
//找到了对应的位置,数量++
if (Left == Right) {
++Tree[root];
return;
}
int&& mid = (Right + Left) >> 1;
if (Loc <= mid) {
Update(Loc, LeftSon[root], Left, mid);
}
else {
Update(Loc, RightSon[root], mid + 1, Right);
}
//UpGrade
this->Tree[root] = this->Tree[LeftSon[root]] + this->Tree[RightSon[root]];
}
//寻找第k小
int getK_th(int Left, int Right, int K, int root) {
if (Left == Right) {
return Left;
}
int&& mid = (Left + Right) >> 1;
const int
& LeftSonValue = this->Tree[this->LeftSon[root]],
& RightSonValue = this->Tree[this->RightSon[root]];
//如果左边的数量大于k,说明在左边
if (LeftSonValue >= K) {
return getK_th(Left, mid, K, LeftSon[root]);
}
//如果右边的数量大于k-左边的数量,说明在右边
else if (RightSonValue >= K - LeftSonValue) {
return getK_th(mid + 1, Right, K - LeftSonValue, RightSon[root]);
}
//反则就不存在
else {
return -1;
}
}
int getAns(int K, int P, int Left = 1, int Right = N) {
int&& Ans = this->getK_th(Left, Right, K, EndPosToTree[P]);
//子串右端位置转左端位置
return (Ans == -1 ? -1 : Ans - (R - L));
}
//计数排序
void CountingSort() {
for (int i = 0; i < sam.size; ++i) {
++Radix[sam.node[i].len];
}
for (int i = 1; i <= sam.node[sam.cur].len; ++i) {
Radix[i] += Radix[i - 1];
}
for (int i = 0; i < sam.size; ++i) {
Order[Radix[sam.node[i].len]--] = i;
}
}
void Init() {
CountingSort();
//按maxlen(p)从小到大更新,就能保父节点后于子节点更新
for (int i = sam.size; i > 1; --i) {
const int& Son = Order[i];
const int& Father = sam.node[Son].link;
EndPosToTree[Father] = SegmentTrees.Merge(EndPosToTree[Father], EndPosToTree[Son]);
}
}
}SegmentTrees;

AC代码

#include<iostream>
#include<cstring>
#include<vector>
using namespace std; const int MAXN = 1e5 + 5; int N, L, R, K, Q;
char S[MAXN];
int u[MAXN];//1...r
int StateToTree[2 * MAXN]; struct SAM {
int size, last, cur;
struct Node {
int len = 0, link = 0;
int next[26];
void clear() {
len = link = 0;
memset(next, 0, sizeof(next));
}
} node[MAXN * 2];
void init() {
for (int i = 0; i < size; i++) {
node[i].clear();
}
node[0].link = -1;
size = 1;
last = 0;
}
void insert(char x) {
int ch = x;
cur = size++;
node[cur].len = node[last].len + 1;
int p = last;
while (p != -1 && !node[p].next[ch]) {
node[p].next[ch] = cur;
p = node[p].link;
}
if (p == -1) {
node[cur].link = 0;
}
else {
int q = node[p].next[ch];
if (node[p].len + 1 == node[q].len) {
node[cur].link = q;
}
else {
int clone = size++;
StateToTree[clone] = 0;
node[clone] = node[q];
node[clone].len = node[p].len + 1;
while (p != -1 && node[p].next[ch] == q) {
node[p].next[ch] = clone;
p = node[p].link;
}
node[q].link = node[cur].link = clone;
}
}
last = cur;
}
}sam;
class {
int Depth[2 * MAXN];
int Father[2 * MAXN][22];
int Log2[2 * MAXN];
void DFS(int root, int father = 0) {
Depth[root] = Depth[father] + 1;
Father[root][0] = father;
for (int i = 1; i <= Log2[Depth[root]]; ++i) {
Father[root][i] = Father[Father[root][i - 1]][i - 1];
}
for (const auto& Next : Edges[root]) {
if (Next != father) {
DFS(Next, root);
}
}
}
public:
vector<int> Edges[2 * MAXN];
void InitLog2() {
for (int i = 1; i < 2 * MAXN; ++i) {
Log2[i] = Log2[i - 1] + (1 << Log2[i - 1] == i);
}
}
void AddEdge(const int& x, const int& y) {
this->Edges[x].emplace_back(y);
this->Edges[y].emplace_back(x);
}
void Start() {
DFS(0);
}
int getAncestor(int Point, bool (*Condition)(const int&))const {
for (int k = Log2[Depth[Point]]; k >= 0; --k) {
if (Condition(Father[Point][k])) {
Point = Father[Point][k];
}
}
return Point;
}
void Clear() {
memset(Depth, 0x0, sizeof(Depth));
memset(Father, 0x0, sizeof(Father));
for (int i = 0; i <= 2 * N + 1; ++i) {
Edges[i].clear();
}
}
void Init() {
Clear();
for (int i = 1; i < sam.size; ++i) {
const int& Link = sam.node[i].link;
MultiplyOnTree.AddEdge(i, Link);
}
Start();
}
}MultiplyOnTree;
class SegmentTree {
friend class SAM;
int Tot;
int
Tree[50 * MAXN],
LeftSon[50 * MAXN],
RightSon[50 * MAXN],
Radix[MAXN],
Order[2 * MAXN];
public:
void Clear() {
Tot = 0;
memset(Tree, 0x0, sizeof(Tree));
memset(Radix, 0x0, sizeof(Radix));
}
SegmentTree() = default;
int Merge(int Left, int Right) {
if (!Left || !Right) {
return Left | Right;
}
int NewRoot = ++Tot;
LeftSon[NewRoot] = Merge(LeftSon[Left], LeftSon[Right]);
RightSon[NewRoot] = Merge(RightSon[Left], RightSon[Right]);
this->Tree[NewRoot] = this->Tree[LeftSon[NewRoot]] + this->Tree[RightSon[NewRoot]];
return NewRoot;
}
void Update(int Loc, int& root, int Left = 1, int Right = N) {
if (!root) {
root = ++Tot;
Tree[root] = LeftSon[root] = RightSon[root] = 0;
}
if (Left == Right) {
++Tree[root];
return;
}
int&& mid = (Right + Left) >> 1;
if (Loc <= mid) {
Update(Loc, LeftSon[root], Left, mid);
}
else {
Update(Loc, RightSon[root], mid + 1, Right);
}
this->Tree[root] = this->Tree[LeftSon[root]] + this->Tree[RightSon[root]];
}
int getK_th(int Left, int Right, int K, int root) {
if (Left == Right) {
return Left;
}
int&& mid = (Left + Right) >> 1;
const int
& LeftSonValue = this->Tree[this->LeftSon[root]],
& RightSonValue = this->Tree[this->RightSon[root]];
if (LeftSonValue >= K) {
return getK_th(Left, mid, K, LeftSon[root]);
}
else if (RightSonValue >= K - LeftSonValue) {
return getK_th(mid + 1, Right, K - LeftSonValue, RightSon[root]);
}
else {
return -1;
}
}
int getAns(int K, int P, int Left = 1, int Right = N) {
int&& Ans = this->getK_th(Left, Right, K, StateToTree[P]);
return (Ans == -1 ? -1 : Ans - (R - L));
}
void CountingSort() {
for (int i = 0; i < sam.size; ++i) {
++Radix[sam.node[i].len];
}
for (int i = 1; i <= sam.node[sam.cur].len; ++i) {
Radix[i] += Radix[i - 1];
}
for (int i = 0; i < sam.size; ++i) {
Order[Radix[sam.node[i].len]--] = i;
}
}
void Init() {
CountingSort();
for (int i = sam.size; i > 1; --i) {
const int& Son = Order[i];
const int& Father = sam.node[Son].link;
StateToTree[Father] = SegmentTrees.Merge(StateToTree[Father], StateToTree[Son]);
}
}
}SegmentTrees; void Init() {
sam.init();
SegmentTrees.Clear();
}
void Input() {
scanf("%d%d", &N, &Q);
scanf("%s", S);
Init();
for (int i = 0; i < N; ++i) {
sam.insert(S[i] - 'a');
StateToTree[sam.cur] = 0;
SegmentTrees.Update(i + 1, StateToTree[sam.cur]);
u[i + 1] = sam.cur;
}
} bool Condition(const int& Point) {
return R - L + 1 <= sam.node[Point].len;
}
int main() {
int T;
scanf("%d", &T);
MultiplyOnTree.InitLog2();
while (T--) {
Input();
MultiplyOnTree.Init();
SegmentTrees.Init();
while (Q--) {
scanf("%d%d%d", &L, &R, &K);
const int&& P = MultiplyOnTree.getAncestor(u[R], Condition);
printf("%d\n", SegmentTrees.getAns(K, P));
}
}
return 0;
}

K-th occurrence (后缀自动机上合并权值线段树+树上倍增)的更多相关文章

  1. [bzoj 2733]启发式合并权值线段树

    题目链接:http://www.lydsy.com/JudgeOnline/problem.php?id=2733 平衡树待学习.从一个博客学到了合并权值线段树的姿势:http://blog.csdn ...

  2. 【bzoj2161】布娃娃 权值线段树

    题目描述 小时候的雨荨非常听话,是父母眼中的好孩子.在学校是老师的左右手,同学的好榜样.后来她成为艾利斯顿第二代考神,这和小时候培养的良好素质是分不开的.雨荨的妈妈也为有这么一个懂事的女儿感到高兴.一 ...

  3. HDU-6704 K-th occurrence (后缀自动机father树上倍增建权值线段树合并)

    layout: post title: HDU-6704 K-th occurrence (后缀自动机father树上倍增建权值线段树合并) author: "luowentaoaa&quo ...

  4. B20J_2733_[HNOI2012]永无乡_权值线段树合并

    B20J_2733_[HNOI2012]永无乡_权值线段树合并 Description:n座岛,编号从1到n,每座岛都有自己的独一无二的重要度,按照重要度可以将这n座岛排名,名次用1到 n来表示.某些 ...

  5. luogu3224 永无乡(动态开点,权值线段树合并)

    luogu3224 永无乡(动态开点,权值线段树合并) 永无乡包含 n 座岛,编号从 1 到 n ,每座岛都有自己的独一无二的重要度,按照重要度可以将这 n 座岛排名,名次用 1 到 n 来表示.某些 ...

  6. 【bzoj3065】带插入区间K小值 替罪羊树套权值线段树

    题目描述 从前有n只跳蚤排成一行做早操,每只跳蚤都有自己的一个弹跳力a[i].跳蚤国王看着这些跳蚤国欣欣向荣的情景,感到非常高兴.这时跳蚤国王决定理性愉悦一下,查询区间k小值.他每次向它的随从伏特提出 ...

  7. 【bzoj4399】魔法少女LJJ 并查集+权值线段树合并

    题目描述 在森林中见过会动的树,在沙漠中见过会动的仙人掌过后,魔法少女LJJ已经觉得自己见过世界上的所有稀奇古怪的事情了LJJ感叹道“这里真是个迷人的绿色世界,空气清新.淡雅,到处散发着醉人的奶浆味: ...

  8. [bzoj2733][HNOI2012]永无乡_权值线段树_线段树合并

    永无乡 bzoj-2733 HNOI-2012 题目大意:题目链接. 注释:略. 想法: 它的查询操作非常友善,就是一个联通块内的$k$小值. 故此我们可以考虑每个联通块建一棵权值线段树. 这样的话每 ...

  9. BZOJ2733/LG3324 「HNOI2014」永无乡 权值线段树合并

    问题描述 BZOJ2733 LG3224 题解 对于每个结点建立一棵权值线段树. 查询操作就去查询第 \(k\) 大,合并操作就合并两颗权值线段树. 并查集维护连通性. 同时 STO hkk,zcr, ...

  10. [BZOJ 3110] [luogu 3332] [ZJOI 2013]k大数查询(权值线段树套线段树)

    [BZOJ 3110] [luogu 3332] [ZJOI 2013]k大数查询(权值线段树套线段树) 题面 原题面有点歧义,不过从样例可以看出来真正的意思 有n个位置,每个位置可以看做一个集合. ...

随机推荐

  1. Jenkins拉取GitHub上代码

    1.github 生成 Personal Access Token 2.github 设置 GitHub webhooks (具体需要持续集成的项目),新建或者设置现有项目的 webhooks 选项, ...

  2. js简单的图片上传

    <input id="file" type="file" name="name" @change="aaa"> ...

  3. k8s_使用k8s部署博客系统-PV PVC(二)

    PV和PVC PV(PersistentVolume)在声明的时候需要指定大小和续写模式:["ReadWriteMany","ReadWriteOnce",&q ...

  4. SCI论文写作技巧-introduction和related works

    introduction怎么写 a)背景介绍,现状(介绍别人研究),存在问题,怎样解决,我的做法,有何亮点 b)研究背景和重要性.引出该领域科研空白.点题-指出本文的研究课题.概述文章的核心方法论和主 ...

  5. C# 笔记--Sendkeys winform窗体控件回车及全选

    SendKeys.Send() 向活动应用程序发送击键 SendKeys.SendWait() 向活动应用程序发送给定的键,然后等待消息被处理 这两个方法可以发送组合键,需要注意的是字母按键应为小写. ...

  6. What is REST and Restful?

    什么是rest 和 restful? 提出rest的作者,目的:符合框架原理的情况下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强,性能好,适宜通讯的架构. Fielding将他对互联网 ...

  7. unity task

    https://blog.csdn.net/weixin_43405845/article/details/105028291

  8. 【C++复习】5.7 多文件结构与编译预处理命令

    1.C++项目结构 C++程序的一般组织架构 类声明文件(.h文件) 类实现文件(.cpp文件) 类的使用文件(main()所在的.cpp文件) 用工程组合各文件 2.编译链接 编译链接过程 3.外部 ...

  9. plsql和instantclient版本都对,依然不能初始化oci.dll解决办法

    这里写到 "初始化错误,不能初始化 oci.dll, 请确认你安装的是64位的Oracle客户端 " ,这个描述还是非常的到位啊,我一检查,果然下载的客户端是32位的,在确保自己的 ...

  10. Mysql数据库基础第三章:DML语言

    Mysql数据库基础系列 软件下载地址 提取码:7v7u 数据下载地址 提取码:e6p9 mysql数据库基础第一章:(一)数据库基本概念 mysql数据库基础第一章:(二)mysql环境搭建 mys ...