您的位置:首页 > 其它

普林斯顿大学算法课程第三周个人总结

2013-09-18 13:47 281 查看
这次的总结拖到现在才写,主要是因为刚刚才拿到第三周编程作业的满分,算法效率的最后一个测试迟迟不达标,昨晚又在论坛里翻看了很多讨论帖,今早进行了一些优化,终于通过了最后一个测试。

经过三周以后的学习,总结起来,算法课就是在课堂上讲解很多理论知识,从算法的产生到历经多年的改进与优化等,重在思想的领会,而编程作业则是要求忽略算法实现细节,使用Java现有的算法库,重在算法的实际使用。

第三周是应用最为广泛的两个排序算法:merge-sort和quick-sort,不过这次总结的重点是折磨了我很多天的编程作业。作业任务的要求是在一个平面上,给定任意N个位置的点,定位出所有能通过4个以上点(包括4个点)的线段。当然不能出现线段覆盖的情况,比如有线段通过5个点,那么该线段上前4个点和后4个点构成的线段就不能被计算在内。(GitHub仓库页面:https://github.com/Revil/algs-collinear,仓库地址:https://github.com/Revil/algs-collinear.git)



最简单粗暴的算法应该就是以其中一个点作为起点,然后计算出与其他点的斜率,斜率相同的点达到4个,就构成了一条线段。实现起来很简单,4个for循环,从前往后依次遍历所有点。当然效率也是极差的(N^4),而且避免不了上面提到的线段覆盖的问题。不过在这里我还犯了一个错误:没有在开始之前将所有的点按照坐标排序,以至于后面即使定位到了线段,但是线段的起点选在了中间,导致线段绘制的时候出错。

for (int i = 0; i < N; i++)
for (int j = i+1; j < N; j++)
for (int k = j+1; k < N; k++)
for (int l = k+1; l < N; l++)
if (a[i].slopeTo(a[j]) == a[i].slopeTo(a[k])
&& a[i].slopeTo(a[j]) == a[i].slopeTo(a[l])) {
StdOut.println(a[i].toString() + " -> "
+ a[j].toString() + " -> " + a[k].toString()
+ " -> " + a[l].toString());
a[i].drawTo(a[l]);
}


根据编程作业的说明,快速的实现方式是选取某一个点之后,按照这个点与其他点的斜率大小进行排序,这样当排序完成以后,相同斜率的点就靠在了一起,只要相邻近的3个以上的点斜率相同,那么这些点和起点在一起就构成了一条线段。

这种实现方式乍看起来很容易实现,而且算法效率也可以达到N^2* logN的效果,不过实际做起来陷阱重重。

为了避免出现线段起点选在线段中间的情况,依然是需要先将所有的点按照坐标进行排序。然后从前往后遍历,每选一个点,就将其后续的所有点按照与该起点的斜率进行排序。然后定位到3个以上相邻且斜率相同的点。当然为了避免按斜率排序扰乱后续点的原先顺序,需要对按坐标排序的点进行一次备份,每次重新选起点的时候,就将之前的顺序再重新恢复,以便进行下一次的斜率排序。

/* first sort the array by coordinates */
Arrays.sort(a);

Point[] aux = new Point
;
/* use one point each as the origin, sort the array using slope order */
/* once all the segments related with the point is over, just leave it */
for (int i = 0; i < N-3; i++) {
for (int j = i; j < N; j++)
aux[j] = a[j];
Arrays.sort(aux, i+1, N, aux[i].SLOPE_ORDER);
int head = i+1;
int tail = i+2;

while (tail < N) {
/* if equal slopes encountered, just move tail forward */
while (tail < N
&& aux[i].slopeTo(aux[head]) == aux[i].slopeTo(aux[tail]))
tail++;
/* on slopes not equal, check if we have a segment */
if (tail - head >= 3) {
/* we have a segment here */
aux[i].drawTo(aux[tail-1]);
String output = aux[i].toString() + " -> ";
for (int l = head; l < tail-1; l++)
output += (aux[l].toString() + " -> ");
output += aux[tail-1].toString();
StdOut.println(output);
}
/* move head to last not equal slope point */
head = tail;
tail = tail+1;
}
}


这是最开始的做法,基本达到了要求,但是没有避免掉线段覆盖的情况,比如有线段a->b->c->d->e,坐标依次递增,当遍历到b的时候,b->c->d->e也构成一个线段,但是实际上这条线段之前已经选出,所以还需要判断b之前的点有没有与b到c斜率相同的点。起初改进的方式是每次检测到可以构成线段的时候,从0到起点遍历所有点与当前起点的斜率,一旦发现斜率相同的点,则忽略该线段。

/* we have a segment here */
/* make sure it's not a duplicate or overlapping one */
int k;
for (k = 0; k < i; k++)
if (aux[k].slopeTo(aux[i]) == aux[i].slopeTo(aux[head]))
break;
if (k >= i) {
aux[i].drawTo(aux[tail-1]);
String output = aux[i].toString() + " -> ";
for (int l = head; l < tail-1; l++)
output += (aux[l].toString() + " -> ");
output += aux[tail-1].toString();
StdOut.println(output);
}
}


但是这种做法,最好的时候也无法通过最后一个效率测试。在寻找重复线段的过程,还可以进一步优化,在选定某一起点后,除了对其后续的点按照与该起点的斜率排序以外,前面的点也按照这种方式重新排序,并且在前面的点中也使用一个索引(称其为前索引)从0到起点,和后面的索引一起向前移动,当在后续点中找到一个线段之后,就移动前索引直到遇见大于或等于该斜率值的点时就停止移动,前者表示该线段有效,而后者表示线段已经存在,直接忽略。这样保证了对于同一起点来说,每找到一个新的线段,不必再从0开始搜索,直接从前索引当前位置继续向前寻找即可。按照这种方式修改之后,顺利通过了最后一个测试。

/* sort points before and after point[i] by slope */
Arrays.sort(aux, i+1, N, aux[i].SLOPE_ORDER);
Arrays.sort(aux, 0, i, aux[i].SLOPE_ORDER);


注意所有的这些操作都是在,所有点按照自然顺序(也就是按照坐标排序)的基础上,才能保证不会出现遗漏或者重复的情况。

除了这些基本的思路以外,还有很多时候需要提防边界性的错误,这些边界值往往也能决定程序能不能够按照预定思路正常执行。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: