[题解]P1452 【模板】旋转卡壳 | [USACO03FALL] Beauty Contest G

P1452 【模板】旋转卡壳 | [USACO03FALL] Beauty Contest G

旋转卡壳模板题。凸包用的是Andrew算法,就不详述了,具体可以查查资料了解,但提一嘴Andrew算法的一些细节问题:

Andrew算法的一些细节

Andrew算法的模板代码如下:

sort(a+1,a+1+n,cmp);
st[++top]=1;
for(int i=2;i<=n;i++){
	while(top>=2&&cross(a[st[top]]-a[st[top-1]],a[i]-a[st[top]])>=0)
		vis[st[top--]]=0;
	vis[i]=1,st[++top]=i;
}
int tmp=top;
for(int i=n-1;i>=1;i--){
	if(vis[i]) continue;
	while(top>tmp&&cross(a[st[top]]-a[st[top-1]],a[i]-a[st[top]])>=0)
		vis[st[top--]]=0;
	vis[i]=1,st[++top]=i;
}

其中求叉积的部分,\(\ge\)\(>\)是有区别的。\(\ge\)表示如果方向相同也要删去,结果就不会出现点在凸包边上的情况;\(>\)则是不删,结果就可能出现点在凸包边上的情况。

如果是求周长的话,倒是两种都可以,不过大多数情况下使用\(>\)会很麻烦。

比如这道题,考虑所有点都共线,这样最终结果就是所有点都在凸包上,如果不特判,下一步找直径就会出现高度相同导致的死循环(具体见下面的实现细节)。如果特判共线的话,代码又难以实现(所以这道题用这种写法几乎是没法做的)。

不如仅保留两端的点,这样我们特判条件就是\(n=3\),还能避免这种死循环。

所以写凸包真的真的不建议保留点在凸包边上,否则可能出一系列奇奇怪怪的问题!

下面的内容都默认没有点在凸包边上。


还有就是\(\ge\)\(\le\)的区别。其实这两种的答案都是对的,只不过求出凸包的顺序一个顺时针一个逆时针而已。


