分治算法和数据结构在多维偏序问题中的应用


友情连接<https://blog.csdn.net/qq_40513946>


分治算法和数据结构在多维偏序问题中的应用

Authors: 刘浩,万昱君

Student ID (浙江师范大学 数学与计算机学院,学号201732110113,201732110118)


摘要:对于二维偏序的计算问题,我们可以用暴力O(n2)的算法来做。但我们可以通过树状数组和分治来优化,使复杂度达到O(nlog(n))。在此基础上,我们可以将树状数组和分治结合形成CDQ分治,计算三维偏序,复杂度为O(nlog(n)log(n))。

关键词: 偏序与多维偏序,树状数组,分治算法,CDQ分治。


1 引言

关系的偏序问题,在离散数学关系论中处于十分重要的地位。在实践运用中也有许多有趣而又令人深思的问题。现给出以下例子:

在紧张的考试周结束后,又迎来了惊心动魄查成绩的日子。考前总是抱抱佛脚的小明在总分排名末流看到了自己。他非常生气,决定要算一下有多少同学的成绩严格比自己差(每门课的成绩都小于等于自己的成绩,),来自我安慰一下。其他同学看了,也纷纷找起了比自己差的学生。

问:每名同学能所找比自己差的学生数量是多少?(假设总共有两门学科)

这就是一个典型的偏序问题,可以将学生每门课的成绩分别为S1,S2,学生作为单元X,学生集合为A = {X1,X2...,Xn},定义关系 ≼ 为 Xi.S1<Xj.S1 ∧ Xi.S2<Xj.S2(1≤i ,j≤n)。偏序集为<A, ≼>。求解每一项Xi下界集合势的值(即满足Xj ≼ Xi关系的单位数量)。

这种单位含有两个比较元素的偏序关系,我们还可以更加形象的表述出来,成绩S1为X轴,成绩S2为Y轴,那么每个学生都像在一个二维坐标轴的点上,我们要查询的关系X1 ≼ X2,就是点X1严格在点X2的左下方。同样,如果再加一个成绩S3,我们的二维坐标系就不够用了,我们需要三维坐标系。前面的我们称为二维偏序关系,后一个我们称为三维偏序关系。


2 预备知识

2.1偏序、严格偏序的概念与多维偏序

偏序集合(英语:Partially ordered set,简写poset)是数学中,特别是序理论中,指配备了部分排序关系的集合。[1]

设R为非空集合A上的关系,如果R满足一下三个性质:

  1. 自反性:对任意x∈A,有xRx;
  2. 反对称的:对任意x, y∈A,若xRy,且yRx,则x=y;
  3. 传递的:对任意x, y, z∈A,若xRy,且yRz,则xRz。

则称R为A上的偏序关系,记作≼。设≼为偏序关系,如果<x, y>∈≼,则记作x≼y,读作x“小于等于”y。[2]

严格偏序就是满足反自反性的偏序,所以也称反自反偏序。

多维偏序表示集合A内的元素之间存在多个维度的比较,在这些维度的基础上从而形成关系R。我们讨论的主要是严格的多维偏序,也就是。比如我们在引言中提到的例子一样,每个人不同科目的成绩就对应不同的维度,所有的科目严格小于才构成一个关系。

2.2基本的消解算法

       暴力的做法时间复杂度为O(n2),空间复杂度为O(n)。

         对于每一个人都暴力扫描其他人与自己进行多维的比较,如果各个维度的数值都比自己小,那么答案就会累加。

for i ← 1 to N   /*i表示自己,N表示总人数*/
    for j ← 1 to N  /*j表示别人*/
        if  j等于i
            then 跳进下一个循环
        if 别人各个维度的数值都比自己低
            then 自己的答案累加
    end
end

3改进的消解算法

3.1利用树状数组优化算法。

3.1.1算法描述[3][4]

假设对于任何一个数i,总存在数k,使得i整除2k,且i不能整除2k+1,k为非负整数,我们称2k是i的最小比特,记为 lowbit(i)。例如12的最小比特是4,8的最小比特是8。

如果我们把i化为2进制数,最后的连续的0的个数就是k。例如12化成二进制为1100,最后有两个连续的0,所以k是2,22就是4。也就是说12的最小比特是4。

我们在最小比特的基础上定义树状数组c[1],c[2],…,c[n],其中c[i]的父节点是 c[i+lowbit(i)]

