把环上滑动窗口建成 de Bruijn 图,每个节点只有两条出边,用 Hierholzer 算法求欧拉回路即得 M=2^k 的最短超排列环。
OJ: roj
题目 ID: 3217
难度:普及+/提高-
标签:图论欧拉回路Hierholzer构造
创建: 2026-10-02 02:30
更新: 2026-10-10 11:30
形式化题目
给定整数
例如 00010111:从 8 个位置出发读 3 位,得到
正解
思路
第一步:把环看成"窗口序列"。 设环串为
第二步:把窗口序列建模成图上的回路。 把每个长度为
第三步:判断
以
graph LR
A["00"] -- "0 : 000" --> A
A -- "1 : 001" --> B["01"]
B -- "0 : 010" --> C["10"]
B -- "1 : 011" --> D["11"]
C -- "0 : 100" --> A
C -- "1 : 101" --> B
D -- "0 : 110" --> C
D -- "1 : 111" --> D
每个节点恰好两条出边、两条入边;8 条边就是全部 8 个长度为 3 的 01 串,走完所有边一次回到起点,环串就是沿途追加的位。
| 步 | 当前窗口(前 2 位=节点) | 新追加位 | 下一个窗口 | 途经的长度 3 串 |
|---|---|---|---|---|
| 1 | 00 |
0 | 00 |
000 |
| 2 | 00 |
1 | 01 |
001 |
| 3 | 01 |
0 | 10 |
010 |
| 4 | 01 |
1 | 11 |
011 |
| 5 | 10 |
1 | 01 |
101 |
| 6 | 11 |
1 | 11 |
111 |
| 7 | 11 |
0 | 10 |
110 |
| 8 | 10 |
0 | 00 |
100 |
样例 00010111 的 8 步滑动过程(首尾相接):
| 步 | 当前窗口(前 2 位=节点) | 新追加位 | 下一个窗口 | 途经的长度 3 串 |
|---|---|---|---|---|
| 1 | 00 |
0 | 00 |
000 |
| 2 | 00 |
1 | 01 |
001 |
| 3 | 01 |
0 | 10 |
010 |
| 4 | 01 |
1 | 11 |
011 |
| 5 | 10 |
1 | 01 |
101 |
| 6 | 11 |
1 | 11 |
111 |
| 7 | 11 |
0 | 10 |
110 |
| 8 | 10 |
0 | 00 |
100 |
观察上表:走到的长度 3 串恰好把 8 个串各出现一次,第 8 步回到起点 00,这条窗口序列就是上图的一条欧拉回路。
第四步:用 Hierholzer 算法求回路并保证字典序最小。 从全零节点 0),每次优先走编号较小的出边(即先尝试追加 0)。用非递归 Hierholzer:维护一个栈,栈顶还有没用过的出边就走出去并标记该边用过,出边用尽就弹栈记录节点;所有节点按弹出顺序的逆序拼起来就是回路方向上的节点访问次序。
为什么这样得到字典序最小?两条经典保证叠加:
- 回路从起点
开始,每一步在两条出边中先尝试 0边,得到的是"贪心字典序"的回路遍历方向——后出栈的节点在回路中更靠前,等于从起点开始总先试 0 边。 - de Bruijn 图每个点出度恰好为 2,且从任一点出发的欧拉回路中,先走小编号边的贪心不会"堵死"回路(回溯阶段的逆后序拼接自动补齐未走完的分支)。
第五步:还原环串。 回路上相邻节点之间隔着一条边,边带来的新位就是环串里一个新字符。把回路写成 0),之后每一步追加
代码
/**
* 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-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int width; // 节点数 = 2^(k-1)
vector<char> used; // 边 v*2+b 是否已走
vector<int> route; // 逆后序节点序列
// de Bruijn 图上的 Hierholzer,优先走 0 边
void euler_route() {
vector<int> stack;
stack.push_back(0);
while (!stack.empty()) {
int v = stack.back();
int base = v << 1;
int nxt = -1;
for (int b = 0; b < 2; b++) {
if (!used[base + b]) { nxt = base + b; break; }
}
if (nxt == -1) {
route.push_back(stack.back());
stack.pop_back();
} else {
used[nxt] = 1;
stack.push_back(nxt & (width - 1));
}
}
reverse(route.begin(), route.end());
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int k;
cin >> k;
width = 1 << (k - 1);
int total = 1 << k;
used.assign(width << 2, 0);
euler_route();
string bits;
for (int i = 0; i < k - 1; i++) bits.push_back('0');
for (size_t i = 1; i < route.size(); i++)
bits.push_back('0' + (route[i] & 1));
cout << total << " " << bits.substr(0, total) << "\n";
return 0;
}#!/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-07-05 21:47
# update_at: 2026-07-05 21:47
import sys
def euler_route(width: int, start: int = 0) -> list[int]:
"""在以 k-1 位窗口为节点的 de Bruijn 图上跑 Hierholzer,返回欧拉回路途经的节点序列。
节点 v 代表当前窗口,加一位 b 得到的边编号为 v*2+b,到达窗口 (v*2+b) 去掉最高位。
每个节点恰好两条出边,全图 M=2^k 条边,回路存在且唯一覆盖所有窗口。
优先走 0 边、逆后序还原,得到字典序最小的回路。
"""
used = bytearray(width << 2) # 边 v*2+b 是否已走:共 2^k 条
stack = [start]
route: list[int] = []
while stack:
v = stack[-1]
base = v << 1
nxt = next((base + b for b in (0, 1) if not used[base + b]), None)
if nxt is None: # 出边用尽,回溯时记录,逆序即为回路方向
route.append(stack.pop())
else:
used[nxt] = 1
stack.append(nxt & (width - 1)) # 新窗口 = 去掉最高一位
route.reverse()
return route
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
k = next(data) # 题面唯一的输入:子串长度 k
width = 1 << (k - 1) # 节点数:长度 k-1 的窗口
total = 1 << k # 环上传感器数 M = 2^k
route = euler_route(width)
# 起点窗口是 k-1 个 0,之后每条边贡献一个新位;环是循环的,截取前 M 位
bits = "0" * (k - 1) + "".join(str(u & 1) for u in route[1:])
print(total, bits[:total])
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:图中节点数
、边数 ,Hierholzer 算法每条边走一次、每个点进出栈一次,为 。 - 空间复杂度:
,即边标记数组和栈、回路的长度。
总结
- 本题把"环上滑动窗口各不相同"翻译成图论模型:长度
的串是节点、长度 的串是边,环串对应图上一条欧拉回路。 - 每个节点恰有两条出边、两条入边且图连通,所以欧拉回路必然存在,最大
。 - Hierholzer 算法用"栈 + 逆后序"在
时间内求出回路;从全零节点出发、每步优先走 0边并逆后序还原,得到字典序最小的环串。 - 关键实现细节:节点编号
的两条出边是 (追加 0)和(追加 1),到达的新节点是;环串末尾无需真正接回开头,输出时按循环串截取前 位即可。