以最小步长为模建立余数图,Dijkstra 求每类余数最早可达楼层。
OJ: luogu
题目 ID: P3403
难度:提高
标签:同余最短路Dijkstra数学python
创建: 2026-07-17 03:00
更新: 2026-10-21 09:59
题意
从 1 楼出发,每次只能向上跳
思路
直接做卡在哪里
从 1 楼开始逐层标记可达性是正确做法,但
该压缩的维度是“楼层”
三个步长都很小,所以可枚举的量是步长而不是楼层。
关键抓手是同余。模
这一求和是合法的,因为同余分类是划分,每个楼层属于且仅属于一个类,不会重复计算。
为什么选最小步长当模
三个步长都能当模,选最小只是为了图小。
| 顶点数 | 答案 | 是否与逐层暴力一致 | |
|---|---|---|---|
| 3 | 96 | 是 | |
| 5 | 96 | 是 | |
| 8 | 96 | 是 |
答案完全相同,区别只在余数类数量,也就是图上点数。所以取
但
| 结果 | |
|---|---|
| 96,与暴力一致 | |
| 96,与暴力一致 | |
| 96,与暴力一致 | |
| 98,多算 2 层 | |
| 97,多算 1 层 |
为什么按余数类计数不漏
“每类只需记
方向一(不多):公式生成的每个楼层都真的可达。
这是构造性的。
方向二(不漏):任何可达楼层都能写成
这里用反证。设
反证落在哪里:假设
- 若
:但 本身是一个模 余 的可达楼层,它比 更小,与“ 是同类最小”矛盾。 - 若
不是 的倍数:但两者都 ,同余的两个数之差必被 整除,矛盾。
两种可能都不成立,所以
两边夹住——方向一保证不多、方向二保证不漏——所以计数恰好等于可达楼层数。
举个例子:拼一个具体楼层
取
所以
从构造上看更清楚:
逐类核对(
| 余数 | 它们都不可达? | |||
|---|---|---|---|---|
| 0 | 6 | 是 | 是 | |
| 1 | 1 | — | 是 | |
| 2 | 11 | 是 | 是 |
余数 2 那行最能说明问题:
为什么“最早可达楼层”可以用最短路求
把一步
它在
图上一条路径对应一串真实跳跃,路径权和就是楼层增量。所以
这个视角顺带说清了两个问题:顶点数为什么是
求
起点是 1,不是 0
从 1 楼出发,所以
h=15, 步长 [4,7,9] 起点 1 → 9(正确)
起点 0 → 10(错)因为楼层从 1 开始,1 楼本身就可达。
计数
每个余数类独立贡献:
样例 1(
| 余数 | 最早可达 | 贡献楼层 |
|---|---|---|
| 0 | 8 | 8, 12(2 层) |
| 1 | 1 | 1, 5, 9, 13(4 层) |
| 2 | 10 | 10, 14(2 层) |
| 3 | 15 | 15(1 层) |
合计 9 层,与题面给出的
Python 知识
- 余数图只有
个节点,与 无关。 - Python 整数直接支持
范围,不必像 C++ 那样用 unsigned long long。 - 生成器求和只统计
的余数;否则 为负,会多算。 - 堆里用
current != distance[residue]跳过过期条目,与普通 Dijkstra 一致。
代码
#!/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-17 01:40
# update_at: 2026-10-21 09:10
#
# main.py:跳楼机。从 1 楼出发,每次可向上跳 x/y/z 层,统计不超过 h 的可达楼层数。
# h 可达 2^63-1,逐层标记不可行;但三个步长都 <= 1e5,所以该压缩的维度是"楼层"。
# 取最小步长 base,把所有楼层按模 base 的余数分成 base 个等价类(商集 Z/baseZ)。
# 同一类里一旦能到 L,则 L+base、L+2base 也都能到——前提是 base 本身是合法步长。
# 所以每类只需知道最早可达楼层 dist[r],其余全部由「加 base」生成。
# 把一步 s 作用到余数上是平移 r -> (r+s) mod base,它在 Z_base 里封闭,
# 于是整张图就是循环群关于生成集 {x mod base, y mod base, z mod base} 的 Cayley 图:
# 点 base 个、每点 3 条出边,与 h 的规模完全无关。在此基础上跑 Dijkstra 求 dist。
# 最后每类贡献 floor((h-dist[r])/base)+1 层;同余分类是划分,按类求和不会重复。
import sys
from heapq import heappop, heappush
INF = 1 << 62 # 整数哨兵;Python int 任意精度,不必担心 h + step 溢出
def earliest_reachable(height: int, steps: list[int]) -> list[int]:
"""返回 dist:dist[r] 为「模 base 余 r」这一类里的最早可达楼层。
base 取 min(steps):它必须是合法步长(否则「加 base 仍可达」不成立),
取最小只是为了让余数图最小。
"""
base = min(steps)
dist = [INF] * base
start = 1 % base
dist[start] = 1 # 起点是 1 楼,不是 0
heap: list[tuple[int, int]] = [(1, start)] # (楼层, 余数),楼层在前才能按它比大小
while heap:
current, residue = heappop(heap)
if current != dist[residue]:
continue # 堆里的过期记录
for step in steps:
next_residue = (residue + step) % base
candidate = current + step
if candidate < dist[next_residue]:
dist[next_residue] = candidate
heappush(heap, (candidate, next_residue))
return dist
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
height = next(data)
steps = [next(data), next(data), next(data)]
dist = earliest_reachable(height, steps)
base = min(steps)
# 每个余数类独立贡献:最早可达楼层之后,每加一次 base 仍可达。
# 必须过滤 dist > height,否则 (height - dist) 为负会多算。
print(sum((height - value) // base + 1 for value in dist if value <= height))
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-07-17 01:40
* update_at: 2026-10-21 09:10
*/
// 跳楼机:从 1 楼出发,每次可向上跳 x/y/z 层,统计不超过 h 的可达楼层数。
// h 可达 2^63-1,逐层标记不可行;但三个步长都 <= 1e5,所以该压缩的维度是"楼层"。
// 取最小步长 base,把所有楼层按模 base 的余数分成 base 个等价类(商集 Z/baseZ)。
// 同一类里一旦能到 L,则 L+base、L+2base 也都能到——前提是 base 本身是合法步长。
// 所以每类只需知道最早可达楼层 dist[r],其余全部由「加 base」生成。
// 把一步 s 作用到余数上是平移 r -> (r+s) mod base,它在 Z_base 里封闭,
// 于是整张图就是循环群关于生成集 {x mod base, y mod base, z mod base} 的 Cayley 图:
// 点 base 个、每点 3 条出边,与 h 的规模完全无关。在此基础上跑 Dijkstra 求 dist。
// 最后每类贡献 floor((h-dist[r])/base)+1 层;同余分类是划分,按类求和不会重复。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXB = 100000 + 5; // base = min(x,y,z) <= 1e5
ll h; // 楼层上限,可达 2^63-1
int step[3]; // 三个步长 x, y, z
int base_len; // base = min(step),同时是余数类数量
ll dist_arr[MAXB]; // dist_arr[r]:模 base 余 r 这一类里的最早可达楼层
const ll INF = (1LL << 62);
void dijkstra() {
for (int r = 0; r < base_len; r++) dist_arr[r] = INF;
// 起点是 1 楼,不是 0:楼层从 1 开始,1 本身可达
int start = 1 % base_len;
dist_arr[start] = 1;
// (楼层, 余数) 的小根堆
priority_queue<pair<ll, int>, vector<pair<ll, int>>, greater<pair<ll, int>>> heap;
heap.push({1, start});
while (!heap.empty()) {
ll current = heap.top().first;
int residue = heap.top().second;
heap.pop();
if (current != dist_arr[residue]) continue; // 堆里的过期记录
for (int i = 0; i < 3; i++) {
int s = step[i];
int next_residue = (residue + s) % base_len;
ll candidate = current + s;
if (candidate < dist_arr[next_residue]) {
dist_arr[next_residue] = candidate;
heap.push({candidate, next_residue});
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> h;
for (int i = 0; i < 3; i++) cin >> step[i];
base_len = min(step[0], min(step[1], step[2]));
dijkstra();
// 每个余数类独立贡献:最早可达楼层之后,每加一次 base 仍可达
ll answer = 0;
for (int r = 0; r < base_len; r++) {
if (dist_arr[r] <= h) {
answer += (h - dist_arr[r]) / base_len + 1;
}
}
cout << answer << '\n';
return 0;
}复杂度
设
总结
这道题的完整链条是三段,缺一不可:
- 同余类给出有限的状态空间(
个等价类,且是同余划分,所以按类求和不会重复); - 类上的加法封闭把它变成循环群的 Cayley 图,每点只有 3 条出边;
是合法生成元,所以“代表元加 仍在同一类且仍可达”,每类只需记最早可达楼层。
识别信号:题目出现「巨大范围 + 少量固定增量」并问可达数量,就该想同余最短路。接着问两件事——拿哪个量取模(最小的那个,且必须是合法增量),类内最小值和其余值如何互相转化(加一次模数)。