P4770 [NOI2018] 你的名字 题解

news/2025/1/15 9:42:14/文章来源:https://www.cnblogs.com/Rock-N-Roll/p/18672269

\(\text{P4770 [NOI2018] 你的名字 题解}\)

注意到 \(l=1,r=|S|\) 有整整 68 分的高分,让我们先来考虑这样的特殊情况。

这样的特殊情形实际上要我们求的是 \(t\) 有多少个本质不同的子串满足其不是 \(s\) 的子串。正着做看上去有些困难,于是维护 \(s,t\) 的本质不同公共子串个数,用 \(t\) 的本质不同子串个数减去即可。于是对 \(s,t\) 建出 SAM,后者是容易维护的,考虑如何维护前者。

首先我们需要知道如何匹配两个串的公共子串个数,不会的左转 OI-wiki,但这样难以解决本质不同的问题。于是我们考虑在进行匹配时顺便建出 \(t\) 的 SAM,记当前匹配的长度为 \(l\),当前字符对应的在 \(t\) 的 SAM 上的节点是 \(p\),那么显然在 parent 树上 \(p\) 的祖先节点会被重复计算,于是事实上该字符的贡献是 \(\max(l-\operatorname{len(link}(p)),0)\),其中 \(\operatorname{len,link}\) 就是在 SAM 中的定义。

给出代码:

#include <bits/stdc++.h>
#define N 2000005
#define M 26
#define ll long long
using namespace std;
int n;
char s[N];struct SuffixAutomation {struct SAM {int len, fa;int s[M];} sam[N];int tot = 1, lst = 1;void insert(int c) {int p = lst, np = lst = ++tot;sam[np].len = sam[p].len + 1;for (; !sam[p].s[c]; p = sam[p].fa) sam[p].s[c] = np;if (!p) sam[np].fa = 1;else {int q = sam[p].s[c];if (sam[q].len == sam[p].len + 1) sam[np].fa = q;else {int nq = ++tot;sam[nq] = sam[q], sam[nq].len = sam[p].len + 1;sam[np].fa = sam[q].fa = nq;for (; sam[p].s[c] == q; p = sam[p].fa) sam[p].s[c] = nq;}}}void init() {for (int i = 0; i <= tot; i++) {sam[i].len = sam[i].fa = 0;for (int j = 0; j < 26; j++) sam[i].s[j] = 0;}tot = lst = 1;}
} S, T;ll res;
void fnd(char *s) {int v = 1, l = strlen(s), ans = 0;for (int i = 0; i < l; i++) {int c = s[i] - 'a';while (v > 1 && !S.sam[v].s[c]) {v = S.sam[v].fa;ans = S.sam[v].len;}if (S.sam[v].s[c]) {v = S.sam[v].s[c];++ans;}T.insert(c);int p = T.lst;res += T.sam[p].len - T.sam[T.sam[p].fa].len;if (ans >= T.sam[T.sam[p].fa].len) res -= ans - T.sam[T.sam[p].fa].len;}
}int main() {ios::sync_with_stdio(0);cin.tie(0);cin >> s;int lth = strlen(s);for (int i = 0; i < lth; i++) S.insert(s[i] - 'a');int q;cin >> q;while (q--) {cin >> s;int l, r;cin >> l >> r;T.init();res = 0;fnd(s);if (l == 1 && r == lth) cout << res << "\n";}return 0;
}

对于普遍的情形,不难发现这个式子仍然适用,不适用的是 SAM 上一些转移边是不能走的。对于询问 \((L,R)\),如果当前匹配的长度为 \(l\),那么某条边能走的条件就是在 \([L+l,R]\) 处有能匹配到的结尾。发现这就是 \(\operatorname{endpos}\) 集合的定义,为了尽量匹配到,我们维护区间 \(\operatorname{endpos}\) 的最大值,对于每个节点,如果其在 \([L+l,R]\) 范围内的 \(\operatorname{endpos}\) 集合是有值的,便符合条件,可以进行转移。然后线段树合并维护即可。实现时需要留意的有两个点:

  1. 这里的线段树合并由于不能破坏其它节点的信息,要新建节点保存信息,也就是可持久化线段树合并。
  2. 我们在判断的过程中会多次减小 \(l\) 的值,如果匹配不上时我们不直接跳父亲,原因是这里的判定条件和 \(l\) 的值同样是强相关的,因此每次将 \(l\) 减去 \(1\) 即可。

