题目
假定有一个无限长的数轴,数轴上每个坐标上的数都是0。
现在,我们首先进行 n 次操作,每次操作将某一位置x上的数加c。
接下来,进行 m 次询问,每个询问包含两个整数l和r,你需要求出在区间[l, r]之间的所有数的和。
输入格式
第一行包含两个整数n和m。
接下来 n 行,每行包含两个整数x和c。
再接下里 m 行,每行包含两个整数l和r。
输出格式
共m行,每行输出一个询问中所求的区间内数字和。
数据范围
−109≤x≤109,
1≤n,m≤105,
−109≤l≤r≤109,
−10000≤c≤10000
输入样例:
3 3
1 2
3 6
7 5
1 3
4 6
7 8
输出样例:
8
0
5
题目分析
本题是目前来说比较综合的题目.
- 因为整个数轴并没有填满,更多的是空的值,所以使用原数轴做角标会很浪费,因此需要将原数轴上的数做一个映射,到一个新的数组中,从而将降低资源浪费节约时间.
- 这里使用的容器是ArrayList,可以使用集合工具类对容器进行排序,但是很有可能容器里有重复的数,去重操作使用的是双指针思想.
- 在建立映射时,原数轴上的数按顺序排布对应新数组上的数,给出任何一个原数轴上的数应该立马得到他在新数组中的位置.这个查找方式使用的是二分查找.
- 在将数组完成映射后,原数组上的部分和就转化成了新数组的部分和,在求部分和时使用前缀和数组,这样在求和时可以实现O(1)的时间复杂度.
算法分析
我们关心的,只是他们在原数轴上的大小顺序,然后将排序之后的结果作为新的index,而不去动他所对应的值.
在找到了新坐标后,为了方便我们对新数组赋值,使用了自建类Pairs,类中只定义了另两个成员变量first,second,方便我们操作成对出现的值(如写入的x和n;查询的l和r)
源代码
import java.util.*;
class Main{
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int m=sc.nextInt();
int N=300010;
int[] a=new int[N];//映射后的数组
int[] s=new int[N];//前缀和数组
List<Integer> alls=new ArrayList<>();
List<Pairs> adds=new ArrayList<>();
List<Pairs> querys=new ArrayList<>();
while(n-->0){
int x=sc.nextInt();
int c=sc.nextInt();
adds.add(new Pairs(x,c));
alls.add(x);
}
while(m-->0){
int l=sc.nextInt();
int r=sc.nextInt();
alls.add(l);
alls.add(r);
querys.add(new Pairs(l,r));
}
Collections.sort(alls);
int unique=unique(alls);
alls=alls.subList(0,unique);//现在数组中全部是不重复的元素
for(Pairs ele:adds){ //将映射数组填充
int index=seek(alls,ele.first);
a[index]+=ele.second;
}
for(int i=1;i<=alls.size();i++){ //前i个数的缀和数组(i从1开始)
s[i]=s[i-1]+a[i];
}
for(Pairs ele:querys){
int l=seek(alls,ele.first);
int r=seek(alls,ele.second);
int res=s[r]-s[l-1];
System.out.println(res);
}
}
static int unique(List<Integer> al){//将已排序数组里的重复元素去掉 整体是相当于两个指针一个挑选元素,一个确定位置
int j=0; //j表示要插入元素的位置
for(int i=0;i<al.size();i++){ //i表示当前选择的元素
if(i==0||al.get(i)!=al.get(i-1)){
al.set(j,al.get(i));
j++;
}
}
return j;//返回不重复的最后一个索引
}
static int seek(List<Integer> list,int x){
int l=0;
int r=list.size()-1;
while(l<r){
int mid=l+r>>1;
if(list.get(mid)>=x){
r=mid;
}else{ l=mid+1;}
}
return l+1;//将数组的每一个角标都加1,这样数组就是从角标为1开始的
}
}
class Pairs{
int first;
int second;
public Pairs (int first,int second){
this.first=first;
this.second=second;
}
}
细节说明
Q:为什么a[]/s[]
数组的容量是300000?
A:首先确认a数组存储的是映射后的数组,s数组存储的是a的前缀和,因此s的容量一定和a相同.那么a为什么是呢? 思考都有哪些需要映射: 数轴上的点一定需要映射,因为数轴上加了n个点,所以要映射n次;同时,查询的l,r也一定要映射,否则,我们无法对数组进行查询(只不过映射的值为0而已).查询m次,因此要映射2m个点,总共映射n+2m,最多为300000.
Q:为什么查询函数返回值为j+1而不是j?
A:因为查询到的其实本质上是从0开始的角标,而在a[]
中,第一个数的角标其实是1,因此直接返回的值+1就可以对应他在a[]
中的位置了.