P4557 [JSOI2018]战争 题解

发布时间 2023-05-25 22:06:05作者: Bloodstalk

闵可夫斯基和

前言

入门建议看吉老师(吉如一)的计算几何入门到放弃。感觉应该是讲的最通俗易懂的了。

本文借鉴了Winxp的博客,以及吉老师视频中的思路。

写这篇博客的初衷是因为我作为一个初学者,此题里的题解对我来说理解起来不算太难,但是实现起来细节比较多,题解里也没有很详细地去解释(可能是因为我太菜了)。所以写这篇博客详细地解释一下这道题目里的一些操作。

定义

两个图形(也就是点集) \(A,B\) 的闵可夫斯基和定义为 \(C = \{a+b\mid a\in A,b\in B\}\)

画一画图可以看出,闵可夫斯基和形成的图形就是一个图形 \(A\) 绕着图形 \(B\) 转一圈。

给个典图。

由于我比较菜,所以我们只考虑凸包的闵可夫斯基和的一些性质。

性质

一、

两个凸包的闵可夫斯基和还是凸包。

证明:

考虑凸集的一个性质:若 \(a,b\in A\),则 \(c = xa+(1-x)b ,c\in A(0\leq x \leq 1)\)

\(a_1,a_2\in A,b_1,b_2 \in B\) ,根据闵可夫斯基和的定义,\(a_1+b_1,a_2+b_2\in C\)

任取一个

\[x\in[0,1],x(a_1+b_1)+(1-x)(a_2+b_2) \]

\[= [xa_1+(1-x)a_2] + [xb_1(1-x)b_2] \]

\[[xa_1+(1-x)a_2]\in A, [xb_1(1-x)b_2]\in B \]

证毕。

二、

闵可夫斯基和上的边是由两个凸包的边构成的

这个不好证,但是我们用瞪眼法看一看,应该是对的。

有了这两个性质,我们就能求闵可夫斯基和了。

求法

思路比较简单,但是细节比较多。

因为我们得到 \(A,B\) 两个凸包后,它们的极角序其实是已经排好了的,所以我们就可以采用类似于归并排序的策略,将两个凸包结合起来。

得到闵可夫斯基和后,因为有可能会出现三点共线的情况,所以要再求一次凸包。

il void Minkovski()
{
	for(re int i=1;i<n;i++) a[i] = s[i+1] - s[i]; a[n] = s[1] - s[n];//s是A凸包
	for(re int i=1;i<m;i++) b[i] = h[i+1] - h[i]; b[m] = h[1] - h[m];//h是B凸包
	int p1 = 1 , p2 = 1;
	tot = 1 , G[tot] = s[1] + h[1];//归并加入一下,G数组就是闵可夫斯基和
	while(p1 <= n && p2 <= m) tot++ , G[tot] = G[tot-1] + (a[p1]*b[p2]>=0 ? a[p1++]:b[p2++]);//极角序从小到大并从G[tot-1]开始往后接
	while(p1 <= n) tot++ , G[tot] = G[tot-1] + a[p1++];
	while(p2 <= m) tot++ , G[tot] = G[tot-1] + b[p2++];
}

我们用类似 a[i] = s[i+1]-s[i] 的操作得出每条边。再以 s[1]+h[1] 为起点开始往后加,每次加的时候就以 G[tot-1] 为基础往后接上。

应用

P4557 [JSOI2018]战争

一句话题意。

给定两个凸包 \(A,B\)\(q\) 次询问,每次给出一个向量 \(\overrightarrow{x}\),问 \(B\) 平移向量 \(\overrightarrow{x}\) 后,\(A\)\(B\) 是否还有交集。\(n,m,q \leq 10^5\)

原题上领地是看三角形内部的区域,实际上就是凸包。而问是否有交集,数学化后就有了下面的式子:

如果有交,那么

\[\exists b + \overrightarrow{x} = a(a\in A,b\in B) \]

转化一下,则有

\[\overrightarrow{x} \in \{a-b\mid a\in A,b\in B\} \]

我们看到这个很像 \(A,B\) 的闵可夫斯基和的形式,但是它们是相减,怎么办?把 \(B\) 取反,就变成闵可夫斯基和的形式了。

按照上述过程求出闵可夫斯基和后,接下来就剩下最后一个问题,如果判断一个点是否在凸包内,这个也比较好算,以图为例:

我们选定凸包上最左下角的点为起点,并将这个凸包平移一下,使得左下角的点坐落在原点上。这样就容易得出其他点的极角序了,我们按极角序进行二分,找到当前这个点与起点的连线位于哪两条线(凸包上的点与起点的连线)之间。然后如图连两条线,运用叉积运算判断这个点是在凸包外还是在凸包内即可。

