知识点: SA,线段树,广义 SAM

原题面 Loj Luogu

给定两字符串 \(S_1, S_2\),求出在两字符串中各取一个子串,使得这两个子串相同的方案数。

两方案不同当且仅当这两个子串中有一个位置不同。

\(1\le |S_1|, |S_2|\le 2\times 10^5\)

分析题意

线段树

考察对 \(\operatorname{lcp}\) 单调性的理解。


\(S_1\) 加个终止符,\(S_2\) 串扔到 \(S_1\) 后面,跑 SA。

显然,答案即后半段的后缀,与前半段的后缀的所有 \(\operatorname{lcp}\) 之和。


按字典序枚举后半段的后缀,设当前枚举到的后缀为 \(sa_i\)。

仅考虑 字典序 \(<sa_i\) 的 前半段的后缀 \(sa_j\ (j<i)\),其对 \(sa_i\) 的贡献为 \(\operatorname{lcp}(sa_i, sa_j)\)。

由 \(\operatorname{lcp}\) 的单调性,当枚举到 第一个 \(>sa_i\) 的 后半段的后缀 \(sa_k\ (k>i)\) 时,有 :\(\operatorname{lcp}(sa_{k}, sa_j)\le \operatorname{lcp}(sa_i,sa_j)\)。

  1. 若 \(\operatorname{lcp}(sa_{k}, sa_j)< \operatorname{lcp}(sa_i,sa_j)\),则 \(sa_j\) 对 \(sa_k\) 的贡献应变为 \(\operatorname{lcp}(sa_k, sa_j) = \min\{\operatorname{lcp}(sa_i,sa_j), \min\limits_{l=i+1}^{k}{\{\operatorname{height}_l}\}\}\)。

  2. 若存在 \(sa_l, l\in (i,k)\) 为 前半段的后缀 时,作出贡献的元素增加。

考虑在枚举后缀的过程中,用权值线段树维护 字典序 \(<sa_i\)前半段 的后缀 \(sa_j\ (j<i)\) 的不同长度的 \(\operatorname{lcp}\) 的数量。

上述两操作,即为区间赋值 与 单点插入。


再按字典序倒序枚举后缀,计算字典序 \(>sa_i\) 的 前半段的后缀的贡献。

分析很屑,代码有详细注释。

复杂度 \(O(n\log n)\)。

线段树写法比较无脑,也可以单调栈简单维护,复杂度也为\(O(n\log n)\) 级别。


广义 SAM

用两个字符串构造广义 SAM。

维护每个状态维护了几个串的 \(\operatorname{endpos}\)。

当一个状态同时维护了两个串的 \(\operatorname{endpos}\),则该状态及其 parent 树上的祖先 所代表的串,均为公共子串。

设 \(size(u,0/1)\) 表示状态 \(u\) 维护了串 1/2 的 \(\operatorname{endpos}\) 的个数,有:

\[\sum size(i,0)\times size(i,1)\times (\operatorname{len}(i)-\operatorname{len}(\operatorname{link}(i))
\]

具体地,每插入串 \(i\) 的一个新字符,就对该字符对应的状态的 \(size(i) +1\)。

在 parent 树上求子树 \(size\) 和,最后枚举状态更新答案。


爆零小技巧 1:在有返回值的函数中不写 return

爆零小技巧 2:边界玄学怎么办?判断正确性可靠对拍实现。


代码实现

广义 SAM

//知识点:SAM
/*
By:Luckyblock
试了试变量写法,挺清爽的。
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e6 + 10;
const int kMaxm = 26;
//=============================================================
ll ans;
char S[kMaxn];
int size[kMaxn][2], id[kMaxn], cnt[kMaxn];
int num, node_num = 1, ch[kMaxn << 1][kMaxm], len[kMaxn <<1], link[kMaxn << 1];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
int Insert(int c_, int last_) {
if (ch[last_][c_]) {
int p = last_, q = ch[p][c_];
if (len[p] + 1 == len[q]) return q;
int newq = ++ node_num;
memcpy(ch[newq], ch[q], sizeof(ch[q]));
len[newq] = len[p] + 1;
link[newq] = link[q];
link[q] = newq;
for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
return newq;
}
int p = last_, now = ++ node_num;
len[now] = len[p] + 1;
for (; p && ! ch[p][c_]; p = link[p]) ch[p][c_] = now;
if (! p) {link[now] = 1; return now;}
int q = ch[p][c_];
if (len[q] == len[p] + 1) {link[now] = q; return now;}
int newq = ++ node_num;
memcpy(ch[newq], ch[q], sizeof(ch[q]));
link[newq] = link[q], len[newq] = len[p] + 1;
link[q] = link[now] = newq;
for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
return now;
}
//=============================================================
int main() {
for (; num <= 1; ++ num) {
scanf("%s", S + 1);
int n = strlen(S + 1), last = 1;
for (int i = 1; i <= n; ++ i) {
last = Insert(S[i] - 'a', last);
size[last][num] = 1;
}
}
for (int i = 1; i <= node_num; ++ i) cnt[len[i]] ++;
for (int i = 1; i <= node_num; ++ i) cnt[i] += cnt[i - 1];
for (int i = 1; i <= node_num; ++ i) id[cnt[len[i]] --] = i;
for (int i = node_num; i >= 2; -- i) {
int now = id[i];
size[link[now]][0] += size[now][0];
size[link[now]][1] += size[now][1];
}
for (int i = 2; i <= node_num; ++ i) {
ans += 1ll * size[i][0] * size[i][1] * (len[i] - len[link[i]]);
}
printf("%lld\n", ans);
return 0;
}

傻逼线段树

//知识点:SA
/*
By:Luckyblock
*/
#include <cstdio>
#include <ctype.h>
#include <cstring>
#include <algorithm>
#define ll long long
#define lson (now_<<1)
#define rson (now_<<1|1)
const int kMaxn = 4e5 + 10;
//=============================================================
char S[kMaxn];
int n1, n, m, sa[kMaxn], rk[kMaxn << 1], oldrk[kMaxn << 1], height[kMaxn];
int id[kMaxn], cnt[kMaxn], rkid[kMaxn];
ll ans, size[kMaxn << 2], sum[kMaxn << 2]; //size 维护数量,sum 维护 lcp 之和。
bool tag[kMaxn << 2];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
bool cmp(int x, int y, int w) { //判断两个子串是否相等。
return oldrk[x] == oldrk[y] &&
oldrk[x + w] == oldrk[y + w];
}
void GetHeight() {
for (int i = 1, k = 0; i <= n; ++ i) {
if (rk[i] == 1) k = 0;
else {
if (k > 0) k --;
int j = sa[rk[i] - 1];
while (i + k <= n && j + k <= n &&
S[i + k] == S[j + k]) {
++ k;
}
}
height[rk[i]] = k;
}
}
void SuffixSort() {
m = std :: max(n, 300);
for (int i = 1; i <= n; ++ i) ++ cnt[rk[i] = S[i]];
for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; -- i) sa[cnt[rk[i]] --] = i;
for (int p, w = 1; w < n; w <<= 1) {
p = 0;
for (int i = n; i > n - w; -- i) id[++ p] = i;
for (int i = 1; i <= n; ++ i) {
if (sa[i] > w) id[++ p] = sa[i] - w;
}
memset(cnt, 0, sizeof (cnt));
for (int i = 1; i <= n; ++ i) ++ cnt[(rkid[i] = rk[id[i]])];
for (int i = 1; i <= m; ++ i) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; -- i) sa[cnt[rkid[i]] --] = id[i];
std ::swap(rk, oldrk);
m = 0;
for (int i = 1; i <= n; ++ i) {
m += (cmp(sa[i], sa[i - 1], w) ^ 1);
rk[sa[i]] = m;
}
}
GetHeight();
}
void Build(int now_, int L_, int R_) {
size[now_] = sum[now_] = 0ll;
tag[now_] = false;
if (L_ == R_) return ;
int mid = (L_ + R_) >> 1;
Build(lson, L_, mid), Build(rson, mid + 1, R_);
}
void Pushdown(int now_) {
tag[lson] = tag[rson] = true;
size[lson] = size[rson] = 0;
sum[lson] = sum[rson] = 0;
tag[now_] = false;
}
void Pushup(int now_) {
size[now_] = size[lson] + size[rson];
sum[now_] = sum[lson] + sum[rson];
}
ll Delete(int now_, int L_, int R_, int ql_, int qr_) {
if (ql_ <= L_ && R_ <= qr_) {
ll ret = size[now_];
tag[now_] = true;
size[now_] = sum[now_] = 0;
return ret;
}
if(tag[now_]) Pushdown(now_);
int mid = (L_ + R_) >> 1;
ll ret = 0ll;
if (ql_ <= mid) ret += Delete(lson, L_, mid, ql_, qr_);
if (qr_ > mid) ret += Delete(rson, mid + 1, R_, ql_, qr_);
Pushup(now_);
return ret;
}
void Insert(int now_, int L_, int R_, int pos_, ll num) {
if (! num) return ;
if (L_ == R_) {
size[now_] += num;
sum[now_] += 1ll * num * (L_ - 1ll); //注意减去偏移量。
return ;
}
if (tag[now_]) Pushdown(now_);
int mid = (L_ + R_) >> 1;
if (pos_ <= mid) Insert(lson, L_, mid, pos_, num);
else Insert(rson, mid + 1, R_, pos_, num);
Pushup(now_);
}
//=============================================================
int main() {
scanf("%s", S + 1); n1 = strlen(S + 1);
S[n1 + 1] = 'z' + 1;
scanf("%s", S + n1 + 2); n = strlen(S + 1);
SuffixSort(); //正序枚举所有后缀,计算字典序 >sa_i 的 前半段的后缀的贡献。
//当枚举到一个 后半段的后缀,仅用于更新 min(lcp)。
//枚举到一个 前半段的后缀,用于更新 min(lcp),且需新插入一个后缀。
//由于 lcp 可能为 0,线段树维护的区间加了偏移量 1。
for (int i = 2; i <= n; ++ i) {
//计算 lcp > height(i) 的 前半段后缀的数量,并将他们删除。
ll num = Delete(1, 1, n + 1, height[i] + 2, n + 1);
Insert(1, 1, n + 1, height[i] + 1, num + (sa[i - 1] <= n1)); //插入被删除的后缀 与 新后缀。注意边界。
if (sa[i] > n1 + 1) ans += sum[1]; //若枚举到一个 后半段后缀,计算贡献。 注意边界。
}
Build(1, 1, n); //清空线段树
//倒序枚举所有后缀,计算字典序 >sa_i 的 前半段的后缀的贡献。
for (int i = n; i >= 2; -- i) {
ll num = Delete(1, 1, n + 1, height[i] + 2, n + 1);
Insert(1, 1, n + 1, height[i] + 1, num + (sa[i] <= n1)); //注意边界
if (sa[i - 1] > n1 + 1) ans += sum[1]; //注意边界
}
printf("%lld", ans);
return 0;
}