关于复杂度,每个字符最多会使 \(l\)\(1\),因此均摊一下是 \(O(|T|)\) 的。总的复杂度是 \(O(|S|+|T|\log |S|)\) 的。

代码:

#include <bits/stdc++.h>
#define N 2000005
#define M 26
#define ll long long
using namespace std;
char s[N];
int n;
ll res;
struct Node {int lc, rc, mx;
} e[N << 4];
#define lc(i) e[i].lc
#define rc(i) e[i].rc
#define mx(i) e[i].mx
int cnt;
void push_up(int p) {mx(p) = max(mx(lc(p)), mx(rc(p)));
}
void insert(int &p, int l, int r, int x) {if (!p) p = ++cnt;if (l == r) return mx(p) = l, void();int mid = (l + r) >> 1;if (x <= mid) insert(lc(p), l, mid, x);else insert(rc(p), mid + 1, r, x);push_up(p);
}
int mge(int p, int q, int l, int r) {if (!p || !q) return p | q;int np = ++cnt;if (l == r) {mx(np) = max(mx(p), mx(q));return np;}int mid = (l + r) >> 1;lc(np) = mge(lc(p), lc(q), l, mid);rc(np) = mge(rc(p), rc(q), mid + 1, r);push_up(np);return np;
}
int query(int p, int l, int r, int ql, int qr) {if (!p || l > r || ql > qr || l > qr || ql > r) return 0;if (ql <= l && r <= qr) return mx(p);int mid = (l + r) >> 1;return max(query(lc(p), l, mid, ql, qr), query(rc(p), mid + 1, r, ql, qr));
}int rt[N];
vector<int>v[N];
void dfs(int x) {for (auto y : v[x]) {dfs(y);rt[x] = mge(rt[x], rt[y], 1, n);}
}struct SuffixAutomation {int tot = 1, lst = 1;struct SAM {int len, fa;int s[M];} sam[N];void ins(int c, int num) {int p = lst, np = lst = ++tot;sam[np].len = sam[p].len + 1;for (; !sam[p].s[c]; p = sam[p].fa) sam[p].s[c] = np;if (!p) sam[np].fa = 1;else {int q = sam[p].s[c];if (sam[q].len == sam[p].len + 1) sam[np].fa = q;else {int nq = ++tot;sam[nq] = sam[q], sam[nq].len = sam[p].len + 1;sam[np].fa = sam[q].fa = nq;for (; sam[p].s[c] == q; p = sam[p].fa) sam[p].s[c] = nq;}}if (num > 0) insert(rt[lst], 1, n, num);}void init() {for (int i = 0; i <= tot; i++) {sam[i].len = sam[i].fa = 0;for (int j = 0; j < M; j++) sam[i].s[j] = 0;}tot = lst = 1;}} S, T;void fnd(char *s, int l, int r) {int len = strlen(s), p = 1, ans = 0;for (int i = 0; i < len; i++) {int c = s[i] - 'a';while (1) {if (S.sam[p].s[c] && query(rt[S.sam[p].s[c]], 1, n, l + ans, r)) {p = S.sam[p].s[c];++ans;break;}if (ans == 0) break;--ans;if (ans == S.sam[S.sam[p].fa].len) p = S.sam[p].fa;}T.ins(c, 0);int q = T.lst;res += T.sam[q].len - T.sam[T.sam[q].fa].len;if (ans >= T.sam[T.sam[q].fa].len) res -= ans - T.sam[T.sam[q].fa].len;}
}int main() {ios::sync_with_stdio(0);cin.tie(0);cin >> s;n = strlen(s);for (int i = 0; i < n; i++) S.ins(s[i] - 'a', i + 1);for (int i = 2; i <= S.tot; i++) v[S.sam[i].fa].push_back(i);dfs(1);int q;cin >> q;while (q--) {int l, r;cin >> s >> l >> r;T.init();res = 0;fnd(s, l, r);cout << res << '\n';}return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/869497.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SendMail C#版

SendMail是我用C#开发的一款发送邮件的工具。 左侧是要发送的内容,右侧是发件人的账户配置,具体可以参考新浪邮箱或者Outlook账户配置方面的资料。输入各项,点击【发送】按钮,对方就收到了邮件。

Windows 行为测试 删除 FileStream 正在读写文件可以继续读写

本文在 Win11 系统下,测试使用 FileStream 对文件进行读写,读写过程中,删除正在读写的文件后的行为测试结论: 使用 FileShare 带 Delete 的共享方式打开的 FileStream 正在对文件进行读写过程中,可以对正在读写的文件进行删除。文件删除之后,不影响已经打开的 FileStream…

如何轻松实现服务器文件自动化传输,保障传输安全与效率?

服务器文件自动化传输是企业数据管理中至关重要的一环,确保数据的一致性、完整性和可用性。常见的服务器文件自动化传输方式,像FTP/HTTP等传统协议的⽂件同步⼯具来实现。但存在一定问题: 1.传输安全可靠性低:传输过程受⽹络环境影响较⼤,易出现延迟、断线、⽂件丢包等情况…

芯片半导体基础(二) :20世纪最伟大的发明,PN结与晶体二极管

liwen01 2025.01.12 前言 PN结 是晶体管的基础,它使得晶体管能够作为一个放大或是开关元器件。晶体管的发明不仅是一个技术上的突破,也标志着电子学的一个新时代。它极大地推动了科技和社会的发展,奠定了现代信息技术的基础,因此也被认为是20世纪最伟大的发明之一。 1947年…

DevExpress gridControl 绑定数据源之后添加非绑定列

using (DevExpress.Utils.WaitDialogForm dlg = new DevExpress.Utils.WaitDialogForm("请稍等", "查询中......", new System.Drawing.Size(100, 50))){string sqlString = "SELECT ITEM ,DESCRIPTION ,CATEGORY3 FROM WIPDBA.TIME_IMA x WHERE x.…

Gitlab搭建npm仓库

由于图片和格式解析问题,为了更好阅读体验可前往 阅读原文:::warning 使用gitlab的仓库注册表特性需要版本14.0+,如果你的版本比较低,请先根据自己的需求合理升级后再使用 ::: npm私有仓库的搭建方式有很多种,比如使用docker(阅读此篇),这里讲述如何使用gitlab作为npm仓库…

k8s~控制deamonset中pod的数量

DaemonSet 是 Kubernetes 中的一种控制器,用于确保集群中的每个节点(或特定标签选择器匹配的节点)运行一个 Pod 的副本。DaemonSet 通常用于运行集群守护进程,如日志收集、监控代理、存储卷插件等。以下是如何控制 DaemonSet 中 Pod 数量的方法:使用节点选择器(Node Sele…

读量子霸权04量子计算机的黎明

量子计算机的黎明1. 晶体管的诞生 1.1. 1956年,三位物理学家因发明了这种神奇的装置而获得诺贝尔奖:贝尔实验室的科学家约翰巴丁、沃尔特布拉顿和威廉肖克利1.1.1. 巴丁、布拉顿和肖克利使用了一种新的量子形式的物质,即半导体1.1.2. 金属是允许电子自由流动的导体1.1.3. 玻…

Arch Linux默认中文输入法设置输入关键字直接给出日期和时间

自定义词组就行,关键字如下#$year年$month月$day日 星期$weekday $fullhour:$minute:$second如下图, 设置里-输入法,进入输入法菜单界面,选择你的输入法设置进入到你的中文输入法设置界面后,拉到中间的位置,有一个【管理自定义词组】,点进去 添加一个词组,把上面的词组…

互联网不景气了那就玩玩嵌入式吧,用纯.NET开发并制作一个智能桌面机器人(二):用.NET IoT库编写驱动控制两个屏幕

前言 从.NET IoT入门开始这篇文章想必大家应该都看过了,也有很多人都该着手购买树莓派Zero 2W进行上手体验了,那么我们这篇文章就开始真正的实践了,玩硬件肯定是要亲自操作得出成果才会开心,由于牵扯到硬件,所以有的时候软件没问题,但是硬件接线错误或者接触不良都会结果…

openGauss训练营第二期结营!一百个QA和PPT合辑大放送

2021年9月11-12日,由openGauss内核项目研发经理、openGauss社区Maintainer、openGauss布道师朱金伟老师领衔,联合openGauss社区、Gauss松鼠会、云和恩墨的专家们组织的第二期“8小时玩转openGauss训练营”活动通过线上直播的方式举办,获得圆满成功。本次参与学员超千人,最终…