USST内部训练4补题

一道神奇的分块题

F. 牛牛的繁星

题意:

给出长度为n的序列a,和m个询问,每次询问区间[l,r]内出现次数第k大的数的出现次数(注意:是出现次数第k大,不是第k大元素的出现次数),且强制在线。

题解:

可参考官方题解:https://ac.nowcoder.com/acm/problem/blogs/204649

如果是第k大元素的出现次数,那就是主席树的裸题了,开n个权值线段树,第i个维护前i个数信息,然后用前缀和思想处理在线区间询问就行了

但是,本题是无法使用线段树维护住的,因此就需要看别人的AC代码使用 暴力+优化 方法过这道题,具体实现应该是 预处理+分块+莫队的一点点思想(其实我自己还没整明白),但我所了解的莫队基本上都是离线做法。。所以这题还需要提前预处理,也就是需要提前预处理出pre[i][j]数组,代表前i个块中j这个元素的出现次数,这是为了方便分块时对于中间整块的处理

更具体的,本题需要开一个桶统计每个数的出现次数,同时还需要统计某个出现次数的数的个数,通过莫队思想更新,但是仅仅这样似乎复杂度还是不够(因为强制在线,所以无法用莫队的玄学排序),因此还需要预处理一些分块数组的信息,这样对于区间很大的询问的处理速度就加快了(应该吧)

对于每一次[l,r]询问,类似普通分块一样 先定位两端点所在块,如果在同一块中,就暴力更新该区间的数的所有信息,更新完后,再查找k大出现次数,这里需要仔细解释一下如何查找:

本题的分块数组有两个作用,除了记录每个元素所在的块之外,还可以用它来记录不同的出现次数所在的块(类似于权值线段树?或者叫做值域分块),出现次数越多,所在的块就越靠后,那么对于每一次的询问,我们就需要从最后一个块开始往第一块遍历(因为越靠后的块对应的出现次数越多),如果当前块所对应的的出现次数小于k,那么就让k减去当前块的出现次数数,然后继续查找前一块,否则就说明这个k大的出现次数在这个块里面,于是进入该块,暴力查找即可

对于不在同一块中的两端点,左右端点对应的零散块也是同上暴力处理,对于中间的完整快,需要利用之前的预处理,$O(1)$得到完整块内的数的素有信息,然后更新即可

在处理完上述所有信息并完成一次查询后,记得要把这些用到的桶和数组归零,以便下一次查询

另外,代码里有个注意点

  • 用emplace_back代替了push_back,否则会TLE(91分)??具体可以百度这两者的区别

个人感觉本题题的思想和 洛谷 P1903 [国家集训队]数颜色 / 维护队列 这道题有一点类似(不会的可以先看看这题找找自信)

最后,本蒟蒻写得很乱,如果看不懂的话 就看看代码理解理解吧!QAQ

更具体的可以看代码

AC代码:

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;

// fread 加速读入
inline char _nc(){
static char buf[100000],*L=buf,*R=buf;
return L==R&&(R=(L=buf)+fread(buf,1,100000,stdin),L==R)?EOF:*L++;
}
template<class T> void read(T &x) {
x=0;int f=0;char ch=_nc();
while(ch<'0'||ch>'9'){f|=(ch=='-');ch=_nc();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=_nc();}
x=f?-x:x;
return;
}

//fast write
inline void write(ll x){
if(x<0)putchar('-'),x=-x;
if(x>9)write(x/10);
putchar(x%10+'0');
}

int n,m,type,mx,sz;
int block[100005],L[100005],R[100005],a[100005],cnt[100005],num[100005],sum[405],pre[405][100005];
bool vis[100005];

vector<pair<int,int>> v[340][340];
vector<int> ve;

void add(int x,int number){//更新出现次数为x的数所在的块
int bo=block[x];
num[x]+=number;
sum[bo]+=number;
}

int query(int k){//查询次数第k大的次数
for (int i=mx;i>=1;i--){
if(k>sum[i]) k-=sum[i];
else{
for (int j=R[i];j>=L[i];j--){
if(k>num[j]) k-=num[j];
else return j;
}
}
}
return 0;
}

