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. koa源代码解析

    koa不愧为小而美,主要代码很少.简单来说,1,koa封装了node的http.createServer((req,res)=>{})的入参req,res到ctx同名属性(一个自定义对象)中,并 ...

  2. 防止react-re-render: Why Suspense and how ?

    近期内部项目基础项目依赖升级,之前使用的路由缓存不再适用,需要一个适配方案.而在此过程中react re-render算是困扰了笔者很久.后来通过多方资料查找使用了freeze解决了此问题.本文主要论 ...

  3. Vulnhub 靶场 BEELZEBUB: 1

    Vulnhub 靶场 BEELZEBUB: 1 前期准备 靶机地址:https://www.vulnhub.com/entry/beelzebub-1,742/ kali攻击机ip:192.168.1 ...

  4. signature

    signature可以翻译成基调.特征标记.签名.

  5. SAP管理员SAP*和DDIC被锁定后如何解锁或重置密码

    SAP*初始化密码是06071992或passDDIC默认密码为19920706 环境信息:win server2003,SQL Server2008 R2 账号信息存在于数据库usr02表中,1.删 ...

  6. 离线谷歌地图API的开发笔记(二)

    一.地图引擎介绍 离线地图引擎运行在WINDOWS平台上,底层由Visual c++语言开发,编译为OCX插件方式.占用文件少,便于二次开发的快速安装部署. 具有专业地图的基础操作功能:地图放大.缩小 ...

  7. pat乙级1012数字分类

    #include<stdio.h> #include<stdlib.h> #include<string.h> #include<math.h> int ...

  8. 在vite中怎么批量注册组件

    1. 在webpack中使用require来获取组件 / 参数:1. 目录 2. 是否加载子目录 3. 加载的正则匹配 //匹配当前文件夹下的所有.vue文件 注册全局组件 const importF ...

  9. h5项目

    h5项目,用vue3,用vite搭建就好,是一个新的项目. 接口还在开发,可以用mock模拟. 现有信息:接口url,ui-url,原型url(各部分的交互关系)

  10. 运用python中装饰器方法来解决工作中为原有代码添加功能问题

          Python  装饰器 在实际的工作中,经常碰到领导或产品经理会提出很多甚至(变态)的产品要求,作为python开发,好不容易完成领导的需求,做出一个产品,并且经过测试成功上线.突然有一天 ...