atcoder abc452 题解
几天不见 abc 更拉了~
A - Gothec
问题描述
下面这5个日期 被称为五季节(gosekku):
- 1月7日
- 3月3日
- 5月5日
- 7月7日
- 9月9日
如果给定的月份 (M)、日期 (D) 是五节句之一,输出 Yes;否则输出 No。
解题思路
直接用 pair 把所有五季节全部储存起来,输入完成之后直接遍历就行,或者可以一大堆 if-else 解决这个问题。
代码
cpp
// 存储 5 个五节句:{月, 日}
pair<int, int> festival[5] = {{1,7}, {3,3}, {5,5}, {7,7}, {9,9}};
int main()
{
int month, day;
cin >> month >> day; // 输入月份和日期
pair<int, int> input = {month, day};
// 遍历比对是否是节日
for(int i=0; i<5; i++){
if(input == festival[i]){
cout << "Yes" << endl;
return 0;
}
}
cout << "No" << endl;
return 0;
}
B - Draw Frame
问题描述
有一个 HHH 行 WWW 列 的网格,高桥准备将网格中的每个格子涂成黑色或白色。
规则:
- 把网格边框上的所有格子涂成黑色
- 其余格子涂成白色 输出涂色后的网格。
形式化定义 对于第 iii 行(从上往下数,1≤i≤H1 \le i \le H1≤i≤H)、第 jjj 列(从左往右数,1≤j≤W1 \le j \le W1≤j≤W)的格子,记为格子 (i,j)(i,j)(i,j)。
-
边相邻 :当且仅当 ∣i−k∣+∣j−l∣=1|i-k|+|j-l|=1∣i−k∣+∣j−l∣=1 时,称格子 (i,j)(i,j)(i,j) 和格子 (k,l)(k,l)(k,l) 边相邻。
-
边框格子 :当且仅当一个格子的边相邻格子数量不超过 3 个时,称该格子为边框格子。
请输出 HHH 个字符串 S1,S2,...,SHS_1,S_2,\dots,S_HS1,S2,...,SH,满足:
- SiS_iSi 长度为 WWW - 若格子 (i,j)(i,j)(i,j) 是边框格子 → 第 iii 个字符串的第 jjj 个字符为
#(黑色) - 否则 → 字符为.(白色)
解题思路
直接枚举网格里面的每一个格子,如果发现是在边界上就输出 #,否则就是 .。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
int n, m; // n=行数,m=列数
int main()
{
cin >> n >> m; // 输入网格的行数和列数
// 遍历每一行 i
for (int i = 1; i <= n; i++, cout << '\n')
// 遍历每一列 j
for (int j = 1; j <= m; j++)
{
// 边框判断:第一行/最后一行/第一列/最后一列 → 输出 #
if (i == 1 || i == n || j == 1 || j == m)
cout << "#";
// 中间格子 → 输出 .
else
cout << ".";
}
return 0;
}
C - Fishbones
问题描述
艺术家高砂制作了一个鱼骨架形状的物体。
该物体由 NNN 根鱼刺 和 1 根脊柱 组成。鱼刺编号为 111 到 NNN。 他想要在这 N+1N+1N+1 根骨头 上各写一个字符串,满足以下所有条件:
- 写在脊柱 上的字符串长度恰好为 NNN。
- 对于每一根鱼刺 i (i=1,...,N)i\ (i=1,\dots,N)i (i=1,...,N),满足:
- 写在第 iii 根鱼刺上的字符串长度恰好为 AiA_iAi。
- 第 iii 根鱼刺上字符串的第 BiB_iBi 个字符 ,与脊柱上字符串的第 iii 个字符相同。
- 写在这 N+1N+1N+1 根骨头上的所有字符串,都必须是 S1,S2,...,SMS_1,S_2,\dots,S_MS1,S2,...,SM 中的某一个**(可以重复使用)**。
S1,...,SMS_1,\dots,S_MS1,...,SM 是由小写英文字母组成的字符串,且互不相同。
对于每个 j=1,...,Mj=1,\dots,Mj=1,...,M,请回答下面的问题:
- 在所有满足条件的写法中,是否存在一种方案 ,使得脊柱上写的字符串恰好是 SjS_jSj?
解题思路
直接双重 for 循环判断,如果某一个字符串写在某一根肋骨上对应的字母是什么,接着用 map 维护第某一个字符能不能出现在脊柱的某一个位置。
最后枚举判断就可以了。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int maxn = 15, maxm = 2e5 + 5;
map<char, bool> M[15]; // M[i]:第i根鱼刺能匹配的字符集合
int n, m, a[maxn], b[maxn]; // n=鱼刺数,m=字符串数;a[i]=鱼刺i长度,b[i]=鱼刺i关键位置
string s[maxm]; // 存储所有候选字符串
int main()
{
cin >> n;
// 输入每根鱼刺的长度和关键位置
for (int i = 1; i <= n; i++)
cin >> a[i] >> b[i];
cin >> m;
// 输入所有候选字符串
for (int i = 1; i <= m; i++)
cin >> s[i];
// 预处理:记录每根鱼刺能匹配的字符
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
// 长度符合要求,就记录该字符串的关键字符
if (a[i] == s[j].size())
{
M[i][s[j][b[i] - 1]] = 1;
}
// 逐个判断每个字符串能否作为脊柱
for (int i = 1; i <= m; i++)
{
int f = 1; // 标记是否合法
// 长度必须等于n,否则直接不合法
if (s[i].size() != n)
{
cout << "No\n";
continue;
}
// 检查每一位字符是否都能被对应鱼刺匹配
for (int j = 0; j < s[i].size(); j++)
if (!M[j + 1][s[i][j]])
f = 0;
// 输出结果
if (f)
cout << "Yes\n";
else
cout << "No\n";
}
return 0;
}
D - No-Subsequence Substring
问题描述
给定由小写英文字母组成的字符串 SSS 和 TTT。
在 SSS 的非空子串 中,统计满足以下条件的子串数量: 不包含 TTT 作为其子序列(不需要连续)。
注意:即使子串内容相同,只要取自不同位置,就视为不同的子串。
- 子串 (substring) :从字符串 XXX 的开头删若干字符、结尾删若干字符(可以删0个)后得到的字符串。
- 子序列 (subsequence) :从字符串 XXX 中删去若干字符(可以删0个),剩余字符保持原有顺序得到的字符串。
解题思路
我们可以用补集思想 简化计算:先求出 SSS 的非空子串总数 ,再减去包含 TTT 作为子序列的子串数量,最终结果即为答案。
我们从左到右遍历 SSS 的每个位置 iii,统计以 iii 为右端点且包含 TTT 的子串个数。维护数组 pospospos,其中 pos[j]pos[j]pos[j] 表示在遍历到当前字符时,恰好匹配 TTT 的前 jjj 个字符时,最靠右的合法左端点位置,这个记录能保证后续计算的最优性。
设 TTT 的长度为 len2len2len2,当遍历完成后,pos[len2]pos[len2]pos[len2] 存储了凑齐整个 TTT 时的最优左端点,此时以 iii 为右端点的合法子串数量就是 pos[len2]pos[len2]pos[len2]。
将所有右端点对应的数量累加得到总非法子串数,用总子串数减去该值,即可得到最终答案。
代码
cpp
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxt = 55;
string s, t;
int pos[maxt]; // pos[j]:匹配到T第j位时,最右的左端点位置
signed main()
{
cin >> s >> t;
int lens = s.size(), lent = t.size();
// 总非空子串数量:n*(n+1)/2
int ans = lens * (lens + 1) / 2;
// 初始化:所有匹配状态初始为-1(未匹配)
for (int i = 0; i < lent; i++)
pos[i] = -1;
// 遍历s的每个字符作为右端点
for (int i = 0; i < lens; i++)
{
// 倒序更新,避免覆盖前面的pos值
for (int j = lent - 1; j >= 1; j--)
if (s[i] == t[j] && pos[j - 1] != -1)
pos[j] = pos[j - 1];
// 匹配T第一个字符,更新起点
if (s[i] == t[0])
pos[0] = i;
// 已经能完整匹配T,减去非法子串数
if (pos[lent - 1] != -1)
ans -= pos[lent - 1] + 1;
}
// 输出答案:合法子串数量
cout << ans;
return 0;
}
E - You WILL Like Sigma Problem
题目翻译
你今日的幸运希腊字母是 σ(sigma)。来解决这道用到两次求和的题目,好运一定会降临于你。
给定长度为 NNN 的正整数序列 A=(A1,...,AN)A=(A_1,\dots,A_N)A=(A1,...,AN) 与长度为 MMM 的正整数序列 B=(B1,...,BM)B=(B_1,\dots,B_M)B=(B1,...,BM)。 求下面式子的值,结果对 998244353 取模:
∑i=1N∑j=1MAi∗Bj(imod j) \sum_{i=1}^N \sum_{j=1}^{M} A_i * B_j (i \mod j) i=1∑Nj=1∑MAi∗Bj(imodj)
解题思路
首先我们给 AAA 数组补充一个元素 A0=0A_0 = 0A0=0。
对于固定的 jjj,它对答案的贡献可以写成: 0⋅A0Bj+1⋅A1Bj+⋯+(j−1)⋅Aj−1Bj+0⋅AjBj+...0 \cdot A_0 B_j + 1 \cdot A_1 B_j + \dots + (j-1) \cdot A_{j-1} B_j + 0 \cdot A_j B_j + \dots0⋅A0Bj+1⋅A1Bj+⋯+(j−1)⋅Aj−1Bj+0⋅AjBj+...
这一形式非常适合用分块思想 处理:以 0,1,...,j−10,1,\dots,j-10,1,...,j−1 为一个周期进行分块,同时维护两个前缀和数组:
- i⋅Aii \cdot A_ii⋅Ai 的前缀和
- AiA_iAi 的前缀和
假设当前处于第 kkk 个块(从 000 开始编号),直接用公式 ∑i⋅Ai−j⋅k∑Ai\sum i\cdot A_i - j \cdot k \sum A_i∑i⋅Ai−j⋅k∑Ai 就能算出当前块的贡献,最后整体乘上 BjB_jBj 计入答案即可。
代码
cpp
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 5e5 + 5, MOD = 998244353;
int a[maxn], b[maxn];
int sum1[maxn]; // sum1[i] = 前缀和 i*a[i]
int sum2[maxn]; // sum2[i] = 前缀和 a[i]
int n, m;
signed main()
{
cin >> n >> m;
// 输入数组 A
for (int i = 1; i <= n; i++)
cin >> a[i];
// 输入数组 B
for (int i = 1; i <= m; i++)
cin >> b[i];
// 预处理两个前缀和数组
for (int i = 1; i <= 500000; i++)
{
sum1[i] = (sum1[i-1] + i * a[i] % MOD) % MOD;
sum2[i] = (sum2[i-1] + a[i] % MOD) % MOD;
}
int ans = 0;
// 枚举 j,用整除分块快速计算 i mod j
for (int j = 1; j <= m; j++)
{
// 按 j 的倍数分块计算每一段 [k*j+1, (k+1)*j]
for (int k = 0; k * j < n; k++)
{
int l = k * j;
int r = min((k+1)*j - 1, n);
// 利用公式 i mod j = i - j * floor(i/j)
int res = 0;
// 累加 i*a[i]
res = (res + sum1[r] - sum1[l] + MOD) % MOD;
// 减去 j*k * a[i] 之和
res = (res - (sum2[r] - sum2[l] + MOD) % MOD * (j * k % MOD) % MOD + MOD) % MOD;
// 乘上 b[j] 计入答案
ans = (ans + res * b[j] % MOD) % MOD;
}
}
cout << ans;
return 0;
}
F - Interval Inversion Count
题目翻译
给定正整数 NNN 和一个由 1,2,...,N1,2,\dots,N1,2,...,N 组成的排列 P=(P1,P2,...,PNP=(P_1,P_2,\dots,P_NP=(P1,P2,...,PN。 给定整数 KKK,求满足以下两个条件的整数对 (l,r)(l,r)(l,r) 的个数:
- 1≤l≤r≤N1\le l\le r\le N1≤l≤r≤N
- 子序列 Pl,Pl+1,...,PrP_l,P_{l+1},\dots,P_rPl,Pl+1,...,Pr 的逆序对的数量恰好等于 KKK
解题思路
我们可以观察到一个非常直观的性质:当使用滑动窗口表示子区间时,窗口向右扩大,区间内的逆序对数量只会单调不减。
基于这个性质,我们可以使用双指针(滑动窗口) 来维护合法区间,同时用树状数组动态维护当前窗口内的逆序对数量。
具体做法是:枚举左端点,然后不断将右端点向右扩展,直到区间内的逆序对数量即将超过 K。此时以当前左端点开头的所有合法区间都可以直接统计。
代码
cpp
#include <bits/stdc++.h>
// 太水了,我直接抄板子了
using namespace std;
// 树状数组:维护区间和,用于快速统计逆序相关数量
struct FenwickTree
{
vector<long long> tree;
int n;
FenwickTree(int size) : n(size), tree(size + 1, 0) {}
// 单点修改
void add(int idx, int val)
{
idx++;
while (idx <= n)
{
tree[idx] += val;
idx += idx & -idx;
}
}
// 查询前缀和
long long query(int idx)
{
idx++;
long long res = 0;
while (idx > 0)
{
res += tree[idx];
idx -= idx & -idx;
}
return res;
}
// 查询 [l, r) 的和
long long sum(int l, int r)
{
if (l >= r) return 0;
return query(r - 1) - query(l - 1);
}
};
// 计算逆序数 ≤ X 的区间 [l,r] 数量
long long count_less_equal(int N, vector<int> &P, long long X)
{
FenwickTree bit(N);
long long now_inv = 0; // 当前窗口逆序数
long long ans = 0;
int r = 0;
// 双指针 + 树状数组 维护合法右端点
for (int l = 0; l < N; ++l)
{
if (r < l) r = l;
// 扩展 r,直到逆序数超 X
while (r < N)
{
long long add = bit.sum(P[r], N); // 新增 r 带来的逆序增量
if (now_inv + add > X) break;
now_inv += add;
bit.add(P[r], 1);
r++;
}
ans += r - l; // 以 l 为左端点的合法区间数
// 移除左端点 l
now_inv -= bit.sum(0, P[l]);
bit.add(P[l], -1);
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
long long K;
cin >> N >> K;
vector<int> P(N);
for (int i = 0; i < N; ++i)
{
cin >> P[i];
P[i]--; // 转为 0 下标
}
// 答案 = 逆序 ≤ K 的区间数 - 逆序 ≤ K-1 的区间数
long long res = count_less_equal(N, P, K);
if (K > 0)
res -= count_less_equal(N, P, K - 1);
cout << res << endl;
return 0;
}
G - 221 Substring
题目翻译
对于正整数序列 X=(X1,...,Xn)X=(X_1,\dots,X_n)X=(X1,...,Xn),如果其游程编码 中每一段的段长 与段内数值 都相等,我们就称 X 为 221 序列。
形式化地,满足以下条件的序列称为 221 序列:
- 对任意满足 1≤l≤r≤n1\le l\le r\le n1≤l≤r≤n 的整数对 (l,r)(l,r)(l,r),若以下三个条件同时成立,则必有 r−l+1=Xlr-l+1 = X_lr−l+1=Xl:
*- l=1l=1l=1,或者 l≥2l\ge2l≥2 且 Xl−1≠XlX_{l-1}\neq X_lXl−1=Xl(段的开头)
-
- r=nr=nr=n,或者 r≤n−1r\le n-1r≤n−1 且 Xr+1≠XrX_{r+1}\neq X_rXr+1=Xr(段的结尾)
-
- Xl=Xl+1=⋯=XrX_l=X_{l+1}=\dots=X_rXl=Xl+1=⋯=Xr(整段数值相同)
换句话说:每一段相同数字构成的连续段,其长度必须恰好等于段内的数字。
例如:
- (2,2,3,3,3,1,2,2)(2,2,3,3,3,1,2,2)(2,2,3,3,3,1,2,2) 是 221 序列
- (1,1)(1,1)(1,1) 和 (4,4,1,4,4)(4,4,1,4,4)(4,4,1,4,4) 不是 221 序列
给定长度为 NNN 的正整数序列 A=(A1,...,AN)A=(A_1,\dots,A_N)A=(A1,...,AN),求 AAA 中本质不同 的非空连续子串里,是 221 序列的个数。
解题思路
这道题可借助后缀数组求解,核心思路是先对原序列 A 进行预处理,将问题转化为求解特定子串的数量,具体优化润色如下:
首先观察原序列 A 的特性:对于 A 中连续的 k 个相同数字 x,根据 k 与 x 的大小关系,可分为三种情况处理,这是后续预处理的关键:
- 若 k<xk<xk<x:这 k 个 x 无法构成合法的 221 序列段,也不能与前后的数字拼接形成合法段,相当于 "无效段";
- 若 k=xk=xk=x:这 k 个 x 本身是合法的 221 序列段,且可灵活与前后的合法段拼接(可连前、连后、都连或都不连);
- 若 k>xk>xk>x:这 k 个 x 可拆分为合法段,但只能与前后合法段中的一侧拼接(无法同时与两侧拼接,否则会破坏段长与数值相等的规则)。
基于以上分析,我们对原序列 A 进行预处理,构造一个新序列 S(初始为空),处理规则如下:
- 遇到上述第 1 种情况(k<xk<xk<x):向 S 中加入一个标记
0(用于分隔无效段,避免无效拼接); - 遇到上述第 2 种情况(k=xk=xk=x):直接向 S 中加入 x(保留合法段,允许灵活拼接);
- 遇到上述第 3 种情况(k>xk>xk>x):向 S 中依次加入 x、
0、x(用0分隔两个可独立拼接的合法段,限制只能单侧拼接)。
举个例子,若原序列 A=(2,2,3,3,3,1,1,1,3,3,3,1,2,2,2,1,9,1,4,4,4,4,4)A=(2,2,3,3,3,1,1,1,3,3,3,1,2,2,2,1,9,1,4,4,4,4,4)A=(2,2,3,3,3,1,1,1,3,3,3,1,2,2,2,1,9,1,4,4,4,4,4),经过预处理后得到的新序列 S=(0,2,3,1,0,1,3,1,2,0,2,1,0,1,4)S=(0,2,3,1,0,1,3,1,2,0,2,1,0,1,4)S=(0,2,3,1,0,1,3,1,2,0,2,1,0,1,4)。
此时,原问题就被转化为一个更简单的问题:求解序列 S 中不包含 0、且本质不同的非空子串数量。后续只需使用后缀数组模板,对处理后的序列 S 进行求解即可(后缀数组可高效统计本质不同的子串个数)。
代码
cpp
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n; // 原序列A的长度
// 核心预处理函数:将原序列A转化为新序列S
// 处理逻辑:按连续相同数字的段长k与数字x的关系,构造S(0用于分隔无效/不可拼接段)
vector<int> build_s(vector<int> &a)
{
vector<int> s; // 存储预处理后的新序列S
int p = 0, t = -1; // p:当前连续段的数字;t:当前连续段的长度(计数)
for (int i : a) // 遍历原序列A的每个元素
{
if (i != p) // 遇到新的数字,说明上一个连续段结束,处理上一段
{
// 上一段的长度t ≥ 数字p(说明上一段可构成合法221段)
if (t >= p)
{
s.push_back(p); // 加入合法段p
if (t > p) // 若段长t > p,需用0分隔,只能单侧拼接
{
s.push_back(0);
s.push_back(p);
}
}
else // 上一段长度t < p,无法构成合法段,加入0标记无效
s.push_back(0);
p = i, t = 1; // 更新当前段的数字为i,长度初始化为1
}
else // 与当前段数字相同,段长+1
t++;
}
// 处理最后一个连续段(循环结束后未处理)
if (t >= p)
s.push_back(p);
return s; // 返回预处理后的序列S
}
// 后缀数组构建函数(模板):将序列s转化为后缀数组sa
// 后缀数组sa[i]:表示s中排名第i的后缀的起始下标
vector<int> build_sa(vector<int> &s)
{
int n = s.size(), m = 10; // m:初始字符集大小(s中元素为0~9,故初始m=10)
vector<int> sa(n), rk(n), cnt(max(n, m)), old_rk(n, 0), px(n), id(n);
// 初始化:统计每个字符的出现次数,确定初始排名
for (int i = 0; i < n; i++)
cnt[rk[i] = s[i]]++; // rk[i]:初始排名(直接用s[i]作为排名,因为s[i]范围小)
for (int i = 1; i < m; i++)
cnt[i] += cnt[i - 1]; // 前缀和,用于确定每个排名对应的后缀位置
for (int i = 0; i < n; i++)
sa[--cnt[rk[i]]] = i; // 构建初始后缀数组
// 倍增法优化后缀数组构建(核心步骤)
for (int k = 1; k <= n; k <<= 1) // k:当前比较的长度(每次翻倍)
{
int p = 0;
// 先处理长度不足k的后缀(排名最前,直接放入id数组)
for (int i = n - k; i < n; i++)
id[p++] = i;
// 处理长度足够的后缀,按上一轮排名排序,放入id数组
for (int i = 0; i < n; i++)
if (sa[i] >= k)
id[p++] = sa[i] - k;
// 重新计算排名rk
for (int &i : cnt)
i = 0; // 重置计数数组
for (int i = 0; i < n; i++)
cnt[px[i] = rk[id[i]]]++; // px[i]:id[i]对应的上一轮排名
for (int i = 1; i < m; i++)
cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; i--)
sa[--cnt[px[i]]] = id[i]; // 重新构建sa
// 更新rk数组,区分不同后缀的排名
swap(rk, old_rk); // old_rk保存上一轮的排名
p = 1; // 新排名的起始值(从1开始)
for (int i = 1; i < n; i++)
{
// 若两个后缀的前k个字符和后k个字符排名都相同,说明排名相同
if (old_rk[sa[i]] == old_rk[sa[i - 1]] && old_rk[sa[i] + k] == old_rk[sa[i - 1] + k])
rk[sa[i]] = p - 1;
else // 否则排名+1
rk[sa[i]] = p++;
}
if (p == n)
break; // 若所有后缀排名都不同,提前结束(优化)
m = p; // 更新字符集大小为当前排名数
}
return sa;
}
// 高度数组构建函数(模板):计算height数组,用于统计本质不同的子串
// height[i]:排名第i的后缀与排名第i-1的后缀的最长公共前缀(LCP)
vector<int> build_height(vector<int> sa, vector<int> s)
{
int n = s.size();
vector<int> height(n), rk(n);
// 先构建rk数组(sa的逆数组:rk[sa[i]] = i,即后缀起始下标为sa[i]的排名是i)
for (int i = 0; i < n; i++)
rk[sa[i]] = i;
int k = 0; // 用于记录当前公共前缀的长度
for (int i = 0; i < n; i++)
{
if (rk[i] == 0)
continue; // 排名第0的后缀没有前一个后缀,height[0]默认为0
if (k)
k--; // 优化:公共前缀长度最多比上一个少1
int j = sa[rk[i] - 1]; // 找到排名前一位的后缀起始下标j
// 计算i和j对应的后缀的最长公共前缀长度
while (i + k < n && j + k < n && s[i + k] == s[j + k])
k++;
height[rk[i]] = k; // 记录当前排名的height值
}
return height;
}
signed main()
{
ios::sync_with_stdio(false); // 关闭同步,加快输入速度
cin.tie(nullptr); // 解除cin与cout的绑定,进一步提速
cin >> n;
vector<int> a; // 存储原序列A
for (int i = 1; i <= n; i++)
{
int x;
cin >> x;
a.push_back(x);
}
// 步骤1:预处理原序列A,得到新序列S(0分隔无效/不可拼接段)
vector<int> s = build_s(a);
// 步骤2:构建S的后缀数组sa
vector<int> sa = build_sa(s);
// 步骤3:构建height数组,用于统计本质不同的子串
vector<int> height = build_height(sa, s);
int m = s.size(); // 新序列S的长度
vector<int> len(m + 1); // len[i]:以S[i]为起点,不包含0的最长连续子串长度(合法子串的最大长度)
len[m] = 0; // 边界:S[m]超出范围,长度为0
// 倒序计算len数组(从后往前,避免重复计算)
for (int i = m - 1; i >= 0; i--)
if (s[i] == 0) // 遇到0,当前起点无法构成合法子串,长度为0
len[i] = 0;
else // 非0,长度 = 下一个位置的长度 + 1
len[i] = len[i + 1] + 1;
// 步骤4:统计答案:本质不同、不包含0的非空子串数量
int sum = 0; // 存储最终答案
for (int i = 0; i < m; i++)
{
// l:当前后缀与前一个后缀的最长公共前缀长度(重复子串的长度)
int l = (i == 0) ? 0 : height[i];
// r:当前后缀的最大合法长度(以sa[i]为起点,不包含0的最长子串长度)
int r = len[sa[i]];
// 新增的本质不同子串数量 = 最大合法长度 - 重复长度(若为负则取0)
sum += max(0ll, r - l);
}
cout << sum; // 输出答案
return 0;
}