#P2930. 第2题-第k小前缀
-
1000ms
Tried: 34
Accepted: 7
Difficulty: 5
所属公司 :
阿里
时间 :2025年5月7日-阿里淘天(开发岗)
-
算法标签>排序算法
第2题-第k小前缀
题目大意
给定一个长度为 n 的小写字母串 s,考虑它的所有前缀 s[1..1], s[1..2], …, s[1..n]。
先把每个前缀内部的字符按字母表顺序排序(例如 “adcb” → “abcd”),得到 t₁, t₂, …, tₙ。
再对这 n 个字符串按字典序排序,求第 k 小的结果。
字典序比较时,从头比到尾,首个不同字符字母小者在前;如果一个串是另一个的前缀,则短串在前。
思路
-
前缀字符计数
- 用 cnt[i][d] 表示前缀 s[1..i] 中字母(‘a’+d)出现的次数。
- cnt[0][*]=0;
- i 从 1 到 n 时:复制 cnt[i]=cnt[i−1],再让 cnt[i][ s[i]−'a' ]++。
- 时间开销约为 26·n。
-
自定义比较函数 cmp(i, j)(比较 tᵢ 和 tⱼ)
- 从字母 d=0 到 25(对应 a…z)逐一比较 cnt[i][d] 和 cnt[j][d]:
- 若不等,则有以下两种情况:
- cnt[i][d] > cnt[j][d],说明 tᵢ 在此位置多了一个相同字母,字典序更靠前,返回 i 小于 j;
- cnt[i][d] < cnt[j][d],说明 tᵢ 在这一轮就“跑完”了字母 d:
- 如果 i == cnt[i][d](前缀长度正好等于这个字母的出现次数),tᵢ 已结束,是短串,返回 i 小于 j;
- 否则 tᵢ 后面还有更大的字母,字典序更靠后,返回 i 大于 j。
- 若不等,则有以下两种情况:
- 如果所有字母次数都相同,则下标小的排在前。
- 从字母 d=0 到 25(对应 a…z)逐一比较 cnt[i][d] 和 cnt[j][d]:
-
排序并取第 k 小
- 构造 idx = [1,2,…,n],用 cmp 作为比较器排序,时间约为 O(n log n × 26)。
- 答案前缀下标 p = idx[k−1]。
-
重建并输出 t_p
- 按字母顺序把字符(‘a’+d)重复 cnt[p][d] 次拼接即可,耗时 O(26 + n)。
C++
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while(T--){
int n, k;
string s;
cin >> n >> k >> s;
// 1) 构造前缀字符计数 cnt[i][d] 和每个前缀的最大字母索引 max_letter[i]
vector<array<int,26>> cnt(n+1);
vector<int> max_letter(n+1, -1);
for(int d = 0; d < 26; ++d) cnt[0][d] = 0;
for(int i = 1; i <= n; ++i){
cnt[i] = cnt[i-1];
int c = s[i-1] - 'a';
cnt[i][c]++;
max_letter[i] = max(max_letter[i-1], c);
}
// 2) 构造所有前缀的下标,并按自定义比较器排序
vector<int> idx(n);
iota(idx.begin(), idx.end(), 1);
sort(idx.begin(), idx.end(), [&](int i, int j){
// 比较排序后前缀 t_i 和 t_j 的字典序
for(int d = 0; d < 26; ++d){
int ci = cnt[i][d], cj = cnt[j][d];
if(ci != cj){
if(ci > cj){
// 在字母 d 上 i 出现更多:此时 t_i[p]=d
// 如果 j 后面还有更大的字母,t_j[p]>d,则 d 更小 → t_i < t_j
if(max_letter[j] > d) return true;
// 否则 j 在此处结束成为短串,短串更小 → t_j < t_i
return false;
} else {
// ci < cj:在字母 d 上 i 比 j 少
// 如果 i 后面还有更大的字母,t_i[p]>d,则 t_i > t_j
if(max_letter[i] > d) return false;
// 否则 i 在此处结束成为短串,短串更小 → t_i < t_j
return true;
}
}
}
// 如果所有字母次数都相同,则下标小的在前
return i < j;
});
// 3) 取第 k 小的前缀下标
int p = idx[k-1];
// 4) 根据 cnt[p] 重建并输出 t_p
string ans;
ans.reserve(n);
for(int d = 0; d < 26; ++d){
ans.append(cnt[p][d], char('a' + d));
}
cout << ans << "\n";
}
return 0;
}
Java
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
int T = Integer.parseInt(in.readLine());
while (T-- > 0) {
StringTokenizer tk = new StringTokenizer(in.readLine());
int n = Integer.parseInt(tk.nextToken());
int k = Integer.parseInt(tk.nextToken());
String s = in.readLine().trim();
// 1) 构造前缀字符计数 cnt[i][d] 和每个前缀的最大字母索引 maxLetter[i]
int[][] cnt = new int[n+1][26];
int[] maxLetter = new int[n+1];
Arrays.fill(maxLetter, -1);
for (int i = 1; i <= n; i++) {
System.arraycopy(cnt[i-1], 0, cnt[i], 0, 26);
int c = s.charAt(i-1) - 'a';
cnt[i][c]++;
maxLetter[i] = Math.max(maxLetter[i-1], c);
}
// 2) 构造下标数组并排序
Integer[] idx = new Integer[n];
for (int i = 0; i < n; i++) idx[i] = i + 1;
Arrays.sort(idx, (i, j) -> {
// 比较 t_i 和 t_j
for (int d = 0; d < 26; d++) {
int ci = cnt[i][d], cj = cnt[j][d];
if (ci != cj) {
if (ci > cj) {
// i 在 d 上更多:若 j 后面有更大的字母,t_i < t_j
if (maxLetter[j] > d) return -1;
// 否则 t_j 是短串更小,t_i > t_j
return 1;
} else {
// ci < cj:若 i 后面有更大字母,t_i > t_j
if (maxLetter[i] > d) return 1;
// 否则 t_i 是短串更小
return -1;
}
}
}
// 完全相同时,下标小者在前
return Integer.compare(i, j);
});
// 3) 第 k 小的前缀下标
int p = idx[k-1];
// 4) 重建并输出 t_p
StringBuilder sb = new StringBuilder();
for (int d = 0; d < 26; d++) {
for (int t = 0; t < cnt[p][d]; t++) {
sb.append((char)('a' + d));
}
}
System.out.println(sb);
}
}
}
Python
import sys
import threading
from functools import cmp_to_key
def main():
input = sys.stdin.readline
T = int(input())
for _ in range(T):
n, k = map(int, input().split())
s = input().strip()
# 1) 构造前缀字符计数 cnt 和每个前缀的最大字母索引 max_letter
cnt = [[0]*26 for _ in range(n+1)]
max_letter = [-1]*(n+1)
for i, ch in enumerate(s, start=1):
c = ord(ch) - ord('a')
cnt[i] = cnt[i-1].copy()
cnt[i][c] += 1
max_letter[i] = max(max_letter[i-1], c)
# 2) 自定义比较函数
# 比较排序后前缀 t_i 和 t_j 的字典序
def cmp(i, j):
for d in range(26):
ci, cj = cnt[i][d], cnt[j][d]
if ci != cj:
if ci > cj:
# 第一个不同字母是 d 且 ci>cj:
# 若前缀 j 中存在更大的字母 (max_letter[j]>d),
# 则 t_i[p]=d < t_j[p]>d → t_i < t_j
# 否则 t_j 在 p 位置跑完了,t_j 短串 → t_j < t_i
return -1 if max_letter[j] > d else 1
else:
# ci < cj,同理:
# 若前缀 i 中存在更大的字母 (max_letter[i]>d),
# 则 t_j[p]=d < t_i[p]>d → t_i > t_j
# 否则 t_i 在 p 位置跑完了,t_i 短串 → t_i < t_j
return 1 if max_letter[i] > d else -1
# 完全相同时,下标小的在前
return -1 if i < j else (1 if i > j else 0)
# 3) 排序并取第 k 小
idx = list(range(1, n+1))
idx.sort(key=cmp_to_key(cmp))
p = idx[k-1]
# 4) 生成并输出排好序的前缀 t_p
out = []
for d in range(26):
if cnt[p][d]:
out.append(chr(ord('a')+d) * cnt[p][d])
print(''.join(out))
if __name__ == "__main__":
threading.Thread(target=main).start()
题目内容
小红拿到一个长度为 n 的字符串 s,对于 s ,共有 n 个前缀字符串,例如:s="adcb" ,那么 s 的前缀字符串依次为 {"a","ad","adc","adcb"},现在小红会对这 n 个字符串先在其内部按照字母表顺序从小到大的排序规则先排好,为 {"a","ad","acd","abcd"} 。然后再对 n 个字符串按照字典序从小到大的排序规则排好,为 {“a",“abcd",“acd","ad"}
小红想知道最后排好集合中第 k 小的字符串。
从字符串的第一个字符开始逐个比较,直到找到第一个不同的位置,通过比较这个位置字符的字母表顺序得出字符串的大小,称为字典序比较。如果比较到其中一个字符串的结尾时依旧相同,则较短的字符串较小。
输入描述
第一行一个整数 T(1≤T≤100) ,表示单个测试文件的期据组数。
对于每组数据的格式为:
第一行两个整数 n,k(1≤n≤2×105,1≤k≤n) ,表示字符串长度和所求字符串的排名。
第二行一个长度为 n 的字符串 s,保证仅由小写字母组成。
数据保证 n 之和小于等于 2×105 。
输出描述
对于每组数据,输出一个字符串,为排序后得到的第 k 小字符串。
样例1
输入
2
4 2
adcb
1 1
a
输出
abcd
a