题目
主题 3:寻友之旅 小青要找小码去玩,他们的家在一条直线上,当前小青在地点 N ,小码在地点 K (0≤N , K≤100 000),并且小码在自己家原地不动等待小青。小青有两种交通方式可选:步行和公交。 步行:小青可以在一分钟内从任意节点 X 移动到节点 X-1 或 X+1 公交:小青可以在一分钟内从任意节点 X 移动到节点 2×X (公交不可以向后走) 请帮助小青通知小码,小青最快到达时间是多久? 输入: 两个整数 N 和 K 输出: 小青到小码家所需的最短时间(以分钟为单位)
BFS写法(C++,Go)
由于N和K只有1e5大小,首先我们能很快地想到BFS写法,因为每个值只会出现一次,所以时间复杂度是O(n) 的。
- C++
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int dis[N];//N到x点的距离
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
memset(dis, -1, sizeof(dis));//将所有没到过的点设置为 -1
queue<int> q;
q.push(n);
dis[n] = 0;//开始点距离为1
while(!q.empty()){
int u = q.front();
q.pop();
if(u * 2 <= 100000 && dis[u * 2] == -1){//如果u*2的位置还没走过,则将其放入队列
dis[u * 2] = dis[u] + 1;
q.push(u * 2);
}
if(u + 1 <= 100000 && dis[u + 1] == -1){//如果u+1的位置还没走过,则将其放入队列
dis[u + 1] = dis[u] + 1;
q.push(u + 1);
}
if(u - 1 >= 1 && dis[u - 1] == -1){//如果u-1的位置还没走过,则将其放入队列
dis[u - 1] = dis[u] + 1;
q.push(u - 1);
}
}
cout << dis[k] <<'\n';
return 0;
}
- Go
package main
import (
"bufio"
. "fmt"
"os"
)
const N = 100005
func main() {
in := bufio.NewReader(os.Stdin)
out := bufio.NewWriter(os.Stdout)
defer out.Flush()
var n, k int
Fscan(in, &n)
Fscan(in, &k)
q := []int{n}
dis := [N]int{}
for i := range dis {
dis[i] = -1;
}
dis[n] = 0
for len(q) != 0 {
u := q[0]
q = q[1:]
if u * 2 <= 100000 && dis[u * 2] == -1 {
q = append(q, u * 2)
dis[u * 2] = dis[u] + 1
}
if u + 1 <= 100000 && dis[u + 1] == -1 {
q = append(q, u + 1)
dis[u + 1] = dis[u] + 1
}
if u - 1 >= 1 && dis[u - 1] == -1 {
q = append(q, u - 1)
dis[u - 1] = dis[u] + 1
}
}
Println(dis[k])
}
注意:在本题中由于边权为1,所以队首元素的距离一定小于队尾元素的距离,故直接BFS比Dijkstra复杂度更优
位运算(C++,Go)
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
if(n >= k) {
cout << n - k << '\n';
}else {
auto solve = [&](int posk, int posn, int init){
int cnt = init, ans = 0;
for(int i = posk - posn - 1; i >= 0; --i){
if((k & (1 << i)) != 0){
cnt++;
}
else{
if(i - 1 >= 0 && (k & (1 << (i - 1))) != 0 && (cnt > 1 || init == 1)) ans++;
else{
if(init == 1) ans += min(cnt, 1), init = 0;
else
ans += min(cnt, 2);
cnt = 0;
}
}
ans++;
}
if(init == 1) ans += min(cnt, 1), init = 0;
else
ans += min(cnt, 2);
return ans;
};
int posn = 31 - __builtin_clz(n), posk = 31 - __builtin_clz(k), premask = 0, ans = 1e9;
for(int i = posk - posn; i <= posk; ++i){
if(k & (1 << i))
premask += 1 << (i - posk + posn);
}
if(posk != posn){
ans = (premask << 1) - n + ((k & (1 << posk - posn - 1)) != 0) + solve(posk, posn + 1, 0);
}
if(premask < n){
ans = min(ans, abs(premask - n) + solve(posk, posn, 1) - 1);
}
ans = min(ans, abs(premask - n) + solve(posk, posn, 0));
premask >>= 1;
posn--;
if(premask < n){
ans = min(ans, abs(premask - n) + solve(posk, posn, 1) - 1);
}
ans = min(ans, abs(premask - n) + solve(posk, posn, 0));
cout << ans << '\n';
}
return 0;
}
首先若n>=k,易知n不断减一直到减到k为止,答案是最优的
否则我们要进行一个小小的分类讨论:
- 考虑n和k的二进制数位
- 令posn为n最前面数位的位置,posk为k最前面数位的位置,我们认为将n通过一系列转化变成和k前面的posn+1个数字相同,再将其不断左移动或加一是比较优的
- premask表示k前面的posn+1个数是什么,ans表示答案,初始化为正无穷
ans = (premask << 1) - n + ((k & (1 << posk - posn - 1)) != 0) + solve(posk, posn + 1, 0);
- 如果n和k的数位不相同,我们可以将premask左移一位,再把n加到和premask左移后的值相等,这个时候由于n位数跟随premask的位数变化,所以我们在计算k后面posk-posn位时将posn加一
auto solve = [&](int posk, int posn, int init){
int cnt = init, ans = 0;
for(int i = posk - posn - 1; i >= 0; --i){
if((k & (1 << i)) != 0){
cnt++;
}
else{
if(i - 1 >= 0 && (k & (1 << (i - 1))) != 0 && (cnt > 1 || init == 1)) ans++;
else{
if(init == 1) ans += min(cnt, 1), init = 0;
else
ans += min(cnt, 2);
cnt = 0;
}
}
ans++;
}
if(init == 1) ans += min(cnt, 1), init = 0;
else
ans += min(cnt, 2);
return ans;
};
solve函数的功能是:计算k的后面posk-posn位对答案的贡献。
- 首先我们知道每次左移(即乘以2)的操作会贡献1的答案,所以每次循环我们将ans++
- cnt用来记录数位1的个数,如果当前位置是1我们就+1,我们发现对于一串长度大于等于2的数位为1的串(例如premask111111111111),我们只需要在premask中加1然后移动到最后(premask+1)0000000000000将其减一,我们就能得到答案,因此这一次操作贡献最多为2,但是如果像0000010000这种,我们最优肯定是在n左移到000000这个位置时将其+1变成000001后,再继续左移四位,贡献是数位1的个数,即1个。
- 那如果是1101010101010这种情况呢,我们发现在这种情况下,只要它不出现两个连续的数位0,那么我们将10000000000000减1变成1111111111110再减去中间为0的数位变成1101010101010是最优的,唯一的例外是cnt<=1时,我们将有的1直接削去最优
- init表示premask一开始就给后面的数位提供了一个1,这个时候我们对于1111111的串不再需要加一操作,直接减1即可,所以对答案的贡献最多为1,用掉后我们将init变为0
- 如果出现init=1但是011111的情况,这个时候我们可能在接触到第一个0时就用掉了init,这样是不优的,我们要特判掉
if(premask < n){
ans = min(ans, abs(premask - n) + solve(posk, posn, 1) - 1);
}
- 如果premask<n,我们将n不断减1减到premask的时候,我们可以将它减到premask-1为止,那么就像上面所说,我们在premask中给它提供了一个+1,所以设置init为1
ans = min(ans, abs(premask - n) + solve(posk, posn, 0));
- 或者我们也不一定要提供这个1,答案如上
premask >>= 1; posn--;
我们考虑最后一种情况,将n一直减小到premask>>1的大小,这个时候posn会减小1
if(premask < n){
ans = min(ans, abs(premask - n) + solve(posk, posn, 1) - 1);
}
ans = min(ans, abs(premask - n) + solve(posk, posn, 0));
我们重复上一步的操作即可 综上我们就完成了求解,输出答案即可 时间复杂度O(1),空间复杂度O(1)
如果这道题的数据改为1<=n,k<=1e18,或者1<=n,k<=10^100000,那么就只能用位运算写了
这道题我研究了一整天,可以说是究极折磨了,我对拍了十分钟也没发现有错误,所以位运算的做法大概率是正确的,如果找到样例可以hack我,也非常欢迎在评论区留言。
最后贴个Go版本(因为最近在学Go语言做项目,后端啥都还不会啊.......)