把路线看成 4×N 网格图上的哈密顿回路,按列扫描时切口上的横边只有 7 种配对方式,逐列线性递推即可得到答案。
OJ: roj
题目 ID: 2094
难度:提高+/省选-
标签:pythonusaco动态规划状态压缩递推计数
创建: 2026-10-01 08:40
更新: 2026-10-07 13:50
形式化题目
郊区由 4 条东西向街道与 N 条南北向街道正交组成,相邻街道的交叉口共有
要求数出满足下列条件的行驶路线条数:从
两条路线只要边的使用顺序不同就算不同,因此沿同一条闭合回路顺时针走与逆时针走算两条。
@ 是邮局,o 是其他交叉口,- / | 是被走过的街道)。
@- o- o @- o- o
| | | |
o o- o o- o o
| | | |
o o- o o- o o
| | | |
o- o- o o- o- o@- o- o- o @- o o- o @- o o- o
| | | | | | | | | |
o o- o o o o- o o o o o o
| | | | | | | | | |
o o o o o o- o o o o- o o
| | | | | | | | | |
o- o o- o o- o o- o o- o- o- o
@- o- o- o @- o- o- o @- o- o- o
| | | | | |
o o- o- o o- o o- o o- o- o o
| | | | | |
o o- o- o o- o o- o o- o- o o
| | | | | |
o- o- o- o o- o- o- o o- o- o- o观察要点:每条路线在网格里都是一个闭合的圈,每个交叉口恰好用掉两条街道。
这正是图论里的哈密顿回路;把起点固定在
正解
思路
建模:从「走法」到「选边」
要求「每个交叉口恰好经过一次」,等价于在每个交叉口上恰好用掉两条街道: 一条进、一条出(无向意义下就是选中两条相邻的街道)。于是问题变成:
在
网格图的边集中选出一个子集,使每个顶点的度数恰好为 2,且选出的边恰好构成一个环。
「度数全为 2」是纯局部条件,逐列处理时只需检查当前列;而「只有一个环」这个连通性条件, 可以通过记住「切口上哪些端点属于同一条未完成的路径」来追踪。
关键观察:竖边不跨列,切口上只有配对信息
所有南北向街道都只在同一列内连接相邻两行,所以扫描可以一列一列地进行。
处理第
每个顶点的度数恒为 2,因此在第
花括号里的数字表示配在一起的两行,例如
一列的合法性判定
固定旧切面后,枚举本列的竖边子集(
- 一个连通块恰好有两个朝右端口 → 给出新切面的一组配对;
- 一个连通块两个端口都朝左 → 闭环了,此时若还有别的连通块、或右侧仍伸着横边,则整列不合法。
由此得到转移表(行 = 旧切面,中间几列 = 新切面,末列 = 本列收口的方案数):
| 旧切面 | 收口 | ||||||
|---|---|---|---|---|---|---|---|
| 1 | 1 | ||||||
| 1 | 1 | ||||||
| 1 | 1 | ||||||
| 1 | 1 | ||||||
| 1 | |||||||
| 1 | 1 | 1 | 1 | 1 | |||
| 1 | 1 | 1 | 1 |
表格读法:从
| 已扫过列数 | 本列收口 | |||||||
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 2 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 |
| 3 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 0 |
| 4 | 0 | 2 | 4 | 1 | 2 | 3 | 2 | 0 |
| 5 | 0 | 6 | 8 | 4 | 6 | 7 | 6 | 0 |
| 6 | 0 | 14 | 23 | 8 | 14 | 19 | 14 | 37 |
第 1 列左侧没有横边,只有空切面一种状态;扫描到第 6 列时收口 37 次,
乘以方向因子 2 恰好是
为什么可以只做 次转移
第一列左侧的空切面是唯一的初始状态,前两个转移结果固定:
- 第 1 列之后:
与 各 1; - 第 2 列之后:
、 、 、 、 、 各 1。
此后每往右一列乘一次上面的转移矩阵,共
main.py 把这个矩阵按列写成 7 个变量的一次元组赋值,与上面的转移表逐列对应。
代码
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-10-02 10:20
# update_at: 2026-10-02 10:20
import sys
# 路线 = 走遍 4×N 个路口、回到邮局的闭合走法,即 4×N 网格上的哈密顿回路。
# 南北向街道只在同一列内连接相邻两行,所以可以从西往东一列一列地决定用哪些边;
# 每个路口度数恰好为 2,于是扫到某条列间切口时只需记住"跨过切口的横边两两配成一组"
# 这一件事,而 4 行之间只有 7 种配得出来的切面:
#
# ∅、{01}、{03}、{12}、{23}、{01,23}、{03,12} (数字 = 配对的两行)
#
# 下表是"扫过一列"的转移(行=旧切面,列=新切面,末列=本列收口数)。
#
# | {03} {01,23} | {01} {23} {12} {03,12} | 收口
# ∅ | 1 1 | |
# {01} | 1 1 | |
# {23} | 1 1 | |
# {01,23} | 1 1 | |
# {12} | 1 | |
# {03} | | 1 1 1 1 | 1
# {03,12} | | 1 1 1 | 1
def count_routes(n: int) -> int:
"""4×n 网格上从左上角出发、每个路口恰好经过一次的闭合路线条数。"""
# 邮局左边的切口:还没有横边跨过来,只有空切面
empty, s01, s03, s12, s23, s01_23, s03_12 = 1, 0, 0, 0, 0, 0, 0
for _ in range(n - 1): # 扫过第 1 … n-1 列;第 n 列右侧没有横边,只能收口
# 右端全部取旧值;每一行对应上表的一列
empty, s01, s03, s12, s23, s01_23, s03_12 = (
0, # 空切面只在第一列左侧存在
s03 + s03_12, # 新 {01}
empty + s01 + s12 + s23 + s01_23, # 新 {03}
s03, # 新 {12}
s03 + s03_12, # 新 {23}
empty + s01 + s23 + s01_23, # 新 {01,23}
s03 + s03_12, # 新 {03,12}
)
return 2 * (s03 + s03_12) # 收口处的一条回路,顺时针与逆时针是两条路线
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
print(count_routes(n))
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-06 13:15
* update_at: 2026-10-06 13:15
*/
// 4×N 网格上的哈密顿回路计数:按列扫描,切口上横边的配对方式只有 7 种,
// 逐列做一次固定的线性转移;N=1000 时答案约 340 位,用压位高精度加法维护。
#include <cstdio>
typedef long long ll;
const ll BASE = 1000000000LL; // 大数按 10^9 压位,每个数组元素存 9 位十进制
const int MAXL = 55; // N=1000 时答案约 340 位(39 段),多留空间给进位
// 状态编号(切口上配在一起的两行):
// 0: 空切面 1:{01} 2:{03} 3:{12} 4:{23} 5:{01,23} 6:{03,12}
ll big[7][MAXL];
int len[7]; // big[s] 当前的段数
ll tmp[7][MAXL];
int tlen[7]; // 下一列的转移结果,算完再拷回 big,避免边加边覆盖
// tmp[dst] += big[src](把旧列的状态加进新列的某个状态)
void add_old(int dst, int src) {
if (len[src] > tlen[dst]) tlen[dst] = len[src];
for (int i = 0; i < len[src]; i++) {
tmp[dst][i] += big[src][i];
if (tmp[dst][i] >= BASE) { // 压位加法的进位
tmp[dst][i] -= BASE;
tmp[dst][i + 1] += 1;
if (tlen[dst] < i + 2) tlen[dst] = i + 2;
}
}
}
void solve() {
int n;
scanf("%d", &n);
// 邮局左侧切口是空的:只有空切面一种初始状态
len[0] = 1;
big[0][0] = 1;
// 扫过第 1 … n-1 列;第 n 列右侧没有横边,只能收口
if (n == 1) { // 只有一列 4 个路口连不成环,路线数为 0
printf("0\n");
return;
}
for (int col = 1; col <= n - 1; col++) {
// 新列的临时结果整块清零,避免上一列残留值混进答案
for (int s = 0; s < 7; s++)
for (int i = 0; i < MAXL; i++) tmp[s][i] = 0;
for (int s = 0; s < 7; s++) tlen[s] = 0;
// 转移表:新 {01}、{23}、{03,12} 来自旧 {03} 和 {03,12}
add_old(1, 2); add_old(1, 6);
add_old(4, 2); add_old(4, 6);
add_old(6, 2); add_old(6, 6);
// 新 {03} 来自除 {03}、{03,12} 外的任意旧切面
add_old(2, 0); add_old(2, 1); add_old(2, 3); add_old(2, 4); add_old(2, 5);
// 新 {01,23} 来自旧 空切面、{01}、{23}、{01,23}
add_old(5, 0); add_old(5, 1); add_old(5, 4); add_old(5, 5);
// 新 {12} 只来自旧 {03}
add_old(3, 2);
// 空切面只在第一列左侧出现,转移后恒为 0
for (int s = 0; s < 7; s++) {
len[s] = tlen[s];
for (int i = 0; i < len[s]; i++) big[s][i] = tmp[s][i];
}
}
// 收口数 = {03} + {03,12};每条回路顺、逆时针算两条路线,再乘 2
// 用大数加法直接算 ans = 2 * (big[2] + big[6])
ll ans[MAXL];
int alen = 0;
if (len[2] > len[6]) alen = len[2]; else alen = len[6];
for (int i = 0; i < alen; i++) {
ll s = 0;
if (i < len[2]) s += big[2][i];
if (i < len[6]) s += big[6][i];
ans[i] = s;
}
for (int i = 0; i < alen; i++) ans[i] *= 2; // 方向因子 2
for (int i = 0; i < alen; i++) { // 逐位进位
if (ans[i] >= BASE) {
ans[i + 1] += ans[i] / BASE;
ans[i] %= BASE;
if (alen < i + 2) alen = i + 2;
}
}
while (alen > 1 && ans[alen - 1] == 0) alen--; // 去掉前导零段
printf("%lld", ans[alen - 1]); // 最高段不补零
for (int i = alen - 2; i >= 0; i--) printf("%09lld", ans[i]); // 其余段补足 9 位
printf("\n");
}
int main() {
solve();
return 0;
}复杂度
- 时间复杂度:
。共进行 次转移,每次是常数条大整数加减。 时答案约 340 位十进制数,大整数加减也是常数级开销。 - 空间复杂度:
额外变量(不计输入输出与答案本身占用的位数)。
总结
- 先做翻译:把「每个交叉口恰好一次并回到起点」翻译成「网格图上每个顶点度数为 2 的哈密顿回路」。
- 再做削减:4 行这个固定宽度让「按列扫描 + 切口配对」成为可行模型, 切口状态只有 7 种,转移是常系数线性递推。
- 最后核对:
输出 0(一列 4 个点连不成环), 输出 12,与样例一致。
图示解析
下面这张图概括从题意到答案的主线(节点不超过 12 个,只用来说明模型关系,不重复代码):
题意:邮局出发走遍每个路口一次再回到邮局
|
v
翻译:网格图上的哈密顿回路
|
v
观察:南北向街道只在同一列内(竖边不跨列)
|
v
按列扫描:切口上只需记"横边两两配对的 7 种方式"
|
v
每列用"每个路口度数为 2"本地筛掉非法组合
|
v
逐列乘 7 种切面的转移矩阵(共 N-1 次,O(N))
|
v
第 N 列收口数 × 2(顺/逆时针各算一条)
|
v
答案看这张图时注意三点:横向的「翻译」一步把走法问题变成了选边问题,是所有后续推导的前提; 中间的「按列扫描」依赖竖边不跨列这一几何事实;最后一行的乘 2 来自题目把方向不同的走法视为不同路线。
