前言:
收费公路重建这个问题,实现的思路很简单,与最朴素的想法一致。先找到几个标准,然后进行穷举,直到找到答案,或者每一种方案都试过,证明无解。
然而,在编写这个代码的时候却出现了非常多的问题,逻辑错误,不方便调试,编码完之后花了3个小时才调通。在编写这个代码的时候,一定要注意逻辑非常清楚,最好是把伪代码详细到约等于代码,再开始编写。
我的github:
我实现的代码全部贴在我的github中,欢迎大家去参观。
https://github.com/YinWenAtBIT
介绍:
收费公路重建:
一、数学模型:
在X轴上给定N个点,那么N个点之间的每两个点之间的距离为一个距离对,一共有N(N-1)/2对距离。
那么现在给出所有的距离对,求出在X点上的各个点的位置。
二、算法步骤:
1. 根据距离对算出一共有N个点。
2. 先确定一个基准,x1位于原点,最大距离为M,最大点xN在M处。第二距离为m,x(N-1)位于m。
3. 然后在x1到x(N-1)之间,填入还剩的最大的距离,先放在x(N-2)位置,如果满足剩下的距离条件,则删除已经满足的距离,继续往前测试。
4. 如果3中的测试不通过,退回至放下x(N-2)点之前。将该点放在x2位置,再重复整个测试过程,直到得到解,或者得出无解。
三、 图解模拟
根据该书中给出的例子,我们先给定15个距离对,即有6个点。
根据算法,确定原点与最大距离点,此时删去已经出现的距离:
再确定距离第二大的点,因为此图是对称的,所以可以选择x5作为第二大点:
确定了这几个点之后,就可以开始进行回溯求解的过程了。
下一个最大距离为7,先放在x4位置上:
然后尝试最大距离6,发现不满足任何一种情况,此时只能回退到上一步,尝试x2 = xN-7。
到此之后,重复这个过程,就可以得到最后的结果是有解的,解如下:
编码实现:
回溯的逻辑:
主要就是两个步骤:
1.尝试编号大的那个点,满足就递归尝试,知道返回成功或者失败
2.失败的话,补齐删去的点。
3.尝试编号小的那个店,再递归尝试。
成功或者失败都返回结果
遇到的困难:
1. 选则编号点的时候,先把x1那边的点与被选点的距离差从集合中删去,左边都删除成功,再删右边。
2.删除失败出现时,就要把刚才删除的点补回去。
3. 左边符合了,右边不符合,先补上右边已经删去的点,再把左边删去的点补上(最初漏了这一部分,花费了不少时间来查找错误逻辑)
4. 即使第一种情况完全符合,如果递推下去的情况不符合,还需要补上刚刚删去左右两边的点,再测试第二种情况。(补全容易被遗漏)
5.调试困难,因为程序只有一个true或者false返回,所以需要一步步的跟着距离的集合,确认集合里的点的变化是否符合预期。这一步特别累,也特别必要,在这样一步步跟着的情况下才能发现是再哪一步遗漏的需要补上的距离。所以,还是最初时把伪代码写的越详细越好。
编码:
存放距离使用的multiset容器,容器会自动根据大小拍顺序,因为主要的算法在于回溯,所以没有使用之前自己编写的搜索二叉树。使用set简化编码。
初始化代码,用来确定原点以及最大点:
bool Turnpike(int X[], multiset<int> D, int N) { X[1] = 0; auto it = D.end(); X[N] = *(--it); --it; D.erase(X[N]); X[N-1] = *(it); D.erase(X[N-1]); if((it = D.find(X[N]- X[N-1])) != D.end()) { D.erase(it); return Place(X, D, N, 2, N-2); } return 0; }回溯求解,这一部分特别麻烦,需要注意补上距离时不能漏:
bool Place(int X[], multiset<int> D, int N, int left, int right) { bool found = false; if(D.empty()) return true; auto it = D.end(); --it; X[right] = *it; int i, j; /*判断左边点到X[right]的距离是否都存在*/ for(i=1; i<left; i++) { if((it = D.find(X[right] - X[i])) != D.end()) D.erase(it); else { for(int k = i-1;k>0; k--) D.insert(X[right] - X[i]); break; } } /*左边点到X[right]距离都存在则判断右边*/ if(i == left) { for(i=right+1; i<=N; i++) { if((it = D.find(X[i] - X[right])) != D.end()) D.erase(it); else { /*右边不符合,还得把先前在左边删除的节点补上再补上右边的*/ for(int k = 1;k<left; k++) D.insert(X[right] - X[k]); for(int k = i-1;k>right; k--) D.insert(X[k] - X[right]); break; } } /*左右两边都符合,进行下一轮判断,不符合就继续执行程序*/ if(i == N+1) { found = Place(X, D, N, left, right-1); /*下一层成功就直接返回成功,否则尝试左边*/ if(!found) { for(i=1; i<left; i++) D.insert(X[right] - X[i]); for(i=right+1; i<=N; i++) D.insert(X[i] - X[right]); } else return true; } } /*如果上一轮没有返回,代表插入到right失败,现在插入到左边*/ X[left] = X[N] - X[right]; /*判断左边点到X[left]的距离是否都存在*/ for(i=1; i<left; i++) { if((it = D.find(X[left] - X[i])) != D.end()) D.erase(it); else { for(int k = i-1;k>= 0; k--) D.insert(X[left] - X[k]); break; } } /*左边点到X[right]距离都存在则判断右边*/ if(i == left) { for(i=right+1; i<=N; i++) { if((it = D.find(X[i] - X[left])) != D.end()) D.erase(it); else { /*右边不符合,还得把先前在左边删除的节点补上再补上右边的*/ for(int k = 1;k<left; k++) D.insert(X[left] - X[k]); for(int k = i-1;k> right; k--) D.insert(X[k] - X[left]); break; } } /*左右两边都符合,进行下一轮判断,不符合就继续执行程序*/ if(i == N+1) { found = Place(X, D, N, left+1, right); /*下一层成功就直接返回成功,否则返回失败*/ if(!found) { for(i=1; i<left; i++) D.insert(X[left] - X[i]); for(i=right+1; i<=N; i++) D.insert(X[i] - X[left]); } else return true; } } return false; }
测试结果
使用输出给出的点列,进行测试,结果如下:
总结:
这一次的算法,其实不算难,很容易就理解了。编码的时候都没有再看书,直接根据自己的理解就开始写代码,果然犯了大错误,导致花费大量时间调试代码。所以以后一定得先写出很接近于代码逻辑的伪代码时,才能开始编码。书上给出的伪代码,不是不正确,而是很多细节的部分没有涉及,比如如何删除距离,如何补回距离。需要变得更加详细时,才能动手写。