AIGC标识 洛谷 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\),那么所有形如

\[d_r,\; d_r+w,\; d_r+2w,\; \dots,\; d_r+k w \le h-1 \]

的高度都可达。
数量为:

\[\left\lfloor \frac{h-1 - d_r}{w} \right\rfloor + 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 生成

posted @ 2026-08-11 21:34  Void__114  阅读(33)  评论(0)    收藏  举报