题目
题目概要
有
n
n
n 个点
(
x
i
,
y
i
)
(x_i,y_i)
(xi,yi) ,选择其中两个点,作为一个矩形的左下角、右上角,求有多少个这样的矩形满足:内部不包含其他点。
数据范围与约定
n
≤
2
×
1
0
5
,
x
i
≠
x
j
,
y
i
≠
y
j
n\le2\times 10^5,x_i\ne x_j,y_i\ne y_j
n≤2×105,xi=xj,yi=yj 。
思路
刚拿到这道题,确实感觉没有什么好做法,不像其他题解里说的那样“显然”。
简化版
对于一个给定的 p ∈ [ 1 , n ] p\in[1,n] p∈[1,n] ,求以 ( x p , y p ) (x_p,y_p) (xp,yp) 为左下角的矩形个数。
等价转换一下题意:将 ( x p , y p ) (x_p,y_p) (xp,yp) 右上角的点(不包括自己)单独拿出来,对于这些点,求有多少个点,满足左下角没有其他点。
我们可以 排序后使用单调栈 解决。
按照
y
y
y 排序,维护一个
x
x
x 递增的单调栈即可。再次注意,只能将
(
x
p
,
y
p
)
(x_p,y_p)
(xp,yp) 右上方的点放入单调栈。
原理:如果
A
A
A 比
B
B
B 更晚入栈,那么
y
a
<
y
b
y_a<y_b
ya<yb ;如果同时满足
x
a
<
x
b
x_a<x_b
xa<xb ,那么
A
A
A 就在
B
B
B 的左下角,我们就要把
B
B
B 弹栈。
内测版
我们想办法把 简化版
中的方法升级。
我们能不能按照 y y y 从大到小枚举,然后维护单调栈呢?
答案是,不行。原因就在 简化版
中的黑体字部分:只将右上方的点放入单调栈。
右上角是什么意思? x p < x , y p < y x_p<x,y_p<y xp<x,yp<y 。
我们充分利用原有的单调栈——如果 x ≤ d x\le d x≤d ,那么无论 x x x 怎么变动, d < x d<x d<x 的点形成的单调栈就没有多余的点;如果我们的 y y y 单减,那么 y y y 的取值范围(也就是 ( y , + ∞ ] (y,+\infty] (y,+∞])不断扩充,就可以在原有单调栈的基础上进行更改。
怎么做到 x ≤ d x\le d x≤d 中的点,在 d < x d<x d<x 的点形成的单调栈里乱来?并且两边都可以按照 y y y 重新排序?
嘿嘿嘿, T h a t ′ s i t \tt That's\;it That′sit 。我愿称之为 C D Q CDQ CDQ 分治!
汉化破解版
我们首先对 x x x 进行排序,然后对 y y y 进行归并。
不妨把 x x x 较小的那些点叫做“ A A A 部”,其他的叫做“ B B B 部”。
我们要计算的,就只有左下角属于 A A A 、右上角属于 B B B 的矩形。
按照我们在 内测版
中的思路,我们应该将
y
y
y 从大到小归并。每次考虑一个点
(
x
p
,
y
p
)
∈
A
(x_p,y_p)\in A
(xp,yp)∈A 时,就把
B
B
B 部中所有
y
p
<
y
y_p<y
yp<y 的点放到栈里。记得维护栈的单调性。
似乎每个点就有 stack . s i z e \text{stack}.size stack.size 这个多个右上角可以用?然而并不是这样的。
我们是不是忘了什么?单调栈仅仅保证了左下角没有 B B B 部内的点。 A A A 部内的点 可能在左下角!
也就是说,对于 p ∈ A , r ∈ stack p\in A,r\in\text{stack} p∈A,r∈stack ,可能存在一个点 i ∈ A i\in A i∈A ,满足 x p < x i < x r x_p<x_i<x_r xp<xi<xr 且 y p < y i < y r y_p<y_i<y_r yp<yi<yr(没有相等的 x x x 或 y y y)。单调栈只保证 i ∈ B i\in B i∈B 时不成立。
x i < x r x_i<x_r xi<xr 由 c d q \tt{cdq} cdq 保证成立,可以用的 ( x i , y i ) (x_i,y_i) (xi,yi) 就满足 x p < x i , y p < y i x_p<x_i,y_p<y_i xp<xi,yp<yi ,并且 y i y_i yi 越小越好——更容易满足 y i < y r y_i<y_r yi<yr 的限制。
形式化地,求一个
λ = min i ∈ A , x p < x i , y p < y i y i \lambda = \min_{i\in A,x_p<x_i,y_p<y_i} y_i λ=i∈A,xp<xi,yp<yiminyi
然后我们不可用的点,就是 ∑ i ∈ stack [ λ < y i ] \sum_{i\in\text{stack}}[\lambda <y_i] ∑i∈stack[λ<yi] 。
怎么求 λ \lambda λ?另用一个 单调栈 即可。此时 A A A 部已经是 y y y 递减了,相当于求右上角中最后加入的一个点。这个只需要维护 x x x 递减的单调栈即可。
求 ∑ i ∈ stack [ λ < y i ] \sum_{i\in\text{stack}}[\lambda < y_i] ∑i∈stack[λ<yi] 又该怎么办呢?注意到栈中的元素是按照 y y y 值有序的(递减),所以可以二分查找。
每一层的时间复杂度是 O ( n log n ) \mathcal O(n\log n) O(nlogn) 的,所以总复杂度为 O ( n log 2 n ) \mathcal O(n\log^2 n) O(nlog2n) 。
代码
#include <cstdio>
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
inline int readint(){ int x; scanf("%d",&x); return x; }
inline void writeint(long long x){ printf("%lld",x); }
struct Point{
int x, y; // 一个点
Point(int X=0,int Y=0):x(X),y(Y){ }
void input(){ x = readint(), y = readint(); }
bool operator < (const Point &p){ return x < p.x; }
};
const int MaxN = 200005;
Point p[MaxN]; int n;
void input(){
n = readint();
for(int i=1; i<=n; ++i)
p[i].input();
}
struct Stack{ /* stupid Stack */
int sta[MaxN], top;
Stack(){ top = 0; }
void push_back(int x){ sta[++ top] = x; }
int back()const{ return sta[top]; }
bool empty()const{ return top == 0; }
int size()const{ return top; }
void pop_back(){ if(top) -- top; }
void clear(){ top = 0; }
} lSta, rSta; /* y单减 */
int BadThing(int bound){ // 二分查找
int L = 0, R = rSta.top, mid;
while(L != R){
mid = (L+R+1)>>1;
if(p[rSta.sta[mid]].y > bound)
L = mid; /* 没有相等的情况 */
else R = mid-1;
}
return L;
}
Point __tmp[MaxN]; long long ans;
void cdq(int left,int right){
/* head: divide ... */
if(left == right) return ;
int mid = (left+right)>>1;
cdq(left,mid), cdq(mid+1,right);
/* body: ... and conquer */
lSta.clear(), rSta.clear();
for(int i=left,j=mid+1; i<=mid; ++i){
for(; j<=right and p[j].y>p[i].y; ++j){
/* insert p[j] */
while(not rSta.empty() and p[j].x < p[rSta.back()].x)
rSta.pop_back(); // 维护x单增
rSta.push_back(j);
}
/* find the "wall" */
while(not lSta.empty() and p[lSta.back()].x < p[i].x)
lSta.pop_back();
/* update ans */
ans += rSta.size();
if(not lSta.empty())
ans -= BadThing(p[lSta.back()].y);
/* update lSta */
lSta.push_back(i);
}
/* tail: sort it out */
for(int i=left,j=mid+1,k=left; k<=right; ++k)
if(i <= mid and (j > right or p[i].y > p[j].y))
__tmp[k] = p[i ++];
else __tmp[k] = p[j ++];
for(int i=left; i<=right; ++i) p[i] = __tmp[i];
}
void solve(){
/* 按照x值排序 ... */
sort(p+1,p+n+1,Point::compareX);
/* ... 然后将y归并 */
cdq(1,n); // 看着很短,不是吗?
/* the most important part */
writeint(ans), putchar('\n');
}
int main(){
input(), solve();
return 0;
}
后记
首先,感谢大佬的博客提供了思路。
其次,对自己的弱小感到一丝凄凉。吐槽一句,我查到的博客大多数都是 大佬的博客
这种风格的……