[学习笔记]多维偏序

一般情况下,我们比较一个数大小,就是ai>aj即可,

而在上升子序列中,当i>j并且ai>aj的时候,才可以认为i这位的数大于j这位的数。

这就是一个二维偏序。

 

类似的,有n个数,每个数m个属性,一个数比另一个数大,当且仅当这个数的所有属性都大于另一个数。

这就是一个m维偏序。

 

对于三维偏序,可以用cdq分治、排序、树状数组处理。

luogu P3810

Description:

有 n 个元素,第 i 个元素有 ai 、bi 、ci 三个属性,设 f(i) 表示满足 ajai 且bjbi 且cjci 的 j 的数量。

对于d[0,n) ,求 f(i) = d的数量

Solution:

三维偏序的模板题。

我们先按照a从小到大排序,

然后cdq分治。

先递归到两边,

回溯到这一层之后,把左儿子的所有的数按照b排序,右儿子的所有数也按照b排序。

这样,左儿子的数之间虽然a不一定递增,但是因为开始按照a排序,所以左边所有的数的a一定都小于右边的数。

b排好序之后,

两个指针j,i分别从1~mid,mid+1~r 即左右儿子区间的起止点开始走,,

对于右边的一个数i,当j的数的b值不大于i的b值时,不断向后走j,并且把这些数的c值放进一个权值树状数组里,

当j的b值大于i之后,当前所有左儿子里面,b小于i这个数的数的c属性都放进树状数组里了。

只有放进去的这些数才可能来更新f值。

在i向后走之前,f[a[i].ans]+=query(a[i].z),满足第三个条件的数也找到了。

就把所有当前这个层里面,符合条件的数都找出来了。

最后,把树状数组加上的 1都消去。

 

由于cdq分治,会把i之前的所有的数都分成logn个区间,更新完f[i]了。

大家可以手动画图,或者模拟一下。

 

复杂度:nlogn^2

 

那么,这个算法是怎么样实现三维偏序的处理呢?

1.对于a,开始直接排序,并且,每次是先递归左右儿子,再处理这一层,

所以保证一个数pi前面的所有的数,不论之后怎么换位置,都不会到i的后面。

这就利用位置保证了所有可能更新f[i]的数对于a都是合法的。

2.对于b,我们每次回溯的时候,按照b排了一个序,

对于左子区间对右子区间的影响,通过指针,把b小于等于i的数的所有数的c放进了树状数组里。

这样,i前面的logn个区间,会把所有b小于i的b的数都考虑一遍的。也合法。

3.对于c,直接通过树状数组前缀和,一步就求出来了当前合法的所有数了。

相当于一个筛,留下a合法的,留下b合法的,最后能被c留下的,就是所有合法的了。

 

注意因为是小于等于号,所以我们先把所有的完全相同的数合并成一个数,统计的时候,一个数也大于等于自己。再加上就好了。

就是细节问题。

 

Code:

#include<bits/stdc++.h>
using namespace std;
const int N=100000+10;
int n,m;
int f[N];
struct node{
    int x,y,z;
    int ans;
    int w;
}a[N],b[N];
int pp;
bool cmpx(node p,node q){
    if(q.x==p.x){
        if(p.y==q.y) return p.z<q.z;
        return p.y<q.y;
    }
    return p.x<q.x;
}
bool cmpy(node p,node q){
    if(q.y==p.y){
        return p.z<q.z;
    }
    return p.y<q.y;
}
struct ta{
    int g[2*N];
    void add(int x,int k){
        for(;x<=pp;x+=x&(-x)) g[x]+=k;
    }
    int ask(int x){
        int ret=0;
        for(;x;x-=x&(-x)) ret+=g[x];return ret;
    }
}t;

void cdq(int l,int r){
    //cout<<l<<" and "<<r<<endl;
    if(l==r) return;
    int mid=(l+r)>>1;
    cdq(l,mid);cdq(mid+1,r);
    sort(a+l,a+mid+1,cmpy);
    sort(a+mid+1,a+r+1,cmpy);
    //cout<<" after sort "<<l<<" || "<<r<<endl;
    int i=mid+1,j=l;
    for(;i<=r;i++){
        while(a[j].y<=a[i].y&&j<=mid){
            t.add(a[j].z,a[j].w),j++;
        }
        a[i].ans+=t.ask(a[i].z);
    }
    for(i=l;i<j;i++){
        t.add(a[i].z,-a[i].w);
    }
}
int ans[N];
int main()
{    
    int m;
    scanf("%d%d",&m,&pp);
    for(int i=1;i<=m;i++){
        scanf("%d%d%d",&b[i].x,&b[i].y,&b[i].z);
    }
    sort(b+1,b+m+1,cmpx);
    int c=0;
    for(int i=1;i<=m;i++){
        c++;
        if(b[i].x!=b[i+1].x||b[i].y!=b[i+1].y||b[i].z!=b[i+1].z){
            a[++n]=b[i],a[n].w=c;c=0;
        }
    }
    cdq(1,n);
    for(int i=1;i<=n;i++){
        ans[a[i].ans+a[i].w-1]+=a[i].w;
    }
    for(int i=0;i<m;i++){
        printf("%d\n",ans[i]);
    }
    return 0;
}
三维偏序

 

要是维数再多了呢??

 

hihocoder 1236 Scores

Decripiton:

给出N个人的5个科目分数,给出q个询问~~,每次给你5个科目的具体分数。求一共有多少个人对应的5个科目都小于等于你的科目分数~~

强制在线。

n,q<=50000

Solution:

这就是5维偏序了。

显然cdq分治不容易解决了。难以巧妙处理5维。

就考虑比较暴力的思路:

把成绩5种分成5组,每一组从小到大按成绩排序。

每次二分出id,id的成绩恰好位于成绩查询边界。

这样可以知道该成绩是有几个人不满足。

但是,由于有5个,所以必须知道都是谁。。。。

用一个bitset<50001>s[5][50000]表示,第i维,前j个人满足不合格的情况下,都是谁(s[i][j][k]=1表示,i维,编号是k的人,成绩比倒数第j名可能还差。)

然后预处理出bitset(每次j加一,就把j+1的人或进去就行了),查询的时候,二分id,之后取出这5个bitset,&一下就知道最后剩谁了,统计1的个数。

 

但是bitset还是太大了,不是MLE,就是TLE。

所以,考虑分块!??!

bitset<50001>s[5][250]表示,第i维,前j块不合格,都是谁

预处理比较容易,一个块一个块内暴力处理,最后从前到后相邻的块一个前缀或就可以了

查询的时候,

二分出来一个id,在块k里,

就找到k,把k-1块的不合格人都找出来,之后剩下的暴力加进去就可以啦

 

复杂度:O(5 * q * sqrtn)(不算预处理)

因为这个是n<=50000,所以分块卡不掉。

这种数据范围,多维偏序都可以类似扩展开来。

 

本质上还是一个筛。不过用了bitset和分块优化。

 

总结:

感觉多维偏序在生活中还是比较常见的,

比方说,你期末考试之后,想看看完虐多少个人?(每一科都比TA高)

就是多维偏序了。

 

posted @ 2018-08-05 19:14  *Miracle*  阅读(1088)  评论(0编辑  收藏  举报