【ACM训练五】区间查询问题

各类乱七八糟数据结构的本质:定义若干正则集合,并将他们组织成某种合适的结构,而查找算法就是要把查找的结果表示成若干个正则集合的划分,进而在每个正则集合中通过枚举的方式实现查找。(转自某大佬博客

ST表

算法详解https://blog.csdn.net/Hanks_o/article/details/77547380
https://www.cnblogs.com/qt666/p/6508208.html
https://blog.csdn.net/qq_31759205/article/details/75008659
特征:O(nlogn)预处理,O(1)查询最值,不支持在线修改
例题
给定一个长度为 N 的数列,进行 M 次询问,求出每一次询问的区间内数字的最大值。
输入格式:
第一行包含两个整数 N,M ,分别表示数列的长度和询问的个数。
第二行包含 N 个整数(记为 ai​),依次表示数列的第 i 项。
接下来 M行,每行包含两个整数 li​,ri​,表示查询的区间为 [li​,ri​]
输出格式:
输出包含 M行,每行一个整数,依次表示每一次询问的结果。

#include <cstdio>
#include <algorithm>
using namespace std;
int A[10000],dp[10000][10000];//A为输入的列表
//打表
void ST(int n) 
{
    for (int i = 1; i <= n; i++)
        dp[i][0] = A[i];
    for (int j = 1; (1 << j) <= n; j++) 
        for (int i = 1; i + (1 << j) - 1 <= n; i++) 
            dp[i][j] = max(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);//当求最小值时为min()
}
//查询
int RMQ(int l, int r) 
{
    int k = 0;
    while ((1 << (k + 1)) <= r - l + 1) k++;
    return max(dp[l][k], dp[r - (1 << k) + 1][k]);
}
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	
	for(int i=1;i<=n;i++)
	scanf("%d",&A[i]);
	
	ST(n);
	
	while(m--)
	{
		int l,r;
		scanf("%d%d",&l,&r);
		printf("%d\n",RMQ(l,r));//输出最大值
	}
	return 0;
} 

分块

