洛谷 P3403 跳楼机 题解
Luogu P3403 跳楼机
题目简述
有一栋 \(h\) 层的摩天大楼,初始在 第 1 层。跳楼机每次可以向上移动 \(x\)、\(y\) 或 \(z\) 层(三种操作),也可以随时回到第 1 层(但这对到达更高楼层没有贡献)。
问:通过若干次向上移动,能够到达的楼层总数是多少?
(\(1 \le h \le 2^{63}-1\),\(1 \le x,y,z \le 10^5\))
问题转化
将楼层编号减 1,得到“高度”:
- 第 1 层 \(\Rightarrow\) 高度 \(0\)
- 第 \(i\) 层 \(\Rightarrow\) 高度 \(i-1\)
每次向上移动 \(a\) 层,相当于高度增加 \(a\)。
因此问题转化为:
求 \([0,\,h-1]\) 范围内,有多少个整数 能被 \(x,y,z\) 的非负整数线性组合表示(即存在非负整数 \(a,b,c\) 使得 \(n = ax+by+cz\))。
初始高度 \(0\) 总是可达的,对应第 1 层。
同余最短路思想
设三种步长中最小值为 \(w = \min(x,y,z)\)。
为什么取 \(w\) 作为“基底”?
- 任意一个可达高度 \(n\),我们关心它模 \(w\) 的余数 \(r = n \bmod w\)。
- 一旦我们知道了最小的可达高度 \(d_r\),它满足 \(d_r \equiv r \pmod w\),那么所有 \(d_r + k \cdot w\)(\(k \ge 0\))也一定可达(只需在 \(d_r\) 的基础上反复加上 \(w\) 即可)。
- 因此,对于每个余数 \(r\)(\(0 \le r < w\)),只需要计算出最小的可达高度 \(d_r\)。
于是我们建立 \(w\) 个状态(节点),代表余数 \(0,1,\dots,w-1\)。
对于每个给定的步长 \(a \in \{x,y,z\}\),从余数 \(u\) 转移到 \((u+a) \bmod w\),边权为 \(a\)(因为增加 \(a\) 高度)。
从起点余数 \(0\) 出发,跑最短路,得到 \(d_r\)。
这样做的正确性依赖于图论:任意一条从 \(0\) 到 \(r\) 的路径,其边权和恰好是一个可达高度(由若干步长相加),且该高度模 \(w\) 的余数就是 \(r\)。最短路径长度就是最小的该余数可达高度。
算法流程
1. 消除公因子
如果 \(g = \gcd(x,y,z) > 1\),那么所有可达高度必然是 \(g\) 的倍数。
将 \(x,y,z\) 和高度上限 \(h-1\) 都除以 \(g\),结果不变,但规模缩小,防止 \(w\) 过大。
2. 确定模数 \(w\)
令 \(w = \min(x,y,z)\)(约分后)。
若 \(w > h-1\),则除了高度 \(0\) 外没有其他可达高度,答案即为 \(1\)(第 1 层)。
3. 建图优化
对于每个余数 \(r\),可能多个步长模 \(w\) 相同,我们只保留其中最小的那个步长,因为更大的步长不可能产生更短的路径(边权只会更大)。
这样边数最多 \(w\) 条。
4. 最短路
使用 Dijkstra(边权为正)计算从余数 \(0\) 出发到所有余数的最短距离 \(d_r\)。
由于 \(d_r\) 的最大值不会超过 \(h-1\),我们可以将 \(INF\) 设为 \(h\)(或 \(h-1+1\))来防止溢出,并提前剪枝。
5. 统计答案
对于每个余数 \(r\),如果 \(d_r \le h-1\),那么所有形如
的高度都可达。
数量为:
将所有余数的贡献累加即为答案。
复杂度分析
- 节点数:\(w \le 10^5\)
- 边数:最多 \(w\) 条(去重后)
- Dijkstra 复杂度:\(O(w \log w)\),完全可行。
细节处理
- 数据类型:\(h\) 最大 \(2^{63}-1\),必须使用
long long。 - INF 设置:在 Dijkstra 函数内动态设为
lim + 1(lim = h-1),保证所有可达距离都能被正确更新。 - 约分顺序:先计算高度上限
lim = h - 1,再与步长一同除以g,避免精度损失。 - 去重:使用
best[r]记录模 \(w\) 余 \(r\) 的最小步长。
参考代码(C++17)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100000 + 5;
int n = 3; // 固定三种步长
ll h, w[5];
ll dist[MAXN];
bool vis[MAXN];
struct Edge {
int r; // 目标余数(转移时的增量)
ll cost;
} edges[MAXN];
ll gcd_all(ll a, ll b) {
return b ? gcd_all(b, a % b) : a;
}
struct Node {
ll d;
int id;
bool operator>(const Node& other) const {
return d > other.d;
}
};
void dijkstra(int mod, int cnt, Edge e[], ll mn, ll lim, ll d[]) {
ll INF = lim + 1; // 大于任何可能的最短路
fill(d, d + mod, INF);
fill(vis, vis + mod, false);
priority_queue<Node, vector<Node>, greater<Node>> pq;
d[0] = 0;
pq.push({0, 0});
while (!pq.empty()) {
auto [cur_d, u] = pq.top(); pq.pop();
if (cur_d > lim || vis[u] || cur_d != d[u]) continue;
vis[u] = true;
if (cur_d + mn > lim) continue; // 剪枝
for (int i = 0; i < cnt; ++i) {
int v = (u + e[i].r) % mod;
ll nd = cur_d + e[i].cost;
if (nd > lim) continue;
if (!vis[v] && nd < d[v]) {
d[v] = nd;
pq.push({nd, v});
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> h;
for (int i = 0; i < 3; ++i) cin >> w[i];
// 1. 求公因子并约分
ll g = 0;
for (int i = 0; i < 3; ++i) g = gcd_all(g, w[i]);
ll lim = h - 1; // 高度上限
if (g > 1) {
for (int i = 0; i < 3; ++i) w[i] /= g;
lim /= g;
}
// 2. 选择最小步长作为模数
ll W = *min_element(w, w + 3);
if (W > lim) { // 只能到达高度 0(第1层)
cout << 1 << '\n';
return 0;
}
// 3. 去重:对每个余数只保留最小步长
int best[MAXN];
fill(best, best + W, -1);
for (int i = 0; i < 3; ++i) {
int r = w[i] % W;
if (best[r] == -1 || best[r] > w[i])
best[r] = (int)w[i];
}
int cnt = 0;
ll mn = LLONG_MAX;
for (int r = 0; r < W; ++r) {
if (best[r] != -1) {
edges[cnt].r = r;
edges[cnt].cost = best[r];
mn = min(mn, (ll)best[r]);
++cnt;
}
}
// 4. 最短路
dijkstra(W, cnt, edges, mn, lim, dist);
// 5. 统计答案
ll ans = 0;
for (int r = 0; r < W; ++r) {
if (dist[r] <= lim) {
ans += (lim - dist[r]) / W + 1;
}
}
cout << ans << '\n';
return 0;
}
总结
- 核心:同余最短路将“组合问题”转化为“图论最短路”,利用模最小值的性质进行压缩。
- 关键点:正确选择模数、去重、约分、统计公式。
- 适用场景:当需要判断“用若干给定数能凑出多少个数”时,同余最短路是常用解法。
本文由 AI 生成

浙公网安备 33010602011771号