D. Frog Jumping 解题思路
核心问题分析
一只青蛙站在数轴的 点,手里攥着两个正整数 和 。在位置 ,它可以跳到 ,也可以跳到 。我们定义 为:在全程不允许跳出 这段区间的前提下,青蛙从 出发能触达的不同整数个数(注意是“能否到达”,不要求一趟跳完,反复横跳也算)。给定 ,要求
数据范围是 ,。 大到离谱,逐个去算 显然是送命。但 很小——这就是突破口的味道: 不会一直“变着花样”地增长,迟早会进入一个干净的数论规律。我们要做的就是抓住“小 阶段”和“大 阶段”这两段,分别用不同的武器收拾它们。
1. 大 阶段:裴蜀定理一出,整个数轴瞬间通透
先看 足够大的时候会发生什么。如果空间足够宽敞,青蛙能用 和 任意组合,能到达的位置就是所有形如 ( 为非负整数、且过程合法)的整数。由裴蜀定理, 的整数线性组合恰好是 的倍数。所以只要 大到不会卡住青蛙的腾挪,可达集合就是 内所有 的倍数:,于是
那“足够大”到底是多大?代码给出的魔法阈值是
当 时,青蛙就有足够的“回旋余地”去把所有 的倍数全部走遍, 彻底退化成上面那个漂亮的闭式。这一段从 一直延伸到 ,长度动辄上亿,绝不能逐个加,得用公式批量结算(见第 4 节)。
2. 小 阶段:让青蛙自己“跑一圈”把答案抖出来
而当 时,空间还不够大,可达集合是逐渐扩张的、不那么规律。这一段的 至多只有 个,规模很小,我们可以老老实实把它模拟出来。代码里限定了这段的上界:
int limit = min(m, T - 1);
vector<int> count(limit + 1, 0);关键问题是:一个点 到底在 取到多少的时候“第一次变得可达”?答案是——取决于到达它的那条路径上,曾经踩过的最高点。如果想摸到 ,路径上必经的最高位置是 ,那么必须 这个点才被解锁。于是我们让青蛙按一条固定的规范路线把所有可达点遍历一遍,沿途记录“走到当前为止踩过的历史最高点 ”,那么每个点就在 这一刻被点亮。
3. 规范贪心路线:能进就进,到顶才退
那条规范路线长这样——在位置 :能往右就往右,逼到墙角再往左。具体地:
while (true) {
if (u < b) {
u += a;
} else {
u -= b;
}
max_val = max(max_val, u);
if (u == 0) break;
if (max_val <= limit) {
count[max_val]++;
}
}规则是:当 时执行 ,否则执行 。为什么这么走?因为 意味着此刻没法安全地 (会跌破 ),那就只能往右冲;一旦 ,就先把欠的“左跳”还掉。这条路线会不重不漏地把一个周期里所有 的倍数(落在 区间内的那些)各访问一次,直到青蛙再次回到 (u == 0 时 break)。
max_val 就是上文说的“历史最高点 ”,它单调不降。每当我们到达一个新点,它被解锁的临界正是当前的 max_val,于是 count[max_val]++——表示“当 恰好等于 max_val 时,新增了一个可达点”。注意起点 在循环外就被记了 count[0] = 1( 时青蛙原地不动,)。
有了 count[],含义就清晰了:count[i] 是“恰好在 时新解锁的点数”。那么对它做前缀和,得到的 cur 就是 本身——窗口放宽到 时累计可达的点数;再把 cur 一路累加,就是这段的 :
int ans = 0;
int cur = 0;
for (int i = 0; i <= limit; i++) {
cur += count[i];
ans += cur;
}一个前缀和套一个前缀和,干净利落。这段循环只跑 次,毫无压力。
4. 大 阶段的批量结算: 的整除求和术
现在回头收拾第 1 节留下的大尾巴。当 时, 从 到 这一整段每个 ,要求的是
那个 累加起来就是项数 terms = R - L + 1,无脑加上即可。麻烦的是前面那坨 。代码用了个辅助函数 sum_floor(R, g),它算的是从 到 的前缀和 ,再用差分 sum_floor(R) - sum_floor(L-1) 取出 这一段:
int sum_floor(int R, int g) {
if (R < 0) return 0;
int q = R / g;
int rem = R % g;
int pairs = (q % 2 == 0) ? (q / 2) * (q - 1) : q * ((q - 1) / 2);
return g * pairs + q * (rem + 1);
}它的原理是:在 里, 取值为 的有 个、取值为 的有 个……一直到取值为 的有 个,最后取值为 的有 个(这里 ,)。所以
代码里那个 pairs 就是在算 ,但它特意按 的奇偶先约分再相乘—— 是偶数就用 , 是奇数就用 ——目的是在 long long 范围内尽量压住中间结果,防止溢出(毕竟 能到 量级)。这是个很地道的小细节。
最后把两段合并:
if (m >= T) {
int L = T;
int R = m;
int sum_div = sum_floor(R, g) - sum_floor(L - 1, g);
int terms = R - L + 1;
ans += sum_div + terms;
}整体复杂度就是模拟那一圈的 加上整除求和的 ,写成
面对 也好、 也罢,全程稳如老狗。我们拿样例 7 5 3 验一下:,,小段把 的 累出来,大段补上 ,最终输出 ,对得上!
CPP 代码实现
// D. Frog Jumping
#include <bits/stdc++.h>
#define lg(x) (63 - __builtin_clzll(x))
#define all(x) (x).begin(), (x).end()
#define low_bit(x) ((x) & (-x))
#define pb push_back
#define db long double
#define int long long
#define sz(x) (int)x.size()
#define endl "\n"
using namespace std;
int sum_floor(int R, int g) {
if (R < 0) return 0;
int q = R / g;
int rem = R % g;
int pairs = (q % 2 == 0) ? (q / 2) * (q - 1) : q * ((q - 1) / 2);
return g * pairs + q * (rem + 1);
}
void solve() {
int m, a, b;
cin >> m >> a >> b;
int g = gcd(a, b);
int T = a + b - g;
int limit = min(m, T - 1);
vector<int> count(limit + 1, 0);
int u = 0;
int max_val = 0;
count[0] = 1;
while (true) {
if (u < b) {
u += a;
} else {
u -= b;
}
max_val = max(max_val, u);
if (u == 0) break;
if (max_val <= limit) {
count[max_val]++;
}
}
int ans = 0;
int cur = 0;
for (int i = 0; i <= limit; i++) {
cur += count[i];
ans += cur;
}
if (m >= T) {
int L = T;
int R = m;
int sum_div = sum_floor(R, g) - sum_floor(L - 1, g);
int terms = R - L + 1;
ans += sum_div + terms;
}
cout << ans << endl;
}
signed main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
// cin >> t;
while (t--) {
solve();
}
}