这题我写了一天后交了一发就过了我好兴奋啊啊啊啊啊啊

题目

洛谷 4482

分析

这题明明可以在线做的,为什么我见到的所有题解都是离线啊 …… 什么时候有机会出一个在线版本坑人。

题目的要求可以转化为求出一个最大的 \(i(i<r)\) 满足 \(i-\mathrm{lcs}(i,r)<l\) ,其中 \(\mathrm{lcs}(i,r)\) 表示 前缀 \(i\) 和 前缀 \(r\) 的最长公共后缀。答案就是 \(i-l+1\) 。

在 SAM 上考虑这个问题。fa 树、max、\(R_u\) 等的定义见 【知识总结】后缀自动机的构建 。两个前缀的 lcs 就是它们对应的点在 fa 树上的 lca 的最长长度。下面来随意证明一下:

设两个 前缀 对应的结点为 \(u\) 和 \(v\) 。根据 fa 树的定义,\(u\) 到根的路径上的所有点都是 \(R_u\) 的子集并且元素数量递减(即 \(u\) 的长度递减的后缀),对于 \(v\) 也是如此。那么 \(R_u\cap R_v\) 就是最大的既是 \(R_u\) 的子集又是 \(R_v\) 的子集的集合(即 lcs 对应的集合),也就是 \(R_{\mathrm{lca}(u,v)}\) 。既然是两个前缀,那么它们一定是等价字符串中最长的。因此如果 LCA 就是 \(u\) 或者 \(v\) 是合法的。否则,\(\mathrm{lca}(u,v)\) 的最长长度一定小于 \(u\) 或 \(v\) 的长度,因此也是合法的。

以下 \(i\) 和 \(r\) 均指对应前缀所在的点,树均指 fa 树。\(m_u\) 表示结点 \(u\) 的最长长度(即 \(u\) 的 max)。

根据以上结论,我们有了一个暴力的做法:枚举 \(r\) 的祖先 \(p\) 作为 \(lca(i,r)\) ,这样最长公共后缀的长度就是固定的 \(m_p\) 了。在 \(p\) 的子树中找到最大的 \(i\) 满足 \(i<r\) 且 \(i-m_p<l\) 即可。虽然会重复计算,但如果 \(p\) 不是 \(\mathrm{lca}(i,r)\) 而是 \(\mathrm{lca}(i,r)\) 的祖先,那么 \(i-m_p\) 大于 \(i-m_{\mathrm{lca}(i,r)}\) ,相当于限制反而更严格了。如果此时 \(i\) 满足条件,那么 \(i\) 一定是合法的。在每个点上建立一棵线段树来维护它的所有儿子,位置 \(i\) 的权值是 \(i-m_p\) ,然后线段树上二分出最大的 \(i<r\) 且权值小于 \(l\) 即可。(当然,现在也可以不引入「权值」,然后直接查找最大的小于 \(\min(r, l+m_p)\) 的点。以后再说为什么一定要引入「权值」)。

这种做法有两个瓶颈:一个是要枚举所有祖先,每次询问复杂度是 \(O(n)\) ;另一个是需要在线段树中共计插入 \(O(n^2)\) 个点。以下分别来解决。

为了优化枚举祖先的时间,考虑树链剖分(好像这里通常叫「链分治」),每次直接爬到重链顶端。设进入重链的第一个点为 \(p\) ,重链顶端为 \(t\) 。怎么一次性在 \(p\) 到 \(t\) 路径上的所有点的线段树上查询呢?由于我们已经很机智地把和路径上具体的点 \(u\) 有关的信息 \(i-m_u\) 作为了权值而非下标,所以可以直接把这些点的线段树全部合并到 \(p\) 的线段树上,然后直接查 \(p\) 的线段树就相当于查完了从 \(p\) 到 \(t\) 的所有线段树。具体地,沿着重链从上往下合并,类似于前缀和。因为可能从任意一个有轻儿子的点进入重链,所以合并的时候要复制一份,不能修改原树。这样复杂度仍然是对的。具体合并方法和复杂度证明见【知识总结】线段树合并及其复杂度证明