设n=8,则对于树状数组c[1],c[2],…,c[8]可表示如下图所示的关系。其中相连的节点位处上方的是父节点,如c[8]c[4]的父节点。

https://images2015.cnblogs.com/blog/786945/201612/786945-20161206222444319-1066528329.jpg

 

我们定义树状数组c与数组A之间存在对应的关系:j=lowbit(i),c[i]= A[i-j+1]+ A[i-j+2]+…+A[i]

设n=8,则:

c[1]=A[1], c[2]=A[1]+A[2],

c[3]=A[3], c[4]=A[1]+A[2]+A[3]+A[4] =c[2]+c[3]+A[4],

c[5]=A[5], c[6]=A[5]+A[6]=c[5]+A[6],

c[7]=A[7], c[8]= A[1]+A[2]+A[3]+A[4]+ A[5]+A[6]+ A[7]+A[8]=c[4]+c[6]+c[7]+A[8]。

我们也可以看出c[i]的值是c[i]所有的子节点以及A[i]的和。

我们在此基础上定义如下三个算法:

算法1 计算i的最小比特:

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

         算法2 计算前x项的和:            

int sum(int x){
    int res = 0;
    while(x > 0){
        res += c[x];
        x -= lowbit(x);
    }
    return res;
}

算法3 如果数组A发生修改,如数组A[x]增加val,则需要对数组c进行更新。我们也可以发现,修改了A[x],只会影响到c[x]以及c[x]之后的所有父节点,我们也仅需要对这些点修改即可。对此我们有了维护数组c的算法:

int add(int x,int val){
    while(x <= N){
        c[x] += val;
        x += lowbit(x);
    }
}

3.1.2算法应用

树状数组这一种数据结构可以有效快速的解决很多的计算问题,能够在时间上大大降低复杂度。对于本题同样可以进行应用。

首先初始化数组c为0,结构体数组X上记录这个人的id,两个分数s1,s2,以及两维都比他小的人数ans。

将数组a按照第一维s1排序。此时我们可以保证对于i<j,X[i].s1<a[j].s1。也就是说,对于X[i],只有在他左边的才会有可能两个维度都小于他。

然后将排好序的数组a从前往后依次扫描,树状数组c记录的是在X[i]左边的第二维数值的个数。所以此时的sum(a[i].s2)记录的就是在X[i]左边(第一维小于X[i]),且第二维小于X[i]的数量,也就是说sum(X[i].s2)就是两个维度都小于他的人数ans。然后我们将X[i].s2加入到数组c中,即add(X[i].s2)来更新树状数组c。

核心代码:

for i ← 1 to N   /*从左到右枚举每个人*/
    记录X[i].id这个人的答案为sum(X[i].s2)
    更新数组c ,即add(X[i].s2)
end

3.1.3算法的正确性、完备性以及先进性分析

利用排序完成第一维的维护,然后再利用树状数组的结构特性,计算出第二维也小于他的数目,这样就能正确的达到计算两维都不大于他的数目。同时树状数组的查询和修改时间复杂度都为 O(log(n)),因为每个人都需要进行一次查询操作以及修改操作,所以总的时间复杂度为O(nlog(n))。空间复杂度仍为O(n)。树状数组算法相对于暴力算法在时间复杂度上大大降低,空间复杂度上也没有提升。是一个不错的优化。

3.2利用分治算法进行优化[4]

3.21算法描述

分治算法是程序设计中常用的算法[1],我们今天主要运用的思想就是归并排序。我们先简单介绍一下归并排序。

         1. 从下往上的归并排序:将待排序的数列分成若干个长度为1的子数列,然后将这些数列两两合并;得到若干个长度为2的有序数列,再将这些数列两两合并;得到若干个长度为4的有序数列,再将它们两两合并;直接合并成一个数列为止。这样就得到了我们想要的排序结果。(参考下面的图片)

2. 从上往下的归并排序:它与"从下往上"在排序上是反方向的。它基本包括3步:
① 分解 -- 将当前区间一分为二,即求分裂点 mid = (low + high)/2; 
② 求解 -- 递归地对两个子区间a[low...mid] 和 a[mid+1...high]进行归并排序。递归的终结条件是子区间长度为1。
③ 合并 -- 将已排序的两个子区间a[low...mid]和 a[mid+1...high]归并为一个有序的区间a[low...high]。

https://images0.cnblogs.com/i/497634/201403/151853346211212.jpg

3.2.2算法应用

