一中单一队列专题

一中单调队列专题

【序言】以前还不知道有如此神奇的东西——单调队列!现在做了一中的专题,感觉这东西真的是博大精深。写写题解,是为了加深印象。                       

                           1.拥挤的奶牛 (Cow)


题目描述 Description

 N头牛在一个坐标轴上,每头牛有个高度。现给出一个距离值D。如果某头牛在它的左边,在距离D的范围内,如果找到某个牛的高度至少是它的两倍,且在右边也能找到这样的牛的话。则此牛会感觉到不舒服。问有多少头会感到不舒服。

输入格式 Input format

第一行两个有空格隔开的整数 n,d ,含义如题中所述。

接下去 n 行,每行两个数字,表示每头牛所站的位置和高度。

输出格式 Output format

一行一个数,表示答案。

样例输入 Sample input

6 4

10 3

6 2

5 3

9 7

3 6

11 2

样例输出 Sample input

2

提示 Hint

1 <= N <= 50,000

1 <= x[i],h[i] <= 1,000,000,000

1 <= D <= 1,000,000,000


【分析与思考】这道题很像以前做过的Sliding Window,只是左右两个都要兼顾(真是单调队列的基础好题!)我们先正着做一遍,维护一个单调递减的高度序列,同时记录进队时间(可以理解为奶牛们的坐标)。每次进队时用tail指针,出队时用head指针。最后倒着再做一遍,统计结果即可。

【代码】

#include<stdio.h>
#include<algorithm>
using namespace std;
const int maxn=50000+5;
struct arr{int x,h;}a[maxn];
int h[maxn],t[maxn],n,d,i,head,tail,f[maxn],ans;
bool cmp(arr a,arr b){return a.x<b.x||a.x==b.x&&a.h>b.h;}
//这里特别要注意:有可能有重复位置的奶牛!为了维护单调递减的序列,我们要在坐标相同的情况下,高度从大到小排序。
int main()
{
  freopen("cow.in","r",stdin);
  freopen("cow.out","w",stdout);
  scanf("%ld%ld",&n,&d);
  for (i=1;i<=n;i++)
    scanf("%ld%ld",&a[i].x,&a[i].h);
  sort(a+1,a+n+1,cmp);
  head=1;tail=1;t[1]=a[1].x;h[1]=a[1].h;
  for (i=2;i<=n;i++)
  {
    while (head<=tail&&t[head]<a[i].x-d) head++;
    if (head<=tail&&h[head]>=a[i].h*2) f[i]++;
    while (tail>=head&&a[i].h>h[tail]) tail--;
    h[++tail]=a[i].h;t[tail]=a[i].x;
  }
  head=1;tail=1;t[1]=a[n].x;h[1]=a[n].h;
  for (i=n-1;i>0;i--)
  {
    while (head<=tail&&t[head]>a[i].x+d) head++;
    if (head<=tail&&h[head]>=a[i].h*2) f[i]++;
    while (tail>=head&&a[i].h>h[tail]) tail--;
    h[++tail]=a[i].h;t[tail]=a[i].x;
  }
  for (i=2;i<n;i++)
    if (f[i]==2) ans++;
  printf("%ld",ans);
  return 0;
}

           2.理想的正方形 (Square)


题目描述 Description

    有一个n*m的整数组成的矩阵,现请你从中找出一个k*k的正方形区域,使得该区域所有数中的最大值和最小值的差最小。

输入格式 Input format

    第一行为3个整数,分别表示n,m,k的值第二行至第n+1行每行为m个非负整数,表示矩阵中相应位置上的数。每行相邻两数之间用一空格分隔。

输出格式 Output format

  仅一个整数,为n*m矩阵中所有“k*k正方形区域中的最大整数和最小整的差值”的最小值。

样例输入 Sample input

5 4 2

1 2 5 6

0 17 16 0

16 17 2 1

2 10 2 1

1 2 2 2

样例输出 Sample input

1

提示 Hint

2<=a,b<=1000,n<=a,n<=b,n<=100 

矩阵中的所有数都不超过1,000,000,000


分析与思考】从一维扩展到了二维。

一开始我想法是:每次预处理最左边的正方形(从上到下一共有n-k+1个),然后一点一点向右拓展——每新的一列来的时候,在删除远的点的同时,把K个元素都进队列处理。但是超时了5个点。

想了一下,在最左边的正方形预处理中,不必一个一个扫一遍,而可以从最左上的那个向下拓展而来。那怎么办?是写成那种函数的形式吗?若是最左边的正方形,则向右、下拓展,否则只向右拓展。(复杂度很高嘛!)

