滑动窗口统计长度在 [A, B] 内的子串频次,结合分桶与多关键字排序格式化输出高频模式串。
OJ: roj
题目 ID: 2044
难度:普及+/提高-
标签:字符串哈希表排序
创建: 2026-10-01 04:41
更新: 2026-10-07 13:50
形式化题目
给定一个长度为
定义合法子串为长度
选出出现频次最高的前
- 先输出该频次数值;
- 再输出拥有该频次的所有子串,子串先按长度升序排序,长度相同时按二进制字典序升序排序;每行最多输出 6 个子串,空格分隔。
正解:滑动窗口统计与多级排序
思路
本题参数规模具有显著特征:子串长度上限
-
状态空间与规模分析:
- 长度在
之间的二进制模式串总数上限为 种。 - 这意味着无论原序列多长,互不相同的模式串最多只有 8190 种,完全可以直接用哈希表/字典统计频次。
- 长度在
-
滑动窗口计数:
- 外层遍历所有待统计的目标长度
; - 内层遍历起始位置
,截取子串 并在计数器中累加频次。 - 总截取与哈希插入操作次数约为
,在常数极小的字符串切片操作下可瞬时完成。
- 外层遍历所有待统计的目标长度
-
分桶与排序规则:
- 将所有出现过的模式串按出现频次归类到一个以频次为键的桶中。
- 频次按从大到小降序排序,取前
个频次。 - 相同频次内的模式串按
(len(s), s)排序:由于 Python 字符串比较按字符 ASCII 字典序进行,而'0' < '1',因此直接比较字符串天然等价于二进制字典序。
-
格式化输出:
- 遍历前
个频次及其对应的排序后列表; - 先单独输出频次数值;
- 模式串列表按步长 6 切块并用空格连接,每行最多输出 6 个。
- 遍历前
图示解析
以样例 A=2, B=4, N=10,输入片段 01010010... 为例:
flowchart TD
A["输入 01 序列 (长达 200,000)"] --> B["滑动窗口切片 (长度 len ∈ [2, 4])"]
B --> C["哈希表计数 Counter (至多 8190 种模式串)"]
C --> D["按频次分桶 (频次 freq 降序)"]
D --> E["组内多关键字排序: (长度升序, 二进制字典序升序)"]
E --> F["格式化输出: 单独行频次 + 每行最多 6 个模式串"]
| 频次 | 包含的模式串(排序后) | 行格式化效果 |
|---|---|---|
| 23 | 00 |
2300 |
| 15 | 01, 10 |
1501 10 |
| 12 | 100 |
12100 |
| 11 | 11, 000, 001 |
1111 000 001 |
| 5 | 011, 110, 1000 |
5011 110 1000 |
代码
Python 版:
python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-03-31 10:00
# update_at: 2026-03-31 10:00
import sys
from collections import Counter, defaultdict
def count_patterns(seq: str, min_len: int, max_len: int) -> Counter[str]:
"""统计字符串中所有长度在 [min_len, max_len] 之间的连续子串频次。"""
n = len(seq)
counts: Counter[str] = Counter()
for length in range(min_len, max_len + 1):
if length > n:
break
for i in range(n - length + 1):
counts[seq[i : i + length]] += 1
return counts
def group_and_sort_patterns(counts: Counter[str]) -> list[tuple[int, list[str]]]:
"""将子串按出现频次降序分桶,同一频次内按长度升序、二进制字典序升序排序。"""
freq_map: defaultdict[int, list[str]] = defaultdict(list)
for pattern, freq in counts.items():
freq_map[freq].append(pattern)
# 频次从大到小排序;同频次内:长度递增、二进制字典序('0' < '1')递增
return [
(freq, sorted(patterns, key=lambda s: (len(s), s)))
for freq, patterns in sorted(freq_map.items(), key=lambda item: -item[0])
]
def format_frequency_groups(groups: list[tuple[int, list[str]]], top_n: int) -> list[str]:
"""将前 top_n 个频次组按每行最多 6 个模式串的规则格式化为输出行。"""
lines: list[str] = []
for freq, patterns in groups[:top_n]:
lines.append(str(freq))
for i in range(0, len(patterns), 6):
lines.append(" ".join(patterns[i : i + 6]))
return lines
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
a = int(next(data))
b = int(next(data))
n = int(next(data))
seq = "".join(token.decode() for token in data)
counts = count_patterns(seq, a, b)
groups = group_and_sort_patterns(counts)
lines = format_frequency_groups(groups, n)
print("\n".join(lines))
if __name__ == "__main__":
solve()C++ 版(同一算法):
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 09:41
* update_at: 2026-10-06 09:41
*/
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MAX_SEQ = 200005; // 输入 01 序列的最大长度
const int MAX_B = 12; // 子串长度上限 B <= 12
const int VALUE_SPACE = 1 << MAX_B; // 单个长度下模式串的数值上界 2^12
char seq[MAX_SEQ]; // 输入的 01 序列
int cnt[MAX_B + 1][VALUE_SPACE]; // cnt[len][value]:定长 len 的模式串出现次数
struct Pattern {
int freq; // 该模式串的出现次数
int len; // 模式串长度
int value; // 模式串二进制对应的数值,定长时数值顺序即二进制字典序
};
Pattern pats[1 << (MAX_B + 1)]; // 不同模式串最多 sum(2^len) < 8192 个
// 排序规则:频次降序,其次长度升序,同长度按二进制字典序(数值)升序
bool cmp(const Pattern &a, const Pattern &b) {
if (a.freq != b.freq) return a.freq > b.freq;
if (a.len != b.len) return a.len < b.len;
return a.value < b.value;
}
// 按定长输出模式串的二进制形式,高位不足补 0
void print_pattern(int value, int len) {
char buf[MAX_B + 1];
for (int i = 0; i < len; i++) {
int bit = (value >> (len - 1 - i)) & 1;
buf[i] = bit ? '1' : '0';
}
buf[len] = '\0';
printf("%s", buf);
}
int main() {
int A, B, N;
if (scanf("%d %d %d", &A, &B, &N) != 3) return 0;
// 读入序列:可能分成多行,直接拼接所有 0/1 字符
int seq_len = 0;
char token[1024];
while (scanf("%1023s", token) == 1) {
for (int i = 0; token[i] != '\0'; i++) {
seq[seq_len] = token[i];
seq_len++;
}
}
// 对每个目标长度做滑动窗口,累计各模式串出现次数
for (int len = A; len <= B; len++) {
if (len > seq_len) break;
int mask = (1 << len) - 1;
int value = 0;
for (int i = 0; i < len; i++) {
value = (value << 1) | (seq[i] - '0');
}
cnt[len][value]++;
for (int i = len; i < seq_len; i++) {
value = ((value << 1) | (seq[i] - '0')) & mask;
cnt[len][value]++;
}
}
// 收集所有出现过的模式串
int m = 0;
for (int len = A; len <= B; len++) {
if (len > seq_len) break;
int total = 1 << len;
for (int value = 0; value < total; value++) {
if (cnt[len][value] > 0) {
pats[m].freq = cnt[len][value];
pats[m].len = len;
pats[m].value = value;
m++;
}
}
}
sort(pats, pats + m, cmp);
// 按频次分组输出,最多输出前 N 种频次,每组内每行最多 6 个模式串
int printed_freq = 0;
int i = 0;
while (i < m && printed_freq < N) {
int freq = pats[i].freq;
printf("%d\n", freq);
int idx = 0;
int j = i;
while (j < m && pats[j].freq == freq) {
if (idx > 0) {
if (idx % 6 == 0) printf("\n");
else printf(" ");
}
print_pattern(pats[j].value, pats[j].len);
idx++;
j++;
}
printf("\n");
printed_freq++;
i = j;
}
return 0;
}复杂度
- 时间复杂度:
,其中 , , 为不同模式串的种类上限。字符串滑动窗口切片和哈希计数开销约为 次操作,耗时约 ,排序与输出开销微秒级,远低于时限 1.0s。 - 空间复杂度:
。存储输入序列需要约 ,哈希表与分桶列表最多存储 个小字符串,内存占用 ,远低于 128MB 限制。
总结
- 核心突破口在于注意到模式串的最大长度
极小,二进制模式串的总数被限制在 8190 之内。 - 借助滑动窗口与哈希表直接统计连续子串频次,再结合 Python 的多关键字排序
(len(s), s)和步长切片patterns[i:i+6],能够简洁而严谨地满足复杂的排版要求。