二维偏序专题

二维偏序:即对于
{ a ≤ x ≤ b c ≤ y ≤ d \left\{ \begin{aligned} a\le x \le b \\ c \le y \le d \\ \end{aligned} \right. {axbcyd
这种二维限制的不等式,我们可以将其转化为二维偏序问题,即我们将该不等式放入二维坐标系中,以 ( a , c ) (a,c) (a,c)为左下顶点, ( b , d ) (b,d) (b,d)为右上顶点,即转化为在给定矩形中的限制问题, 其一般情况为统计矩形内部信息。

转化到此后,我们对其中一维进行排序,然后对另外一维使用数据结构进行维护,例如对 y 轴进行排序,那么在 y 这一轴有序的条件下,去计算每个点对于查询中的贡献,说的具体一点就是,对于的查询一个点 ( i , j ) (i,j) (i,j),对其有贡献的点只会在其左下方。我们把原始点和查询全部放入一个数据结构进行维护,当遇到原始点时,就把该点插入数据结构,当遇到查询的点时,就对查询的点在数据结构中进行查询即可。
又因为我们计算的是矩形的贡献,而并不是一个单点,所以需要运用二维前缀和中的容斥相关思想,把一个矩形的贡献转化为4个点的贡献即可。

还是单独考虑一个查询点,对其能够产生贡献的点为其左下方的点,又因为我们对所有的点都进行了按y轴进行排序, 所以我们想知道的只是对于点 ( i , j ) (i,j) (i,j),在前面已经插入的点中,有多少个点的横坐标小于 i 的即可,因为排序后已经保证了,其前面所有的点的纵坐标都是小于等于 j 的。由此,我们很容易想到使用树状数组进行维护,当遇到原始点时,将其放入树状数组即可。

洛谷 P2163 [SHOI2007] 园丁的烦恼

题目描述

看来一般的难题是难不倒这位园丁的,国王最后打算用车轮战来消耗他的实力: “年轻人,在我的花园里有 n n n 棵树,每一棵树可以用一个整数坐标来表示,一会儿,我的 m m m 个骑士们会来轮番询问你某一个矩阵内有多少树,如果你不能立即答对,你就准备走人吧!”说完,国王气呼呼地先走了。

这下轮到园丁傻眼了,他没有准备过这样的问题。所幸的是,作为“全国园丁保护联盟”的会长——你,可以成为他的最后一根救命稻草。

输入格式

第一行有两个整数 n , m n, m n,m,分别表示树木个数和询问次数。

接下来 n n n 行,每行两个整数 x , y x, y x,y,表示存在一棵坐标为 ( x , y ) (x, y) (x,y) 的树。有可能存在两棵树位于同一坐标。

接下来 m m m 行,每行四个整数 a , b , c , d a, b, c, d a,b,c,d,表示查询以 ( a , b ) (a, b) (a,b) 为左下角, ( c , d ) (c, d) (c,d) 为右上角的矩形内部(包括边界)有多少棵树。

输出格式

对于每个查询,输出一行一个整数表示答案。

板子题,不多进行解释了,因为题目的坐标是从 0 开始,所以容斥的时候会出现负数,因此把所有坐标全部加一即可。

#include 

using namespace std;
const int N = 5e5 + 5;
typedef long long ll;
typedef pair<ll, ll> pll;
typedef array<int, 4> ar;
int mod = 1e9+7;
const int maxv = 4e6 + 5;
#define endl "\n"

struct MIT
{
ll tr[N];
int lowbit(int x) {
    return x & (-x);
}

void add(int u, int v) {
    for (int i = u; i < N; i += lowbit(i)) {
        tr[i] += v;
    }
}

ll query(int x) {
    ll res = 0;

    for (int i = x; i > 0; i -= lowbit(i)) {
        res += tr[i];
    }

    return res;
}
};

MIT tr;

int n,m;

void solve()
{
	cin>>n>>m;
	vector<ar> eve;
	for(int i=1;i<=n;i++){
		int x,y;
		cin>>x>>y;
		x++,y++;
		eve.push_back({x,0,y});
	}
	for(int i=1;i<=m;i++){
		int x1,y1,x2,y2;
		cin>>x1>>y1>>x2>>y2;
		x1++,y1++,x2++,y2++;
		eve.push_back({x1-1,2,y1-1,i});
		eve.push_back({x1-1,1,y2,i});
		eve.push_back({x2,1,y1-1,i});
		eve.push_back({x2,2,y2,i});
	}
	sort(eve.begin(),eve.end());
	vector<int> ans(m+5);
	for(auto [a,b,c,d]: eve){
		if(d==0) tr.add(c,1);
		else{
			if(b==2) ans[d]+=tr.query(c);
			else ans[d]-=tr.query(c);
		}
	}
	for(int i=1;i<=m;i++) cout<<ans[i]<<endl;
}

int main()
{
    ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int t;
	t=1;
	// cin>>t;
	while(t--){
		solve();
	}
   	system("pause");
    return 0;
}

Educational Codeforces Round 10 D. Nested Segments

题意:给定 n 条线段,保证线段的两端不重合,求对于每一段线段,其内部的线段数。

思路:我们考虑将其转化为不等式,即对于线段 [ l , r ] [l,r] [l,r],我们需要找到线段 [ x , y ] [x,y] [x,y]的个数,满足:

{ l ≤ x ≤ r l ≤ y ≤ r \left\{ \begin{aligned} l\le x \le r \\ l \le y \le r \\ \end{aligned} \right. {lxrlyr
由此可见,这题依旧可以转化为二维偏序问题,我们将线段的左右坐标看作二维坐标系中的点 ( l , r ) (l,r) (l,r),因此,我们把 ( l , l ) (l,l) (l,l) ( r , r ) (r,r) (r,r)分别看作是矩形的左下顶点和右上顶点,然后套板子即可。因为数据范围过大,所以需要进行离散化。最后注意的是,因为是求的每一段线段内部的线段数,因为每个线段又是原始点,又是查询点,所以最后每个线段的贡献需要减去自身。

#include 

using namespace std;
const int N = 5e5 + 5;
typedef long long ll;
typedef pair<ll, ll> pll;
typedef array<int, 4> ar;
int mod = 1e9+7;
const int maxv = 4e6 + 5;
#define endl "\n"

struct MIT
{
ll tr[N];
int lowbit(int x) {
    return x & (-x);
}

void add(int u, int v) {
    for (int i = u; i < N; i += lowbit(i)) {
        tr[i] += v;
    }
}

ll query(int x) {
    ll res = 0;

    for (int i = x; i > 0; i -= lowbit(i)) {
        res += tr[i];
    }

    return res;
}
};

MIT tr;

void solve()
{
	int n;
	cin>>n;
	vector<array<int,3> > se(n+5);
	vector<int> p;
	for(int i=1;i<=n;i++){
		int l,r;
		cin>>l>>r;
		p.push_back(l),p.push_back(r);
		se[i]={l,r,i};
	}
	vector<ar> eve;
	sort(p.begin(),p.end());
	p.erase(unique(p.begin(),p.end()),p.end());
	for(int i=1;i<=n;i++){
		auto [l,r,id]=se[i];
		int x1,y1,x2,y2;
		x1=y1=lower_bound(p.begin(),p.end(),l)-p.begin()+1;
		x2=y2=lower_bound(p.begin(),p.end(),r)-p.begin()+1;
		eve.push_back({x1,0,x2});
		eve.push_back({x1-1,2,y1-1,id});
		eve.push_back({x1-1,1,y2,id});
		eve.push_back({x2,2,y2,id});
		eve.push_back({x2,1,y1-1,id});
	}
	vector<int> ans(n+5);
	sort(eve.begin(),eve.end());
	for(auto [a,b,c,d]: eve){
		if(!d) tr.add(c,1);
		else {
			if(b==2) ans[d]+=tr.query(c);
			else ans[d]-=tr.query(c);
		}
	}
	for(int i=1;i<=n;i++) cout<<ans[i]-1<<endl;
}

int main()
{
    ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int t;
	t=1;
	// cin>>t;
	while(t--){
		solve();
	}
   	system("pause");
    return 0;
}

同时这题也不用将其看作矩形,因为每个点既是原始点,又是查询点的原因,所以我们可以直接统计,以对 x 轴进行排序为例:对于点 ( i , j ) (i,j) (i,j)而言,在已经插入的点中,其中有多少个点的的横坐标比 i 大,且纵坐标比 j 小。由此我们可以知道能够提供贡献的点位于其右下方,所以我们对 x 轴进行从大到小进行排序即可,然后统计有多少个点的纵坐标小于 j 即为答案。

#include 

using namespace std;
const int N = 2e6 + 5;
typedef long long ll;
typedef pair<ll, ll> pll;
typedef array<ll, 3> p3;
int mod = 998244353;
const int maxv = 4e6 + 5;
// #define endl "\n"

struct MIT//树状数组
{
ll tr[N];
int lowbit(int x) {
    return x & (-x);
}

void add(int u, int v) {
    for (int i = u; i < N; i += lowbit(i)) {
        tr[i] += v;
    }
}

ll query(int x) {
    ll res = 0;

    for (int i = x; i > 0; i -= lowbit(i)) {
        res += tr[i];
    }

    return res;
}
};
MIT c;
void solve()
{	

	int n;
	cin>>n;
	vector<p3> se(n+5);
	for(int i=1;i<=n;i++){
		int l,r;
		cin>>l>>r;
		se[i]={l,r,i};//因为要进行离散化,所以需要储存编号
	}
	sort(se.begin()+1,se.begin()+1+n,[](p3 x,p3 y){
		return x[0]>y[0];
	});
	vector<int> g(n+5);
	for(int i=1;i<=n;i++){
		auto [x,y,z]=se[i];
		g[i]=y;
	}
	sort(g.begin()+1,g.begin()+1+n);
	for(int i=1;i<=n;i++){
		int t=lower_bound(g.begin()+1,g.begin()+1+n,se[i][1])-g.begin();//经典离散化操作
		se[i][1]=t;//第二维才是我们需要的值
	}
	vector<int> ans(n+5);
	for(int i=1;i<=n;i++){//对每个点进行遍历
		auto [x,y,z]=se[i];
		int res=c.query(y);
		ans[z]=res;
		c.add(y,1);
	}
	for(int i=1;i<=n;i++) cout<<ans[i]<<endl;
}

int main()
{
    ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int t;
	t=1;
	//cin>>t;
	while(t--){
		solve();
	}
    system("pause");
    return 0;
}

Codeforces Round 909 (Div. 3) G. Unusual Entertainment

题意:给定一棵树和一个排列p,然后给出 q 个询问,每个询问的格式为 l , r , x l,r,x l,r,x,即在排列中从 l − r l-r lr中是否存在一个节点,其为 x 的子树。
我们去记录每个数在排列中的出现位置,对于该问题就转化为了,是否存在一个节点 y:
{ l ≤ y ≤ r d f s n [ x ] ≤ d f s n [ p o s [ x ] ] ≤ d f s n [ x ] + x . s i z e ( ) − 1 \left\{ \begin{aligned} &l\le y \le r \\ dfsn[x] &\le dfsn[pos[x]] \le dfsn[x]+x.size()-1 \\ \end{aligned} \right. {dfsn[x]lyrdfsn[pos[x]]dfsn[x]+x.size()1

式子已经转化成这样了,就是我们熟悉的二维偏序问题,首先跑出来个dfs序然后使用树状数组进行维护即可。

#include 

using namespace std;
const int N = 3e5 + 5;
typedef long long ll;
typedef pair<ll, ll> pll;
typedef array<int, 4> p4;
int mod = 1e9+7;
const int maxv = 4e6 + 5;
#define endl "\n"


int n,q,tot;
int dfsn[N],sz[N];
vector<int> e[N];
int p[N];

struct MIT
{
ll tr[N];

void init(int x)
{
	for(int i=0;i<=x;i++) tr[i]=0;
}

int lowbit(int x) {
    return x & (-x);
}

void add(int u, int v) {
    for (int i = u; i < N; i += lowbit(i)) {
        tr[i] += v;
    }
}

int query(int x) {
    ll res = 0;

    for (int i = x; i > 0; i -= lowbit(i)) {
        res += tr[i];
    }

    return res;
}
};

void add(int u,int v)
{
	e[u].push_back(v);
	e[v].push_back(u);
}

void dfs(int x,int f)
{
	tot++;
	dfsn[x]=tot,sz[x]=1;
	for(auto u: e[x]){
		if(f!=u){
			dfs(u,x);
			sz[x]+=sz[u];
		}
	}
}
MIT tr;

void solve()
{
	cin>>n>>q;
	tr.init(n);
	tot=0;
	for(int i=1;i<=n;i++) e[i].clear(),dfsn[i]=0;
	for(int i=1;i<=n-1;i++){
		int u,v;
		cin>>u>>v;
		add(u,v);
	}
	dfs(1,-1);
	vector<p4> eve;
	for(int i=1;i<=n;i++) {
		int x;
		cin>>x;
		p[x]=i;
	}
	for(int i=1;i<=n;i++) eve.push_back({p[i],0,dfsn[i]});
	for(int i=1;i<=q;i++){
		int l,r,x;
		cin>>l>>r>>x;
		int nl=dfsn[x],nr=dfsn[x]+sz[x]-1;
		eve.push_back({l-1,2,nl-1,i});
		eve.push_back({l-1,1,nr,i});
		eve.push_back({r,1,nl-1,i});
		eve.push_back({r,2,nr,i});
	}
	vector<int> ans(q+5);
	sort(eve.begin(),eve.end());
	for(auto [a,b,c,d]: eve){
		if(d==0){
			tr.add(c,1);
		}
		else{
			if(b==2) ans[d]+=tr.query(c);
			else ans[d]-=tr.query(c);
		}
	}
	//for(int i=1;i<=q;i++) cout<
	for(int i=1;i<=q;i++){
		if(ans[i]>0) cout<<"YES"<<endl;
		else cout<<"NO"<<endl;
	}
}

int main()
{
    ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int t;
	t=1;
	cin>>t;

	while(t--){
		solve();
	}
   	system("pause");
    return 0;
}

同时这题可以使用启发式合并进行维护,我们因为是要查询从 p l − p r p_l-p_r plpr中是否存在 x 的的子节点,我们可以开 n 个set去维护每个节点的子节点信息,在向上合并的过程中进行启发式合并即可,即对于当前子节点的 size 若大于父节点的 size ,那么我们就进行启发式合并,先把两个节点大小交换一下,然后再正常进行合并即可。
对于查询,因为set内部有序,所以我们只需要查询是否存在大于等于 l 的位置即可。

#include 

using namespace std;
const int N = 2e5 + 5;
typedef long long ll;
typedef pair<ll, ll> pll;
typedef array<int, 3> ar;
int mod = 1e9+7;
const int maxv = 4e6 + 5;
#define endl "\n"


int n,q;
vector<int> e[N];

void add(int u,int v)
{
	e[u].push_back(v);
	e[v].push_back(u);
}
int p[N];
vector<set<int> > se(N);
int ans[N];
vector<ar> qu[N];
void dfs(int x,int f)
{
	se[x].insert(p[x]);
	for(auto u: e[x]){
		if(u!=f){
			dfs(u,x);
			if(se[u].size()>se[x].size()){
				swap(se[u],se[x]);
			}
			se[x].merge(se[u]);
		}

		//se[u].clear();
	}
	for(auto [l,r,i] :qu[x]){
		auto it=se[x].lower_bound(l);
		if(it!=se[x].end()&&*it<=r){
			ans[i]=1;
		}
	}
}

void solve()
{
	cin>>n>>q;
	for(int i=1;i<=n;i++){
		e[i].clear();
		se[i].clear();
		qu[i].clear();
		ans[i]=0;
	}
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		add(u,v);
	}
	for(int i=1;i<=n;i++){
		int x;
		cin>>x;
		p[x]=i;
	}
	for(int i=1;i<=q;i++){
		int l,r,x;
		cin>>l>>r>>x;
		qu[x].push_back({l,r,i});
	}
	dfs(1,-1);
	for(int i=1;i<=q;i++){
		if(ans[i]) cout<<"YES"<<endl;
		else cout<<"NO"<<endl;
	}
}

int main()
{
    ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int t;
	t=1;
	cin>>t;
	while(t--){
		solve();
	}
   	system("pause");
    return 0;
}

AtCoder Beginner Contest 327 F - Apples

最后再来说说这个类似思想的题目。
题意:给定 n 个点,然后给一个矩形,问这个矩形最多能框多少个点。
思路:这题不能使用二维偏序,若是考虑以每个点为矩形的左下顶点很容易就想到反例。
我们同样考虑一个点对于其所在矩形的贡献,我们将这个贡献看作两部分,即矩形的上边界和下边界,当其下边界进入时,且在未离开上边界时,这个点的贡献一直存在,所以我们可以运用扫描线,首先对y轴进行排序,然后再考虑各部分的贡献即可。
具体使用线段树进行区间加和区间查询最大值即可。

#include 

using namespace std;
const int N = 4e5 + 5;
typedef long long ll;
typedef pair<ll, ll> pll;
typedef array<ll, 3> p3;
int mod = 1e9+7;
const int maxv = 4e6 + 5;
// #define endl "\n"

int n,d,w,cnt;

struct node
{
	int l,r,w,v;
}b[N];

struct seg
{
    ll l,r,sum,add;
    #define l(x) tr[x].l
    #define r(x) tr[x].r
    #define sum(x) tr[x].sum
    #define add(x) tr[x].add
}tr[N*4];

void update(int p)
{
    sum(p)=max(sum(p*2),sum(p*2+1));
}


void build(int p,int l,int r)
{
    if(l==r){
        tr[p]={l,r,0,0};
        return ;
    }
    l(p)=l,r(p)=r;
    int mid=(l+r)/2;
    build(p*2,l,mid);
    build(p*2+1,mid+1,r);
    update(p);
}

void pushdown(int p)
{
    if(add(p)){
        sum(p*2)+=add(p);
        sum(p*2+1)+=add(p);
        add(p*2)+=add(p);
        add(p*2+1)+=add(p);
        add(p)=0;
    }
}

void modify(int p,int l,int r,int tag)
{
    if(l<=l(p)&&r(p)<=r){
        add(p)+=tag;
        sum(p)+=tag;
        return ;
    }
    pushdown(p);
    int mid=(l(p)+r(p))/2;
    if(l<=mid) modify(p*2,l,r,tag);
    if(r>mid) modify(p*2+1,l,r,tag);
    update(p);
    
}

int query(int p,int l,int r)
{
    if(l<=l(p)&&r(p)<=r){
        //cout<
        return sum(p);
    }
    pushdown(p);
    int mid=(l(p)+r(p))/2;
    int res=0;
    if(l<=mid) res=query(p*2,l,r);
    if(r>mid) res=max(res,query(p*2+1,l,r));
    return res;
}


void solve()
{
	cin>>n>>d>>w;
	int len=0;
	for(int i=1;i<=n;i++){
		int c,l;
		cin>>c>>l;
		b[i].l=b[i+n].l=l;
		b[i].r=b[i+n].r=l+w-1;
		b[i].w=1;
		b[i].v=c;
		b[i+n].w=-1;
		b[i+n].v=c+d;
		len=max(len,l+w-1);
	}
	sort(b+1,b+n*2+1,[](node x,node y){
		if(x.v==y.v) return x.w<y.w;
		return x.v<y.v;
	});
	build(1,1,len);
	int ans=0;
	for(int i=1;i<=2*n;i++){
		auto [l,r,w,v]=b[i];
		modify(1,l,r,w);
		ans=max(ans,query(1,1,len));
	}
	cout<<ans<<endl;


}

int main()
{
    ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int t;
	t=1;
	//cin>>t;
	while(t--){
		solve();
	}
   	system("pause");
    return 0;
}

你可能感兴趣的:(数据结构,算法,深度优先,c++,数据结构)