[题解] CF GYM 106554 J. 极角排序 / 双指针 / 单调队列

[题解] CF GYM 106554 J. 极角排序 / 双指针 / 单调队列

TAG: 计算几何,极角排序,双指针,单调队列,CF GYM,南昌邀请赛
题目链接: 点击此处打开题目

题目大意

平面上有 n 个食物,蛇位于原点。选择一个以原点为顶点的扇形,使它覆盖至少 k 个食物,并最小化扇形面积。

若扇形圆心角为 \( \theta \),半径为 \( R \),面积为:

\[S=\frac{1}{2}\theta R^2 \]

前置模块记忆

1. 不使用 atan2 的极角排序

极角排序只需要比较两个方向的先后,不必真的求出每个向量的浮点极角。

先把平面分成两部分:

  • h=0:正 \( x \) 轴和上半平面,极角属于 \( [0,\pi) \);
  • h=1:负 \( x \) 轴和下半平面,极角属于 \( [\pi,2\pi) \)。

若两个向量位于不同半平面,按 h 排序;否则利用叉积判断逆时针方向。对于同一半平面的向量 \( a,b \),若 \( a\times b>0 \),则 a 排在 b 前面。

int half(int x,int y){
	return (y>0||(y==0&&x>0))?0:1;
}

struct node{
	int x,y;
	int cnt,dis;
	bool operator<(const node&t)const{
		int h1=half(x,y);
		int h2=half(t.x,t.y);
		if(h1!=h2) return h1<h2;
		__int128 cr=(__int128)x*t.y-(__int128)y*t.x;
		return cr>0;
	}
};

这个模块使用时要注意:

  • 零向量不能参与极角排序;
  • 同方向向量的叉积为 0,本题会提前合并同方向食物;
  • 坐标较大时,叉积建议使用 __int128

2. 使用 atan2(cross,dot) 计算有向夹角

要求从向量 \( a \) 逆时针旋转到向量 \( b \) 的夹角,范围为 \( [0,2\pi) \)。

db angle(node a,node b){
	__int128 cr=(__int128)a.x*b.y-(__int128)a.y*b.x;
	__int128 dt=(__int128)a.x*b.x+(__int128)a.y*b.y;
	db res=atan2l((db)cr,(db)dt);
	if(res<0) res+=2.0L*PI;
	return res;
}

原理是:

\[a\times b=|a||b|\sin\theta,\qquad a\cdot b=|a||b|\cos\theta \]

因此:

\[\theta=\operatorname{atan2}(a\times b,a\cdot b) \]

不要分别计算两个向量的极角再相减。两个极角非常接近时,大浮点数相减会损失有效精度;先用整数叉积、点积,再调用一次 atan2l 更稳定。

核心思路

1. 突破口:最优扇形的边界一定经过食物方向

固定要覆盖的一组食物后,半径只需扩张到其中最远的食物,两条角边也应向最外侧的两个食物方向收紧。因此不需要枚举任意实数角度,只需枚举食物方向组成的区间。

2. 合并同方向食物

同一方向上的食物会被同时吃掉,不能只选择其中一部分。将方向 \( (x,y) \) 除以 \( \gcd(|x|,|y|) \),按最简方向向量合并:

  • cnt:该方向上的食物数量;
  • dis:该方向上最大的距离平方 \( x2+y2 \)。

3. 极角排序并拆环

极角排序后,一个扇形覆盖的方向一定是方向环上的连续段。将排好序的方向数组复制一遍接到末尾,就能把跨过正 \( x \) 轴的环形区间转化成普通连续区间。

4. 双指针寻找最小合法右端点

固定左端点 l,不断右移 r,直到窗口中的食物数量第一次达到 k

此时的 r 一定最优,因为继续右移只会使圆心角不减、窗口最大距离平方不减,面积不可能变小。随着 l 右移,r 也只会右移,因此可以使用双指针。

5. 单调队列维护最大半径

窗口 \( [l,r] \) 对应的面积为:

\[\frac{1}{2}\operatorname{angle}(a_l,a_r) \times \max_{i=l}^{r} dis_i \]