还有还有,代码中的\(vis\)是可以去掉的,原因显然
(如果你的代码中while循环的循环条件非得要写>或者<的话,那么应对所有点共线的情况,每个点两次循环都会进栈,因此栈空间需要开成\(2\)倍,用>=<=则不需要。)
(到现在才直到原来\(vis\)数组是不需要的(汗)

旋转卡壳

一个拥有\(2^4\)种读音的算法

旋转卡壳是用于解决凸包直径、最小矩形覆盖等问题的算法。求得凸包后时间复杂度为\(O(n)\)。这里介绍求凸包直径的做法。

凸包直径,就是凸包上所有点对的最长距离。

我们定义“对踵点对”为:\(2\)组平行的切线与凸多边形相交形成的顶点点对。

如下图,\(C\)\(G\)是一组对踵点对,而\(C\)\(H\)也是一组对踵点对,然而\(C\)\(A\)不是对踵点对。

对踵点对可以分为\(3\)种形态:点-点、点-边、边-边:

然后我们发现,“点-边”情况就是\(2\)个“点-点”情况,而“边-边”情况就是\(4\)个“点-点”情况。我们可以发现,所有对踵点对都能通过“点-边”情况来确定,也就是枚举每一条边,找到距离这条边最远的点,这样找到的点一定与这条边的\(2\)个端点分别对踵。这也是旋转卡壳的核心思想。

如果我们枚举点呢?这种情况就不是很好做了,因为一个点的对踵点不一定是离它最远的点,其对踵点可以有很多个,代码实现不太容易,而且不好处理重复计算的情况,效率可能较低。

还想到了另一种枚举边的方法,似乎不是很传统的做法所以放到文章最后了。

需要注意的一点是“凸多边形中,到一个顶点距离最远的顶点一定是它的对踵点”这个命题是错误的。
可以放上一个反例:

如上图,到\(M\)最远的点是\(L\),但\(L\)\(M\)并不是对踵点对。

看到有个题解是用“有一个点到指定边的距离最大,那么就可以推出他到这条边的两个端点中的一个一定是最远的”这个结论来说明旋转卡壳的正确性的。根据上面的反例,我们同样可以推翻这个结论。


那么旋转卡壳的正确性是怎么保证的?我们枚举对踵点对有什么用呢?

我们凭直觉就能感受到:直径一定是\(1\)组对踵点对的连线!
(注意和上面错误的结论区分)

但仔细一想这个结论并不是那么显然的,我们理论证明一下:

证明“直径一定是对踵点对确定的”,即证明“非对踵点对一定不是直径”。
我们思考一下,怎么判断两个点是不是对踵点对呢?

如上图,\(\angle\alpha+\angle\beta>180^\circ\),故\(L\)\(N\)不是对踵点对。
\(\angle\delta+\angle\gamma\le 180^\circ\),故\(K\)\(P\)是对踵点对。

故我们判断两点是否对踵,就要判断它们和周围边的夹角(一般是两组,上面为了方便看只给了\(1\)组)的和与\(180^\circ\)的关系。

对于图中不对踵的两点\(L,N\),我们仅需证明\(LN\)一定不是直径即可。
由于\(\angle\alpha+\angle\beta>180^\circ\),则必有\(1\)个角是钝角。我们选择那个钝角(这里选择\(\angle\alpha\)),连接形成一个三角形:

由于这是一个钝角三角形,钝角的对边一定\(>\)其他的边。自然\(LN\)就不是最长边。

理论支撑有了,我们有一套完整的过程了:

  • 枚举每一条边,找到离它最远的点(即对踵点),用它分别连接这条边的\(2\)个端点,用形成的\(2\)条线段的长度更新答案即可。

由于这是一个凸多边形,所以从一边开始,到所有点的距离构成一个单峰函数。所以我们只需要按边移动的顺序去移动点,直到找到这个峰值。

但这种做法是\(O(n^2)\)的,我们可以用双指针优化,即每次点可以从上一次的位置开始移动。为什么这样可行呢?因为如果我们枚举顺时针方向的下一条边,点是一定不会逆时针移动的。这样,边转一圈,点也只会转一圈(这个应该不太需要证明),就是\(O(n)\)了。

找峰值的话,一般有\(2\)种写法:

  • 一种是\(>\)作为循环条件,即遇到距离相等就停止,不继续找。如果你写的是这种情况,则指针的初始值应为\(2\),否则指针就会一直卡在第\(1\)个点不动了(想一想为什么)。
  • 一种是\(\ge\)作为循环条件,遇到距离相等仍然继续找。如果你写的是这种情况,则需要特判凸包大小为\(2\)的情况,否则距离相同会不断去取一个节点,就会死循环。

为什么\(2\)种写法都对呢?


如图,两条红色线是平行的,相当于对踵点对的边-边形态(我们已经保证凸包的边上不存在节点)。假设我们求凸包的顺序是顺时针。

那么对于第\(1\)种写法,我们找到\(HG\)的对应点就是\(B\),用\(HB\)\(GB\)更新了答案,找到\(BC\)的对应点是\(G\),又用\(HC\)\(GC\)更新了答案,可以发现\(BC,HG\)形成的\(4\)组边都参与了更新。
\(2\)种写法\(HG\)找到点\(C\)\(BC\)找到点\(G\),同样所有\(4\)条边都参与了答案的更新。就是说两种写法都能遍历到所有对踵点对,所以说两种写法都是正确的。

\(2\)种写法都有需要注意的事项,具体实现需要留心。


总结一下实现细节:

  • 凸包实现上,不要让点出现在凸包边上,否则很多步骤都可能出问题。
  • 旋转卡壳找峰值,不同的写法有不同的注意事项:
    • \(>\)作为循环条件的话,指针的初始值应为\(2\),否则指针就会一直卡在第\(1\)个点不动了。
    • \(\ge\)作为循环条件的话,需要特判凸包大小为\(2\)的情况,否则距离相同会不断去取一个节点,就会死循环。

该题的实现细节

  • 注意要输出长度的平方,如果你是中途计算距离,最后再平方,可能会出现浮点误差导致科学计数法,需要取一下整。
    当然更好的做法是中途不开根号,这样最后也不用平方了,避免误差还高效。

Code

点击查看代码
#include<bits/stdc++.h>
#define nxtnode(x) (x%top)+1
using namespace std;
struct vec{
	double x,y;
	vec operator+(const vec b){return {x+b.x,y+b.y};}
	vec operator-(const vec b){return {x-b.x,y-b.y};}
	int squalen(){return x*x+y*y;}
}a[50010];
bool cmp(vec a,vec b){
	if(a.x<b.x) return 1;
	if(a.x>b.x) return 0;
	return a.y<b.y;
}
inline double cross(vec a,vec b){return a.x*b.y-a.y*b.x;}
int n,st[50010],top;
int ans;
void convex_hull(){
	sort(a+1,a+1+n,cmp);
	st[top=1]=1;
	for(int i=2;i<=n;i++){
		while(top>1&&cross(a[st[top]]-a[st[top-1]],a[i]-a[st[top]])>=0)
			top--;
		st[++top]=i;
	}
	int tmp=top;
	for(int i=n-1;i>=1;i--){
		while(top!=tmp&&cross(a[st[top]]-a[st[top-1]],a[i]-a[st[top]])>=0)
			top--;
		st[++top]=i;
	}
	top--;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i].x>>a[i].y;
	convex_hull();
	if(top==2) ans=(a[st[2]]-a[st[1]]).squalen();
	else{
		int cur=1;
		for(int i=1;i<=top;i++){
			vec bottom=a[st[i+1]]-a[st[i]];
			while(fabs(cross(bottom,a[st[nxtnode(cur)]]-a[st[i]]))>=
				fabs(cross(bottom,a[st[cur]]-a[st[i]]))){
				cur=nxtnode(cur);
			}
			ans=max(ans,max((a[st[cur]]-a[st[i]]).squalen(),(a[st[cur]]-a[st[i+1]]).squalen()));
		}
	}
	cout<<ans<<"\n";
	return 0;
}

