[Luogu 3332] K大数查询

题目链接

Luogu 3332

前言

这道题真的是好题,写了好多遍,本篇博客讲解两种方法,整体二分和线段树套线段树的做法。

整体二分

如果只有一个询问,那么我们对于这个询问二分一个答案mid,我们判断大于mid的数是否有询问的k个,遍历每一个修改,如果加入的这个数是大于mid的,说明会对这个k有影响,那么我们就用数据结构维护一个区间加,然后把它扔到q2队列里。
如果加入的这个数小于mid,说明对目前二分的答案没有影响,就扔到q1队列里。最后数据结构区间查询看一下我询问的这个区间里有多少个数比我大,如果大于等于k,说明我mid还要增大,这时候所有q1队列里的修改都没有意义了,我们直接到q2里去做就行了,反之如果区间查询出来小于k,那么我就把mid减小,然后到q1队列里做,记得把数据结构初始化一下。
那么如果多个询问呢,我们就多个询问一起二分答案,这就是整体二分。
对于每一个询问,如果大于mid的数ans大于等于k,我们到分治到左边,然后把q[i].k-=ans,因为我们之后就不会让所有大于mid的修改影响这个询问了。否则就分治到右边做下去。

给同学讲了一遍,发现他理解不了,我觉得还可以再总结总结。

每一次divide,修改操作A分为两种,1.修改数大于mid 2.修改数小于mid
询问操作B也分为两种,1.mid还需要变大的 2.mid还需要变小的
我们发现A.1对于B.2的贡献我们再这次divide当中已经计算完毕了,只要B.2减掉就行了
同样的,A.2对于B.1是没有贡献的,所以我们只要把A.1和B.1配对,A.2和B.2配对,然后分别递归下去就好了

整体二分的精髓在于每一次没有贡献的操作,或者贡献已知的操作,直接不要就行了,这样就保证了复杂度的正确性。

可能这样口述有点苍白,我们结合代码进行理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include<cstdio>
#include<algorithm>
#include<cctype>
#include<cstring>
#include<iostream>
#include<cmath>
#define LL long long
#define int long long
#define INF (2139062143)
#define N (50001)
using namespace std;
int n,m,cnt;
int T1[N],T2[N],ans[N];
inline void add(int x,int data){
int add1=data,add2=data*x;
for (int i=x;i<=n;i+=i&-i){
T1[i]+=add1,T2[i]+=add2;
}
}
inline int query(int x){
int sum1=0,sum2=0;
for (int i=x;i;i-=i&-i){
sum1+=T1[i],sum2+=T2[i];
}
return (x+1)*sum1-sum2;
}
struct node{
int op,l,r,data,num;
}a[N],q1[N],q2[N];
template <typename T> void read(T&t) {
t=0;
bool fl=true;
char p=getchar();
while (!isdigit(p)) {
if (p=='-') fl=false;
p=getchar();
}
do {
(t*=10)+=p-48;p=getchar();
}while (isdigit(p));
if (!fl) t=-t;
}
void divide(int head,int tail,int l,int r){
//整体二分,表示我现在的操作队列为head..tail,包括修改和查询,我二分的左右端点是l,r
if (head>tail) return;//如果操作队列是空的就return
if (l==r){//如果已经找到答案就把答案记录下来
for (int i=head;i<=tail;i++){
if (a[i].op==2){
ans[a[i].num]=l;
}
}
return;
}
int len1=0,len2=0,mid=l+r>>1;
for (int i=head;i<=tail;i++){//顺序就行了,自动按时间线排序,在我后面的修改是不会对我产生影响的
if (a[i].op==1){
if (a[i].data<=mid) q1[++len1]=a[i];//这个表示加入左队列
else{
q2[++len2]=a[i];
add(a[i].l,1),add(a[i].r+1,-1);//如果这个修改对当前mid有影响,区间加
}
}
else{
int tmp=query(a[i].r)-query(a[i].l-1);
//区间查询,比我大的数有tmp个,如果这个tmp<我的data,说明我二分的答案还太大,我就压入左队列,然后我要把现在这些数的贡献减掉,因为我分治下去的时候右队列里的数的贡献就不会在考虑了,应该是整体二分的精髓所在了。
if (tmp<a[i].data){
a[i].data-=tmp;
q1[++len1]=a[i];
}
else q2[++len2]=a[i];
}
}
for (int i=head;i<=tail;i++){
if (a[i].op==1&&a[i].data>mid) add(a[i].l,-1),add(a[i].r+1,1);//直接memset清空就爆了
}
for (int i=1;i<=len1;i++) a[head+i-1]=q1[i];
for (int i=1;i<=len2;i++) a[head+len1+i-1]=q2[i];
divide(head,head+len1-1,l,mid);
divide(head+len1,tail,mid+1,r);
//分别递归左边和右边
}
signed main(){
read(n),read(m);
for (int i=1;i<=m;i++){
read(a[i].op);
read(a[i].l),read(a[i].r),read(a[i].data);
if (a[i].op==2){
a[i].num=++cnt;
}
}
divide(1,m,-50000,50000);
for (int i=1;i<=cnt;i++) printf("%lld\n",ans[i]);
return 0;
}

