【luogu P4218】珠宝商(SAM)(点分治)(根号分治)

珠宝商

题目链接:luogu P4218

题目大意

给你一棵树,每个点有一个字符。再给你一个字符串 s。
然后问你树上的所有简单的路径在 s 上的出现次数的和。

思路

一个比较神奇的题目。

首先考虑 n2 暴力,不难想象使用 SAM,试着给 s 串建一下 SAM 看看。
然后不难想象你就枚举根,然后跑树在 SAM 上跑链,然后出现次数其实就是 DAG 能走到的部分的 sz 和。
(因为那些位置都是你的后缀)

然后考虑别的算法,因为是树上,我们试着用点分治。
那我们考虑把这条路径拆成了两个部分,那我们可以考虑枚举中间衔接的是 s 串的哪个位置。
(不能枚举颜色,因为可能是在不同的位置)

也就是说我们需要统计子树里面有多少个到根的链满足出现在 s 中而且末尾为 p(设 p 为你当前枚举的位置),还有跟到这个点的字符串开头为 p 的。
不难看出我们只需要求出一种,另一种反转一下串即可。

那一个比较容易想到的想法是记录一个数组 numi 表示 SAM 上 i 这个位置存了多少贡献。
对于每个那些串我们可以在 SAM 上跑(继承地跑),然后给对于的位置 numi 加一。
然后再 DP 一下 numi 加上祖先的值 numfai 即可。
找答案的时候 s 中第 i 个位置在 SAM 的位置是 x,就直接是 numx 了。

但是会发现一个问题,它好像不能继承?
因为你是在前面加字符,不能直接用 soni 数组。
那考虑自己再弄一个数组 Soni
考虑看它那个 endpose 集合,因为是连续的,所以如果当前的长度不是最大长度,就一定要跟前面那个对应上,对应上了就还是这个位置,否则就没有了。
那如果是最大长度,就一定会往后跳,那就是要找到一个 fa 是它的点,满足前面能补上你要的那个,否则也没有答案。

