老李每天骑自行车回家需要经过一条狭长的林荫道。道路由于年久失修,变得非常不平整。虽然每次都很颠簸,但他仍把骑车经过林荫道当成一种乐趣。
由于颠簸,骑车回家的路径是一条上下起伏的曲线,老李想知道,他回家的这条曲线的长度究竟是多长呢?更准确的,老李想知道从林荫道的起点到林荫道的终点,他的车前轮的轴(圆心)经过的路径的长度。老李对路面进行了测量。他把道路简化成一条条长短不等的直线段,这些直线段首尾相连,且位于同一平面内。并在该平面内建立了一个直角坐标系,把所有线段的端点坐标都计算好。假设老李的自行车在行进的过程中前轮一直是贴着路面前进的。
上图给出了一个简单的路面的例子,其中蓝色实线为路面,红色虚线为车轮轴经过的路径。在这个例子中,老李的前轮轴从A点出发,水平走到B点,然后绕着 地面的F点到C点(绕出一个圆弧),再沿直线下坡到D点,最后水平走到E点,在这个图中地面的坐标依次为:(0, 0), (2, 0), (4, -1), (6, -1),前轮半径为1.50,前轮轴前进的距离依次为:
AB=2.0000;弧长BC=0.6955;CD=1.8820;DE=1.6459。总长度为6.2233。
下图给出了一个较为复杂的路面的例子,在这个例子中,车轮在第一个下坡还没下完时(D点)就开始上坡了,之后在坡的顶点要从E绕一个较大的圆弧到F点。这个图中前轮的半径为1,每一段的长度依次为:AB=3.0000;弧长BC=0.9828;CD=1.1913;DE=2.6848;弧长EF=2.6224; FG=2.4415;GH=2.2792。
总长度为15.2021。
现在给出了车轮的半径和路面的描述,请求出车轮轴轨迹的总长度。
输入的第一行包含一个整数n和一个实数r,用一个空格分隔,表示描述路面的坐标点数和车轮的半径。接下来n行,每个包含两个实数,其中第i行的两个实数x[i], y[i]表示描述路面的第i个点的坐标。路面定义为所有路面坐标点顺次连接起来的折线。给定的路面的一定满足以下性质:
*第一个坐标点一定是(0, 0);
*第一个点和第二个点的纵坐标相同;
*倒数第一个点和倒数第二个点的纵坐标相同;
*第一个点和第二个点的距离不少于车轮半径;
*倒数第一个点和倒数第二个点的的距离不少于车轮半径;
*后一个坐标点的横坐标大于前一个坐标点的横坐标,即对于所有的i,x[i+1]>x[i]。
输出一个实数,四舍五入保留两个小数,表示车轮轴经过的总长度。你的结果必须和参考答案一模一样才能得分。数据保证答案精确值的小数点后第三位不是4或5。
4 1.50
0.00 0.00
2.00 0.00
4.00 -1.00
6.00 -1.00
以上对应于第一幅图
6 1.00
0.00 0.00
3.00 0.00
5.00 -3.00
6.00 2.00
7.00 -1.00
10.00 -1.00
以上样例对应于第二幅图
6.22
15.20
第一道把我做哭的题目……
从上星期五晚自习开始做,一直做了整整五天都没做出来,一直在错。做到后面越做越委屈最后哭了起来……
我以后再也不想见到这种题目了!!
言归正传,哭可以哭,但是题还是得做。我们先看题。首先这是一道生活模拟的题目,题目大概可以理解成这样:一个圆在一系列线段上向前滚动,它的圆心运动的路程是多少?
刚开始,我相信大家的想法就是:模拟这个过程。题目给出了两种特殊情况,第一种是“凸出来”的节点,在这种节点上,轮子会绕转,直到落地。计算方法很简单,就是计算那个圆弧的长度;第二种则是“凹下去”的节点,这种节点轮子会“卡”在中间,也就是说他不会走到线段的末端,而是在这之前就先碰到另外一块地了。
那么按照模拟法,我们只需要先计算所有线段的长度,然后加上“凸出来”的节点的圆弧长度,再减去“凹下去”节点损失的长度就可以了。
然而真的会这么简单吗?
这么难的题目,我自然不可能完全靠自己写出来,毕竟这道题出自蓝桥杯的压轴难题……所以我请教了很多大佬,甚至参考了部分代码才写出正确答案。我在查阅了蓝桥杯的题目版本后,发现在题目最后有这样一个小提示:
锦囊2:对于每个点画一个圆,每条线画一条平行线,然后求外轮廓的长度
很显然,这个提示已经告诉你怎么做这道题了。但是这种方法无论从复杂度还是直接程度上都不如模拟法,这说明模拟法肯定更加复杂。
究竟哪里复杂呢?请看图:
图中的圆表示车轮在A点处旋转时的轨迹。按照正常的理解,车轮的圆心应该从最左边的绿线开始,运动到与下一个地面相切的时候(红线)停止。
但是实际上车轮运动到第二条绿线的位置就不再运动了,因为它先落在了水平的那条路上。图中紫色的线是下面那条路的平行线,它们形成一个交点,这个交点是圆心接触地面一瞬间所在的位置。
也就是说:车轮在绕某个点旋转的过程中,可能会出现没有接触到下一条路,而是直接碰到下下条路的情况。在这种情况下,就有线段被“跳过”了,我们根本不用去计算这条路。
这还没完,因为它不仅可以“跳过”一条路,还可以同时跳过两条路,三条路,甚至还能刚好落在两条路交界的位置,让你的算法彻底崩溃。
所以模拟法反而更复杂了,而只考虑运动轨迹反而更简单。
运动轨迹法的核心就是:对每条线段都作一个平行线段,对每个点(除了起点和终点)都作一个圆。然后计算他们靠上的一部分的长度。
什么叫靠上的一部分呢?举个例子:
如图,上面那个是我们说的坑爹情况,我们对两个节点作圆,然后画出每条线的平行线段,最后选取最上面的一部分(黑色部分),这个黑色的线段长度就是我们要求的长度。下面那张图也是一样的,取最上面的那一部分,计算出来的长度就是结果。
来梳理一下做法:
首先读取所有的点
对n个点,创建出n-1条线,然后每条线都向右上/左上移动一定距离,使得这个线段是原来线段的平行线段,距离是题目所给出的“半径”
对n个点,以它们为圆心,半径为题目所给的“半径”,作n-2个圆。
让这些线和圆、线和线、圆和圆一一配对,计算交点。这些交点就是外轮廓线的“转折点”。
然后把这些转折点排序,从前往后计算轮廓线长度。
代码很长,248行。
首先我们需要做3个类,分别是Point、Line、Circle。这里使用类而不是结构体的原因是……需要的属性和方法太多了,用类管理起来更加合理。
我们注意到,在我们的算法里,我们需要把这些线、圆一一配对计算交点,那么如果把他们分开储存,配对起来就会有些麻烦。好的方法是把这些线和圆存在一个数组里,这样我们只需要对这个数组做金字塔形遍历(for(i=0 to LEN) for(j=i+1 to LEN)
共C(2,N)次)就可以了。
我们使用vector来做存储这些线和圆的容器(vector可以视为是一个可变化长度的数组 具体的特性大家可以百度)。但是线和圆是两个不同的类,很明显是无法混合着存储在一个vector里的。这里我们要用一个混合类存储的方法:那就是让Line和Circle继承自同一个父类,然后把vector的存储类型设置为这个父类,那么父类的所有子类自然也可以存储进这个vector之中。
但是特别特别要注意的是,虽然存储进去的是我们的子类,但如果你对某个成员进行了操作(如调用了它的方法),实际上被调用的是父类的方法而非子类的方法,如果父类没有这个方法,就会抛出错误。
所以我们需要在父类里声明好可能被调用的几个方法,把他们设为虚函数,然后再在子类里实现,这样调用vector里某个元素的某个方法的时候,就不会抛出错误了。
接下来还要注意的是我们的重头戏:计算长度部分的代码。
首先我们读取了所有的点,并且创建好线和圆以后,我们先把所有的线和圆的左、右端点横坐标存入一个vector数组里。这个数组里存储的应该是我们外轮廓线所有的“转折点”,那么线段和圆的左右端点自然就是转折点的一部分。
接着,我们需要对线和圆进行一一配对,计算出他们的交点的横坐标,把他也存入那个vector数组里。这些交点当然也是外轮廓线上的“转折点”。
然后我们对这个vector数组进行排序,让这些转折点从小到大排列。
最后我们对这些转折点进行计算,计算外轮廓线的长度。计算方法很简单,两个转折点就决定了一段轮廓,如何判断这个轮廓的形状呢?首先计算这两个转折点的中点横坐标,然后找到这个位置所有图形最上面的那个,就是这个轮廓的形状了,接着对这个轮廓求长即可。
如果没听懂看图:
最后还有一个大佬告诉我的要点……要加一个什么迭代精度,我写在代码里了,如果不加那句会导致有一个数据有0.03左右的误差,导致WA。希望有大神能告诉我为什么。
源代码:1569车轴轨迹
这个代码还能优化!
我们在模拟法里引出了一个概念:凹凸节点。这个概念可以描述这个节点相对它的两个相邻节点的位置情况。并且我们知道只有凸节点会被车轮触碰到,而凹节点永远不会,因为凹节点的角度永远小于180°,这个角度圆形是进不来的。
所以对于所有的凹节点都不需要画圆了,因为这个圆必定不参与计算。那么圆的数量平均会少掉1/2。
因为我们求交点的遍历是金字塔型遍历,会运行C(2,N)次,也就是N个里选2个。组合数的N下降所带来的下降还是比较显著的。
这道题实在是太难了……全网没有一个题解。
这部分的代码虽然都是我一个字一个字写的,但是思路和架构却是我问大佬/翻代码才理解的。以前我们有个开玩笑的词叫“面向搜索引擎编程”,但是我对题解、样例代码的态度是,只要你理解是怎么做的,并且不看样例代码和题解能自己做出来,并且有自己的思考,这就足够了。这也是我写这么多题解的意义所在。
这可能是本题的第一个题解,有很多漏洞,也希望大家能指出。