前面我们在研究归并算法的时候,将每步分治都将数组分成了前后两块部分,易得,前半部分的数组第一维,一定是大于后半部分的第一维s1数据的,因为我们原先就是就已经按照第一维s1排过了序。

我们每次进行归并排序时,每次统计前半部分对后半部分的贡献,即假设pl代表现在前半部分加入临时数组tmp[]的个数,pr代表后半部分加入临时数组tmp[]的个数。

当我们要把X[pr]这个点放入tmp[]时,计算当前pl-l个点是前半部分s2小于X[pr].s2的。而因为前半部分的s1必定小于后半部分的s1,所以这部分的贡献有效。

核心伪代码

Merg (全部) {
    递归Merg左边;
    递归Merg右边;
    while (右边数据没有记录完整)  do
        while (左边值小于右边值且在边界内)  do
            临时数组<-左边当前值
                     end
        右边的答案 += 左边当前进行个数;
        临时数组<-右边当前值
    end
    while (左边没有进行完全)  do
         临时数组<-左边当前值
    end
    将值临时变量里的值赋回来
}

3.2.3算法的正确性、完备性以及先进性分析    

我们在进行第一维的排序时,就已经可以保证了第一维的值从前往后时递增的,而我们归并排序是对第二维进行的,我们在归并的过程中就保证了前半部分的第二维大于后半部分的第二维。

假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N),归并排序的形式就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的可以得出它的时间复杂度是O(N*logN)。

 

3.3 三维偏序CDQ分治。

在二维的基础上,我们就可以方便的计算三维偏序,这个算法也被叫做CDQ分治。数组的第一维用排序来维护,第二维利用分治来维护,第三维利用树状数组来维护。

CDQ分治合并的时候,我们计算的是前半部分对后半部分的贡献,所以我们要用前半部分的数据来更新树状数组即add操作。而对于后半部分的数据只需要更新答案即可也就是sum操作。

这样的算法时间复杂度是O(n*log(n)*log(n)),仍然远远小于暴力的复杂度O(n2)。


4实验结果及总结

通过一系列实验测试,我们得到一下数据:

N的数据大小

暴力算法(ms)

分治算法(ms)

树状数组算法(ms)

10

0

0

0

100

1

1

0

1000

6

3

3

10000

312

31

28

100000

29684

324

298

根据实验数据显示,当N的数据量越来越大时,算法运行所消耗的时间也在逐渐增加。但是不同算法之间算法的时间增长区别较大,原始暴力算法在数据量增长的时候,数据运算数据增长极快,在数据量到达100000时甚至所花费的时间是分治和树状数组的近百倍。而分治算法与树状数组算法之间的时间差异较少。

需要注意的是,利用树状数组优化的算法在单体数据过大时,会超出bit-tree的空间,所以这种情况下,我们需要将其进行离散化,按照从小到大排序重新赋值。


5 结论

       在多维偏序的算法中,我们使用了树状数组和分治算法,这两种算法比起原本的暴力算法都优化了不少,特别是时间复杂度方面从O(N2)降到了O(NlogN),在处理数据量非常大情况很有优势。但是他们也有着相对于暴力算法不足的地方,就是编码复杂度较高,特别是树状数组,在使用时要进行离散化。在掌握了二维的算法以后,我们可以顺势推演出三维的偏序关系,这就往往需要我们将树状数组和分治算法相结合起来。

         总的来说,使用了高级数据结构和算法对于我们研究离散数学中存在的偏序关系非常有帮助,在数据过大时,大大节省我们的时间。


6 参考文献:

[1] 维基百科:偏序https://zh.wikipedia.org/wiki/偏序关系

[2] 屈婉玲,耿素云,张立昂 编著.《离散数学第2版》,2015.

[3] 刘汝佳.《算法竞赛入门经典》:清华大学出版社,2009

[4] 刘汝佳. 《算法艺术与信息学竞赛》:清华大学出版社,2004-1

[5] Thomas H. Cormen, Charles E.Leiserson, Ronald L.Rivest, Clifford Stein. Introducing to Algorithms2017


7 附件

暴力算法

#include <bits/stdc++.h>