「HAOI2016」找相同字符的更多相关文章

  1. 【LOJ】#2064. 「HAOI2016」找相同字符

    题解 做后缀自动机题要一点脑洞,脑洞一开,就过了 我们显然要拿第二个串跑第一个串的后缀自动机 我们可以求出第二个串每个位置匹配到的节点,和匹配的长度L 那么我们统计一个后缀树上的根缀和,表示这样个节点 ...

  2. 「HAOI2016」字符合并

    「HAOI2016」字符合并 题意: ​ 有一个长度为\(n\)的\(01\)串,你可以每次将相邻的\(k\)个字符合并,得到一个新的字符并获得一定分数.得到的新字符和分数由这\(k\)个字符确定.你 ...

  3. 「JSOI2010」找零钱的洁癖

    「JSOI2010」找零钱的洁癖 传送门 个人感觉很鬼的一道题... 首先我们观察到不同的数最多 \(50\) 个,于是考虑爆搜. 但是这样显然不太对啊,状态数太多了. 然后便出现了玄学操作: \(\ ...

  4. 【LOJ】#2063. 「HAOI2016」字符合并

    题解 dp[i][j][S]表示区间[i,j]内剩余的数位状压后为S的最大值 这样转移起来不就是\(n^3 2^8\)了吗 冷静一下,我们可以发现一段区间内剩下的数位的个数是一定的,也就是我们可以在枚 ...

  5. loj2063 「HAOI2016」字符合并

    ref #include <iostream> #include <cstring> #include <cstdio> using namespace std; ...

  6. 【BZOJ4566】【HAOI2016】找相同字符

    后缀自动姬好,好写好调好ac 原题: 给定两个字符串,求出在两个字符串中各取出一个子串使得这两个子串相同的方案数.两个方案不同当且仅当这两 个子串中有一个位置不同. 1 <=n1, n2< ...

  7. 【LOJ】#2062. 「HAOI2016」地图

    题解 我对莫队真是一无所知 这个东西显然可以用圆方树转成一个dfs序列 然后呢,用莫队计算每个询问区间的每个数出现的次数,从而顺带计算每个数字的奇偶性 但是我们要查的数字也用一个范围,可以直接用分块维 ...

  8. 【LOJ】#2061. 「HAOI2016」放棋子

    题解 水题,可惜要写高精度有点烦 一看障碍物的摆放方式和最后的答案没有关系,于是干脆不读了,直接二项式反演可以得到 设\(g_k\)为一种摆放方式恰好占了k个障碍物 \(f_k = \sum_{i = ...

  9. 「HAOI2016」放棋子

    题目链接 戳这 前置知识 错位排序 Solution 我们可以观察发现,每一行的障碍位置对答案并没有影响. 于是我们可以将此时的矩阵化成如下形式: \[ 1\ \ 0\ \ 0\ \ 0\\ 0\ \ ...

随机推荐

  1. A Child's History of England.41

    When intelligence of this new affront [hit in the face, c-o-n-frontation!] was carried to the King i ...

  2. Java Swing布局管理器GridBagLayout的使用示例 [转]

    GridBagLayout是java里面最重要的布局管理器之一,可以做出很复杂的布局,可以说GridBagLayout是必须要学好的的, GridBagLayout 类是一个灵活的布局管理器,它不要求 ...

  3. 百度 IP 查询

    查询 IP 地址以及百度爬虫 IP 我们如果要查询 IP 地址,互联网上有很多提供IP查询服务的网站,我这里总结和归纳如下: 国内提供 IP 查询的网站: IP138 IPIP,提供 IP 详细信息, ...

  4. 容器之分类与各种测试(四)——multimap

    multiset和multimap的具体区别在于,前者的key值就是自己存储的value,后者的key与value是分开的不相关的. 例程 #include<stdexcept> #inc ...

  5. 如何设置eclipse下查看java源码

    windows--preferences--java--installed jres --选中jre6--点击右边的edit--选中jre6/lib/rt.jar --点击右边的 source att ...

  6. 监控网站是否异常的shell脚本

    本节内容:shell脚本监控网站是否异常,如有异常就自动发邮件通知管理员. 脚本检测流程,如下:1,检查网站返回的http_code是否等于200,如不是200视为异常.2,检查网站的访问时间,超过M ...

  7. 动态规划系列(零)—— 动态规划(Dynamic Programming)总结

    动态规划三要素:重叠⼦问题.最优⼦结构.状态转移⽅程. 动态规划的三个需要明确的点就是「状态」「选择」和「base case」,对应着回溯算法中走过的「路径」,当前的「选择列表」和「结束条件」. 某种 ...

  8. linux系统下命令的学习

    本博客是本人工作时做的笔记 ps aux |grep ^profile |grep A190200024 ^ 表示行首匹配 linux查看文件大小: 具体可查看:https://www.cnblogs ...

  9. C++11 关键字 const 到底怎么用?

    Const 的作用及历史 const (computer programming) - Wikipedia 一.历史 按理来说,要想了解一件事物提出的原因,最好的办法就是去寻找当时的历史背景,以及围绕 ...

  10. idea开发环境搭建ssh

    idea2020完整web开发(struts2+spring+hibernate) idea破解 第一步: 下载最新的 IDEA 2020.3.2 版本安装包 https://www.jetbrain ...