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(\theta)=\max_i x_i(\theta)-\min_i x_i(\theta), \]

\[W_y(\theta)=\max_i y_i(\theta)-\min_i y_i(\theta). \]

正方形可以自由平移,所以只需要让正方形边长同时不小于这两个跨度。

因此,当前最小正方形边长为

\[s(\theta)=\max\left(W_x(\theta),W_y(\theta)\right), \]

周长为

\[P(\theta)=4s(\theta). \]

题目要求的是

\[\overline P = \frac{1}{2\pi}\int_0^{2\pi}P(\theta)\,d\theta. \]

如果直接枚举旋转角,需要处理凸包支撑点切换、长宽函数分段以及两者大小关系的变化,实现非常复杂。

真正困难的不是求某个时刻的答案,而是积分中出现了

\[\max\left(W_x(\theta),W_y(\theta)\right). \]

如果分别研究 \(W_x,W_y\),还要额外寻找二者的交点。更自然的方向是把这个最大值整体解释成某个凸图形的支撑函数,再用凸几何中的周长公式一次完成积分。


二、只需要保留原点集的凸包

对于任意方向,点集投影的最大值和最小值一定在凸包顶点处取得。

因此,内部点不会影响任何方向上的投影宽度,可以先求原点集的凸包 \(K\)。

将原点 \(p_i=(x_i,y_i)\) 逆时针旋转 \(\theta\) 后,有

\[\begin{aligned} x_i(\theta)&=x_i\cos\theta-y_i\sin\theta,\\ y_i(\theta)&=x_i\sin\theta+y_i\cos\theta. \end{aligned} \]

因此定义两个单位向量

\[\boldsymbol u=(\cos\theta,-\sin\theta), \qquad \boldsymbol v=(\sin\theta,\cos\theta). \]

其中 \(\boldsymbol u\) 的极角是 \(-\theta\)。当 \(\theta\) 遍历完整周期时,\(-\theta\) 也恰好遍历完整周期,因此这个符号方向不会影响后面的平均值积分。下文写 \(h_A(\theta)\) 时,表示支撑方向随该参数转动。

凸包 \(K\) 在方向 \(\boldsymbol u\) 上的宽度定义为

\[w_K(\boldsymbol u) = \max_{p\in K}p\cdot\boldsymbol u - \min_{p\in K}p\cdot\boldsymbol u. \]

由旋转坐标公式可知

\[W_x(\theta)=w_K(\boldsymbol u), \qquad W_y(\theta)=w_K(\boldsymbol v). \]

那么 \(\boldsymbol u,\boldsymbol v\) 是一组相互垂直的单位向量,当前最小正方形边长可以写为

\[s(\theta) = \max\left(w_K(\boldsymbol u),w_K(\boldsymbol v)\right). \]

问题变成了:如何处理两个互相垂直方向上的宽度最大值。


三、差分凸包 \(D=K+(-K)\)

1. 支撑函数

对于一个凸集 \(A\),定义它在方向 \(\boldsymbol u\) 上的支撑函数为

\[h_A(\boldsymbol u) = \max_{p\in A}p\cdot\boldsymbol u. \]

支撑函数记录了凸集沿某个方向能够到达的最远位置。

2. 构造差分凸包

定义

\[D=K+(-K)=K-K = \{a-b\mid a,b\in K\}. \]

这里的 \(+\) 是闵可夫斯基和。由于 \(K\) 是凸集,\(D\) 也是凸集;它又常被称为 \(K\) 的差分体。

计算 \(D\) 的支撑函数:

\[\begin{aligned} h_D(\boldsymbol u) &= \max_{a,b\in K}(a-b)\cdot\boldsymbol u\\ &= \max_{a\in K}a\cdot\boldsymbol u - \min_{b\in K}b\cdot\boldsymbol u\\ &= w_K(\boldsymbol u). \end{aligned} \]

因此有重要结论:

\[\boxed{h_D(\boldsymbol u)=w_K(\boldsymbol u)}. \]

也就是说,原凸包在某个方向上的宽度,恰好等于差分凸包 \(D\) 在该方向上的支撑函数。

于是

