主题 3:寻友之旅

122 阅读4分钟

当青训营遇上码上掘金。

错误

尴尬,这个方法错了。 请不要使用,dp的方法好像是可以的。

题目

题目链接

小青要找小码去玩,他们的家在一条直线上,当前小青在地点 N ,小码在地点 K (0≤N , K≤100 000),并且小码在自己家原地不动等待小青。小青有两种交通方式可选:步行和公交。
步行:小青可以在一分钟内从任意节点 X 移动到节点 X-1 或 X+1
公交:小青可以在一分钟内从任意节点 X 移动到节点 2×X (公交不可以向后走)

请帮助小青通知小码,小青最快到达时间是多久? 输入: 两个整数 N 和 K
输出: 小青到小码家所需的最短时间(以分钟为单位)

分析和递归解法

也就是在相知范围和限制操作内,如何使得数字N变为数字K.
一开始很自然想到的方法自然就是递归。
但由于X既可以加一,又能减一,所以必须要对时间加以限制,不然会无限递归下去把栈爆掉。
一个很简单的时间就是只通过加一(或者减一)到达K所需要的时间。
另外我们分析可以知道,令X减小的方法只有一个,就是X-1,所以当X>=K的时候,所需要的时间必然就是X-K。
在这两个限制下很容易写成一个很简单的代码:

class Solution {
public:
    int time2(int N, int K) {
        // 最差只需要走两个距离的时间
        int limitTime = std::abs(N-K);
        // 用于防止在一次检查中回到路径中的点
        std::unordered_set<int> visited;
        return time_aux(N, K, 0, limitTime, visited);
    }
    int time_aux(int cur, int des, int cost, int limit, std::unordered_set<int>& visited) {
        if (cur == des) {
          return 0; // 已经到达了目的地
        }
        if (cost > limit) {
          return -1; // 超过时间限制
        }
        if (cur >= des) {
          return cur-des; // 当前位置在des右边
        }
        int ret = -1;
        // 检查是否访问过,
        // 访问过直接放弃,在一次探查中不能回到途经的点
        if (visited.find(cur) != visited.end())
          return -1;

        visited.insert(cur);
        // 只能在正数范围内
        if(cur > 0) {
          int case1 = time_aux(cur-1, des, cost+1, limit, visited);
          if (case1 != -1 && (ret == -1 || case1 + 1 < ret))
              ret = case1 + 1;
        }
        int case2 = time_aux(cur+1, des, cost+1, limit, visited);
        if (case2 != -1 && (ret == -1 || case2 + 1 < ret))
          ret = case2 + 1;

        // 超过或者等于0的话再*2没什么用
        // 不过第一个条件这里其实没用
        // 因为cur 一定会 > des
        if (cur < des && cur > 0) {
          int case3 = time_aux(cur*2, des, cost+1, limit, visited);
          if (case3 != -1 && (ret == -1 || case3 + 1 < ret))
              ret = case3 + 1;
        }
        visited.erase(cur);
        return ret;
    }
};

但题目中的N和K的范围太大,0 <= N, K <= 100,000,我只是在0-100内测试100轮,使用g++即使开了-O2也要消耗一段时间(时间不一定,有时长有时短)。

另一种解法

我们再思考改变X的方法:
步行:小青可以在一分钟内从任意节点 X 移动到节点 X-1 或 X+1
公交:小青可以在一分钟内从任意节点 X 移动到节点 2×X (公交不可以向后走)
当N >= K时我们是不需要考虑的,因为只有N-1这一种情况使得N变小,只需要返回N-K即可。
那当N < K时呢?要让N迅速到达K,我们必然要尽量多地使用×2这个操作,但这个操作也不能使用太多,多了就要消耗更多的减操作。
这样×2这个操作我们猜测来说是最少使用a此,其中a为使得(N<<a) <= K的最大的值,在少就需要使用更多加操作了;最多也就只能使用a+1次,再多就需要更多减操作了。
那剩下的加和减的操作呢?

初次尝试

