枚举目标数保留的二进制前缀,贪心压缩起点并统计中间加一和恢复低位的代价。
OJ: usaco
题目 ID: 1182
难度:普及+/提高
标签:贪心二进制数学usaco
日期: 2026-07-11 19:28
题意
给定若干组 (a,b)。只能对 a 做这些操作:
- 如果
a是偶数,
问最少多少次操作能把 a 变成 b。
思路
先看一个小数据暴力。它直接把数字看成状态,用 BFS 枚举三种操作。
cpp
/**
* 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-11 19:28
* update_at: 2026-07-11 19:30
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
int T;
int bfs(int a, int b) {
int limit = max(a, b) * 4 + 20;
vector<int> dist(limit + 1, -1);
queue<int> q;
dist[a] = 0;
q.push(a);
while (!q.empty()) {
int x = q.front();
q.pop();
if (x == b) return dist[x];
int nxt[3];
int cnt = 0;
if (x + 1 <= limit) nxt[cnt++] = x + 1;
if (x * 2 <= limit) nxt[cnt++] = x * 2;
if (x % 2 == 0) nxt[cnt++] = x / 2;
for (int i = 0; i < cnt; i++) {
int y = nxt[i];
if (y >= 1 && y <= limit && dist[y] == -1) {
dist[y] = dist[x] + 1;
q.push(y);
}
}
}
return -1;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> T;
while (T--) {
int a, b;
cin >> a >> b;
cout << bfs(a, b) << '\n';
}
return 0;
}满分做法利用一个关键性质:最优方案中,不会出现“先乘 2,后面又除 2”的结构。因为形如:
text
x -> 2x -> 2x+k -> x+k/2总可以改成更短的:
text
x -> x+k/2所以操作可以分成三段:
- 先对
a做若干次“除以 2”相关操作,把它缩小; - 中间做若干次
+1; - 最后做若干次
和少量 +1,恢复到b。
关键是枚举第 3 段有多少次 removed,那么第 3 段开始前要达到:
text
prefix = b >> removed也就是 b 去掉最后 removed 个二进制位后的前缀。
固定 prefix 后,把 a 缩小到不超过 prefix 的过程是贪心确定的:
- 如果当前数是偶数,直接
; - 如果当前数是奇数,必须先
+1,再。
这样每次都会少一位二进制长度,并且不会吃亏。
当 a 被压到 prefix-cur 次 +1。
最后恢复 b 的低 removed 位:
- 每一位都需要一次
,所以贡献 removed; - 低位中每个
1还需要一次+1,所以贡献popcount(b 的低 removed 位)。
枚举所有可能的 removed,取最小值即可。
代码
cpp
/**
* 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-11 19:28
* update_at: 2026-07-11 19:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int T;
ll solve_pair(ll a, ll b) {
ll ans = (1LL << 62);
for (int removed = 0; (b >> removed) > 0; removed++) {
ll prefix = b >> removed;
ll cur = a;
ll steps = 0;
// 固定除法次数后,S1 阶段是唯一的:奇数先 +1,再 /2。
while (cur > prefix) {
if (cur % 2 == 1) {
cur++;
steps++;
}
cur /= 2;
steps++;
}
// 中间只需要补若干次 +1,让 cur 变成 prefix。
steps += prefix - cur;
// S3 阶段:removed 次 *2,加上恢复 b 的低 removed 位中所有 1 的代价。
steps += removed;
if (removed > 0) {
ll mask = (1LL << removed) - 1;
steps += __builtin_popcountll((unsigned long long)(b & mask));
}
ans = min(ans, steps);
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> T;
while (T--) {
ll a, b;
cin >> a >> b;
cout << solve_pair(a, b) << '\n';
}
return 0;
}复杂度
removed 最多枚举到
单组时间复杂度为
总结
本题的关键是从二进制角度看操作。
固定最后的乘二次数后,前面的缩小过程和后面的恢复低位代价都可以直接算出来,于是枚举这个分界点即可。