大佬讲解:ACM竞赛中数据结构题目心得:分块【With HDU4366】
总结:对不完整的块暴力处理,对完整的块直接查询,分块大小为根号序列长度时时间复杂度最优
例题:
1、P2424 约数和
2、给出一个长为n的数列,以及n个操作,操作涉及区间加法,单点查值。
(转自博客:https://www.cnblogs.com/CHerish_OI/p/8548715.html
输入格式
第一行输入一个数字n。
第二行输入n个数字,第i个数字为ai,以空格隔开。
接下来输入n行询问,每行输入四个数字 opt、l、r、c,以空格隔开。
若opt=0,表示将位于l,r的之间的数字都加c。
若opt=1,表示询问ar的值( l和r忽略)。
输出格式
对于每次询问,输出一行一个数字表示答案。
题解:
   做分块的题需要一定的知识基础,这里就不一一讲解了,主要讲应用到每道题目是的具体做法:
   对于单点询问相对比较简单,数组简单存储之后就可以O(1)输出。
   主要就是对于区间修改:
   分块之后,对于需要修改的L~R的区间,可以简单分情况讨论:
   1、如果l和r同属一块,那么直接枚举修改
   2、如果不属于同一块,那么先将左端点所属的块从L开始到该块结尾修改,再将右端点所属的块从该块开头到R修改
   然后就是修改中间没有涉及到的块了,其实也是直接暴力,用一个新的数组统计该块整块都增加的值,枚举L的下一个块到R的上一个块就OK。

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
using namespace std;
int n,a[51000],ba[51000];
int block,pos[51000];
void update(int l,int r,int c)
{
    for(int i=l;i<=min(pos[l]*block,r);i++)a[i]+=c;
    if(pos[l]!=pos[r])
        for(int i=(pos[r]-1)*block+1;i<=r;i++)
            a[i]+=c;
    for(int i=pos[l]+1;i<=pos[r]-1;i++)ba[i]+=c;
}
int main()
{
    scanf("%d",&n);for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    block=sqrt(n);
    for(int i=1;i<=n;i++)pos[i]=(i-1)/block+1;
    for(int i=1;i<=n;i++)
    {
        int l,r,c,opt;
        scanf("%d%d%d%d",&opt,&l,&r,&c);
        if(opt==0)update(l,r,c);
        else printf("%d\n",a[r]+ba[pos[r]]);
    }
    return 0;
    }

树状数组

转载自洛谷题解

特征:不需要构造
1、单点修改
这里有一个很关键的东西,叫做lowbit,lowbit是将一个二进制数的所有高位一都去掉,只留下最低位的1,
比如lowbit(5)=lowbit(0101(二进制))=0001(二进制)
而如果改变x的值,就要加上自己的lowbit,一直加到n,这些节点都要加,比如一共有8个数第3个数要加上k,那么
c[0011]+=k;
c[0011+0001] (c[0100])+=k;
c[0100+0100] (c[1000])+=k;
这样就能维护树状数组

  void add(int x,int k)
    {
        while(x<=n)
        {
            tree[x]+=k;
            x+=lowbit(x);
        }
    }

2、区间查询

就是前缀和,比如查询x到y区间的和,那么就将从1到y的和-从1到x的和。
从1到y的和求法是,将y转为2进制,然后一直减去lowbit(y),一直到0
比如求1到7的和
ans+=c[0111];
ans+=c[0111-0001(0110)];
ans+=c[0110-0010(0100)];
ans+=c[0100-0100(c[0]无意义,结束)]

 int sum(int x)
    {
        int ans=0;
        while(x!=0)
        {
            ans+=tree[x];
            x-=lowbit(x);
        }
        return ans;
    }

区间修改
这就会变的很好玩。如果将x到y区间加上一个k,那就是从x到n都加上一个k,再从y+1到n加上一个-k
加的移动还是i+=lowbit(i);

 void add(int x,int k)
    {
        while(x<=n)
        {
            tree[x]+=k;
            x+=lowbit(x);
        }
    }

单点查询
从x点,一直x-=lowbit(x),沿途都加上就好啦

   int search(int x)
    {
        int ans=0;
        while(x!=0)
        {
            ans+=tree[x];
            x-=lowbit(x);
        }
        return ans;
    }

模板:
题目描述:已知一个数列,你需要进行下面两种操作:
1.将某一个数加上x
2.求出某区间每一个数的和
输入输出格式
输入格式:
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含3个整数,表示一个操作,具体如下:
操作1: 格式:1 x k 含义:将第x个数加上k
操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和
输出格式:
输出包含若干行整数,即为所有操作2的结果。
题解:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
int n,m,tree[2000010];
 int lowbit(int k)
  {
      return k & -k;
  }
  void add(int x,int k)
  {
      while(x<=n)
      {
          tree[x]+=k;
          x+=lowbit(x);
      }
  }
  int sum(int x)
  {
      int ans=0;
      while(x!=0)
      {
          ans+=tree[x];
          x-=lowbit(x);
      }
      return ans;
  }
  int main()
  {
      cin>>n>>m;
      for(int i=1;i<=n;i++)
      {
          int a;
          scanf("%d",&a);
          add(i,a);
      }
      for(int i=1;i<=m;i++)
      {
          int a,b,c;
          scanf("%d%d%d",&a,&b,&c);
          if(a==1)
              add(b,c);
          if(a==2)
              cout<<sum(c)-sum(b-1)<<endl;
      }
  }

线段树

转载自洛谷题解

构造用到递归:
先设left=1,right=n,然后每一次递归,left、mid和mid+1、right。代码如下:

void build(int left,int right,int index)
{
    tree[index].left=left;
    tree[index].right=right;
       if(left==right)
        return ;
    int mid=(right+left)/2;
    build(left,mid,index*2);
    build(mid+1,right,index*2+1);
}

单点修改
单点修改就是每到一个节点,看这个节点代表着的区间包括不包括这个点,包括就加上。

void my_plus(int index,int dis,int k)
{
    tree[index].num+=k;
    if(tree[index].left==tree[index].right)
        return ;
    if(dis<=tree[index*2].right)
        my_plus(index*2,dis,k);
    if(dis>=tree[index*2+1].left)
        my_plus(index*2+1,dis,k);
}

单点查询
就是从根节点,一直搜索到目标节点,然后一路上都加上就好了。

  void search(int index,int dis)
    {
        ans+=tree[index].num;
        if(tree[index].left==tree[index].right)
            return ;
        if(dis<=tree[index*2].right)
            search(index*2,dis);
        if(dis>=tree[index*2+1].left)
            search(index*2+1,dis);
    }

区间查询
每查到一个区间,有三种选择:
1、如果这个区间被完全包括在目标区间内,那么加上这个区间的和,然后return;
2、如果这个区间的right>目标区间的left,那么查询这个区间;
3、如果这个区间的left<目标区间的right,也查询这个区间;

void search(int index,int l,int r)
{
    if(tree[index].left>=l && tree[index].right<=r)
    {
        ans+=tree[index].num;
        return ;
    }
    if(tree[index*2].right>=l)
        search(index*2,l,r);
    if(tree[index*2+1].left<=r)
        search(index*2+1,l,r);
}

区间修改
和线段树区间查询类似,分为三种
1、如果当前区间完全属于要加的区间,那么这个区间,也就是节点加上,然后return;
2、如果这个区间的right>目标区间的left,那么查询这个区间;
3、如果这个区间的left<目标区间的right,也查询这个区间;

void pls(int index,int l,int r,int k)
{
    if(tree[index].left>=l && tree[index].right<=r)
    {
        tree[index].num+=k;
        return ;
    }
    if(tree[index*2].right>=l)
       pls(index*2,l,r,k);
    if(tree[index*2+1].left<=r)
       pls(index*2+1,l,r,k);
}

模板:
题目描述:已知一个数列,你需要进行下面两种操作:
1.将某一个数加上x
2.求出某区间每一个数的和
输入输出格式
输入格式:
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含3个整数,表示一个操作,具体如下:
操作1: 格式:1 x k 含义:将第x个数加上k
操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和
输出格式:
输出包含若干行整数,即为所有操作2的结果。
题解:
题解:

  #include <iostream>
    #include <algorithm>
    #include <cstdio>
    #include <cstring>
    #include <cmath>
    #include <queue>
    using namespace std;
    int n,m;
    int ans;
    int he=0;
    int input[500010];
    struct node
    {
        int left,right;
        int num;
    }tree[2000010];
    void build(int left,int right,int index)
    {
        he++;
        tree[index].left=left;
        tree[index].right=right;
           if(left==right)
            return ;
        int mid=(right+left)/2;
        build(left,mid,index*2);
        build(mid+1,right,index*2+1);
    }
    int add(int index)
    {
        if(tree[index].left==tree[index].right)
        {
            //cout<<index<<" "<<input[tree[index].right]<<endl;
            tree[index].num=input[tree[index].right];
            return tree[index].num;
        }
        tree[index].num=add(index*2)+add(index*2+1);
        return tree[index].num;
    }
    void my_plus(int index,int dis,int k)
    {
        tree[index].num+=k;
        if(tree[index].left==tree[index].right)
            return ;
        if(dis<=tree[index*2].right)
            my_plus(index*2,dis,k);
        if(dis>=tree[index*2+1].left)
            my_plus(index*2+1,dis,k);
    }
    void search(int index,int l,int r)
    {
        //cout<<index<<" ";
        if(tree[index].left>=l && tree[index].right<=r)
        {
            ans+=tree[index].num;
            return ;
        }
        if(tree[index*2].right>=l)
            search(index*2,l,r);
        if(tree[index*2+1].left<=r)
            search(index*2+1,l,r);
    }
    int main()
    {
        cin>>n>>m;
        for(int i=1;i<=n;i++)
            scanf("%d",&input[i]);
        build(1,n,1);
        add(1);
        for(int i=1;i<=m;i++)
        {
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            if(a==1)
            {
                my_plus(1,b,c);
            }
            if(a==2)
            {
                ans=0;
                search(1,b,c);
                printf("%d\n",ans);
            }
        }
    }

线段树懒标记

转自大佬博客:https://www.cnblogs.com/shanyr/p/5710215.html

例题:
recursion有一个正整数序列a[n]。现在recursion有m次操作:
(A)对于给定的x,y,使所有满足下标i在x,y之间的数a[i]开方下取整。
(B)对于给定的x,y,输出满足下标i在x,y之间的数a[i]的和。
这么简单的题,recursion当然会做啦,但是为了维持她的傲娇属性,她决定考考你。
Input
包含多组数据,文件以EOF结尾。对于每组数据,第一行包含一个正整数n。第二行包含n个正整数,表示a[n]序列。第三行包含一个正整数m。接下来m行,每行包含三个整数i,x,y。i=0表示修改操作,i=1表示询问操作。
Output
对于每组数据,你需要先输出一个"Case #:",然后接下来每行输出一个询问的答案,最后留一个空行。具体见样例。
Hint
n,m<=100000,保证整个序列的和不超过1018
题意:
中文题意就不说了,但是要注意开根号的特点,一般像开根号,求连续数值的gcd这些都是下降非常快的函数,所以可以通过剪枝来优化复杂度,即满足一定条件就不算了
题解:
这个题就是一个普通的线段树加上一个懒操作,即如果当前的区间和正好等于当前的区间长度的话就不再更新这个节点
注意:
这个题要注意查询区间的正确性,l<r
代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
#define ll long long
#define mid (l+r>>1)
#define lc (d<<1)
#define rc (d<<1|1)
const int N = 100004;
ll sum[N<<2];
//int idl[N],idr[N];
//ll mp[N];
//int c;
void build(int l, int r, int d)
{
    if(l==r)
    {
        scanf("%lld",&sum[d]);
        //  printf("%d ",sum[d]);
       // idl[d] = idr[d] = c-1;
        return;
    }
    build(l,mid,lc);
    build(mid+1,r,rc);
    sum[d] = sum[lc]+sum[rc];
   // idl[d] = idl[lc];
   // idr[d] = idr[rc];
    //printf("%d %d %d\n",d,idl[d],idr[d]);
    return;
}

void Update(int L, int R, int l, int r, int d)
{
    if(sum[d] == r-l+1) return;
    if(l==r){
        sum[d] = sqrt((double)sum[d]);
        //mp[l] = sum[d];
        return;
    }
    if(R <= mid) Update(L,R,l,mid,lc);
    else if(L > mid) Update(L,R,mid+1,r,rc);
    else {
        Update(L,mid,l,mid,lc);
        Update(mid+1,R,mid+1,r,rc);
    }
    sum[d] = sum[lc]+sum[rc];
    return;
    /*
    if(L <= l && R >= r)
    {
        if(L==R)
        {
            sum[d] = pow((double)mp[idl[d]],0.5);
            return;
        }
        for(int i = idl[d]; i <= idr[d]; i++)
        {
            sum[d] = sum[d]+pow((double)mp[i],0.5)-mp[i];
        }
        Update(L,R,l,mid,lc);
        Update(L,R,mid+1,r,rc);
    }
    if(L <= mid) Update(L,R,l,mid,lc);
    if(R > mid) Update(L,R,mid+1,r,rc);
    return;
    */
}

ll query(int L, int R, int l, int r, int d)
{
    if(L==l&&R==r)
    {
        return sum[d];
    }
    else if(R<=mid) return query(L,R,l,mid,lc);
    else if(L>mid) return query(L,R,mid+1,r,rc);
    ll t1 = query(L,mid,l,mid,lc);
    ll t2 = query(mid+1,R,mid+1,r,rc);
    return t1+t2;
}
int main()
{
    int n,m,cnt;
    cnt = 0;
    int id, x, y;
    while(~scanf("%d",&n))
    {
        cnt++;
        //memset(sum,0,sizeof(sum));
        //for(int i = 1; i <= n; i++)
       // {
          //  scanf("%lld",&mp[i]);
       // }
        //c = 1;
        build(1,n,1);
        scanf("%d",&m);
        printf("Case #%d:\n",cnt);
        //for(int i = 1; i <= m; i++)
        while(m--)
        {
            scanf("%d%d%d",&id,&x,&y);
            if(x>y) swap(x,y);//这句话很重要。。。不加就超时了
            if(id==0)
            {
                Update(x,y,1,n,1);
            }
            else if(id==1)
            {
                ll ans = query(x,y,1,n,1);
                printf("%lld\n",ans);
            }
        }
    }
    return 0;
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值