[题解] CF GYM 106554 J. 极角排序 / 双指针 / 单调队列
[题解] CF GYM 106554 J. 极角排序 / 双指针 / 单调队列
TAG: 计算几何,极角排序,双指针,单调队列,CF GYM,南昌邀请赛
题目链接: 点击此处打开题目
题目大意
平面上有 n 个食物,蛇位于原点。选择一个以原点为顶点的扇形,使它覆盖至少 k 个食物,并最小化扇形面积。
若扇形圆心角为 \( \theta \),半径为 \( R \),面积为:
前置模块记忆
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;
}
原理是:
因此:
不要分别计算两个向量的极角再相减。两个极角非常接近时,大浮点数相减会损失有效精度;先用整数叉积、点积,再调用一次 atan2l 更稳定。
核心思路
1. 突破口:最优扇形的边界一定经过食物方向
固定要覆盖的一组食物后,半径只需扩张到其中最远的食物,两条角边也应向最外侧的两个食物方向收紧。因此不需要枚举任意实数角度,只需枚举食物方向组成的区间。
2. 合并同方向食物
同一方向上的食物会被同时吃掉,不能只选择其中一部分。将方向 \( (x,y) \) 除以 \( \gcd(|x|,|y|) \),按最简方向向量合并:
cnt:该方向上的食物数量;dis:该方向上最大的距离平方 \( x2+y2 \)。
3. 极角排序并拆环
极角排序后,一个扇形覆盖的方向一定是方向环上的连续段。将排好序的方向数组复制一遍接到末尾,就能把跨过正 \( x \) 轴的环形区间转化成普通连续区间。
4. 双指针寻找最小合法右端点
固定左端点 l,不断右移 r,直到窗口中的食物数量第一次达到 k。
此时的 r 一定最优,因为继续右移只会使圆心角不减、窗口最大距离平方不减,面积不可能变小。随着 l 右移,r 也只会右移,因此可以使用双指针。
5. 单调队列维护最大半径
窗口 \( [l,r] \) 对应的面积为:
使用 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:当前左端点对应的最小合法右端点。
核心公式
代码中 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;
}

浙公网安备 33010602011771号