另外还有一些小细节,我会在代码中写清。

#include<bits/stdc++.h>
//#define int long long
#define ll long long
#define next nxt
#define re register
#define il inline
const int N = 1e5 + 5;
const double eps = 1e-6;
const double Pi = acos(-1.0);
using namespace std;

struct Point{
	double x,y;
}a[N],b[N],s[N],h[N],A[N],G[N],d,st;
int n,m,q,tot;

il int read()
{
	int f=0,s=0;
	char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f |= (ch=='-');
	for(; isdigit(ch);ch=getchar()) s = (s<<1) + (s<<3) + (ch^48);
	return f ? -s : s;
}

Point operator +(Point a,Point b) { return Point{a.x+b.x,a.y+b.y}; }//加

Point operator -(Point a,Point b) { return Point{a.x-b.x,a.y-b.y}; }//减

il double operator *(Point a,Point b) { return a.x*b.y - a.y*b.x; }//叉积

il double operator &(Point a,Point b) { return a.x*b.x + a.y*b.y; }//点积

il double cross(Point a,Point b,Point c) { return (b-a)*(c-a); }//判断c和直线ab的关系 

il double len(Point a) { return sqrt(a&a); }

il bool cmp(Point a,Point b) { return a.x == b.x ? a.y < b.y : a.x < b.x; }//以x为第一关键字,y为第二关键字排序

il bool cmp2(Point a,Point b) { return a*b > 0 || (a*b == 0 && len(a) < len(b)); }//按极角序排序,如果极角相同短的放前边

il void ConvexHull(Point *s,Point *p,int &n)//s是凸包数组,p是原数组
{
	int cnt = 0;
	sort(p+1,p+n+1,cmp);
	s[++cnt] = p[1];
	for(re int i=2;i<=n;i++) //Andrew求凸包
	{
		while(cnt > 1 && cross(s[cnt-1],s[cnt],p[i]) <= 0) cnt--;
		s[++cnt] = p[i];
	}
	int t = cnt;
	for(re int i=n-1;i>=1;i--)
	{
		while(cnt > t && cross(s[cnt-1],s[cnt],p[i]) <= 0) cnt--;
		s[++cnt] = p[i];
	}
	n = cnt - 1;//第一个点加了两次,别忘了-1
}

il void Minkovski()
{
	memset(a , 0 , sizeof a);
	memset(b , 0 , sizeof b);//重复利用一下a,b数组,其实可以不memset
	for(re int i=1;i<n;i++) a[i] = s[i+1] - s[i]; a[n] = s[1] - s[n];//最后一条线单独写一下
	for(re int i=1;i<m;i++) b[i] = h[i+1] - h[i]; b[m] = h[1] - h[m];
	int p1 = 1 , p2 = 1;
	tot = 1 , G[tot] = s[1] + h[1];//以s[1]+t[1]为起点开始加
	while(p1 <= n && p2 <= m) tot++ , G[tot] = G[tot-1] + (a[p1]*b[p2]>=0 ? a[p1++]:b[p2++]);
	while(p1 <= n) tot++ , G[tot] = G[tot-1] + a[p1++];
	while(p2 <= m) tot++ , G[tot] = G[tot-1] + b[p2++];
}

il bool Judge(Point a)
{
	if(a*A[1] > 0 || A[tot]*a > 0) return 0;//绝对不可能在凸包内的情况
	int pos = lower_bound(A+1,A+tot+1,a,cmp2) - A - 1;//lower_bound求的是第一个极角序不小于它的,所以减个1
	return (a-A[pos])*(A[pos%tot+1]-A[pos])<=0;//如上文所说
}

signed main()
{
	n = read() , m = read() , q = read();
	for(re int i=1;i<=n;i++) scanf("%lf%lf",&a[i].x,&a[i].y);
	for(re int i=1;i<=m;i++) scanf("%lf%lf",&b[i].x,&b[i].y) , b[i].x = -b[i].x , b[i].y = -b[i].y;
	ConvexHull(s,a,n);
	ConvexHull(h,b,m);
	Minkovski();
	ConvexHull(A,G,tot);
	st = A[1];//选定一个基准点,并且把这个基准点平移到(0,0)的位置上,这样有利于我们求极角序
	for(re int i=1;i<=tot;i++) A[i] = A[i] - st;//别的也减一下
	while(q--)
	{
		scanf("%lf%lf",&d.x,&d.y);
		printf("%d\n",Judge(d-st));//这个也别忘了减
	}
	return 0;
}

由此可以看出,整个复杂度是 \(O((n+m+q)\log(n+m))\) 的,\(n,m,q\) 同阶,复杂度也就是 \(O(n\log n)\),可以通过。