使用 dis 单调递减的队列:

  • 加入 r 时,弹出队尾所有 dis 不大于 a[r].dis 的位置;
  • 删除 l 时,如果队首就是 l,弹出队首;
  • 队首始终对应当前窗口最大的距离平方。

复杂度

设不同方向数为 \( p\le n \)。

  • 时间复杂度:\( O(n\log n) \)。方向合并和极角排序为 \( O(n\log n) \),双指针与单调队列为 \( O(p) \)。
  • 空间复杂度:\( O(n) \)。需要存储方向数组、复制数组和单调队列。

代码前说明

关键变量

  • a[i].x,a[i].y:约分后的方向向量;
  • a[i].cnt:该方向上的食物数量;
  • a[i].dis:该方向上的最大距离平方;
  • sum:当前窗口包含的食物数量;
  • q[hd...tl]:按 dis 单调递减的方向下标队列;
  • r:当前左端点对应的最小合法右端点。

核心公式

\[S(l,r)=\frac{1}{2}\operatorname{angle}(a_l,a_r) \times \max_{i=l}^{r}a_i.dis \]

代码中 r 必须初始化为 0。原写法 for(int l=1,r;...) 中的 r 未初始化,会产生未定义行为。

完整代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define db long double
#define fi first
#define se second
#define endl '\n'
#define INF 0x3f3f3f3f3f3f3f3f
const db PI=acosl(-1.0L);
const int N=2e5+5;
int n,k;
struct node{
	int x,y;
	int cnt,dis;
	bool operator<(const node&t)const{//手写极角排序
		//h=0:极角属于 [0,π)
		//h=1:极角属于 [π,2π)
		int h1=(y>0||(y==0&&x>0))?0:1;
		int h2=(t.y>0||(t.y==0&&t.x>0))?0:1;
		if(h1!=h2) return h1<h2;
		//在同一半平面内用叉积旋转的方向来判断
		__int128 cr=(__int128)x*t.y-(__int128)y*t.x;
		return cr>0;
	}
}a[N<<1];
int p;
int q[N<<1];//单调队列(方向下标)
db angle(node a,node b){
	__int128 cr=(__int128)a.x*b.y-(__int128)a.y*b.x;
	__int128 dt=(__int128)a.x*b.x+(__int128)a.y*b.y;
	db res=atan2l((db)cr,(db)dt);
	if(res<0) res+=2.0L*PI;
	return res;
}
void solve(){
	cin>>n>>k;
	map<pair<int,int>,pair<int,int>> mp;
	for(int i=1;i<=n;i++){
		int x,y;cin>>x>>y;
		int d=x*x+y*y;
		int g=__gcd(abs(x),abs(y));
		x/=g,y/=g;
		mp[{x,y}].fi++;
		mp[{x,y}].se=max(d,mp[{x,y}].se);
	}
	p=0;
	for(auto [x,y]:mp){
		a[++p]={x.fi,x.se,y.fi,y.se};
	}
	sort(a+1,a+p+1);
	for(int i=1;i<=p;i++){
		a[i+p]=a[i];
	}
	int sum=0;
	int hd=1,tl=0;
	int r=0;
	db ans=1e100L;
	for(int l=1;l<=p;l++){
		//找到第一个使食物数量达到k的右端点
		while(sum<k){
			r++;
			sum+=a[r].cnt;
			//加入右端点,维护dis单调递减
			while(hd<=tl&&a[q[tl]].dis<=a[r].dis) tl--;
			q[++tl]=r;
		}
		db w=angle(a[l],a[r]);
		ans=min(ans,w*(db)a[q[hd]].dis/2.0L);
		sum-=a[l].cnt;
		if(hd<=tl&&q[hd]==l) hd++;
	}
	cout<<fixed<<setprecision(12)<<ans<<endl;
}
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t=1;cin>>t;
    while(t--) solve();
    return 0;
}
posted @ 2026-07-18 15:56  艾拉别哭  阅读(6)  评论(0)    收藏  举报