掌握用 STL 中的 SET 動態維護 “各類型凸殼” / “凸包”

一、例題引入

題意:

主人公小智一共會捕捉 nn 只寶可夢,寶可夢有兩個屬性,攻擊值 AA,防禦值 BB。每當捕捉到一隻新的寶可夢 TT,小智有兩種方法判斷這隻寶可夢是否是無用的。

  1. 存在一隻寶可夢 XX,使得 X.A>T.AX.A > T.A 並且 X.B>T.BX.B > T.B
  2. 存在兩隻寶可夢 X,YX,Y,使得 cX.A+(1c)Y.A>T.Ac*X.A+(1-c)*Y.A>T.A 並且 cX.B+(1c)Y.B>T.Bc*X.B+(1-c)*Y.B>T.B0c10\leq c\leq 1

每當捕捉到一隻新寶可夢,需要輸出當前共有幾隻無用寶可夢。(1n105,0Ai,Bi109)(1\leq n\leq 10^5,0\leq A_i,B_i\leq 10^9)

題目來源:Gym​ ​102302-I

思路:

一開始一直在推導式子,妄圖想要用一個指標同時維護這兩個變量,然後就自閉了…

後來在隊友提醒之下,發現這是一個線段參數方程。線段兩端點爲 (a,b)(c,d)(a,b)、(c,d),則線段上的點可以如下表示。
X=ka+(1k)cY=kb+(1k)dk[0,1] X=k*a+(1-k)*c \\ Y=k*b+(1-k)*d \\ k\in[0,1]
可能不夠直觀,但我們可以通過斜率來證明這是線段的參數方程。
XaYb=(1k)(ca)(1k)(db)=cadb= \displaystyle\frac{X-a}{Y-b}=\displaystyle\frac{(1-k)*(c-a)}{(1-k)*(d-b)}=\displaystyle\frac{c-a}{d-b}=線段斜率
發現這是一個線段參數方程之後,此題就轉化成了一個動態維護凸殼的問題,凸殼如下所示。
在這裏插入圖片描述
觀察這個凸殼,我們定義比較函數如下。

struct Node {
	int x,y;
	bool operator < (Node T) const {
		return x == T.x ? y > T.y : x < T.x;		
	}
}

然後每次增加一個點的時候,向兩邊進行擴展刪點即可。刪點的標準爲左右兩端點連線是否能夠覆蓋自己,如果能則刪,不能則保留。具體過程見下文代碼,代碼很短,簡單易懂。


例題總結:

此題最重要的兩個關鍵點。

  1. 識別線段的參數方程
  2. 根據題意構建凸殼模型,並用 SET 求解

代碼:

#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
typedef long long ll;
using namespace std;

struct Node{
	ll x,y;
	Node operator - (Node p) const {return {x-p.x,y-p.y};}
	ll operator ^ (Node p) const {return x*p.y-y*p.x;}
	bool operator < (Node p) const {return (x == p.x ? y > p.y : x < p.x);}
};

struct Hull : public multiset<Node>{
	bool inside(iterator p){
		auto t2 = next(p);
		if(t2 == end()) return 0;
		if(p == begin()) return t2->x > p->x && t2->y > p->y;
		auto t1 = prev(p);
		if(((*t1-*p)^(*t2-*p)) < 0) return 1;
		else return 0;
	}
	void ins(Node p){
		auto t = insert(p);
		if(inside(t)) { erase(t); return; }
		while(t != begin() && inside(prev(t))) erase(prev(t));
		while(next(t) != end() && inside(next(t))) erase(next(t));
	}
};

int main()
{
	int n; scanf("%d",&n);
	Hull hull;
	rep(i,1,n){
		ll x,y; scanf("%lld%lld",&x,&y);
		hull.ins({x,y});
		printf("%d\n",i-(int)hull.size());
	}
	return 0;
}

二、各類型凸殼總結

凸殼處理過程中,主要注意點在於左右兩邊的垂直,在接下來所舉的例子中,應仔細觀察對於垂直的處理。

先按 xx 升序,再按 yy 升序

每次插入新節點,只需要插入 set 並找到對應位置,然後依次判斷兩邊的節點是否應該被刪除。(代碼部分同例題)

  1. 上凸殼(可包含左邊的垂直線段

在這裏插入圖片描述

  1. 下凸殼(可包含右邊的垂直線段

在這裏插入圖片描述

先按 xx 升序,再按 yy 降序

  1. 上凸殼(可包含右邊的垂直線段

在這裏插入圖片描述

  1. 下凸殼(可包含左邊的垂直線段

在這裏插入圖片描述

特殊凸殼

此類凸殼兩邊都有垂直線段,因此我們需要重新思考左邊垂直線段的問題。我們以左邊點是否固定來進行區分。

  • 左邊點固定
    • 如果左邊點固定,我們則可以按照先按 xx 升序,如果 x=xminx=x_{min}yy 升序,否則 yy 降序
  • 左邊點不固定
    • 如果左邊點不固定,我們可以考慮使用兩個 SET 來進行維護,其中一個用於維護左邊界垂直線,實現起來細節較繁瑣

在這裏插入圖片描述

三、凸包維護

相較於種類繁多的凸殼,凸包維護較爲清晰。

  • 按照 “xx 升序 ++ yy 升序” 方法進行排序
    • 起點爲最小值處,終點爲最大值處
    • 上凸殼維護上半部分,下凸殼維護下半部分
  • 用兩個 setset 分別維護上下凸殼
  • 上下凸殼判斷一個點是否刪除的代碼不同,但均使用叉乘
    • 判斷點 PP 是否該刪去,左邊點爲 XX,右邊點爲 YY
    • 上凸殼 (XP)(X-P)^(YP)<0(Y-P)<0 則刪除點 PP
    • 下凸殼 (XP)(X-P)^(YP)>0(Y-P)>0 則刪除點 PP

在這裏插入圖片描述


後記

setset 維護 “凸殼” / “凸包” 的內容到此就結束了,最後補充幾點:

  1. 除了 setset,也可以直接手寫 SplaySplay 等平衡樹進行維護
  2. 凸包維護過程也可以尋找一個定點,根據極角序進行排序

最後祝大家 ACAC 愉快,一起愛上凸包把!(๑•̀ㅂ•́)و✧

ACMACM 的旅行雖然充滿荊棘但一擡頭便能看見無數束光,請務必堅持下去,負重前行終有云開霧散之日!💪💪💪

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章