using namespace std;
const int MAXN = (int)1e6+7;
int N;
struct Node {
    int s1,s2,id,ans;
};
Node X[MAXN];
void work(){
    for(int i = 1;i <= N;i ++) {
        for(int j = 1;j <= N;j ++) {
            if(i==j) continue;
            if(X[i].s1>X[j].s1&&X[i].s2>X[j].s2){
                X[i].ans++;
            }
        }
    }
}
int main()
{
    cin >> N;
    for(int i = 1;i <= N;i ++) {
        cin >> X[i].s1 >> X[i].s2;
        X[i].id = i;
    }
    work();
    for (int i = 1;i <= N;i ++)  cout << X[i].id << ":" << X[i].ans << endl;
}

树状数组优化

#include <bits/stdc++.h>
using namespace std;
const int MAXN = (int)1e6+7;
int N;
struct Node {
    int s1,s2,id,ans;
};
Node X[MAXN];
void work(){
    for(int i = 1;i <= N;i ++) {
        for(int j = 1;j <= N;j ++) {
            if(i==j) continue;
            if(X[i].s1>X[j].s1&&X[i].s2>X[j].s2){
                X[i].ans++;
            }
        }
    }
}
int main()
{
    cin >> N;
    for(int i = 1;i <= N;i ++) {
        cin >> X[i].s1 >> X[i].s2;
        X[i].id = i;
    }
    work();
    for (int i = 1;i <= N;i ++)  cout << X[i].id << ":" << X[i].ans << endl;
}

分治优化

#include <bits/stdc++.h>
using namespace std;
const int MAXN = (int)1e6+7;
struct Node {
    int s1,s2,id,ans;
    bool operator < (const Node a) const {
        return s1 < a.s1;
    }
};
bool cmp(const Node&a,const Node&b) {
    return a.id < b.id;
}
Node X[MAXN];
Node tmp[MAXN];
void Merg(int l,int r) {
    if (l == r) return;
    int m = (l+r)/2;
    Merg(l,m);
    Merg(m+1,r);
    int pl = l,pr = m+1,cnt = l;
    while (pr <= r) {
        while (pl <= m && X[pl].s2 < X[pr].s2)
            tmp[cnt++] = X[pl++];
        X[pr].ans += pl-l;
        tmp[cnt++] = X[pr++];
    }
    while (pl <= m) tmp[cnt++] = X[pl++];
    for (int i = l;i <= r;i ++) X[i] = tmp[i];
}
int main()
{
    int N;
    cin >> N;
    for(int i = 1;i <= N;i ++) {
        cin >> X[i].s1 >> X[i].s2;
        X[i].id = i;
    }
    sort(X+1,X+1+N);
    Merg(1,N);
    sort(X+1,X+1+N,cmp);
    for (int i = 1;i <= N;i ++)  cout << X[i].id << ":" << X[i].ans << endl;
}

CDQ分治

#include <bits/stdc++.h>
using namespace std;
const int MAXN = (int)1e6+7;
struct Node {
    int s1,s2,s3,id,ans;
    bool operator < (const Node a) const {
        return s1 < a.s1;
    }
};
bool cmp(const Node&a,const Node&b) {
    return a.id < b.id;
}
Node X[MAXN];
Node tmp[MAXN];
int c[MAXN],N;
int lowbit(int x){
    return x&(-x);
}
int sum(int x){
    int res = 0;
    while(x > 0){
        res += c[x];
        x -= lowbit(x);
    }
    return res;
}
int add(int x,int val){
    while(x <= N){
        c[x] += val;
        x += lowbit(x);
    }
}
void Merg(int l,int r) {
    if (l == r) return;
    int m = (l+r)/2;
    Merg(l,m);
    Merg(m+1,r);
    int pl = l,pr = m+1,cnt = l;
    while (pr <= r) {
        while (pl <= m && X[pl].s2 < X[pr].s2){
            add(X[pl].s3, 1);
            tmp[cnt++] = X[pl++];
        }
        X[pr].ans += sum(X[pr].s3);
        tmp[cnt++] = X[pr++];
    }
    while (pl <= m) tmp[cnt++] = X[pl++];
    for (int i = l;i <= m;i ++) add(X[i].s3, -1);
    for (int i = l;i <= r;i ++)  X[i] = tmp[i];
}
int main()
{
    cin >> N;
    for(int i = 1;i <= N;i ++) {
        cin >> X[i].s1 >> X[i].s2 >> X[i].s3;
        X[i].id = i;
    }
    sort(X+1,X+1+N);
    Merg(1,N);
    sort(X+1,X+1+N,cmp);
    for (int i = 1;i <= N;i ++)  cout << X[i].id << ":" << X[i].ans << endl;
}

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值