多种方法求解区间最值问题

 

 

   著名计算机学家曾提出:程序=算法+数据结构,这句话被广大程序员们奉为圭臬。我是这样理解这句话的:如果说算法是指导我们用什么样的方法与步骤来解决一个问题,则在问题中不可避免的要处理各种数据信息,如何来组织这些数据信息,就依赖于数据结构了,是将这些数据组织成线性的,还是树型的,则见仁见智、不一而足了。

例如下面这个问题:

给定M及一列数,每个数在0到100,000之间。(为了方便描述,设共N个数,1< N < = 2500000) 输出每M个数中的最大数,即1~M中的最大数,2~M+1中的最大数……N-M+1~N中的最大数,共N-M+1个。

输入

第一行是一个数M,接下来是N个数,每个数一行,以-1作为结尾.

输出

输出N-M+1个最大数,每个数一行。

样例输入

3

10

11

10

0

0

0

1

2

3

2

-1

样例输出

11

11

10

0

1

2

3

3

   这个题意非常简单,就是求一个固定长度区间内的最大值。如果我们不加任何思考的话,可以将读入的数据放到一个线性表f数组中,然后枚举开始点i,遍历求出区间[i,i+m-1]中最大值,于是时间复杂度为O(n*m),程序代码如下:

for (int i=1;i<=n-m+1;i++)

{

   ans=f[i];

   for (int j=i+1;j<=i+m-1;j++)

if (f[j]>ans)

    ans=f[j];

cout<<ans<<endl;

}

  对于题中的信息,有两个要素即位置和权值。上面这个做法优先考虑了位置关系,即先固定好要考察的区间[i,i+m-1],然后再来解决求最大值的问题。如果我们变换下思维方式,优先考虑最大值,再来解决区间这个约束条件,会发现这个问题中最核心的需求就是不断的求最大值,而堆是解决这一类需要的一个利器。于是我们可以将数据组织成树型结构,即将读入的数值设计成一个大根堆,即堆顶元素就是全局最大值,并记下每个数值对应的在输入时的位置,由于本题是求一个指定区间的最大值,于是我们还需要对堆顶元素进行判断一下它是否位于指定区间,如果在的话,则直接输出堆顶元素的值,否则踢掉堆顶元素。程序代码如下:

#include<bits/stdc++.h>

using namespace std;

const int N=6e5+5;

struct num

{

       int w,v;

       bool operator <(const num x)const

       {

              return (v<x.v)||(v==x.v&&w<x.w);

       }

} p;

priority_queue<num>q;

int n,k,top,a[N],ans[N];

int main() {

       int x,k;

       cin>>k;

       while(~scanf("%d",&x)&&x!=-1)

            a[++n]=x;

       for(int i=1; i<=k; i++)

       {

              p.w=i,p.v=a[i];

              q.push(p);

       }

       top=1;

       ans[top]=q.top().v;

       for(int i=k+1; i<=n; i++)

       {

              p.w=i,p.v=a[i];

              q.push(p);

              while(i-q.top().w>=k)

q.pop();

              ans[++top]=q.top().v;

       }

       for(int i=1; i<=top; i++)

           printf("%d\n",ans[i]);

}

进一步反思上面这个做法,会发现存在大量的数据冗余------存在大量明显无用的数据在堆里面。而在求出一个区间[i,i+m-1]的结果后,接下来我们要求[i+1,i+m]这个区间的结果,这两个区间比较一下就会发现,无非将第i个位置上的数值去掉,加入第i+m个位置上的值。此时的结果有两种可能,要么仍是从前区间[i,i+m-1]结果,要么是新加入的元素。于是我们又回到线性数据结构,用代表第个数对应的答案,表示第个数,于是维护这样一个队列:队列中的每个元素有两个域{position,value},分别代表他在原队列中的位置和,我们随时保持这个队列中的元素position域单调递增,value单调递减,。则在计算的时候,先将加入到队列中,如何加入队列呢?我们让a[i]与队尾元素的 value域进行比较,但凡发现小于a[i]的,一律从队列中踢掉,为什么要踢掉呢?那是因为由于是随着单调递增的,所以对于,在计算任意一个状态的时候,都不会比优,所以j被踢掉是有理有据的,并且通过这样的“踢数据”的操作,我们有效的降低了需要维护的数据的量,对比堆的操作中,这些无效的数据仍放在堆中,在加数据的操作时,无疑增加了操作的次数。于是采用这种方法,每个数据进出队列都只有一次,所以时间复杂度为O(n),而堆的时间复杂度为O(n*log2n)。接下来我们还要在队首不断删除,直到队首的position大于等于,那此时队首的value必定是的不二人选,因为队列是单调的!程序代码如下:

#include<bits/stdc++.h>

using namespace std;

int n,d,num,f[2500001],a[2500001],b[2500001];

int main()

{

num=0;

scanf("%d",&n);

while(true)

{

scanf("%d",&d);

if(d==-1)

break;

num++;

a[num]=d;

}

int head=1,tail=1,now=1;

f[1]=a[1]; //存放值域

b[1]=1;  //存放位置

for (int now=2;now<=num;now++)

{

while(tail>=head&&a[now]>f[tail])

//维护一个值域单调不上升的队列

tail--;

tail++;

f[tail]=a[now];

b[tail]=now;

if(now-b[head]>=n)

//控制队列头的位置域在所要求的范围之内

head++;

if(now>=n)

printf("%d\n",f[head]);

}

}

   此外本题还可以使用st表,线段树等数据结构来进行维护,介于篇幅原因,不再赘述,列表如下:

数据结构

空间复杂度

时间复杂度

编码难度

O(n)

O(n*log2n)

简单

单调队列

O(n)

O(n)

简单

线段树

O(4*n)

O(n*log2n)

中等

St表

O(n*log2n)

O(n*log2 m)

中等

   综上所述,有两点心得体会,首先对于给定的信息,往往会有多个属性,当我们选择的主攻方向,如果实际效果并不好时,就要变换思考的方式了,这个现象是在编程学习中经常遇到的。其次数据结构与算法如同习武之人的内功与外功,两者相辅相成,我们在针对某个问题设计程序时,当发现在算法上没有好的方法时,就想想如何从数据结构上进行突破,反之亦然,而当有多种数据结构可供选择时,则需要细细体会它们之间的优劣,例如此题用线段树也可以完成,然而线段树的常数较大,并且编码相对来说要复杂一些,所以并不推荐,但如果本题进行改编,变成即有询问操作,又有数值的更改操作时,线段树就能发挥其特长了,所以每种数据结构都有其适用的场景,关键是我们在了解其来龙去源的前提下,活学活用。

 

posted @ 2021-08-22 17:40  我微笑不代表我快乐  阅读(554)  评论(0编辑  收藏  举报