复杂度分析一波。
我们最多递归log层,每个修改每次只可能分在左队列或右队列,所以只会树状数组log次,所以复杂度为$log^2$
同理,询问操作也是一样,所以中复杂度是$m*log(值域)*log(n)$第一个log为二分,第二个log为树状数组。

如果有什么没有看懂的,希望大家在评论中指出,博主尽量解答并改进,感激不尽。

树套树

我使用的是权值线段树套线段树,如果外层区间为$[l,r]$,内层为$[a,b]$,则表示在$[a,b]$区间内,权值在$[l,r]$中的数有多少个。
那么区间$[x,y]$插入k的话我们直接在每个包括k的权值区间中,内层线段树$[x,y]$区间加一下。
查询$[a,b]$第k大的话,看一下右儿子$[a,b]$区间中的数是不是大于k个,如果是,则到右儿子查询,不然k-=ans,然后到做儿子中查询就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include<cstdio>
#include<algorithm>
#include<cctype>
#include<cstring>
#define LL long long
#define N (100001)
using namespace std;
int opt,l,r,x,y,K,n,m,cnt,L,R;
int lson[N<<7],rson[N<<7],addv[N<<7],root[N<<2];
LL sum[N<<7];
template <typename T> void read(T&t) {
t=0;
bool fl=true;
char p=getchar();
while (!isdigit(p)) {
if (p=='-') fl=false;
p=getchar();
}
do {
(t*=10)+=p-48;p=getchar();
}while (isdigit(p));
if (!fl) t=-t;
}
void add(int &u,int l,int r){
if (!u) u=++cnt;
if (L<=l&&r<=R){
sum[u]+=(r-l+1);
addv[u]++;
return;
}
int mid=l+r>>1;
if (L<=mid) add(lson[u],l,mid);
if (R>mid) add(rson[u],mid+1,r);
sum[u]=sum[lson[u]]+sum[rson[u]]+addv[u]*(r-l+1);
}
void insert(int u,int l,int r){
add(root[u],1,n);
if (l==r) return;
int mid=l+r>>1,v=u<<1;
if (K<=mid) insert(v,l,mid);
else insert(v|1,mid+1,r);
}
LL ask(int u,int l,int r){
if (L<=l&&r<=R){
return sum[u];
}
int mid=l+r>>1;
LL ret=0;
if (L<=mid) ret+=ask(lson[u],l,mid);
if (R>mid) ret+=ask(rson[u],mid+1,r);
ret+=(min(R,r)-max(l,L)+1)*addv[u];
return ret;
}
int query(int u,int l,int r){
if (l==r) return l;
int mid=l+r>>1,v=u<<1;
LL rsize=ask(root[v|1],1,n);
if (K<=rsize) return query(v|1,mid+1,r);
else{
K-=rsize;
return query(v,l,mid);
}
}
int main(){
read(n),read(m);
for (int i=1;i<=m;i++){
read(opt),read(L),read(R),read(K);
if (opt==1){
insert(1,-50000,50000);
}
else{
printf("%d\n",query(1,-50000,50000));
}
}
return 0;
}

这道题的树套树还是蛮好码的。