我们以×2操作次数为a为例,假设dist = k - (N<<a),我们(可能)需要使用多次加操作来使得N达到a。如果在×2开始之前加1一次,那么就会使得最终的结果大(1<<a),dist中的(1<<a)的整数倍部分就可以通过在最开始进行进行加一得到。受此启发,剩下的部分我们是不是就可以数一下其二进制中有多少个1,每一个1只需要在对应位置加一即可。
×2操作次数为a+1时类似。 将以上过程转化为代码如下:

class Solution {
public:
    int howManyOne(int n) {
        int ret = 0;
        while (n) {
          if (n&1) {
              ++ret;
          }
          n >>= 1;
        }
        return ret;
    }
    int time(int N, int K) {
        if (N >= K)
          return N - K;
        // 从0开始的话必须在一开始先加一
        bool startFrom0 = false;
        if (N == 0) {
          startFrom0 = true;
          N = 1;
        }
        int nGreat = N;
        int times = 0;
        while (nGreat < K) {
          times++;
          nGreat <<= 1;
        }
        int nGreatDist = nGreat - K;
        int greatLen = (1 << times);
        int ret1 = times;
        if (nGreatDist / greatLen >= 1) {
            ret1 += nGreatDist / greatLen - 1;
            nGreatDist %= greatLen;
            nGreatDist += greatLen;
        }
        ret1 += howManyOne(nGreatDist);
        int nLess = (nGreat>>1);
        int nLessDist = K - nLess;
        int lessLen = (1 << (times - 1));
        int ret2 = times - 1;
        if (nLessDist / lessLen >= 1) {
            ret2 += nLessDist / lessLen;
            nLessDist %= lessLen;
            nLessDist += lessLen;
        }
        ret2 += howManyOne(nLessDist);
        return std::min(ret1, ret2) + (startFrom0 ? 1 : 0);
    }
};

这样对不对呢?我们将上述代码和使用递归实现的代码在0-100范围内测试(太大分为递归方法时间开销太大)。
很遗憾,结果不对,我这里提供一个错误实例:
N = 2, K = 93. 其变化序列是这样的:

3  +
6  *
12 *
24 *
23 -
46 *
92 *
93 +

它的×2操作次数是没有错的,但他不止使用了两次加操作,还有一次减操作。

改正

也就是说剩余的dist这个距离只使用加操作并不是最少的,我们来观察一下我们的操作流程:
2<<5 = 64
dist = 93 - 64 = 29 = 0001 1101(二进制)
我们观察dist的二进制,除了1<<5 = 0010 0000外,剩余的是0001 1101(不变),其中有一簇1连着(三个),对于单个的1,我们可以在对应位置加一,但连着的1,我们可以先加一,再多个×2,再减一,这样最多只需要两个多余的操作,以 0111为例,0111 = (1<<3)-1,本来在对应位置加一需要三个操作,这样只需要两个操作。
所以我们可以得出结论:对于对于两个1的连着的1的簇,最多只需要两个操作就可以了。将其转化为代码如下:

class Solution {
public:
    int howManyOneGrp(int n) {
        int ret = 0;
        bool prev = false;
        bool grp2 = false;
        while (n) {
          if (n&1) {
              if (!prev) {
                  prev = true;
                  ++ret;
              } else if (!grp2) {
                  grp2 = true;
                  ++ret;
              }
          } else {
              prev = false;
              grp2 = false;
          }
          n >>= 1;
        }
        return ret;
    }
    int time(int N, int K) {
        if (N >= K)
          return N - K;
        bool startFrom0 = false;
        if (N == 0) {
          startFrom0 = true;
          N = 1;
        }
        int nGreat = N;
        int times = 0;
        while (nGreat < K) {
          times++;
          nGreat <<= 1;
        }
        int nGreatDist = nGreat - K;
        int greatLen = (1 << times);
        int ret1 = times;
        if (nGreatDist / greatLen >= 1) {
            ret1 += nGreatDist / greatLen - 1;
            nGreatDist %= greatLen;
            nGreatDist += greatLen;
        }
        ret1 += howManyOneGrp(nGreatDist);
        int nLess = (nGreat>>1);
        int nLessDist = K - nLess;
        int lessLen = (1 << (times - 1));
        int ret2 = times - 1;
        if (nLessDist / lessLen >= 1) {
            ret2 += nLessDist / lessLen;
            nLessDist %= lessLen;
            nLessDist += lessLen;
        }
        ret2 += howManyOneGrp(nLessDist);
        return std::min(ret1, ret2) + (startFrom0 ? 1 : 0);
    }
};