下面来解决第二个瓶颈。剖都剖了,应该能想到一个性质:所有点的轻子树的大小之和是 \(O(n\log n)\) ,因为一个点到根的路径上只有 \(O(\log n)\) 条轻边。于是,我们不把整棵子树都插进线段树,而是只插轻子树和这个点本身。考虑如果从 \(p\) 进入一条重链,哪些本该考虑到的点(即和 \(r\) 的 lca 在这条重链上的点)没有考虑到呢?通过画图可以轻易发现,这些点一定都在 \(p\) 的子树中。那么直接在 \(p\) 的子树中求最大的 \(i\) 满足 \(i<\min(r,l+m_p)\) 。这可以通过线段树合并和线段树上二分(这里的线段树和上面说的线段树没有关系)实现。

代码

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cctype>
#include <vector>
using namespace std; namespace zyt
{
const int N = 2e5 + 10, CH = 26, B = 20, INF = 0x3f3f3f3f;
int n, pos[N], id[N << 1];
vector<int> g[N << 1];
char str[N];
namespace Tree_Chain_Cut
{
int size[N << 1], fa[N << 1], son[N << 1], top[N << 1];
void dfs1(const int u, const int f)
{
size[u] = 1;
fa[u] = f;
for (vector<int>::iterator it = g[u].begin(); it != g[u].end(); it++)
{
int v = *it;
dfs1(v, u);
size[u] += size[v];
if (size[v] > size[son[u]])
son[u] = v;
}
}
void dfs2(const int u, const int t)
{
top[u] = t;
if (son[u])
dfs2(son[u], t);
for (vector<int>::iterator it = g[u].begin(); it != g[u].end(); it++)
{
int v = *it;
if (v == son[u])
continue;
dfs2(v, v);
}
}
}
class Segment_Tree
{
private:
struct node
{
int min, lt, rt;
}*tree;
int cnt;
public:
int *rot;
Segment_Tree(const int n = 0)
{
cnt = 0;
rot = new int[n + 1];
memset(rot, 0, sizeof(int[n + 1]));
tree = new node[n * B * 2];
}
void change(int &rot, const int lt, const int rt, const int pos, const int x)
{
if (!rot)
rot = ++cnt, tree[rot].min = INF, tree[rot].lt = tree[rot].rt = 0;
tree[rot].min = min(tree[rot].min, x);
if (lt == rt)
return;
int mid = (lt + rt) >> 1;
if (pos <= mid)
change(tree[rot].lt, lt, mid, pos, x);
else
change(tree[rot].rt, mid + 1, rt, pos, x);
}
int merge(const int x, const int y)
{
if (!x || !y)
return x + y;
int a = ++cnt;
tree[a].min = min(tree[x].min, tree[y].min);
tree[a].lt = merge(tree[x].lt, tree[y].lt);
tree[a].rt = merge(tree[x].rt, tree[y].rt);
return a;
}
int query(const int rot, const int lt, const int rt, const int r, const int lim)
{
if (!rot)
return -1;
if (lt == rt)
return tree[rot].min < lim ? lt : -1;
int mid = (lt + rt) >> 1;
if (mid + 1 < r && tree[rot].rt && tree[tree[rot].rt].min < lim)
{
int tmp = query(tree[rot].rt, mid + 1, rt, r, lim);
if (~tmp)
return tmp;
}
return query(tree[rot].lt, lt, mid, r, lim);
}
}t1, t2;
namespace Suffix_Auto_Chicken
{
int ctoi(const char c)
{
return c - 'a';
}
struct node
{
int fa, max, s[CH];
}tree[N << 1];
int cnt, last;
void init()
{
cnt = last = 1;
}
void insert(const char c, const int id)
{
int p = last, np = ++cnt, x = ctoi(c);
tree[np].max = tree[p].max + 1;
while (p && !tree[p].s[x])
tree[p].s[x] = np, p = tree[p].fa;
if (!p)
tree[np].fa = 1;
else
{
int q = tree[p].s[x];
if (tree[q].max == tree[p].max + 1)
tree[np].fa = q;
else
{
int nq = ++cnt;
memcpy(tree[nq].s, tree[q].s, sizeof(int[CH]));
tree[nq].max = tree[p].max + 1;
tree[nq].fa = tree[q].fa;
tree[q].fa = tree[np].fa = nq;
while (p && tree[p].s[x] == q)
tree[p].s[x] = nq, p = tree[p].fa;
}
}
pos[id] = last = np;
}
void build()
{
static int count[N], buf[N << 1];
init();
for (int i = 0; i < n; i++)
insert(str[i], i), t1.change(t1.rot[pos[i]], 0, n - 1, i, i);
memset(id, -1, sizeof(int[cnt + 1]));
for (int i = 0; i < n; i++)
id[pos[i]] = i;
memset(count, 0, sizeof(int[n + 1]));
for (int i = 1; i <= cnt; i++)
++count[tree[i].max];
for (int i = 1; i <= n; i++)
count[i] += count[i - 1];
for (int i = 1; i <= cnt; i++)
buf[count[tree[i].max]--] = i;
for (int i = cnt; i > 1; i--)
{
t1.rot[tree[buf[i]].fa] = t1.merge(t1.rot[tree[buf[i]].fa], t1.rot[buf[i]]);
g[tree[buf[i]].fa].push_back(buf[i]);
//fprintf(stderr, "%d %d\n", tree[buf[i]].fa, buf[i]);
}
}
}
void dfs(const int u, const int r, const int d)
{
if (~id[u])
t2.change(t2.rot[r], 0, n - 1, id[u], id[u] - d);
for (vector<int>::iterator it = g[u].begin(); it != g[u].end(); it++)
dfs(*it, r, d);
}
int work()
{
using namespace Tree_Chain_Cut;
using namespace Suffix_Auto_Chicken;
scanf("%s", str);
n = strlen(str);
t1 = Segment_Tree(n * 3), t2 = Segment_Tree(n * 8);
build(), dfs1(1, 0), dfs2(1, 1);
for (int i = 1; i <= cnt; i++)
{
if (~id[i])
t2.change(t2.rot[i], 0, n - 1, id[i], id[i] - tree[i].max);
for (vector<int>::iterator it = g[i].begin(); it != g[i].end(); it++)
if (*it != son[i])
dfs(*it, i, tree[i].max);
}
for (int i = 1; i <= cnt; i++)
if (i == top[i])
{
int tmp = son[i];
while (tmp)
t2.rot[tmp] = t2.merge(t2.rot[tmp], t2.rot[fa[tmp]]), tmp = son[tmp];
}
int q;
scanf("%d", &q);
while (q--)
{
int l, r;
scanf("%d%d", &l, &r);
--l, --r;
int p = pos[r], ans = 0;
while (p)
{
ans = max(ans, t1.query(t1.rot[p], 0, n - 1, r, l + tree[p].max));
ans = max(ans, t2.query(t2.rot[p], 0, n - 1, r, l));
p = fa[top[p]];
}
printf("%d\n", max(0, ans - l + 1));
}
return 0;
}
}
int main()
{
return zyt::work();
}

