二维偏序:即对于
{ a ≤ x ≤ b c ≤ y ≤ d \left\{ \begin{aligned} a\le x \le b \\ c \le y \le d \\ \end{aligned} \right. {a≤x≤bc≤y≤d
这种二维限制的不等式,我们可以将其转化为二维偏序问题,即我们将该不等式放入二维坐标系中,以 ( 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 的。由此,我们很容易想到使用树状数组进行维护,当遇到原始点时,将其放入树状数组即可。
看来一般的难题是难不倒这位园丁的,国王最后打算用车轮战来消耗他的实力: “年轻人,在我的花园里有 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;
}
题意:给定 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. {l≤x≤rl≤y≤r
由此可见,这题依旧可以转化为二维偏序问题,我们将线段的左右坐标看作二维坐标系中的点 ( 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;
}
题意:给定一棵树和一个排列p,然后给出 q 个询问,每个询问的格式为 l , r , x l,r,x l,r,x,即在排列中从 l − r l-r l−r中是否存在一个节点,其为 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]l≤y≤r≤dfsn[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 pl−pr中是否存在 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;
}
最后再来说说这个类似思想的题目。
题意:给定 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;
}