另一种找对踵点的方法

上面的方法是通过叉积法找到一边最远的点,就是边上两点的对踵点了。

还可以通过向量的方向来求,其实是差不多的(而且代码实现还短点?)。

如图,红色边和所有绿色边求叉积都是\(<0\)的,和所有蓝色边求叉积都是\(>0\)的。我们要找的红色边的两个端点的对踵点,就是绿色和蓝色的分界处那个点。所以只需要从\(A\)开始遍历每个点,直到该点即将走的下一条边和红色边求叉积\(>/\ge0\)为止。

实际上,停止条件需要根据凸包求出的顺序来决定,如果凸包求出来的是顺时针,则是\(>/\ge\)为止,逆时针就是\(</\le\)为止。这里拿顺时针举例,代码也是顺时针的凸包。


同样考虑一下边界,还是\(2\)种写法,和上面的很像:

  • 叉积\(\le 0\)作为循环条件,则需要特判大小为\(2\)的情况。
  • 叉积\(<0\)作为循环条件,指针需要从\(2\)开始。

这个过程很像上面的过程逆过来,上面是用边找点,这个是用点找边,正确性证明可以参照上面。


给出代码实现。

点击查看代码
#include<bits/stdc++.h>
#define nxtnode(x) (x%top+1)
#define N 50010
using namespace std;
struct vec{
	double x,y;
	vec operator+(vec b){return {x+b.x,y+b.y};}
	vec operator-(vec b){return {x-b.x,y-b.y};}
	int squalen(){return x*x+y*y;}
}a[N];
bool cmp(vec a,vec b){return a.x==b.x?a.y<b.y:a.x<b.x;}
double cross(vec a,vec b){return a.x*b.y-a.y*b.x;}
int n,st[N],top,ans;
bool vis[N];
void convex_hull(){
	sort(a+1,a+1+n,cmp);
	memset(vis,0,sizeof vis);
	st[top=1]=1;
	for(int i=2;i<=n;i++){
		while(top>1&&cross(a[st[top]]-a[st[top-1]],a[i]-a[st[top]])>=0){
			vis[st[top--]]=0;
		}
		vis[i]=1,st[++top]=i;
	}
	int tmp=top;
	for(int i=n-1;i>=1;i--){
		if(vis[i]) continue;
		while(top!=tmp&&cross(a[st[top]]-a[st[top-1]],a[i]-a[st[top]])>=0){
			vis[st[top--]]=0;
		}
		vis[i]=1,st[++top]=i;
	}
	top--;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i].x>>a[i].y;
	convex_hull();
	int j=2;
	for(int i=1;i<=top;i++){
		double t;
		while((t=cross(a[st[nxtnode(i)]]-a[st[i]],a[st[nxtnode(j)]]-a[st[j]]))<0){
			j=nxtnode(j);
		}
		ans=max(ans,(a[st[i]]-a[st[j]]).squalen());
		if(t==0) ans=max(ans,(a[st[i]]-a[st[nxtnode(j)]]).squalen());
	}
	cout<<ans;
	return 0;
}
posted @ 2024-07-18 11:29  Sinktank  阅读(43)  评论(0编辑  收藏  举报
★CLICK FOR MORE INFO★ TOP-BOTTOM-THEME
Enable/Disable Transition
Copyright © 2023 ~ 2024 Sinktank - 1328312655@qq.com
Illustration from 稲葉曇『リレイアウター/Relayouter/中继输出者』,by ぬくぬくにぎりめし.