那我们只需要记录达到最大长度的那个跳到哪即可,前面那个我们直接可以那个时候推。
求这个 Son 的时候也不难,就直接每个点给它的父亲的 Son 数组贡献一下。
(所以我们这里需要记录每个点的 endpose 集合的右端点,随便一个即可,这里是 Ri

然后看一下复杂度:O(nlogn+nm)(因为每个点你都要枚举 m 中间的断点位置)
麻了怎么更垃圾了。

但是观察到带上了 m,而 n 大看起来是没问题的。
于是考虑一下能不能两个算法都用上,分析一下各自适用处:
暴力就是 n2n 小的时候能用。
这个算法是 nlogn+nm,那 n 大的时候似乎就可以,因为 nm 里面的 n 是你作为断点点数。
那我们考虑一下 n 的用暴力,别的用第二个算法。
首先看暴力杂度,T(n)=2T(n2)+O(n2),主定理之类的能证明是 nn
感性理解一下也不难,你每次只用 1 的大小是 O(n),每次用 n 的大小是 nnn2=nn
也不会用更大的因为会用另一个算法继续点分治。

接着看另一个算法,考虑根号,你深度就 logn 层,而且到 n 大小的层就不走了,所以 nm 里面的 n 就是 n 级别的(大概)。
所以就是对的。

具体操作建议看看打代码。
小细节:
两个 SAM 的 s 数组是不一样的。(第二个是翻转的,后面也要用的 s 数组也是不一样的)
区分好 soniSoni 数组,暴力里面只会用到 soni
SAM 经典必写错(好吧这个也许只有我

代码

#include<cmath> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #define ll long long using namespace std; const ll N = 5e4 + 100; struct node { ll to, nxt; }e[N << 1]; ll n, m, le[N], KK, a[N], s[N], B; ll root, max_root, sz[N], pla1[N], pla2[N]; char s1[N], s2[N]; bool in[N]; ll ans; void add(ll x, ll y) { e[++KK] = (node){y, le[x]}; le[x] = KK; } struct SAM { struct nde { ll sz, len, R, fa; ll son[26], Son[26]; }d[N << 1]; ll tot, lst; ll num[N << 1]; ll tong[N], xl[N << 1], s[N]; void Init() { tot = lst = 1; } ll insert(ll x) { ll p = lst, np = ++tot; lst = np; d[np].len = d[p].len + 1; d[np].sz = 1; d[np].R = d[np].len; for (; p && !d[p].son[x]; p = d[p].fa) d[p].son[x] = np; if (!p) d[np].fa = 1; else { ll q = d[p].son[x]; if (d[q].len == d[p].len + 1) d[np].fa = q; else { ll nq = ++tot; d[nq] = d[q]; d[nq].len = d[p].len + 1; d[nq].sz = 0; d[np].fa = d[q].fa = nq; d[nq].R = 0; for (; p && d[p].son[x] == q; p = d[p].fa) d[p].son[x] = nq; } } return np; } void build() { for (ll i = 1; i <= m; i++) tong[i] = 0; for (ll i = 1; i <= tot; i++) tong[d[i].len]++; for (ll i = 1; i <= m; i++) tong[i] += tong[i - 1]; for (ll i = 1; i <= tot; i++) xl[tong[d[i].len]--] = i; for (ll i = tot; i > 1; i--) { ll now = xl[i]; d[d[now].fa].sz += d[now].sz; d[d[now].fa].R = d[now].R; d[d[now].fa].Son[s[d[now].R - d[d[now].fa].len]] = now; } } void clear() { for (ll i = 1; i <= tot; i++) num[i] = 0; } void clac(ll now, ll father, ll x, ll len) { if (d[x].len == len) x = d[x].Son[a[now]];//到了最大长度 else if (s[d[x].R - len] != a[now]) x = 0;//没到最大长度,直接看是否对上字符 if (!x) return ; num[x]++; for (ll i = le[now]; i; i = e[i].nxt) if (e[i].to != father && !in[e[i].to]) clac(e[i].to, now, x, len + 1); } void DP() { for (ll i = 2; i <= tot; i++) { ll now = xl[i]; num[now] += num[d[now].fa]; } } }S1, S2; void dfs(ll now, ll father) { sz[now] = 1; for (ll i = le[now]; i; i = e[i].nxt) if (e[i].to != father && !in[e[i].to]) { dfs(e[i].to, now); sz[now] += sz[e[i].to]; } } void get_root(ll now, ll father, ll sum) { ll maxn = sum - sz[now]; for (ll i = le[now]; i; i = e[i].nxt) if (e[i].to != father && !in[e[i].to]) { get_root(e[i].to, now, sum); maxn = max(maxn, sz[e[i].to]); } if (maxn < max_root) max_root = maxn, root = now; } // ll st[N]; void dfs1(ll now, ll father) { st[++st[0]] = now; for (ll i = le[now]; i; i = e[i].nxt) if (e[i].to != father && !in[e[i].to]) dfs1(e[i].to, now); } void dfs2(ll now, ll father, ll pl) { pl = S1.d[pl].son[a[now]]; if (!pl) return ; ans += S1.d[pl].sz; for (ll i = le[now]; i; i = e[i].nxt) if (e[i].to != father && !in[e[i].to]) { dfs2(e[i].to, now, pl); } } void clacnn(ll now) { st[0] = 0; dfs1(now, 0); for (ll i = 1; i <= st[0]; i++) { dfs2(st[i], 0, 1); } } //n^2 void clac(ll now, ll father, ll op) { S1.clear(); S2.clear(); if (father) { S1.clac(now, 0, S1.d[1].Son[a[father]], 1); S2.clac(now, 0, S2.d[1].Son[a[father]], 1); } else { S1.clac(now, 0, 1, 0); S2.clac(now, 0, 1, 0); } S1.DP(); S2.DP(); for (ll i = 1; i <= m; i++) ans += op * S1.num[pla1[i]] * S2.num[pla2[m - i + 1]];//记得第二个串翻转了所以第二个是 m-i+1 } void slove(ll now) { in[now] = 1; dfs(now, 0); if (sz[now] <= B) {//n^2 in[now] = 0; clacnn(now); in[now] = 1; return ; } clac(now, 0, 1); for (ll i = le[now]; i; i = e[i].nxt) if (!in[e[i].to]) { clac(e[i].to, now, -1); max_root = sz[e[i].to] + 1; get_root(e[i].to, now, sz[e[i].to]); slove(root); } } int main() { scanf("%lld %lld", &n, &m); B = sqrt(n); // B = 1; for (ll i = 1; i < n; i++) { ll x, y; scanf("%lld %lld", &x, &y); add(x, y); add(y, x); } scanf("%s", s1 + 1); scanf("%s", s2 + 1); for (ll i = 1; i <= n; i++) a[i] = s1[i] - 'a'; for (ll i = 1; i <= m; i++) s[i] = s2[i] - 'a'; S1.Init(); for (ll i = 1; i <= m; i++) pla1[i] = S1.insert(s[i]), S1.s[i] = s[i]; S1.build(); reverse(s + 1, s + m + 1); S2.Init(); for (ll i = 1; i <= m; i++) pla2[i] = S2.insert(s[i]), S2.s[i] = s[i]; S2.build(); reverse(s + 1, s + m + 1); dfs(1, 0); max_root = n + 1; get_root(1, 0, n); slove(root); printf("%lld", ans); return 0; }

__EOF__

本文作者あおいSakura
本文链接https://www.cnblogs.com/Sakura-TJH/p/luogu_P4218.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   あおいSakura  阅读(85)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示