1 条题解
-
0
1. 问题分析
我们有一个字符串 ,长度为 。
定义 为删除位置 的字符后得到的字符串。
我们要按字典序对 排序,如果两个字符串相同,则位置编号小的排在前面。
最后输出排序后的位置编号序列。
2. 直接模拟的复杂度
如果直接构造 再排序,每个 长度 ,构造总 ,再排序 ,显然不可行。
3. 关键观察
比较 和 (假设 ):
- 当 时, 与 的前 个字符相同(都是 )。
- 第 个字符:
- 在 中,第 个字符是原串的 (因为 被删了)。
- 在 中,第 个字符是原串的 (因为 ,所以 还在)。
- 因此,比较 和 时,在位置 处比较的是 (来自 )和 (来自 )。
更一般地,对于 :
- 对于 :。
- 对于 :,。
- 对于 且 :,。
- 对于 :,(因为 删掉 后,这个位置是 ,而 中 位置也是 )。
- 对于 :,。
所以实际上,比较 和 时,第一个不同的位置是 吗?
不对,因为 时,第一个不同位置是 ,比较的是 。
4. 更简单的比较方法
相当于原字符串去掉第 个字符。
比较 和 时,可以这样看:- 它们的前 个字符相同。
- 在第 个字符:
- 的第 个字符 =
- 的第 个字符 =
- 如果 ,则可以直接分出大小。
- 如果相等,则继续比较后面的字符,但注意 和 在 到 段, 取 , 取 ,这其实相当于比较原串从 开始的后缀和从 开始的后缀(当 时)。
更简单的办法:
可以看作原串 在位置 跳过,所以:比较 和 时,可以按顺序比较字符,遇到位置 时跳过,遇到位置 时跳过。
但这样比较仍然是 的,总排序 不行。
5. 高效比较方法
我们注意到,对于 ,比较 和 等价于比较两个字符串:
- $X = a_1 a_2 \dots a_{i-1} a_{i+1} a_{i+2} \dots a_n$
- $Y = a_1 a_2 \dots a_{i-1} a_i a_{i+1} \dots a_{j-1} a_{j+1} \dots a_n$
它们的前 个字符相同,第 个字符分别是 和 。
所以:
- 如果 ,那么 (因为 在第 个字符 小于 的 )。
- 如果 ,那么 。
- 如果 ,则继续比较后面的字符,但后面的字符相当于比较 和 (因为 从 开始和 从 开始只差一个字符的偏移)。
实际上,当 时, 和 的比较等价于比较原串中从 开始的后缀和从 开始的后缀(如果 ),但这样递归比较复杂。
6. 已知技巧:相邻字符比较法
有一个已知结论(类似 Codeforces 875B 或一些后缀排序题):
对于 , 与 的大小关系由第一个 的位置 决定,具体来说:- 如果 ,那么 ,并且 在 之后。
- 如果 ,那么 。
- 如果 ,则比较关系与更后面的第一个不同字符有关,但可以证明:
设 是满足 的最小位置 ,如果 ,则 ,否则 。
更简单的做法:
比较 和 时,第一个不同的位置是 吗?不,是 吗?我们仔细推导:对于 :
- 前 个字符相同。
- 第 个字符: 是 , 是 。
- 所以比较 即可决定顺序。
因此:
当且仅当 ,对于
当且仅当
如果 ,则 吗?不是,还要继续比较,但继续比较时, 从 开始相当于 从 开始,所以可以递归。
7. 最终简单结论(来自已知解法)
有一个经典做法:
定义 :- 如果 ,则 在排序中应该排在 后面(因为删除 后下一个字符更大,字符串变大)。
- 如果 ,则 在排序中应该排在 前面。
- 如果 ,则 与 相同。
这样我们可以 预处理出每个 的“类型”,然后按照类型排序:
类型 0( 或 相等但后续第一个不同是大于)的排在类型 1 前面。
同类型的 按原顺序排列(因为相等时编号小的优先)。
8. 算法步骤
- 从后往前处理,设 (因为 视为最小)。
- 对于 从 到 1:
- 如果 ,则 ( 大)
- 如果 ,则 ( 小)
- 如果 ,则
- 将 按 分组, 的在前, 的在后。
- 在每组内, 的按 升序排列(因为对于 , 越小字符串越小), 的按 降序排列(因为对于 , 越小字符串越大,所以大的 放前面)。
9. 样例验证
输入:
7 aabaaab=
a a b a a a b
比较相邻:- i=1: a=a → type=type[2]
- i=2: a<b → type=0
- i=3: b>a → type=0
- i=4: a=a → type=type[5]
- i=5: a=a → type=type[6]
- i=6: a<b → type=0
- i=7: 默认 0
从后往前:
- type[7]=0
- type[6]: a<b → 0
- type[5]: a=a → type[6]=0
- type[4]: a=a → type[5]=0
- type[3]: b>a → 0
- type[2]: a<b → 0
- type[1]: a=a → type[2]=0
所以所有 type=0。
type=0 组内按 i 升序:1 2 3 4 5 6 7?
但样例输出是 3 7 4 5 6 1 2,说明我们还需要进一步区分。
10. 更精确的已知解法
实际上这类题的标准解法是:
- 比较 和 时,相当于比较原串从 开始的后缀和从 开始的后缀(当 且 时继续)。
- 因此,可以先求出原串的后缀数组 SA 和 rank。
- 然后 与 的比较:
- 如果 ,直接比较 和 。
- 如果 ,则比较 和 。
但这里 和 是删除的位置,所以对于 :
- 如果 ,直接得出结果。
- 如果 ,则比较 和 等价于比较后缀 和后缀 。
因此,我们可以 预处理,或者直接使用后缀数组 排序。
11. 实现方案(后缀数组法)
- 构建字符串 的后缀数组 SA 和 rank 数组。
- 定义比较函数 :
- 如果 ,则 。
- 如果 ,则 。
- 如果 ,则比较 和 (如果 ,则 ,最小;同理 类似)。
- 用这个比较函数对 排序。
复杂度 ,可过 。
12. 代码框架(C++)
#include <bits/stdc++.h> using namespace std; const int MAXN = 1e6 + 5; int n; char a[MAXN]; int rk[MAXN], sa[MAXN], tmp[MAXN], k; bool cmp_sa(int i, int j) { if (rk[i] != rk[j]) return rk[i] < rk[j]; int ri = i + k <= n ? rk[i + k] : -1; int rj = j + k <= n ? rk[j + k] : -1; return ri < rj; } void build_sa() { for (int i = 1; i <= n; i++) { sa[i] = i; rk[i] = a[i]; } for (k = 1; k <= n; k *= 2) { sort(sa + 1, sa + n + 1, cmp_sa); tmp[sa[1]] = 1; for (int i = 2; i <= n; i++) { tmp[sa[i]] = tmp[sa[i - 1]] + (cmp_sa(sa[i - 1], sa[i]) ? 1 : 0); } for (int i = 1; i <= n; i++) rk[i] = tmp[i]; } } int rank_[MAXN]; bool cmp_i(int x, int y) { // 比较 s_x 和 s_y if (a[x] != a[y]) return a[x] < a[y]; if (x == n) return true; // s_x 已结束 if (y == n) return false; return rank_[x + 1] < rank_[y + 1]; } int main() { scanf("%d", &n); scanf("%s", a + 1); build_sa(); for (int i = 1; i <= n; i++) rank_[sa[i]] = i; vector<int> idx(n); iota(idx.begin(), idx.end(), 1); sort(idx.begin(), idx.end(), cmp_i); for (int i = 0; i < n; i++) printf("%d ", idx[i]); puts(""); return 0; }
13. 总结
这道题的核心在于理解删除一个字符后的字符串比较可以转化为对原串后缀的比较,从而利用后缀数组快速排序。
- 1
信息
- ID
- 4928
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者