当青训营遇上码上掘金之“寻友之旅”

87 阅读3分钟

当青训营遇上码上掘金

征文的主题如下:

image.png

看到这个题目直观上的路线一般是两个:

第一个路线是小青直接在家门口坐公交,一直坐到距离目的地距离最近的地方,然后开始向目的地步行。

第二个路线是小青先步行一段路,然后开始坐公交,到距离目的地最近的时候下车,然后向目的地步行。

第一个路线思路的代码写起来还是非常的简单的,主要的函数代码如下:

void solution1(int aLocation, int bLocation) {
    cout << "solution1: " << endl;
    int tempB = bLocation / aLocation;
    int bus = 1;//记录坐公交路程翻的倍数
    int time = 0;
    while (tempB > 1) {
        tempB /= 2;
        bus *= 2;
        time++;
    }
    //公交不驶过,所花时间更短
    if (bus * 2 - bLocation / aLocation >= bLocation / aLocation - bus) {
        time += bLocation - aLocation * bus;
    } else {
        time += 1 + aLocation * bus * 2 - bLocation;
    }
    cout << "the least time: " << time << "min" << endl;
}

如果我们输入的数据是小青家坐标3,小码家坐标666的话,那么小青将要使用110min;如果小青家的坐标变成14,小码家的坐标不变,常理上应该是时间减少(如果路线规划的正确的话确实应该是这样的),但是小青花费的时间变成了223min,是原来的两倍还多!

image.png

显然这个方法是有问题的了,既然第一个方法有问题,能预计到第二个方法也不太可能是正确的,第一个方法失败的原因大概率是连续坐公交车。那么公交车不能连续坐,应该怎么坐呢?(虽然我们日常生活中公交车是应该要连着坐节省时间)

只要我们仔细思考一下就会发现,小青和小码的世界里,公交车貌似不应该这样用。他们的公交车有一个特点,就是随时可以上车,并且目的地的坐标是上车处坐标的两倍。这一个特性就比较有意思了

image.png

我们可以这样规划路线,出发点是小码家,如果当前坐标是奇数那么坐标-1变成偶数,对应的花费时间+1;如果当前坐标是偶数,那么直接除2,对应的花费时间+1。 这样反复操作直到当前的坐标比小青家的坐标小了为止。然后比较最后一次坐标和倒数第二次坐标距离小青家的位置,选择一个距离小青家最近的去就可以了!

这个思路的代码实现如下:

void solution2(int aLocation, int bLocation) {
    cout << endl << "solution2: " << endl;
    int tempB = bLocation, tempA = aLocation, last, time = 0;
    while (tempB > tempA) {
        last = tempB;
        if (tempB % 2 == 0) {
            tempB /= 2;
        } else {
            tempB -= 1;
        }
        time++;
        cout << tempB << " " << tempA << " " << last << endl;
    }
    time += min(last - aLocation, aLocation - tempB);
    cout << "the least time: " << time << "min" << endl;
}

看看这样小青到小码家要花费多少时间。还是一样,假如小青坐标3,小码坐标666,那么需要花费的时间是13min

代码的输出如下:

image.png

如果小青坐标变成15,那么需要花费的时间是14min

代码的输出如下:

image.png

多输入几组数据发现,只要小码和小青家坐标差距比较大,这种方法花费的时间都比较小(看上去好像是比较合理的路线选择了)

另外如果小青坐标是3,小码坐标是23的话,这种方法花费的时间是7min,而第一种路线规划方式反而只花了4min

image.png

其实本质上最后那种方法是采用了分治的思想,对于小青到小码家所使用的最短时间这个问题,可以根据情况转换成两种子问题

  • 如果当前坐标是偶数,那么直接转换成小青到当前坐标的一半所用的最短时间,最后时间再+1即可;

  • 如果当前坐标是奇数,那么转换成小青到当前坐标-1所用的最短时间,最后时间再+1;

理智的读者看到这可能已经发现这个子问题划分有问题,当前坐标是奇数的时候,还可以转换成小青到当前坐标+1所用的最短时间,之后时间再+1。这种情况没考虑会不会影响最后的结果呢?

肯定会的,就比如上面讲的异常数据,小青坐标是3,小码坐标是23。如果将问题分解成求小青到24所用的最短时间然后时间+1的话,最后的结果就和第一种路线规划方式一样,是4min了。(23->24->12->6->3一共转移了4次)

修改分治的代码,最终代码如下(采用了递归的结构):

int getTime(int aLocation, int bLocation, int time) {
    cout << bLocation << " " << time << endl;
    int time1, time2;
    //叶子节点返回此路线花费的时间
    if (bLocation == aLocation) {
        return time;
    }
    if (bLocation < aLocation) {
        time += min(aLocation - bLocation, bLocation * 2 - aLocation);//步行去距离最近的坐标花费的时间
        if (bLocation * 2 - aLocation <= aLocation - bLocation) {
            //如果选择的是靠近根节点的节点,那么删除一次坐公交车的时间
            time--;
        }
        cout << "return: " << time << endl;
        return time;
    }

    //非叶子节点直接返回递归得到的数据
    if (bLocation % 2 == 0) {
        time1 = getTime(aLocation, bLocation / 2, time + 1);
        return time1;
    } else {
        time1 = getTime(aLocation, bLocation - 1, time + 1);
        time2 = getTime(aLocation, bLocation + 1, time + 1);
        return min(time1, time2);
    }
}

void solution3(int aLocation, int bLocation) {
    cout << endl << "solution3: " << endl;
    int time = getTime(aLocation, bLocation, 0);
    cout << "the least time: " << time << "min" << endl;
}

分治的过程实际上是一棵二叉树,偶数节点只有一个子节点,数值是自己的一半;奇数节点有两个子节点,左子树数值是自己数值-1,右子树是自己数值+1;在生成树的每一条分支时,如果当前节点的数值比起始点坐标小,进入如下判断:

  • 如果选择的是靠近根节点的节点(坐标减半之前的节点),那么算出移动到该节点的距离然后去除坐公交(坐标减半消耗的时间),再加上传入当前函数的time,返回时间之和。这个数值就是这条分支需要花费的时间

  • 如果选择的是远离根节点的节点,那么直接算出移动到这个节点的距离,加上传入当前函数的time,返回时间之和即可。

如果碰到当前节点的数值和起始点坐标相同,那么直接返回函数传入的time。

这样应该就万无一失了!看来在小青小码那个世界,坐公交车还是一件不那么容易弄明白的事情啊

image.png