解题报告(离散化处理前缀和类问题)

 在谈到离散化之前,可以从简单的前缀和问题上入手,来理解离散化处理问题的精妙

Acwing 795 前缀和

 

(该题限时2s)

   对于小范围的前缀和,可以先开n大小的数组,存储整数序列,其中再开另外一个n大小的数组s,用来存储到达每一个下标前的前缀和。即s [i]=s [i-1]+a [i]; 此后在询问区间内的和的时候,直接s [r]- s [l-1] 即为区间和;(这里是l-1是因为下标为 l 的话,就少减去了s [l]);  对于本题来说,l,r的数据范围是到 [1,1e5],数组下标直接用来表示l,r的话,也够用。但是,当l,r的范围扩大到1e9的时候,数组开不了那么大,此时不能单单用数组下标来表示了。

如题:

Acwing 802 区间和

 

   如果该题我们用上一题的思路去解,首先我们就需要开一个2e9大小的数组,此时需要大概有4个G的内存空间才行,内存受限。而我们观察题目,总共对于点的个数也就1e5个,对于没有用到的点的初始值也就是为0,对于前缀和来说,是没有任何影响的。所以,相当于开2e9大小的数组有相当一部分都是浪费了的,所以我们不能一个点一个位置都给分配。由于我们用到的点,就是一个点一个点的零散的点,所以也就是利用离散化的方式,将这些点,映射到一个数组中去,这就是离散化的处理方式。

  离散化过程:

  若现在有1,2,1e7,1e9。四个点的数存在某个数组中,并且各个点对应各自的值

 

 

 接下来将该点整理到一个数组中,从大到小。也就是将该数组的下标变成了离散数的下标,离散数变成了该数组的各个元素

实际上,就是将其做下图的对应方式。

 

 

 

 

 当元素有重复的时候,我们需要去除重复的元素。

sort(alls.begin(),alls.end());
alls.erase(unique(alls.begin(),alls.end()),alls.end());

去重的理由在于:

当存在两个相同的元素的时候,他们映射的数是不同的,而通过二分查找的,找到的都是前一个元素的映射的数,此时去除可以减少映射数组的内存大小,提高二分的效率

接下来,当我们需要用到这些点的时候,就可以从这个数组中去找。

如何找,如果从头到尾遍历就慢了,还是O(n)的时间复杂度,这里采用二分的算法,可以从O(n)到O(logn);

int find(int x)//找相应的下标
{
    int l=0,r=alls.size();
    while(l<r)
    {
        int mid=(l+r)/2;
        if(alls[mid]>x) r=mid;
        else if(alls[mid]<x) l=mid;
        else return mid+1;
        //cout<<l<<r<<endl;
    }
    return r+1;//这里返回的是r+1是因为数组是从0开始,但是接下来计算前缀和的话,最好是从1开始,比较好计算
}

 

例题:

Acwing 802 区间和(即上题)

 

对于本题来说,各个点的数值其实是零散开来,我们首先需要一个数组来记录所有的区间端点数值,包括x,l, r。可记作数组a

对于特殊的点,即端点有自己的value值的点,我们需要用结构体来将其包装,因为一个点对应一个值,放入对应的数组中,记作数组b。

对于题目要求的区间值,我们也需要一个数组来储存,在输出答案的时候。(因为前期为了把区间端点值放到数组a中,已经读过一遍了,后期要输出,还要拿出来)记作数组c。

我们要用作计算前缀和的数组记作前缀数组a1,记录需要我们计算的各个点的值,记总和为前缀和数组s1,用a1中的数值将其转化为前缀和的形式,再输出

此时的时间复杂度是<=O((n+m)log(n+m));//具体看有无去重

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int M=1e6+5;

typedef pair<int,int> PI;

vector<int>alls;//alls就是用来映射的映射数组,配合find函数找点的对应的映射下标
vector<PI>add,query;//add是存储对点的操作数,query是存储区间值

int a[M],s[M];

int find(int x)//找相应的下标,其实就是映射函数,找到的是大数映射的下标
{
    int l=0,r=alls.size();
    while(l<r)
    {
        int mid=(l+r)/2;
        if(alls[mid]>x) r=mid;
        else if(alls[mid]<x) l=mid;
        else return mid+1;
        //cout<<l<<r<<endl;
    }
    return r+1;
}

int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=0;i<n;i++)//读入对点操作的数 
    {
        int x,c;
        cin>>x>>c;
        add.push_back({x,c});
        alls.push_back(x);
    }
    for(int i=0;i<m;i++)
    {
        int l,r;
        cin>>l>>r;
        query.push_back({l,r});
        alls.push_back(l);
        alls.push_back(r);
    }
    //去重
    sort(alls.begin(),alls.end());
    alls.erase(unique(alls.begin(),alls.end()),alls.end());
    for(auto item: add)
    {
        int i=find(item.first);//找该数的映射下标
        a[i]+=item.second;
    } 
    for(int i=1;i<=alls.size();i++) s[i]=s[i-1]+a[i];//计算前缀和
    for(auto item: query)
    {
        int l=find(item.first),r=find(item.second);
        cout<<s[r]-s[l-1]<<endl;
    }
    return 0;
}

该题是模板题,接下来是题目的小变式

 

Acwing 1952金发姑娘和N头牛

 

 

 