int cal(int l,int r,int k){
if(l>r) swap(l, r);
int ans=0;
int bol=block[l],bor=block[r];
if(bol==bor){//处理左右端点在同一块内的情况
for (int i=l;i<=r;i++){
cnt[a[i]]++;
vis[a[i]]=true;
}
for (int i=l;i<=r;i++){
if(vis[a[i]]){
vis[a[i]]=false;
add(cnt[a[i]],1);
}
}
ans=query(k);//询问
//询问完了给它重置回去
for (int i=l;i<=r;i++){
if(!cnt[a[i]]) continue;
add(cnt[a[i]],-1);
cnt[a[i]]=0;
}
}else{//处理左右端点不在同一块内的情况
for (auto x:v[bol+1][bor-1]) add(x.first,x.second);
for (int i=l;i<=R[bol];i++){
cnt[a[i]]++;
vis[a[i]]=true;
}
for (int i=L[bor];i<=r;i++){
cnt[a[i]]++;
vis[a[i]]=true;
}
for (int i=l;i<=R[bol];i++){
if(vis[a[i]]){
int res=pre[bor-1][a[i]]-pre[bol][a[i]];
add(res,-1);
add(res+cnt[a[i]],1);
vis[a[i]]=false;
}
}
for (int i=L[bor];i<=r;i++){
if(vis[a[i]]){
int res=pre[bor-1][a[i]]-pre[bol][a[i]];
add(res,-1);
add(res+cnt[a[i]],1);
vis[a[i]]=false;
}
}
ans=query(k);//询问
//询问完了给它重置回去
for (auto x:v[bol+1][bor-1]) add(x.first,-x.second);
for (int i=l;i<=R[bol];i++){
if(cnt[a[i]]){
int res=pre[bor-1][a[i]]-pre[bol][a[i]];
add(res,1);
add(res+cnt[a[i]],-1);
cnt[a[i]]=0;
}
}
for (int i=L[bor];i<=r;i++){
if(cnt[a[i]]){
int res=pre[bor-1][a[i]]-pre[bol][a[i]];
add(res,1);
add(res+cnt[a[i]],-1);
cnt[a[i]]=0;
}
}
}
return ans;
}

int main()
{
// freopen("test.in","r",stdin);
read(n),read(m),read(type);//读入
mx=sqrt(n);//mx个块
for(int i=1;i<=mx;i++){
L[i]=R[i-1]+1;
R[i]=n/mx*i;
}
R[mx]=n;//最后一个块的右端为第n,自己的习惯,也可以再开一个块
for(int i=1;i<=mx;i++){
for(int j=L[i];j<=R[i];j++)
block[j]=i;//j号元素在i号块内
}
//上面是分块预处理
for (int i=1;i<=n;i++) read(a[i]);//读入
//下面这波是预处理前i个块中j这个元素的出现次数,即预处理pre[i][j]数组
for (int l=1;l<=mx;l++){
ve.clear();
for (int i=1;i<=n;i++) pre[l][i]=pre[l-1][i];
for (int i=L[l]; i<=R[l];i++) pre[l][a[i]]++;
for (int r=l; r<=mx;r++){
for (int i=L[r];i<=R[r];i++){
num[cnt[a[i]]]--;
cnt[a[i]]++;
num[cnt[a[i]]]++;
if(num[cnt[a[i]]]==1) ve.emplace_back(cnt[a[i]]);
//emplace_back的使用
}
for (auto x:ve) if(num[x]!=0&&!vis[x]) v[l][r].emplace_back(make_pair(x,num[x])),vis[x]=true;
for (auto x:ve) vis[x]=false;
}
memset(cnt,0,sizeof(cnt));
memset(num,0,sizeof(num));
}

int ans=0;
for (int i=1,l,r,k;i<=m;i++){
read(l),read(r),read(k);
if(type==1) l^=ans,r^=ans,k^=ans;
ans=cal(l,r,k);
write(ans);
puts("");
}

return 0;
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!