[ABC202F]Integer Convex Hull

Integer Convex Hull

题解

首先一看到这种求凸包面积为整数的,我们应该很容易想到dp 大概真的很容易想到吧

我们先考虑一下对于一个凸包我们怎么求它的面积,我们可以考虑把它拆分成三角形。
首先,对于一个凸包,它是有上下两条轮廓线的。
对于上轮廓线,我们就加上相邻的两个点与原点组成的三角形,对于下轮廓线,就将其减去。
这样,对于一个有 n n n个点的三角形,我们就可以 O ( n ) O\left(n\right) O(n)地将其求出。
由于它只要求面积是整数,并不要求面积的大小,三角形面积是加还是减并不重要,不妨就让下轮廓的三角形也是加,这并不会影响答案。
对于 S △ A B C S_{\triangle} ABC S△​ABC,我们可以通过正弦定理得到:
S △ A B C = 1 2 A B ⋅ A C ⋅ sin ⁡   A = 1 2 ∣ A B → ∣ ∣ A C → ∣ sin ⁡   A = 1 2 ∣ A B → × A C → ∣ S_{\triangle} ABC=\frac{1}{2}AB\cdot AC\cdot \sin\,A=\frac{1}{2}\left|\overrightarrow{AB}\right|\left|\overrightarrow{AC}\right|\sin\,A=\frac{1}{2}\left|\overrightarrow{AB}\times \overrightarrow{AC}\right| S△​ABC=21​AB⋅AC⋅sinA=21​∣∣∣​AB ∣∣∣​∣∣∣​AC ∣∣∣​sinA=21​∣∣∣​AB ×AC ∣∣∣​
恰好是向量的叉乘,我们可以将那个 1 2 \frac{1}{2} 21​去掉,转化成判断奇偶性,也就是求出 ∣ O A → × O B → ∣ = X A Y B − X B Y A \left|\overrightarrow{OA}\times \overrightarrow{OB}\right|=X_{A}Y_{B}-X_{B}Y_{A} ∣∣∣​OA ×OB ∣∣∣​=XA​YB​−XB​YA​。
我们只需要求出轮廓线上相邻的两个点的叉乘就可以了。

于是我们很容易就联想到通过轮廓线来做 d p dp dp,我们需要分上轮廓线与下轮廓线两条来进行。
设 u p i , j , 0 / 1 up_{i,j,0/1} upi,j,0/1​表示上轮廓线上最后两个点的为 i , j i,j i,j,且此时已经得到面积的奇偶性为 0 / 1 0/1 0/1时的点的选择方案数, d n i , j , 0 / 1 dn_{i,j,0/1} dni,j,0/1​就表示下轮廓线上同样的东西。
很容易发现,对于上轮廓线,我们要求其上所有的向量都是不断向下弯曲的,即下一条边可以由上一条边顺时针翻折得到,这同样可以通过叉乘的值与 0 0 0的大小关系得到。
也就是说当 ∣ P i P j → × P j P k → ∣ < 0 \left|\overrightarrow{P_{i}P_{j}}\times \overrightarrow{P_{j}P_{k}}\right|< 0 ∣∣∣​Pi​Pj​ ​×Pj​Pk​ ​∣∣∣​<0,我们就可以用 u p i , j up_{i,j} upi,j​去更新 u p j , k up_{j,k} upj,k​。
下轮廓线同理。

为了避免最后翻折到往回跑的情况,我们可以先将所有的点按 x x x排序。
对于两点垂直的情况,我们可以单独将其放到下轮廓线中进行处理,这也要求我们对所有点要将 y y y作为第二关键字进行排序。
由于我们求的是最后的方案数,我们势必就要对身处其中的点的数量进行统计,记 o v e r i , j over_{i,j} overi,j​表示在线段 P i P j P_{i}P_{j} Pi​Pj​之上的点的个数。
那么下轮廓线的转移相当于
d n i , j = ∑ k = 1 j − 1 [ ∣ P i P j → × P j P k → ∣ ⩾ 0 ] 2 o v e r k , i d n k , i dn_{i,j}=\sum_{k=1}^{j-1}\left[\left|\overrightarrow{P_{i}P_{j}}\times \overrightarrow{P_{j}P_{k}}\right|\geqslant 0\right]2^{over_{k,i}}dn_{k,i} dni,j​=k=1∑j−1​[∣∣∣​Pi​Pj​ ​×Pj​Pk​ ​∣∣∣​⩾0]2overk,i​dnk,i​
对于上轮廓线的转移,我们只需要将其变成 1 2 \frac{1}{2} 21​的幂,
u p i , j = ∑ k = 1 j − 1 [ ∣ P i P j → × P j P k → ∣ < 0 ] 2 − o v e r k , i u p k , i up_{i,j}=\sum_{k=1}^{j-1}\left[\left|\overrightarrow{P_{i}P_{j}}\times \overrightarrow{P_{j}P_{k}}\right|< 0\right]2^{-over_{k,i}}up_{k,i} upi,j​=k=1∑j−1​[∣∣∣​Pi​Pj​ ​×Pj​Pk​ ​∣∣∣​<0]2−overk,i​upk,i​
这样就可以通过差分的手段求出选择的点的方案数。