对于本题来说,用暴力的思想去看,就是对三段区间内加上x,y,z。(给出A,B值,在区间[1,A-1]上加上x,在区间[A,B]上加上y,在区间[B+1,1e9]上加上z),本质上还是需要对一段区间做操作,为了降低复杂度,可以采用差分的做法。即对每一个操作

是a[1]+=x,a[A]-=x,a[A]+=y,a[B+1]-=y,a[B+1]+=z。最后计算前缀和最大的值,就是题目所求的答案。而由于每个点的范围大,而点的数量小,需要用离散化来处理各个端点

 

 

#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int M=2e6+5;
int a[M],s[M];
typedef pair<int,int> PII;
vector<int>all;//用来映射的数组
vector<PII>add,que;

int find(int x)//映射函数
{
  int l=0,r=all.size();
  while(l<r)
  {
      int mid=(l+r)/2;
      if(all[mid]>=x) r=mid;
      else l=mid+1;
  }
  return r+1;
}

int main()
{
  memset(a,0,sizeof(a));
  int n,x,y,z;
  cin>>n>>x>>y>>z;
  for(int i=0;i<n;i++)
  {
      int l,r;
      cin>>l>>r;
      all.push_back(l);
      all.push_back(r);
      que.push_back({l,r});
      
  }
  int ans=0;
  sort(all.begin(),all.end());
  //去重
  all.erase(unique(all.begin(),all.end()),all.end());
  for(auto item : que)
  {
      int l=find(item.first),r=find(item.second);
      a[1]+=x;
      a[l]-=x;
      a[l]+=y;
      a[r+1]-=y;
      a[r+1]+=z;
      //这里也可以整合一下,写成a[1]+=x;a[l]+=y-x;a[r+1]+=z-y;
  }
  for(int i=1;i<=all.size();i++) 
  {
      a[i]+=a[i-1];//计算前缀和
      ans=max(ans,a[i]);
  }
  cout<<ans;
}

 

Acwing 1987 粉刷栅栏

对于本题来说,也可以转化为对区间做一次操作,即当奶牛走过[L,R]的区间的时候,在[L,R]上加上1。当一个区间内的值是大于二的时候,说明就是不止走了两遍

有一些问题需要转化:

对于[L,R]来说,当向右走的时候,L就起始点,R就是L+向左走的步长

                            当向左的时候,L就是上一次向右走的位置D-向右走的步长,R就是D,上一次的起始位置

 而判断一个区间内的值大于二,可以用前缀和来判断,当某点的前缀和大于2的时候,说明这个点,奶牛已经经过了两次了,那么这个点到下一个点之间的距离,奶牛肯定要走。若是奶牛走的路程不止下一个点,那么在前缀和计算到下一个点的时候,下一个点的初始值应该是1。由于也是区间操作问题,最好用差分算法来降低时间复杂度,而在本题中,注意差分的后一个点是a[R]--;

由下图可知,对于右端点来说,是不算走过的路,因为计算下一段前缀和的时候,还是从右端点开始,会重复计算。

 

(箭头表示:当前点的前缀和的值是从哪几段来的)

而由于贝茜的行走距离的范围是[1,2e9],数值过大,想要直接用下标来表示走做的坐标的话,数组开的内存太大了,而且点是零散的,各个点的默认值为0,对前缀和也没有影响,所以也需要用离散化来储存各个点。

#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int M=1e6+5;
typedef pair<int,int>PII;
vector<int>all;
vector<PII>query;
int a[M],s[M];

int find(int x)
{
    int l=0,r=all.size();
    while(l<r)
    {
       int mid=(l+r)/2;
       if(all[mid]>=x) r=mid;
       else l=mid+1;
    }
    return r+1;
}

int main()
{
    memset(s,0,sizeof(s));
    int n,sum=0;//查看总共走过了多少路径
    cin>>n;
    int l=0;
    for(int i=0;i<n;i++)
    {
        int length,r;
        char c;
        cin>>length>>c;
        if(c=='L')  r=l-length;
        else r=l+length;
        all.push_back(l);
        all.push_back(r);
        if(c=='R') query.push_back({l,r});
        else query.push_back({r,l});
        //cout<<l<<" "<<r<<endl;
        l=r;//是下一个点从上一个点的目的地开始
    }
    sort(all.begin(),all.end());
    all.erase(unique(all.begin(),all.end()),all.end());
    for(auto item: query)
    {
        int li=find(item.first),ri=find(item.second);//找到映射的下标
       // cout<<li<<" "<<ri<<endl;
        //标记走过的路段,用差分
        a[li]++;
        a[ri]--;//这里不是a[r+1]
    }
    for(int i=1;i<=all.size();i++)
    {
       s[i]=s[i-1]+a[i];//计算前缀和表示走过的路段总路径
       //cout<<s[i]<<" "<<a[i]<<endl;
       if(s[i]>=2) sum+=all[i]-all[i-1];//这里注意,由于传回来的是r+1,all数组的下标和前缀和数组想要表达的下标的差一
    }
    cout<<sum;
}

To sum up:

 一般离散化是为了处理点的数量是不多,但是点的值较大,数组太大开不了的问题,可以将这些点映射到一个数组中去,用二分法来代替数组直接的用指针访问下标。

     核心的代码就是去重,以及利用二分去查找下标

 

posted @ 2022-01-20 19:28  dueyu  阅读(351)  评论(0)    收藏  举报