\[s(\theta) = \max\left(h_D(\boldsymbol u),h_D(\boldsymbol v)\right). \]

另外,\(D\) 一定关于原点中心对称。事实上,任取 \(a-b\in D\),交换 \(a,b\) 就得到

\[-(a-b)=b-a\in D. \]

所以

\[D=K-K=-(K-K)=-D. \]


四、把两个方向的最大值合并成一个支撑函数

设 \(R(D)\) 表示将 \(D\) 逆时针旋转 \(90^\circ\) 后得到的图形。

构造

\[Q=\operatorname{conv}\left(D\cup R(D)\right). \]

对于任意两个凸集 \(A,B\),有

\[h_{\operatorname{conv}(A\cup B)}(\boldsymbol u) = \max\left(h_A(\boldsymbol u),h_B(\boldsymbol u)\right). \]

原因是线性函数 \(p\mapsto p\cdot\boldsymbol u\) 在凸组合上的值不会超过端点最大值,因此先取并集再取凸包不会改变这个方向上的最大投影。

因此

\[h_Q(\boldsymbol u) = \max\left(h_D(\boldsymbol u),h_{R(D)}(\boldsymbol u)\right). \]

设 \(R\) 表示逆时针旋转 \(90^\circ\),则

\[h_{R(D)}(\boldsymbol u) = h_D(R^{-1}\boldsymbol u). \]

\[R^{-1}\boldsymbol u = (-\sin\theta,-\cos\theta) = -\boldsymbol v. \]

由于 \(D\) 关于原点中心对称,

\[h_D(-\boldsymbol v)=h_D(\boldsymbol v). \]

所以

\[h_{R(D)}(\boldsymbol u)=h_D(\boldsymbol v). \]

最终得到

\[\begin{aligned} h_Q(\boldsymbol u) &= \max\left(h_D(\boldsymbol u),h_D(\boldsymbol v)\right)\\ &= s(\theta). \end{aligned} \]

\[\boxed{h_Q(\boldsymbol u)=s(\theta)}. \]

原问题中的“当前最小正方形边长”,已经变成了新凸包 \(Q\) 的支撑函数。


五、利用 Cauchy 周长公式完成积分

1. Cauchy 周长公式

对于任意平面凸图形 \(Q\),Cauchy 周长公式为

\[\operatorname{Perimeter}(Q) = \int_0^\pi w_Q(\theta)\,d\theta, \]

其中 \(w_Q(\theta)\) 是图形在方向 \(\theta\) 上的宽度。

由 \(D=-D\) 可知 \(R(D)=-R(D)\),所以 \(Q\) 同样关于原点中心对称。于是

\[w_Q(\theta)=2h_Q(\theta). \]

所以

\[\operatorname{Perimeter}(Q) = 2\int_0^\pi h_Q(\theta)\,d\theta. \]

这里已经把原题中的积分与几何周长联系起来:只要再利用 \(90^\circ\) 旋转对称性缩短积分区间,就能得到最终系数。

2. \(Q\) 具有 \(90^\circ\) 旋转对称性

由定义

\[Q=\operatorname{conv}(D\cup R(D)). \]

将 \(Q\) 再旋转 \(90^\circ\):

\[\begin{aligned} R(Q) &= \operatorname{conv}\left(R(D)\cup R^2(D)\right)\\ &= \operatorname{conv}\left(R(D)\cup(-D)\right). \end{aligned} \]

因为 \(D=-D\),所以

\[R(Q)=Q. \]

因此 \(Q\) 具有 \(90^\circ\) 旋转对称性,其支撑函数以 \(\pi/2\) 为周期:

\[h_Q(\theta+\pi/2)=h_Q(\theta). \]

于是

\[\begin{aligned} \operatorname{Perimeter}(Q) &= 2\int_0^\pi h_Q(\theta)\,d\theta\\ &= 4\int_0^{\pi/2}h_Q(\theta)\,d\theta\\ &= 4\int_0^{\pi/2}s(\theta)\,d\theta. \end{aligned} \]

3. 原观测框周长的平均值

由于整体再旋转 \(90^\circ\) 后,横纵跨度只是互换,而正方形边长取二者最大值,因此

