当青训营遇上码上掘金
征文的主题如下:
看到这个题目直观上的路线一般是两个:
第一个路线是小青直接在家门口坐公交,一直坐到距离目的地距离最近的地方,然后开始向目的地步行。
第二个路线是小青先步行一段路,然后开始坐公交,到距离目的地最近的时候下车,然后向目的地步行。
第一个路线思路的代码写起来还是非常的简单的,主要的函数代码如下:
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,是原来的两倍还多!
显然这个方法是有问题的了,既然第一个方法有问题,能预计到第二个方法也不太可能是正确的,第一个方法失败的原因大概率是连续坐公交车。那么公交车不能连续坐,应该怎么坐呢?(虽然我们日常生活中公交车是应该要连着坐节省时间)
只要我们仔细思考一下就会发现,小青和小码的世界里,公交车貌似不应该这样用。他们的公交车有一个特点,就是随时可以上车,并且目的地的坐标是上车处坐标的两倍。这一个特性就比较有意思了
我们可以这样规划路线,出发点是小码家,如果当前坐标是奇数那么坐标-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;
代码的输出如下:
如果小青坐标变成15,那么需要花费的时间是14min。
代码的输出如下:
多输入几组数据发现,只要小码和小青家坐标差距比较大,这种方法花费的时间都比较小(看上去好像是比较合理的路线选择了)
另外如果小青坐标是3,小码坐标是23的话,这种方法花费的时间是7min,而第一种路线规划方式反而只花了4min。
其实本质上最后那种方法是采用了分治的思想,对于小青到小码家所使用的最短时间这个问题,可以根据情况转换成两种子问题
-
如果当前坐标是偶数,那么直接转换成小青到当前坐标的一半所用的最短时间,最后时间再+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。
这样应该就万无一失了!看来在小青小码那个世界,坐公交车还是一件不那么容易弄明白的事情啊