在SYC大神的指导下,懂了一个新的方法。其实没必要怎么麻烦。每次只要枚举每行,从左向右做一遍单调队列。再枚举每一列,做一遍单调队列。典型的从一维拓展为二维的思想。

【超时代码】

#include<stdio.h>
using namespace std;
const int maxn=1000+5;
const int maxn2=maxn*maxn;
const int INF=2100000000;
int x1[maxn2],x2[maxn2],y1[maxn2],y2[maxn2],a[maxn][maxn];
int n,m,k,ans,i,j,up,head1,head2,tail1,tail2;
void insert1(int data,int time)
{
  while (data>x1[tail1]&&tail1>=head1) tail1--;
  x1[++tail1]=data;y1[tail1]=time;
}
void insert2(int data,int time)
{
  while (data<x2[tail2]&&tail2>=head2) tail2--;
  x2[++tail2]=data;y2[tail2]=time;
}
void del(int now)
{
  while (y1[head1]+k<=now&&head1<=tail1) head1++;
  while (y2[head2]+k<=now&&head2<=tail2) head2++;
}
int main()
{
  freopen("square.in","r",stdin);
  freopen("square.out","w",stdout);
  scanf("%ld%ld%ld",&n,&m,&k);
  ans=INF;
  for (i=1;i<=n;i++)
    for (j=1;j<=m;j++)
      scanf("%ld",&a[i][j]);
  for (up=1;up<=n-k+1;up++)
  {
    head1=1;tail1=0;head2=1;tail2=0;
    if (up==8)
      up=8;
    for (i=up;i<up+k;i++)
      for (j=1;j<k;j++)
      {
        insert1(a[i][j],j);
        insert2(a[i][j],j);
      }
    for (j=k;j<=m;j++)
    {
      del(j);
      if (ans==4)
        ans=4;
      for (i=up;i<up+k;i++)
      {
        insert1(a[i][j],j);
        insert2(a[i][j],j);
      }
      if (x1[head1]-x2[head2]<ans) ans=x1[head1]-x2[head2];
    }
  }
  printf("%ld",ans);
  return 0;
} 

【AC代码】

#include<stdio.h>
using namespace std;
const int maxn=1000+5;
const int maxn2=maxn*maxn;
const int INF=2100000000;
int x1[maxn2],x2[maxn2],y1[maxn2],y2[maxn2];
int a[maxn][maxn],max[maxn][maxn],min[maxn][maxn];
int n,m,k,ans,i,j,up,down,head1,head2,tail1,tail2;
void insert1(int data,int time)
{
  while (data>x1[tail1]&&tail1>=head1) tail1--;
  x1[++tail1]=data;y1[tail1]=time;
}
void insert2(int data,int time)
{
  while (data<x2[tail2]&&tail2>=head2) tail2--;
  x2[++tail2]=data;y2[tail2]=time;
}
void del(int now)
{
  while (y1[head1]+k<=now&&head1<=tail1) head1++;
  while (y2[head2]+k<=now&&head2<=tail2) head2++;
}
int main()
{
  freopen("square.in","r",stdin);
  freopen("square.out","w",stdout);
  scanf("%ld%ld%ld",&n,&m,&k);
  ans=INF;
  for (i=1;i<=n;i++)
    for (j=1;j<=m;j++)
      scanf("%ld",&a[i][j]);
  for (up=1;up<=n;up++)
  {
    head1=1;tail1=0;head2=1;tail2=0;
    for (i=1;i<k;i++)
    {
      insert1(a[up][i],i);
      insert2(a[up][i],i);
    }
    for (i=k;i<=m;i++)
    {
      del(i);
      insert1(a[up][i],i);
      insert2(a[up][i],i);
      max[up][i-k+1]=x1[head1];
      min[up][i-k+1]=x2[head2];
    }
  }
  for (down=1;down<=m-k+1;down++)
  {
    head1=1;tail1=0;head2=1;tail2=0;
    for (i=1;i<k;i++)
    {
      insert1(max[i][down],i);
      insert2(min[i][down],i);
    }
    for (i=k;i<=n;i++)
    {
      del(i);
      insert1(max[i][down],i);
      insert2(min[i][down],i);
      if (x1[head1]-x2[head2]<ans) ans=x1[head1]-x2[head2];
    }
  }
  printf("%ld",ans);
  return 0;
} 
 

              3.最大数 (Max)

    

题目描述 Description

   现在请求你维护一个数列,要求提供以下两种操作: 1、 查询操作。语法:Q L 功能:查询当前数列中末尾L个数中的最大的数,并输出这个数的值。限制:L不超过当前数列的长度。 2、 插入操作。语法:A n 功能:将n加上t,其中t是最近一次查询操作的答案(如果还未执行过查询操作,则t=0),并将所得结果对一个固定的常数D取模,将所得答案插入到数列的末尾。限制:n是非负整数并且在长整范围内。注意:初始时数列是空的,没有一个数。