\[s(\theta+\pi/2)=s(\theta). \]

所以

\[\begin{aligned} \overline P &= \frac{1}{2\pi} \int_0^{2\pi}4s(\theta)\,d\theta\\ &= \frac{4}{2\pi} \cdot 4\int_0^{\pi/2}s(\theta)\,d\theta\\ &= \frac{8}{\pi} \int_0^{\pi/2}s(\theta)\,d\theta. \end{aligned} \]

又因为

\[\operatorname{Perimeter}(Q) = 4\int_0^{\pi/2}s(\theta)\,d\theta, \]

所以最终答案为

\[\boxed{ \overline P = \frac{2}{\pi} \operatorname{Perimeter}(Q) }. \]

容易在这里漏掉一个 \(4\):题目平均的是正方形周长 \(4s(\theta)\),而不是边长 \(s(\theta)\)。最终的 \(\frac{2}{\pi}\) 正是把这个 \(4\)、四个长度为 \(\pi/2\) 的周期以及 Cauchy 公式中的系数合并后的结果。


六、算法流程

  1. 对所有输入点求凸包 \(K\);
  2. 将 \(K\) 关于原点取反,得到 \(-K\);
  3. 用闵可夫斯基和计算

\[D=K+(-K); \]

  1. 将 \(D\) 中每个点旋转 \(90^\circ\),得到 \(R(D)\);
  2. 对 \(D\cup R(D)\) 求凸包,得到 \(Q\);
  3. 计算 \(Q\) 的周长 \(L\);
  4. 输出

\[\frac{2L}{\pi}. \]

闵可夫斯基和如何做到线性

Andrew 算法得到的凸包顶点按逆时针排列。把两个凸多边形都旋转到“纵坐标最小、横坐标最小”的顶点作为起点后,各条边的极角已经循环有序。

因此计算 \(K+(-K)\) 时,只需像归并排序一样合并两组边向量:

  • 当前两条边叉积大于 \(0\),先取第一条边;
  • 叉积小于 \(0\),先取第二条边;
  • 叉积等于 \(0\),两条边方向相同,将它们相加后一起前进。

每条边至多处理一次,所以非退化凸多边形的闵可夫斯基和复杂度是 \(O(h)\)。


七、正确性证明

下面证明算法输出的值恰好等于题目要求的平均最小周长。

引理 1:固定角度时,最小正方形边长为两个投影宽度的最大值

旋转 \(\theta\) 后,所有点横坐标的极差为 \(w_K(\boldsymbol u)\),纵坐标的极差为 \(w_K(\boldsymbol v)\)。

任何轴对齐正方形若要覆盖全部点,边长必须同时不小于这两个极差;反过来,边长取二者最大值时,可以分别平移正方形的左右、上下边界,使其覆盖两个坐标区间。

因此

\[s(\theta)=\max\left(w_K(\boldsymbol u),w_K(\boldsymbol v)\right). \]

引理 2:\(s(\theta)\) 是凸包 \(Q\) 的支撑函数

\[D=K-K,\qquad Q=\operatorname{conv}(D\cup R(D)). \]

由闵可夫斯基和的支撑函数性质,

\[h_D(\boldsymbol u)=h_K(\boldsymbol u)+h_{-K}(\boldsymbol u) =w_K(\boldsymbol u). \]

又因为 \(D=-D\),且 \(R^{-1}\boldsymbol u=-\boldsymbol v\),所以

\[h_{R(D)}(\boldsymbol u) =h_D(R^{-1}\boldsymbol u) =h_D(-\boldsymbol v) =h_D(\boldsymbol v) =w_K(\boldsymbol v). \]

对并集取凸包会把支撑函数变成最大值,故

\[h_Q(\boldsymbol u) =\max\left(h_D(\boldsymbol u),h_{R(D)}(\boldsymbol u)\right) =s(\theta). \]

引理 3:\(Q\) 的周长等于一个周期内边长积分的四倍

\(Q\) 关于原点中心对称,所以由 Cauchy 周长公式,

\[\operatorname{Perimeter}(Q) =2\int_0^\pi h_Q(\theta)\,d\theta. \]

