当青训营遇上码上掘金之”寻友之旅“(位运算+贪心O1时间复杂度求解)

144 阅读4分钟

题目

主题 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语言做项目,后端啥都还不会啊.......)

(寻友之旅 - 码上掘金 (juejin.cn))