把长度 K 的 01 子串视为 de Bruijn 图的边,用 0 优先的 Hierholzer 求欧拉回路得到 M=2^K 的环,再旋转到唯一的全 0 串处,得到字典序最小方案。
OJ: roj
题目 ID: 1532
难度:提高+/省选-
标签:欧拉路图论构造
创建: 2026-09-30 16:32
更新: 2026-10-07 11:01
形式化题目
给定整数
长度为
样例:8 00010111。把 00010111 首尾相接,8 个子串依次为
正解
思路
1. 朴素做法与瓶颈
最朴素的想法:枚举所有候选环形串(若环长取到
2. 关键观察:相邻子串错位一位 de Bruijn 图
环上相邻两个子串
- 节点:所有
位 01 串,共 个(代码里用整数 表示); - 边:节点
( 位)有两条出边,标号 ,对应 位串 “ 拼接 ”, 终点是该 位串的后 位,即 ; - 每个
位串与一条边一一对应(前 位定起点,末位是标号)。
下图是
graph LR
n00["00"]
n01["01"]
n10["10"]
n11["11"]
n00 -->|0| n00
n00 -->|1| n01
n01 -->|0| n10
n01 -->|1| n11
n10 -->|0| n00
n10 -->|1| n01
n11 -->|0| n10
n11 -->|1| n11
图中每个节点都是 2 位串,各有两条出边:例如从 00 出发的 0 边对应子串 000、
1 边对应子串 001。8 个节点度数完全对称(每个节点出 2 入 2),8 条边恰好一一对应
全部 8 个 3 位串——在图上找出一条经过所有边恰好一次的回路,就得到样例答案。
于是得到本题的核心等价关系:
回路上各边的标号依次连起来,就是所求的环形串;每条边恰用一次
3. 欧拉回路一定存在( 可达)
- 度数平衡:每个
位串既是有 2 个 串的前缀、也是 2 个 串的后缀, 所以每个节点出度 = 入度 = 2; - 强连通:任意节点沿标号 0 的边连续走
步(不断左移补 0)必到全 0 节点; 反过来从全 0 节点按任意目标串的各位依次走,可到达任何节点。
有向图"度数平衡 + 强连通" 1 << K。
4. 求欧拉回路:Hierholzer(0 优先)
Hierholzer 算法从起点出发沿未用边深入,走不动时才回溯并把"进入边"后序记录
(代码里的 emit),最终把记录逆序(reversed(emit))即得回路 circuit。
实现上有两个要点:
- 每个节点只有 2 条出边,按 0 → 1 的顺序尝试(这是后面字典序最小的伏笔);
- 用显式栈
[(节点, 进入边标号)]代替递归:时回路长达 , 迭代不受 Python 递归深度限制。
5. 字典序最小:旋转到全 0 串 + 优先 0
旋转不变性:环形串的任意旋转仍是合法方案。把回路旋转到环上唯一的 circuit + circuit 上 find('0' * K),因为
优先 0 性质:0 优先的回路旋转后满足——沿环按顺序走边,只要当前节点的标号 0 边
还没被用过,走的一定是 0。直观原因是 Hierholzer 在每个节点都先沿 0 边深入、把 0 侧的
子回路走完才轮到 1 边;该性质也可以在结果串上直接验证,对 00010111(起点节点取环上第 0 步的前驱,即末尾 11):
| 步骤 | 当前节点 | 标号 | 对应 |
|---|---|---|---|
| 0 | 11 | 0 | 110 |
| 1 | 10 | 0 | 100 |
| 2 | 00 | 0 | 000 |
| 3 | 00 | 1 | 001 |
| 4 | 01 | 0 | 010 |
| 5 | 10 | 1 | 101 |
| 6 | 01 | 1 | 011 |
| 7 | 11 | 1 | 111 |
观察表格:8 步恰好走出全部 8 个互不相同的
唯一性
代码
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-09-30 16:34
# update_at: 2026-09-30 16:34
import sys
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
K = next(data)
mask = (1 << (K - 1)) - 1 # 节点只保留低 K-1 位(de Bruijn 图的后缀)
edge_used = [False] * (1 << K) # 边 (u << 1) | b 正好对应一个长度 K 的 01 串
# 迭代式 Hierholzer 欧拉回路:栈元素为 (节点, 进入该节点的边标号),起点记 -1。
emit: list[int] = [] # 后序记录的边标号,逆序才是回路走向
stack: list[tuple[int, int]] = [(0, -1)]
while stack:
u, entry = stack[-1]
for b in (0, 1): # 每个节点至多两条出边,先试 0 再试 1
e = (u << 1) | b
if not edge_used[e]:
edge_used[e] = True
stack.append((e & mask, b)) # 沿边走到新的 K-1 位后缀
break
else: # 出边耗尽才回溯,此时记录进入边
stack.pop()
if entry >= 0:
emit.append(entry)
circuit = ''.join(map(str, reversed(emit))) # 边标号序列 = 环上的 01 串
head = (circuit + circuit).find('0' * K) # 全 0 子串环上唯一,旋到开头即字典序最小
ring = circuit[head:] + circuit[:head]
print(f"{1 << K} {ring}")
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* 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-05 06:54
* update_at: 2026-10-05 06:54
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXK = 11;
const int MAXE = 1 << MAXK; // 边数最多 2^11 = 2048
typedef long long ll;
// de Bruijn 图:节点是 K-1 位 01 串,边是 K 位 01 串。
// 节点 u 追加标号 b 后走到 ((u << 1) | b) 的后 K-1 位。
// 边编号 e = (u << 1) | b 恰好就是该 K 位串的整数值,所以直接用 e 标记边。
ll K;
ll mask; // 只保留低 K-1 位的掩码
bool edge_used[MAXE]; // edge_used[e] 表示长度 K 的 01 串 e 是否已经被用过
ll stack_node[MAXE]; // Hierholzer 显式栈:栈顶节点的 K-1 位编号
int stack_entry[MAXE]; // 进入该节点时走过的边标号(只有 0/1),起点记为 -1
int emit[MAXE]; // 后序记录的边标号,逆序才是欧拉回路的走向
char circuit[MAXE]; // 欧拉回路的边标号序列,即环上的 01 串
char ring[MAXE]; // 旋转到全 0 子串开头后的最终方案
// 迭代式 Hierholzer:每个节点先试 0 边再试 1 边,出边耗尽才回溯。
void build_circuit() {
ll top = 0;
stack_node[0] = 0;
stack_entry[0] = -1;
ll emit_cnt = 0;
while (top >= 0) {
ll u = stack_node[top];
bool moved = false;
for (int b = 0; b <= 1; b++) {
ll e = (u << 1) | b;
if (!edge_used[e]) {
edge_used[e] = true;
top++;
stack_node[top] = e & mask;
stack_entry[top] = b;
moved = true;
break;
}
}
if (!moved) {
int entry = stack_entry[top];
top--;
if (entry >= 0) {
emit[emit_cnt] = entry;
emit_cnt++;
}
}
}
// emit 是后序记录,逆序即为回路经过各边的顺序。
for (ll i = 0; i < emit_cnt; i++) {
circuit[i] = '0' + emit[emit_cnt - 1 - i];
}
circuit[emit_cnt] = '\0';
}
// 把回路旋转到唯一的 0^K 子串处开头,得到字典序最小的方案。
void rotate_to_all_zero() {
ll len = 1LL << K;
ll head = -1;
for (ll i = 0; i < len && head < 0; i++) {
bool all_zero = true;
for (ll j = 0; j < K; j++) {
if (circuit[(i + j) % len] != '0') {
all_zero = false;
break;
}
}
if (all_zero) {
head = i;
}
}
for (ll i = 0; i < len; i++) {
ring[i] = circuit[(head + i) % len];
}
ring[len] = '\0';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> K;
mask = (1LL << (K - 1)) - 1;
build_circuit();
rotate_to_all_zero();
cout << (1LL << K) << ' ' << ring << '\n';
return 0;
}复杂度
- 时间:节点数
、边数 ,每条边只被标记与弹栈各一次, , 与标准 C++ 解法同阶。 - 空间:
edge_used、显式栈与输出串均为; 时最多 2048 条边, 远低于 1000ms / 128MB 限制,实测单组约 0.02s。
总结
本题是 de Bruijn 序列的经典构造:先用计数得到上界