2026 杭电多校第三场 1012. Shift Square(差分凸包 + 闵可夫斯基和)
[题解] 2026 杭电多校第三场 1012. Shift Square(差分凸包 + 闵可夫斯基和)
题目链接:HDU Contest 1231 - 1012 Shift Square
比赛链接:2026“钉耙编程”中国大学生算法设计暑期联赛(3)
TAG:计算几何、凸包、闵可夫斯基和、支撑函数、Cauchy 周长公式
题意
平面上有 \(n\) 个点,所有点都会绕原点 \(O(0,0)\) 以相同角速度旋转,因此任意时刻所有点相对于原点的旋转角相同。
天文台使用一个边平行于坐标轴的正方形观测框覆盖所有点,并要求:
- 正方形必须覆盖当前所有点;
- 正方形可以任意平移;
- 不要求覆盖原点;
- 在满足覆盖条件的正方形中,选择周长最小的一个。
随着点集不断旋转,最小观测框的周长也会变化。求这个周长在无限长时间内的平均值。
一、从固定旋转角开始分析
设当前点集相对于初始位置旋转了 \(\theta\)。
由于观测框的边始终平行于坐标轴,固定 \(\theta\) 后,覆盖点集所需的最小轴对齐矩形的长、宽分别为:
正方形可以自由平移,所以只需要让正方形边长同时不小于这两个跨度。
因此,当前最小正方形边长为
周长为
题目要求的是
如果直接枚举旋转角,需要处理凸包支撑点切换、长宽函数分段以及两者大小关系的变化,实现非常复杂。
真正困难的不是求某个时刻的答案,而是积分中出现了
如果分别研究 \(W_x,W_y\),还要额外寻找二者的交点。更自然的方向是把这个最大值整体解释成某个凸图形的支撑函数,再用凸几何中的周长公式一次完成积分。
二、只需要保留原点集的凸包
对于任意方向,点集投影的最大值和最小值一定在凸包顶点处取得。
因此,内部点不会影响任何方向上的投影宽度,可以先求原点集的凸包 \(K\)。
将原点 \(p_i=(x_i,y_i)\) 逆时针旋转 \(\theta\) 后,有
因此定义两个单位向量
其中 \(\boldsymbol u\) 的极角是 \(-\theta\)。当 \(\theta\) 遍历完整周期时,\(-\theta\) 也恰好遍历完整周期,因此这个符号方向不会影响后面的平均值积分。下文写 \(h_A(\theta)\) 时,表示支撑方向随该参数转动。
凸包 \(K\) 在方向 \(\boldsymbol u\) 上的宽度定义为
由旋转坐标公式可知
那么 \(\boldsymbol u,\boldsymbol v\) 是一组相互垂直的单位向量,当前最小正方形边长可以写为
问题变成了:如何处理两个互相垂直方向上的宽度最大值。
三、差分凸包 \(D=K+(-K)\)
1. 支撑函数
对于一个凸集 \(A\),定义它在方向 \(\boldsymbol u\) 上的支撑函数为
支撑函数记录了凸集沿某个方向能够到达的最远位置。
2. 构造差分凸包
定义
这里的 \(+\) 是闵可夫斯基和。由于 \(K\) 是凸集,\(D\) 也是凸集;它又常被称为 \(K\) 的差分体。
计算 \(D\) 的支撑函数:
因此有重要结论:
也就是说,原凸包在某个方向上的宽度,恰好等于差分凸包 \(D\) 在该方向上的支撑函数。
于是
另外,\(D\) 一定关于原点中心对称。事实上,任取 \(a-b\in D\),交换 \(a,b\) 就得到
所以
四、把两个方向的最大值合并成一个支撑函数
设 \(R(D)\) 表示将 \(D\) 逆时针旋转 \(90^\circ\) 后得到的图形。
构造
对于任意两个凸集 \(A,B\),有
原因是线性函数 \(p\mapsto p\cdot\boldsymbol u\) 在凸组合上的值不会超过端点最大值,因此先取并集再取凸包不会改变这个方向上的最大投影。
因此
设 \(R\) 表示逆时针旋转 \(90^\circ\),则
而
由于 \(D\) 关于原点中心对称,
所以
最终得到
即
原问题中的“当前最小正方形边长”,已经变成了新凸包 \(Q\) 的支撑函数。
五、利用 Cauchy 周长公式完成积分
1. Cauchy 周长公式
对于任意平面凸图形 \(Q\),Cauchy 周长公式为
其中 \(w_Q(\theta)\) 是图形在方向 \(\theta\) 上的宽度。
由 \(D=-D\) 可知 \(R(D)=-R(D)\),所以 \(Q\) 同样关于原点中心对称。于是
所以
这里已经把原题中的积分与几何周长联系起来:只要再利用 \(90^\circ\) 旋转对称性缩短积分区间,就能得到最终系数。
2. \(Q\) 具有 \(90^\circ\) 旋转对称性
由定义
将 \(Q\) 再旋转 \(90^\circ\):
因为 \(D=-D\),所以
因此 \(Q\) 具有 \(90^\circ\) 旋转对称性,其支撑函数以 \(\pi/2\) 为周期:
于是
3. 原观测框周长的平均值
由于整体再旋转 \(90^\circ\) 后,横纵跨度只是互换,而正方形边长取二者最大值,因此
所以
又因为
所以最终答案为
容易在这里漏掉一个 \(4\):题目平均的是正方形周长 \(4s(\theta)\),而不是边长 \(s(\theta)\)。最终的 \(\frac{2}{\pi}\) 正是把这个 \(4\)、四个长度为 \(\pi/2\) 的周期以及 Cauchy 公式中的系数合并后的结果。
六、算法流程
- 对所有输入点求凸包 \(K\);
- 将 \(K\) 关于原点取反,得到 \(-K\);
- 用闵可夫斯基和计算
- 将 \(D\) 中每个点旋转 \(90^\circ\),得到 \(R(D)\);
- 对 \(D\cup R(D)\) 求凸包,得到 \(Q\);
- 计算 \(Q\) 的周长 \(L\);
- 输出
闵可夫斯基和如何做到线性
Andrew 算法得到的凸包顶点按逆时针排列。把两个凸多边形都旋转到“纵坐标最小、横坐标最小”的顶点作为起点后,各条边的极角已经循环有序。
因此计算 \(K+(-K)\) 时,只需像归并排序一样合并两组边向量:
- 当前两条边叉积大于 \(0\),先取第一条边;
- 叉积小于 \(0\),先取第二条边;
- 叉积等于 \(0\),两条边方向相同,将它们相加后一起前进。
每条边至多处理一次,所以非退化凸多边形的闵可夫斯基和复杂度是 \(O(h)\)。
七、正确性证明
下面证明算法输出的值恰好等于题目要求的平均最小周长。
引理 1:固定角度时,最小正方形边长为两个投影宽度的最大值
旋转 \(\theta\) 后,所有点横坐标的极差为 \(w_K(\boldsymbol u)\),纵坐标的极差为 \(w_K(\boldsymbol v)\)。
任何轴对齐正方形若要覆盖全部点,边长必须同时不小于这两个极差;反过来,边长取二者最大值时,可以分别平移正方形的左右、上下边界,使其覆盖两个坐标区间。
因此
引理 2:\(s(\theta)\) 是凸包 \(Q\) 的支撑函数
令
由闵可夫斯基和的支撑函数性质,
又因为 \(D=-D\),且 \(R^{-1}\boldsymbol u=-\boldsymbol v\),所以
对并集取凸包会把支撑函数变成最大值,故
引理 3:\(Q\) 的周长等于一个周期内边长积分的四倍
\(Q\) 关于原点中心对称,所以由 Cauchy 周长公式,
同时 \(R(Q)=Q\),故 \(h_Q\) 以 \(\pi/2\) 为周期。结合引理 2,
定理:算法输出正确
题目所求平均周长为
算法恰好构造 \(Q\),计算其周长并输出 \(\frac{2}{\pi}\operatorname{Perimeter}(Q)\),所以输出就是题目要求的答案。
八、退化情况
1. 所有点共线
此时原凸包 \(K\) 只有两个端点。
差分凸包 \(D=K-K\) 仍然是一条线段。将它与旋转 \(90^\circ\) 后的线段放在一起求凸包,会得到一个菱形,前面的支撑函数与 Cauchy 公式仍然成立。
代码中对点数不超过 \(2\) 的闵可夫斯基和直接枚举所有点对和,再求一次凸包,避免退化多边形的边向量归并问题。
2. 凸包中的共线点
Andrew 凸包会删除边上的中间共线点,只保留真正的凸包顶点,不影响答案。
3. 所有点重合
此时 \(K,D,Q\) 都只有一个点,周长和最终答案均为 \(0\)。代码中的周长累加自然得到 \(0\)。
九、样例解释
第一组样例
两个点为
原点集是一条长度为 \(1\) 的线段。
差分凸包 \(D\) 是从 \((-1,0)\) 到 \((1,0)\) 的线段。旋转 \(90^\circ\) 后得到从 \((0,-1)\) 到 \((0,1)\) 的线段。
二者取凸包后得到一个顶点为
的菱形,其周长为
因此答案为
与样例输出一致。
第二组样例
四个点都在 \(y\) 轴上,最远两点距离为 \(5\)。
同理答案为
与样例输出一致。
十、复杂度分析
设原点数为 \(n\),原凸包点数为 \(h\)。
- 求原凸包:\(O(n\log n)\);
- 非退化情况下计算闵可夫斯基和:\(O(h)\);
- 当某个凸包点数不超过 \(2\) 时,代码只会枚举常数个点对,随后求凸包;
- 对 \(D\cup R(D)\) 求凸包:\(O(h\log h)\);
- 计算周长:\(O(h)\)。
总时间复杂度为
空间复杂度为
十一、完整代码
展开完整代码(共 156 行)收起代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
using lll=__int128;
using db=long double;
const db PI=acosl(-1.0L);
struct point{
int x,y;
point(int x=0,int y=0):x(x),y(y){}
point operator+(const point &b)const{
return point(x+b.x,y+b.y);
}
point operator-(const point &b)const{
return point(x-b.x,y-b.y);
}
bool operator==(const point &b)const{
return x==b.x&&y==b.y;
}
bool operator<(const point &b)const{
if(x!=b.x) return x<b.x;
return y<b.y;
}
point rot90()const{
return point(-y,x);
}
};
lll cross(point a,point b){
return (lll)a.x*b.y-(lll)a.y*b.x;
}
lll cross(point a,point b,point c){
return cross(b-a,c-a);
}
db dis(point a,point b){
db dx=a.x-b.x;
db dy=a.y-b.y;
return sqrtl(dx*dx+dy*dy);
}
vector<point> Andrew(vector<point> p){
sort(p.begin(),p.end());
p.erase(unique(p.begin(),p.end()),p.end());
int n=p.size();
if(n<=1) return p;
vector<point> s;
for(int i=0;i<n;i++){
while(s.size()>=2&&cross(s[s.size()-2],s.back(),p[i])<=0){
s.pop_back();
}
s.push_back(p[i]);
}
int t=s.size();
for(int i=n-2;i>=0;i--){
while((int)s.size()>t&&cross(s[s.size()-2],s.back(),p[i])<=0){
s.pop_back();
}
s.push_back(p[i]);
}
if(s.size()>1) s.pop_back();
return s;
}
void reorder_lowest_left(vector<point>&p){
int id=0;
for(int i=1;i<(int)p.size();i++){
if(p[i].y<p[id].y||
(p[i].y==p[id].y&&p[i].x<p[id].x)){
id=i;
}
}
rotate(p.begin(),p.begin()+id,p.end());
}
vector<point> Minkowski(vector<point> A,vector<point> B){
if(A.empty()||B.empty()) return {};
if(A.size()<=2||B.size()<=2){
vector<point> C;
for(auto x:A){
for(auto y:B){
C.push_back(x+y);
}
}
return Andrew(C);
}
reorder_lowest_left(A);
reorder_lowest_left(B);
int n=A.size(),m=B.size();
vector<point> ea(n),eb(m);
for(int i=0;i<n;i++){
ea[i]=A[(i+1)%n]-A[i];
}
for(int i=0;i<m;i++){
eb[i]=B[(i+1)%m]-B[i];
}
vector<point> C;
C.push_back(A[0]+B[0]);
int i=0,j=0;
while(i<n||j<m){
if(i==n){
C.push_back(C.back()+eb[j]);
j++;
}else if(j==m){
C.push_back(C.back()+ea[i]);
i++;
}else{
lll cr=cross(ea[i],eb[j]);
if(cr>0){
C.push_back(C.back()+ea[i]);
i++;
}else if(cr<0){
C.push_back(C.back()+eb[j]);
j++;
}else{
C.push_back(C.back()+ea[i]+eb[j]);
i++;
j++;
}
}
}
C.pop_back();
return Andrew(C);
}
void solve(){
int n;cin>>n;
vector<point> p(n);
for(int i=0;i<n;i++){
cin>>p[i].x>>p[i].y;
}
vector<point> K=Andrew(p);
vector<point> neg=K;
for(auto &x:neg){
x.x=-x.x;
x.y=-x.y;
}
neg=Andrew(neg);
vector<point> D=Minkowski(K,neg);
vector<point> all=D;
for(auto x:D){
all.push_back(x.rot90());
}
vector<point> Q=Andrew(all);
db perimeter=0;
if(Q.size()==2){
perimeter=2*dis(Q[0],Q[1]);
}else{
for(int i=0;i<(int)Q.size();i++){
perimeter+=dis(Q[i],Q[(i+1)%Q.size()]);
}
}
db ans=2*perimeter/PI;
cout<<fixed<<setprecision(15)<<ans<<endl;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;cin>>t;
while(t--) solve();
return 0;
}十二、关键结论总结
本题最关键的三步转化是:
1. 宽度转支撑函数
2. 两个垂直方向的最大值转凸包并集
3. 支撑函数积分转周长
最终公式为

浙公网安备 33010602011771号