这个方法我在0-100范围内测试没有错,但是没有OJ,所以我也不能确定一定对。

全部代码

#include <vector>
#include <numeric>
#include <algorithm>
#include <unordered_set>

class Solution {
public:
    int howManyOneGrp(int n) {
        int ret = 0;
        bool prev = false;
        bool grp2 = false;
        while (n) {
            if (n&1) {
                if (!prev) {
                    prev = true;
                    ++ret;
                } else if (!grp2) {
                    grp2 = true;
                    ++ret;
                }
            } else {
                prev = false;
                grp2 = false;
            }
            n >>= 1;
        }
        return ret;
    }
    int time(int N, int K) {
        if (N >= K)
            return N - K;
        bool startFrom0 = false;
        if (N == 0) {
            startFrom0 = true;
            N = 1;
        }
        int nGreat = N;
        int times = 0;
        while (nGreat < K) {
            times++;
            nGreat <<= 1;
        }
        int nGreatDist = nGreat - K;
        int greatLen = (1 << times);
        int ret1 = times;
        if (nGreatDist / greatLen >= 1) {
            ret1 += nGreatDist / greatLen - 1;
            nGreatDist %= greatLen;
            nGreatDist += greatLen;
        }
        ret1 += howManyOneGrp(nGreatDist);
        int nLess = (nGreat>>1);
        int nLessDist = K - nLess;
        int lessLen = (1 << (times - 1));
        int ret2 = times - 1;
        if (nLessDist / lessLen >= 1) {
            ret2 += nLessDist / lessLen;
            nLessDist %= lessLen;
            nLessDist += lessLen;
        }
        ret2 += howManyOneGrp(nLessDist);
        return std::min(ret1, ret2) + (startFrom0 ? 1 : 0);

    }
    int time2(int N, int K) {
        int limitTime = std::abs(N-K);
        std::vector<int> dp(1000, -1);
        std::unordered_set<int> visited;
        return time_aux(N, K, 0, limitTime, visited);
    }

    int time_aux(int cur, int des, int cost, int limit, std::unordered_set<int>& visited) {
        if (cur == des) {
            return 0;
        }
        if (cost > limit) {
            return -1; // 超过,不记录
        }

        if (cur > des) {
            return cur-des;
        }
        int ret = -1;
        if (visited.find(cur) != visited.end())
            return -1;

        visited.insert(cur);

        // 不能溢出
        if (cur < des && cur > 0) {
            int case3 = time_aux(cur*2, des, cost+1, limit, visited);
            if (case3 != -1 && (ret == -1 || case3 + 1 < ret))
                ret = case3 + 1;
        }

        int case2 = time_aux(cur+1, des, cost+1, limit, visited);
        if (case2 != -1 && (ret == -1 || case2 + 1 < ret))
            ret = case2 + 1;

        // 只能在正数范围内
        if(cur > 0) {
            int case1 = time_aux(cur-1, des, cost+1, limit, visited);
            if (case1 != -1 && (ret == -1 || case1 + 1 < ret))
                ret = case1 + 1;
        }

        visited.erase(cur);
        return ret;
    }
};

#include <random>
#include <iostream>

int main() {
    std::random_device rd;
    std::default_random_engine dre(rd());
    std::uniform_int_distribution<> uid(0, 150);
    Solution s;
    for (long idx = 0; idx < 100; ++idx) {
        int N = uid(dre);
        int K = uid(dre);
        int res1 = s.time(N, K);
        int res2 = s.time2(N, K);
        if (res1 != res2) {
            std::cout << "N = " << N << ", K = " << K
                      << ", res1 = " << res1
                      << ", res2 = " << res2 << '\n';
            break;
        }
        std::cout << idx << ": " << N << ", " << K << ", " << res1 << '\n';
    }
}