由于从不同的点出发的方案数不好合并,我们可以先枚举最左边的起始点,在依次更新 d p dp dp值。
最后求这个点答案时,我们需要枚举最后的终止点与上轮廓与下轮廓靠着终止点的那一条线段。
对于两条线段可能会产生的垂直的情况,由于在这种情况下,前面一条边可能会将其误判成不选,而它又只会在最后出现,我们可以考虑将其特别处理一下。
所以选每个点作为起始点跑一次 d p dp dp,在统计一下答案就可以了。

时间复杂度 O ( n 4 ) O\left(n^4\right) O(n4),如果没有预处理快速幂的话可能会达到 O ( n 4 l o g   n ) O\left(n^4log\,n\right) O(n4logn) ,不过据OneInDark巨佬说 O ( n 5 l o g   n ) O\left(n^5log\,n\right) O(n5logn)都能过
不过我看好像还有用奇怪的随机化方法做的,跑得还挺快的。

源码

#include<bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define lowbit(x) (x&-x)
#define reg register
#define mkpr make_pair
#define fir first
#define sec second
typedef long long LL;
typedef unsigned long long uLL;
const int INF=0x3f3f3f3f;
const int mo=1e9+7;
const int iv2=5e8+4;
const LL jzm=2333;
const int orG=3,invG=332748118;
const double Pi=acos(-1.0);
typedef pair<int,int> pii;
const double PI=acos(-1.0);
template<typename _T>
_T Fabs(_T x){return x<0?-x:x;}
template<typename _T>
void read(_T &x){
	_T f=1;x=0;char s=getchar();
	while(s>'9'||s<'0'){if(s=='-')f=-1;s=getchar();}
	while('0'<=s&&s<='9'){x=(x<<3)+(x<<1)+(s^48);s=getchar();}
	x*=f;
}
template<typename _T>
void print(_T x){if(x<0){x=(~x)+1;putchar('-');}if(x>9)print(x/10);putchar(x%10+'0');}
int add(int x,int y){return x+y<mo?x+y:x+y-mo;}
int qkpow(int a,int s){int t=1;while(s&1){if(s&1)t=1ll*a*t%mo;a=1ll*a*a%mo;s>>=1;}return t;}
int n,up[85][85][2],dn[85][85][2],over[85][85],ans,pow2[105],inv2[105];
struct point{
	int x,y;point(){x=y=0;}
	point(int X,int Y){x=X;y=Y;}
	bool friend operator < (const point &x,const point &y){
		if(x.x==y.x)return x.y<y.y;
		return x.x<y.x;
	}
	point operator - (const point &rhs)const{return point(x-rhs.x,y-rhs.y);}
	int det(const point &rhs)const{return x*rhs.y-y*rhs.x;}
}s[105];
int query(int u,int v){
	int res=0;
	for(int i=1;i<=n;i++)
		if(s[u].x<=s[i].x&&s[i].x<=s[v].x&&(s[v]-s[u]).det(s[i]-s[u])>0)res++;
	return res;
}
signed main(){
	read(n);for(int i=1;i<=n;i++)read(s[i].x),read(s[i].y);sort(s+1,s+n+1);
	pow2[0]=inv2[0]=1;for(int i=1;i<=80;i++)pow2[i]=2ll*pow2[i-1]%mo,inv2[i]=1ll*iv2*inv2[i-1]%mo;
	for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++)over[i][j]=query(i,j);
	for(int x=1;x<=n;x++){
		for(int i=x;i<=n;i++)for(int j=i;j<=n;j++)up[i][j][0]=up[i][j][1]=dn[i][j][0]=dn[i][j][1]=0;
		for(int i=x+1;i<=n;i++){
			int d=s[x].det(s[i])&1;dn[x][i][d]=pow2[over[x][i]];
			if(s[x].x==s[i].x)continue;up[x][i][d]=inv2[over[x][i]];
		}
		for(int k=x+1;k<=n;k++)for(int i=x;i<k;i++)for(int j=i+1;j<k;j++)for(int d=0;d<2;d++)
			if((s[j]-s[i]).det(s[k]-s[j])>=0){
				int d1=(s[k].det(s[j])&1)^d;
				dn[j][k][d1]=add(dn[j][k][d1],1ll*dn[i][j][d]*pow2[over[j][k]]%mo);
			}
			else if(s[k].x!=s[j].x){
				int d1=(s[k].det(s[j])&1)^d;
				up[j][k][d1]=add(up[j][k][d1],1ll*up[i][j][d]*inv2[over[j][k]+1]%mo);
			}
		for(int k=x+1;k<=n;k++)
			for(int j=x;j<=k;j++)for(int i=x;i<=k;i++)
				if((s[k]-s[i]).det(s[j]-s[k])>0)for(int d=0;d<2;d++){
					if(s[k].x==s[i].x)dn[i][k][d]=1ll*iv2*dn[i][k][d]%mo;
					ans=add(ans,1ll*dn[i][k][d]*up[j][k][d]%mo);
				}
	}
	printf("%d\n",ans);
	return 0;
}

谢谢!!!

上一篇:一文带你了解GaussDB(DWS) 的Roach逻辑备份实现原理


下一篇:GYM101673J(模拟)