知识点: 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. 【Android】No Android SDK found(mac)+ 真机调试

     [1]No Android SDK found 如果没下载SDK,可以去google官方下载 如果因为上网问题,这里提供两个网址,有人整理好了,这里先谢谢他们,下面两个择其一下载 http://to ...

  2. KMP算法中的next函数

    原文链接:http://blog.csdn.net/joylnwang/article/details/6778316/ 其实后面大段的代码都可以不看 KMP的关键是next的产生 这里使用了中间变量 ...

  3. 集合类——Collection、List、Set接口

    集合类 Java类集 我们知道数组最大的缺陷就是:长度固定.从jdk1.2开始为了解决数组长度固定的问题,就提供了动态对象数组实现框架--Java类集框架.Java集合类框架其实就是Java针对于数据 ...

  4. Linux学习 - ifconfig

    ifconfig 1.功能 用来查看和配置网络设备,当网络环境发生改变时可通过此命令对网络进行相应的配置. 2.用法 ifconfig  [网络设备]  [参数] (1).参数 up 启动指定网络设备 ...

  5. ReactiveCocoa操作方法-重复

    retry重试      只要失败,就会重新执行创建信号中的block,直到成功. __block int i = 0; [[[RACSignal createSignal:^RACDisposabl ...

  6. vue2 中的 export import

    vue中组件的引用嵌套通过export import语法链接 Nodejs中的 export import P1.js export default { name: 'P1' } index.js i ...

  7. transient关键字和volatile关键字

    看到HashSet的源代码的时候,有一个关键字不太认识它..transient,百度整理之: Java的Serialization提供了一种持久化对象实例的机制,当持久化对象时,可能有一些特殊的对象数 ...

  8. 连接opcserver时报错 connecting to OPC Server "****" CoCreateInstance 服务器运行失败

    在普通windows系统连接OPCServer可能会报这样的错,排查很长时间,OPCServer跟Client都运行正常,点号录入也正常. 最后发现,其实是OPCServer 与OPCClient 权 ...

  9. Apache log4j2 远程代码执行漏洞复现👻

    Apache log4j2 远程代码执行漏洞复现 最近爆出的一个Apache log4j2的远程代码执行漏洞听说危害程度极大哈,我想着也来找一下环境看看试一下.找了一会环境还真找到一个. 漏洞原理: ...

  10. HTTP强缓存和协商缓存

    一.浏览器缓存 Web 缓存能够减少延迟与网络阻塞,进而减少显示某个资源所用的时间.借助 HTTP 缓存,Web 站点变得更具有响应性. (一).缓存优点: 减少不必要的数据传输,节省带宽 减少服务器 ...