[LOJ#3311]「ZJOI2020」字符串
前沿字符串神题 QAQ
题目链接
简要算法
字符串理论、最长公共前缀(LCP)、容斥、扫描线、BIT
前置定理
定义一个串 \(S\) 为「本原平方串」,当且仅当 \(S\) 能够写成 \(AA\) 的形式,并且 \(A\) 不是循环串。换句话说,\(S\) 是本原平方串,当且仅当 \(S\) 只有一个长度为 \(\frac{|S|}2\) 的循环节(这里的循环必须是整数次)。
结论:一个长度为 \(n\) 的串中,位置不同的本原平方子串个数不超过 \(O(n\log n)\)。
证明:
设 \(S\) 为本原平方串且 \(|S|=2r\),且 \(S\) 存在一个长度大于 \(r\) 的本原平方串作为严格后缀,其长度为 \(2r'\),则根据对称性及 border-period 理论可得 \(S\) 的一半符合 \(AA'AAA\dots A\) 的形式,其中 \(A'\) 为 \(A\) 的一段后缀,\(|A|=r-r'\)。
设 \(3(r-r')<r\) 也就是 \(r'>\frac 23r\),这时 \(AA'AAA\dots A\) 中 \(A'\) 之后的 \(A\) 至少重复了两次。设 \(S\) 还存在一个本原平方串作为后缀,其长度 \(2t\) 满足 \(r'<t<r\),则 \(S\) 的一半还符合 \(BB'BBB\dots B\),其中 \(|B|=r-t\),这时串 \(AA\) 同时存在 \(|A|\) 和 \(|B|\) 两个 period,由 \(|B|<|A|\) 得 \(AA\) 存在 period \(\gcd(|A|,|B|)\),即 \(A\) 是循环串。
设 \(A\) 和 \(B\) 公共的最小循环节为 \(T\),我们尝试证明:\(|A'|\) 是 \(T\) 的倍数。
考虑反证法,由于找出了最小循环节 \(T\),故假设 \(S\) 的一半符合 \(TT\dots TT'TT\dots T\)(\(T'\) 是 \(T\) 的严格后缀),\(T'\) 左边的总串长为 \(|A|\)。
这时 \(B'\) 必然符合 \(T'TT\dots T\),又由于 \(|B|<|A|\),故会有一个 \(T\) 匹配上 \(B'\) 的前缀,也就是说 \(T\) 存在一个不为本身的循环位移和本身相同,这与 \(T\) 为最小循环串矛盾。得证。
这时我们得出 \(S\) 的一半存在循环节 \(T\),这与 \(S\) 为本原平方串矛盾。于是对于长度为 \(n\) 的本原平方串 \(S\),最多存在一个长度大于 \(\frac 23n\) 的本原平方串作为后缀。
也就是说,对一个串不断找出最长本原平方串后缀,每找出 \(2\) 个之后长度必然小于原来的 \(\frac 23\),即一个串最多有 \(O(\log n)\) 个本原平方串后缀。
证毕。
Solution
先考虑找出所有的三元组 \((l,r,T)\),满足 \(S[l:r]\) 的最小 period 为 \(T\),\(2T\le r-l+1\)。换句话说,\(S[l:r]\) 的所有长度为 \(2T\) 的倍数的子串都是平方串,其中长度为 \(2T\) 的子串为本原平方串。
方法即为枚举 \(T\) 和 \(i\ge 0\) 满足 \(T|i,i+2T\le n\),使用 NOI2016 优秀的拆分 那题的做法,设 \(lt=\min(T,\text{lcs}(S[1:i+T],S[1:i+2T]))\),\(rt=\min(T-1,\text{lcp}(S[i+T+1:n],S[i+2T+1:n]))\),若 \(lt+rt\ge T\),则右端点在区间 \([i+3T-lt,i+2T+rt]\) 内,长度为 \(2T\) 的所有子串都是平方串。然后对于同一个 \(T\),把这些区间合并起来(如果一个区间的右端点加一等于另一个区间的左端点),就求出了所有三元组。
考虑一个三元组对答案的贡献,先考虑长度为 \(2T\) 的串。
如果不考虑重复,则对于任意 \(l\le i\le r-2T+1\),串 \(S[i:i+2T-1]\) 都是一个合法串,如果是 \(S[L:R]\) 的子串就对询问 \([L,R]\) 贡献 \(1\)。
考虑一个三元组内出现重复子串怎么处理,根据本原串的性质,\(S[l:r]\) 的两个子串 \(S[i:i+T-1]=S[j:j+T-1]\) 当且仅当 \(T|j-i\)。所以考虑容斥,类似于 \(点数-边数=1\),如果两个距离为 \(T\),长度均为 \(2T\) 的串都是 \(S[L:R]\) 的子串,就贡献 \(-1\),也就是对于任意 \(l\le i\le r-3T+1\),如果 \(S[i:i+3T-1]\) 是 \(S[L:R]\) 的子串就贡献 \(-1\)。
长度为 \(4T,6T\) 的串同理。注意枚举串长的次数一定不超过本原平方串的个数,为 \(O(n\log n)\) 级别。
还有一个问题就是不同的三元组产生的相同子串。仍然考虑容斥,设有三元组 \((a,b,T)(c,d,T)\) 满足 \(b<d\),长度为 \(2kT\)(\(k\) 为正整数)的串 \(s\) 在 \(S[a:b]\) 中最后一次出现的左端点为 \(x\),\(S[c:d]\) 中第一次出现的右端点为 \(y\),则可以在 \([x,y]\subseteq[L,R]\) 时把答案减掉 \(1\)。
注意到需要枚举的 \(s\) 的个数和本原平方串的个数相等,均为 \(O(n\log n)\)。故可以外层枚举 \(T\),按右端点从小到大枚举三元组,用 hash+map
维护每种子串最后出现的位置,遇到一个三元组 \((l,r,T)\) 时,枚举 \(l'\in[l,l+T-1]\),\(2T|r'-l'+1\),\(r'\le r\),即找到了一个 \(s=S[l':r']\),设 \(s\) 上次出现的左端点为 \(lst\),则我们找到了一个 \([x,y]\) 为 \([lst,r']\)。然后把所有满足 \(r'\in[r-T+1,r]\),\(2T|r'-l'+1\),\(l'\ge l\) 的子串 \(S[l':r']\) 加入 map
。
问题就转化成二维平面上加入一些点,再加入一组形如 \((x,y)(x+1,y+1)(x+2,y+2)\dots\) 的点组,之后每次矩形查询。
扫描线 + BIT 即可。\(O(n\log^2n)\)。
Code
以下代码使用了二分 hash
求最长公共前后缀。
#include <bits/stdc++.h>
template <class T>
inline void read(T &res)
{
res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
if (bo) res = ~res + 1;
}
template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}
typedef long long ll;
const int N = 2e5 + 5, E = 19, djq = 1e9 + 7, zyy = 1e9 + 9;
int n, q, w[N], Log[N], ml[N], p1[N], p2[N], h1[N], h2[N];
char s[N];
ll ans[N];
std::map<ll, int> occ;
struct interv
{
int T, l, r;
} arr[N];
struct query
{
int t, l, r, id, v;
friend inline bool operator < (query a, query b)
{
return a.t < b.t;
}
};
std::vector<query> que;
struct BIT
{
ll A[N];
void change(int x, ll v)
{
for (; x <= n; x += x & -x)
A[x] += v;
}
ll ask(int x)
{
ll res = 0;
for (; x; x -= x & -x) res += A[x];
return res;
}
} C, CX, CY, CI, CT, XY;
struct modify
{
int x, y, v;
friend inline bool operator < (modify a, modify b)
{
return a.x < b.x;
}
};
std::vector<modify> a1, a2;
ll nealchen(int l, int r)
{
int res1 = (h1[r] - 1ll * h1[l - 1] * p1[r - l + 1] % djq + djq) % djq,
res2 = (h2[r] - 1ll * h2[l - 1] * p2[r - l + 1] % zyy + zyy) % zyy;
return 1ll * res1 * zyy + res2;
}
int lcpA(int x, int y)
{
int l = 1, r = Min(n - x + 1, n - y + 1);
while (l <= r)
{
int mid = l + r >> 1;
if (nealchen(x, x + mid - 1) == nealchen(y, y + mid - 1)) l = mid + 1;
else r = mid - 1;
}
return r;
}
int lcpB(int x, int y)
{
int l = 1, r = Min(x, y);
while (l <= r)
{
int mid = l + r >> 1;
if (nealchen(x - mid + 1, x) == nealchen(y - mid + 1, y)) l = mid + 1;
else r = mid - 1;
}
return r;
}
ll nctxdy(int k, int r)
{
int p = k - r; if (p < 0) p = 0; ll tmp = C.ask(p);
return tmp * r - CX.ask(p) + (C.ask(n) - tmp) * k - CY.ask(n - p - 1)
- CI.ask(n - k) * k + CT.ask(n - k);
}
int main()
{
int l, r;
scanf("%d%d%s", &n, &q, s + 1); Log[0] = -1;
p1[0] = p2[0] = 1;
for (int i = 1; i <= n; i++)
{
p1[i] = 20050131ll * p1[i - 1] % djq;
p2[i] = 1312005ll * p2[i - 1] % zyy;
h1[i] = (20050131ll * h1[i - 1] + s[i] - 'a' + 1) % djq;
h2[i] = (1312005ll * h2[i - 1] + s[i] - 'a' + 1) % zyy;
}
for (int i = 1; i <= q; i++)
{
read(l); read(r);
que.push_back((query) {r, l, r, i, 1});
if (l > 1) que.push_back((query) {l - 1, l, r, i, -1});
}
for (int i = 1; i <= n; i++) Log[i] = Log[i >> 1] + 1;
memset(ml, 0x3f, sizeof(ml));
std::sort(que.begin(), que.end());
for (int T = 1; (T << 1) <= n; T++)
{
int tot = 0;
for (int i = 0; i + (T << 1) <= n; i += T)
{
int lt = Min(T, lcpB(i + T, i + (T << 1))),
rt = Min(T - 1, lcpA(i + T + 1, i + (T << 1) + 1));
if (lt + rt >= T)
{
int r = i + (T << 1) + rt, l = r - (lt + rt - T);
if (!tot || arr[tot].r + 1 < l) arr[++tot] = (interv) {T, l, r};
else arr[tot].r = r;
}
}
occ.clear();
for (int i = 1; i <= tot; i++)
{
int l = arr[i].l, r = arr[i].r;
if (ml[r] <= l - (T << 1) + 1) continue;
for (int j = l; j <= r; j++) ml[j] = Min(ml[j], l - (T << 1) + 1);
l -= (T << 1) - 1;
for (int x = (T << 1); x <= r - l + 1; x += T)
{
a1.push_back((modify) {l, l + x - 1, x / T & 1 ? -1 : 1});
if (r < n) a1.push_back((modify)
{r - x + 2, r + 1, x / T & 1 ? 1 : -1});
if (!(x / T & 1))
{
for (int i = l; i < l + T && i + x - 1 <= r; i++)
{
int nxt = occ[nealchen(i, i + x - 1)];
if (nxt) a2.push_back((modify) {nxt, i + x - 1, -1});
}
for (int i = r; i > r - T && i - x + 1 >= l; i--)
occ[nealchen(i - x + 1, i)] = i - x + 1;
}
}
}
}
std::sort(a1.begin(), a1.end()); std::sort(a2.begin(), a2.end());
for (int i = 0, j = 0, k = 0; i < que.size(); i++)
{
while (j < a1.size() && a1[j].x <= que[i].t)
C.change(a1[j].y - a1[j].x, a1[j].v),
CX.change(a1[j].y - a1[j].x, (a1[j].x - 1) * a1[j].v),
CY.change(n - (a1[j].y - a1[j].x), (a1[j].y - 1) * a1[j].v),
CI.change(n - a1[j].y + 1, a1[j].v),
CT.change(n - a1[j].y + 1, (a1[j].y - 1) * a1[j].v), j++;
while (k < a2.size() && a2[k].x <= que[i].t)
XY.change(a2[k].y, a2[k].v), k++;
ans[que[i].id] += (nctxdy(que[i].r, que[i].t) - nctxdy(que[i].l - 1, que[i].t)
+ XY.ask(que[i].r) - XY.ask(que[i].l - 1)) * que[i].v;
}
for (int i = 1; i <= q; i++) printf("%lld\n", ans[i]);
return 0;
}