【洛谷4482】Border的四种求法(后缀自动机_线段树合并_链分治)的更多相关文章

  1. [BJWC2018]Border 的四种求法(后缀自动机+链分治+线段树合并)

    题目描述 给一个小写字母字符串 S ,q 次询问每次给出 l,r ,求 s[l..r] 的 Border . Border: 对于给定的串 s ,最大的 i 使得 s[1..i] = s[|s|-i+ ...

  2. 「BZOJ2733」「洛谷3224」「HNOI2012」永无乡【线段树合并】

    题目链接 [洛谷] 题解 很明显是要用线段树合并的. 对于当前的每一个连通块都建立一个权值线段树. 权值线段树处理操作中的\(k\)大的问题. 如果需要合并,那么就线段树暴力合并,时间复杂度是\(nl ...

  3. 【洛谷4770】 [NOI2018]你的名字(SAM,线段树合并)

    传送门 洛谷 Solution 做过的比较玄学的后缀自动机. 果然就像\(Tham\)所讲,后缀自动机这种东西考场考了不可能做的出来的... 考虑如果\(l=1,r=|S|\)的怎么做? 直接建后缀自 ...

  4. 洛谷P4770 [NOI2018]你的名字 [后缀自动机,线段树合并]

    传送门 思路 按照套路,直接上后缀自动机. 部分分:\(l=1,r=|S|\) 首先把\(S\)和\(T\)的后缀自动机都建出来. 考虑枚举\(T\)中的右端点\(r\),查询以\(r\)结尾的串最长 ...

  5. luogu P4482 [BJWC2018] Border 的四种求法 - 后缀数组

    题目传送门 传送门 题目大意 区间border. 照着金策讲稿做. Code /** * luogu * Problem#P4482 * Accepted * Time: 8264ms * Memor ...

  6. 「BJWC2018」Border 的四种求法

    「BJWC2018」Border 的四种求法 题目描述 给一个小写字母字符串 \(S\) ,\(q\) 次询问每次给出 \(l,r\) ,求 \(s[l..r]\) 的 Border . \(1 \l ...

  7. [BJWC2018]Border 的四种求法

    description luogu 给一个小写字母字符串\(S\),\(q\)次询问每次给出\(l,r\),求\(s[l..r]\)的\(Border\). solution 我们考虑转化题面:给定\ ...

  8. 【洛谷4770/UOJ395】[NOI2018]你的名字(后缀数组_线段树合并)

    题目: 洛谷4770 UOJ395 分析: 一个很好的SAM应用题-- 一句话题意:给定一个字符串\(S\).每次询问给定字符串\(T\)和两个整数\(l\).\(r\),求\(T\)有多少个本质不同 ...

  9. luogu P4482 [BJWC2018]Border 的四种求法

    luogu 对于每个询问从大到小枚举长度,哈希判断是否合法,AC 假的(指数据) 考虑发掘border的限制条件,如果一个border的前缀部分的末尾位置位置\(x(l\le x < r)\)满 ...

随机推荐

  1. For 32-bit BOOL is a signed char, whereas under 64-bit it is a bool.

    https://stackoverflow.com/questions/31267325/bool-with-64-bit-on-ios/31270249#31270249 Definition of ...

  2. vue 命名路由

    有时候,通过一个名称来标识一个路由显得更方便一些,特别是在链接一个路由,或者是执行一些跳转的时候.你可以在创建 Router 实例的时候,在 routes 配置中给某个路由设置名称. const ro ...

  3. arduino入门笔记

    以 ARDUINO® UNO R3为例 一.将板子与电脑连接 初次使用会自动安装驱动. Arduino Uno通过USB连接到计算机或外部电源自动获取电源,因此此时能看到电源指示灯会亮. 我的L13也 ...

  4. 013_matlab读取ecxel(脚本读取)

    MATLAB读取ecxel文件数据 视频教程:https://v.qq.com/x/page/b3039we542o.html 资料下载:https://download.csdn.net/downl ...

  5. 洛谷 P1993 小K的农场 题解

    每日一题 day55 打卡 Analysis 这是我们一次考试的T1,但我忘了差分约束系统怎么写了,所以就直接输出Yes混了60分 首先转化题目: 1:表示农场 a 比农场 b 至少多种植了 c 个单 ...

  6. 0.学习springmvc补充

    一.我的截图中的程序错误: 其中的路径写的是:"/user/testString" 这样写会错误当tomcat中配置的根目录为"/"或” “成功,但当配置为”x ...

  7. React的基本使用

    一.初始化和安装依赖 ①建立项目文件夹 mkdir react-democd react-demo ②在项目里执行命令:初始项目 npm init -y ③安装相关依赖 npm install --s ...

  8. nginx 反向代理之 proxy_set_header

    proxy_set_header用来设定被代理服务器接收到的header信息. 语法:proxy_set_header field value; field :为要更改的项目,也可以理解为变量的名字, ...

  9. jQuery获取各种标签的文本和value值

    <select id="test"> <option value ="volvo">Volvo</option> <o ...

  10. Review of Semantic Segmentation with Deep Learning

    In this post, I review the literature on semantic segmentation. Most research on semantic segmentati ...