同时 \(R(Q)=Q\),故 \(h_Q\) 以 \(\pi/2\) 为周期。结合引理 2,

\[\operatorname{Perimeter}(Q) =4\int_0^{\pi/2}s(\theta)\,d\theta. \]

定理:算法输出正确

题目所求平均周长为

\[\begin{aligned} \overline P &=\frac{1}{2\pi}\int_0^{2\pi}4s(\theta)\,d\theta\\ &=\frac{8}{\pi}\int_0^{\pi/2}s(\theta)\,d\theta\\ &=\frac{2}{\pi}\operatorname{Perimeter}(Q). \end{aligned} \]

算法恰好构造 \(Q\),计算其周长并输出 \(\frac{2}{\pi}\operatorname{Perimeter}(Q)\),所以输出就是题目要求的答案。


八、退化情况

1. 所有点共线

此时原凸包 \(K\) 只有两个端点。

差分凸包 \(D=K-K\) 仍然是一条线段。将它与旋转 \(90^\circ\) 后的线段放在一起求凸包,会得到一个菱形,前面的支撑函数与 Cauchy 公式仍然成立。

代码中对点数不超过 \(2\) 的闵可夫斯基和直接枚举所有点对和,再求一次凸包,避免退化多边形的边向量归并问题。

2. 凸包中的共线点

Andrew 凸包会删除边上的中间共线点,只保留真正的凸包顶点,不影响答案。

3. 所有点重合

此时 \(K,D,Q\) 都只有一个点,周长和最终答案均为 \(0\)。代码中的周长累加自然得到 \(0\)。


九、样例解释

第一组样例

两个点为

\[(0,0),(1,0). \]

原点集是一条长度为 \(1\) 的线段。

差分凸包 \(D\) 是从 \((-1,0)\) 到 \((1,0)\) 的线段。旋转 \(90^\circ\) 后得到从 \((0,-1)\) 到 \((0,1)\) 的线段。

二者取凸包后得到一个顶点为

\[(1,0),(0,1),(-1,0),(0,-1) \]

的菱形,其周长为

\[4\sqrt 2. \]

因此答案为

\[\frac{2}{\pi}\cdot4\sqrt2 = \frac{8\sqrt2}{\pi} = 3.601265264628424\ldots \]

与样例输出一致。

第二组样例

四个点都在 \(y\) 轴上,最远两点距离为 \(5\)。

同理答案为

\[\frac{8\sqrt2}{\pi}\cdot5 = \frac{40\sqrt2}{\pi} = 18.006326323142121\ldots \]

与样例输出一致。


十、复杂度分析

设原点数为 \(n\),原凸包点数为 \(h\)。

  • 求原凸包:\(O(n\log n)\);
  • 非退化情况下计算闵可夫斯基和:\(O(h)\);
  • 当某个凸包点数不超过 \(2\) 时,代码只会枚举常数个点对,随后求凸包;
  • 对 \(D\cup R(D)\) 求凸包:\(O(h\log h)\);
  • 计算周长:\(O(h)\)。

总时间复杂度为

\[\boxed{O(n\log n)}. \]

空间复杂度为

\[\boxed{O(n)}. \]


十一、完整代码

展开完整代码(共 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. 宽度转支撑函数

\[w_K(\boldsymbol u) = h_{K+(-K)}(\boldsymbol u). \]

2. 两个垂直方向的最大值转凸包并集

\[\max\left( h_D(\boldsymbol u), h_D(\boldsymbol v) \right) = h_{\operatorname{conv}(D\cup R(D))}(\boldsymbol u). \]

3. 支撑函数积分转周长

\[\overline P = \frac{2}{\pi} \operatorname{Perimeter} \left( \operatorname{conv}(D\cup R(D)) \right). \]

最终公式为

\[\boxed{ \overline P = \frac{2}{\pi} \operatorname{Perimeter} \left( \operatorname{conv} \left( (K-K)\cup R_{90^\circ}(K-K) \right) \right) }. \]

posted @ 2026-07-28 18:47  艾拉别哭  阅读(23)  评论(0)    收藏  举报