输入格式 Input format

    第一行两个整数,M和D,其中M表示操作的个数(M <= 200,000),D如上文中所述

输出格式 Output format

    对于每一个查询操作,你应该按照顺序依次输出结果,每个结果占一行。

样例输入 Sample input

5 100

A 96

Q 1

A 97

Q 1

Q 2

样例输出 Sample input

96

93

96

 

分析与思考】只是BZOJ上的原题,当时在练线段树的时候,我就觉得可以用单调队列做(虽然当时还不知道这种算法)只是单调队列会有点麻烦——还要记录队列里的数是原来总的数中第几个,然后用二分查找。

没有编,反正可以用线段树水过去的!


             4.盖房子 (Build)


题目描述 Description

  贵族灰最近搞到手了一块 n*m (n,m≤2000)大小的土地,他决定在这片土地上盖一栋房子。不幸的是,在这块土地上有这一些小坑,小坑内灌满了HCl①,贵族灰不会选择这些土地来盖自己的房子。现在贵族灰聘请你来为他选择地基,要求你选择的地基为一个矩形且在这块矩形中不含有任何一个填了 HCl 的小坑,当然了,为了显示自己的贵族身份,贵族灰自然希望地基的面积越大越好。贵族灰答应你在事成之后分给你 100000000...000 % 10 ¥ 的报酬。

输入格式 Input format

    第一行两个有空格隔开的整数 n,m ,表示土地的大小。

    接下去n行,每行m个字符,#表示这个位置上填满了HCl,*则表示没有。

输出格式 Output format

    一行一个数,表示答案。

样例输入 Sample input

5 10

# * # * # # # * # *

# * # * # * * * # *

# # # * # * * * # *

# * # * # * * * # *

# * # * # # # * # #

样例输出 Sample input

9

 

【分析与思考】据考证,这道题是USACO training6.1.2上类似的题目,可以用极大化的思想做(不知道是不是这个叫单调队列。)我们维护三个数组h[i][j],l[i][j],r[i][j]。h[i][j]表示从i,j这个点(含)向上共有几个连续的‘*’,l[i][j]和r[i][j]分别表示从i,j这条悬线向左或是向右连续的'*’能达到的最远的点的坐标。每次更新答案是,就是ans和(r[i][j]-l[i][j]+1)*h[i][j]。详细预处理看代码。

【代码】

#include<stdio.h>
#include<iostream>
using namespace std;
const int maxn=2000+5;
bool a[maxn][maxn];
int h[maxn][maxn],l[maxn][maxn],r[maxn][maxn],tl[maxn],tr[maxn];
char c;
int n,m,i,j,ans,temp;
int main()
{
  freopen("build.in","r",stdin);
  freopen("build.out","w",stdout);
  scanf("%ld%ld",&n,&m);
  for (i=1;i<=n;i++)
  {
    c=getchar();
    for (j=1;j<=m;j++)
    {
      c=getchar();
      if (c=='*') {a[i][j]=true;ans=1;}
      c=getchar();
    }
  }
  for (j=1;j<=m;j++)
  {
    for (i=1;i<=n;i++)
      if (a[i][j]) h[i][j]=h[i-1][j]+1;
      else {l[i][j]=1;r[i][j]=m;h[i][j]=0;}
  }
  for (j=1;j<=m;j++) 
    if (a[1][j]) 
      {
        if (!a[1][j-1]) l[1][j]=j;
        else l[1][j]=l[1][j-1];
      }
  for (j=m;j>0;j--)
    if (a[1][j])
      {
        if (!a[1][j+1]) r[1][j]=j;
        else r[1][j]=r[1][j+1];
      }
  for (i=2;i<=n;i++)
  {
    for (j=1;j<=m;j++) 
      if (a[i][j]) 
      {
        if (tl[j-1]==0) tl[j]=j;
        else tl[j]=tl[j-1];
      }
     else tl[j]=0;
    for (j=m;j>0;j--)
      if (a[i][j])
      {
        if (tr[j+1]==0) tr[j]=j;
        else tr[j]=tr[j+1];
      }
      else tr[j]=0;
    for (j=1;j<=m;j++)
      if (a[i][j])
      {
        l[i][j]=max(l[i-1][j],tl[j]);
        r[i][j]=min(r[i-1][j],tr[j]);
        temp=h[i][j]*(r[i][j]-l[i][j]+1);
        if (temp>ans) 
          ans=temp;
      }  
  }
  printf("%ld",ans);
  return 0;
}