Dyd's Blog

He who has a strong enough why can bear almost any how.

大力分块

lxl是yyds

大力分块

我估计会长的一比

背景

众所周知,lxl喜欢数据结构

众所周知,lxl很毒瘤

众所周知,lxl有点中二+宅

于是lxl出了Yn(由乃)oi的大分块题目,把以上三者结合了起来

然后,就让我们来大力分块吧!

最初分块

「望月悲叹的最初分块」

未来日记

维护一个序列,支持区间修改(把所有的 $x$ 变成 $y$ )和区间第 $k$ 小, $n, m, a \le 10^5$

做题

当然考虑分块

先设块长为 $B = \sqrt{n}$ ,考虑询问,先找到询问的区间,这个区间必定是由整块+散点组成,而这步寻找可以 $O(1)$ 找到

注意到 $a \le 10^5$ ,值域与 $n$ 同级,考虑对值域也分块(为区分称其为值块),设块长为 $V = \sqrt{n}$ ,记 cnt[i][j] 表示第 $i$ 块内的数值在第 $j$ 值块内的有几个

但是这个区间最多有 $\sqrt{n}$ 个块,又最多要枚举 $\sqrt{n}$ 个值块,这显然不可接受,由于对于单次询问,包括的块不变,考虑对块进行前缀和,设 cnt[i][j] 表示前 $i$ 块内的数值在第 $j$ 值块内的有几个,再开一个临时数组 tcnt[j] 表示散点中数值在第 $j$ 值块内的有几个,枚举可得答案所在值块,我们发现同理可得答案,具体的,设 rel[i][j] 表示前 $i$ 块内的数值是 $j$ 的有几个, trel[j] 同理,枚举可得答案,注意到枚举 cnt[i][j] 的时间是 $O(\sqrt{n})$ 的(只用枚举 $j$ ),枚举 rel[i][j] 也是 $O(\sqrt{n})$ 的,于是,我们用单次 $O(\sqrt{n})$ 的时间(目前看来), $O(n \sqrt{n})$ 的空间( rel 数组)解决了询问

考虑修改,先看散点,明显暴力就行了,修改的区间也可以 $O(1)$ 找到,对于整块每次修改需要一并修改 cntrel ,并且由于他们是“前缀和”,必须要把后面的每个块都改了,时间是 $O(\sqrt{n})$ (注意这里不要每个块都去修改后面,那样是 $O(n)$ 的,应该记一个修改量的前缀和),就……行了?

行个鬼啊!区间在修改完后一定要打上懒标记,不然下一次单点修改时查到的就是错误的值啊!

发现区间修改有一个优美的性质:值相等的点无论如何一定相等,这启发我们打并查集(对每一块开一个),把相等的点直接并到一起,这样,区间的标记可以 $O(\log \sqrt{n})$ 解决,但是,副作用是:单点修改的时候怎么办?不能直接更改这个点的 $fa$ ,因为它下面可能还有点,直接重构其实是可以的办法(因为每次散点只在两个块,最多 $2\sqrt{n}$ 个点),但我们还有别的办法

考虑建一些辅助点,设点 nd[i][j] 代表第 $i$ 块值为 $j$ ,对于一个块里的点,把他们全都并到 $nd$ 上去(换句话说,只有 $nd$ 有儿子),这样,散点就直接改就行了,而整块可以让 nd[i][x] 的 $fa$ 改为 nd[i][y] ,并让 nd[i][x] = 0 (这代表这个区间没有值为 $x$ 的点),特殊情况是 nd[i][y] == 0 ,这个时候就新建一个节点(浪费空间ing

考虑空间, $nd$ 数组大小为 $O(n \sqrt{n})$ ,因为最初有 $n$ 个点(它们不做父亲),每一块最多有 $\sqrt{n}$ 个值,每次操作最多加 $\sqrt{n}$ 个辅助点(每块一个),并查集的节点数最多是 $n + \sqrt{n} * \sqrt{n} + m * \sqrt{n}$ 可以看作 $O(n \sqrt{n})$ 的空间;而对于时间,并查集的修改都是 $O(1)$ 的,单点的查询是 $O(\log \sqrt{n})$ (这个 $\log \sqrt{n}$ 非常解决于 $1$ ,因为并查集的深度只有辅助点个数,加上有路径压缩),散点是 $O(\sqrt{n})$ 个,块是 $O(\sqrt{n})$ 个,而修改那两个数组要用 $O(\sqrt{n})$ ,单次修改的总时间为 $O(\sqrt{n} \log \sqrt{n})$ ,而由于单点的查询是 $O(\log \sqrt{n})$ ,上面的单次询问也变成了 $O(\sqrt{n} \log \sqrt{n})$

额……先打出来

(受苦了一下午以后……)

好,TLE $0pts$ ,wtm**lxl!!!,我只能望月悲叹

吃了个饭,回来尝试卡块长,发现 $B = 500, V = 317$ 是既不会TLE,也不会MLE,只是WA了,貌似有戏……?

开始调试受苦(这记着是为了防止我忘了,可以跳过不看) :

  • $2022/1/24$ $18:00 \sim 20:50$ 自以为操作 $2$ 基本调试完毕

  • $2022/1/24$ $20:55$ 自以为调完然后交(谁给我的自信),WA $10pts$ ,WA的点都是输出了负数,不知为何(疑似炸 int ?)

  • $2022/1/24$ $21:00$ 发现负数是我调试的时候把范围改了没改回去,修正后手造大样例,结果WA了(幸好没交)

  • $2022/1/24$ $21:20$ 发现我的make date不够严谨,而且在大数据下操作 $2$ 也挂了

  • $2022/1/24$ $21:30$ 发现操作 $2$ 挂在没特判同块

  • $2022/1/24$ $21:51$ 同机房wfy巨佬帮我调出了一个很难的RE,这次应该是真的吧操作 $2$ 调好了

  • $2022/1/24$ $22:00$ $73pts$ 该睡觉了

  • 话说我昨晚睡觉时想到,并查集上加一个按秩合并,可以优化到单次 $O(\sqrt{n} \alpha(n))$

  • $2022/1/25$ $8:00$ 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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
#include <iostream>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#define STC static
#define IL inline
//求块号
#define get(x, len) (((x) - 1) / (len) + 1)
//块左端点
#define LB(x, len) (((x) - 1) * (len) + 1)
//块右端点
#define RB(x, len) ((x) * (len))
using namespace std;
namespace Fast //长的和shit一样的快读
{
const int L = 10000 + 5;
char buf[L], out[L], *iS, *iT;
int l = 0;
#define gh() (iT == iS ? iT = (iS = buf) + fread(buf, 1, L, stdin), (iT == iS ? EOF : *iS++) : *iS++)
template<class T>
IL void read(T &x)
{
x = 0;
char ch = gh(), t = 0;
while (ch < '0' || ch > '9')
t |= ch == '-', ch = gh();
while (ch >= '0' && ch <= '9')
x = x * 10 + (ch ^ 48), ch = gh();
if (t)
x = -x;
}
IL void flus()
{
fwrite(out, 1, l, stdout);
l = 0;
}
IL void putc(char x)
{
out[l++] = x;
if (l == L - 5)
flus();
}
template<class T>
IL void write(T x)
{
if (x < 0)
putc('-'), x = -x;
if (x > 9)
write(x / 10);
out[l++] = x % 10 + 48;
if (l == L - 5)
flus();
}
}
using Fast::flus;
using Fast::putc;
using Fast::read;
using Fast::write;
//B:点块长,N:注意要多开一个B的大小,V:值块长,NB:开空间用
const int B = 500, N = 1e5 + B + 1, V = 317, NB = B + 5;
//num:块数,wi:最大值
int num, wi;
//如题
int n, m;
struct Block
{
//cnt:块值前缀和,rel:真实值前缀和
int cnt[NB], rel[N];
//b[i].nd[j]:第i块值为j的并查集根
int nd[N];
} b[NB];
//并查集:父亲,大小,值(仅当为根时有意义)
int fa[N * NB + N * 2], si[N * NB + N * 2], val[N * NB + N * 2], tot = 0;
IL int find(int x) //路径压缩
{
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
IL void newnd(int &x, int v) //新建并查集根节点
{
x = ++tot, fa[x] = x, si[x] = 0, val[x] = v;
}
IL void merge(int &x, int &y) //按秩合并,把x并给y
{
if (si[x] > si[y])
{
val[x] = val[y];
swap(x, y);
}
fa[x] = y, si[y] += si[x], x = 0;
}
IL int va(int x) //返回点x的真实值
{
return val[find(x)];
}
IL int ask(int l, int r, int k)
{
//栈用于还原tcnt和trel
STC int tcnt[NB], trel[N], stk[NB << 1], top;
STC int t, tt, x;
//特判同块
if (get(l, B) == get(r, B))
{
for (; l <= r; ++l)
{
x = va(l);
++tcnt[get(x, V)], ++trel[x];
stk[++top] = x;
}
for (x = 0, t = 1, tt = get(wi, V); t <= tt; ++t)
if ((x += tcnt[t]) >= k)
{
for (tt = RB(t, V); ; --tt)
if ((x -= trel[tt]) < k)
{
t = tt;
break;
}
break;
}
}
else
{
//处理散点
top = 0;
if (get(l - 1, B) == (t = get(l, B))) //这里虽然第一块多了个点0但不影响答案
for (; get(l, B) == t; ++l)
{
x = va(l);
++tcnt[get(x, V)], ++trel[x];
stk[++top] = x;
}
if (get(r + 1, B) == (t = get(r, B)))
for (; get(r, B) == t; --r)
{
x = va(r);
++tcnt[get(x, V)], ++trel[x];
stk[++top] = x;
}
//找到答案(这里假设答案一定存在)
for (x = 0, t = 1, l = get(l, B), r = get(r, B), tt = get(wi, V); t <= tt; ++t)
if ((x += tcnt[t] + b[r].cnt[t] - b[l - 1].cnt[t]) >= k)
{
for (tt = RB(t, V); ; --tt)
if ((x -= trel[tt] + b[r].rel[tt] - b[l - 1].rel[tt]) < k)
{
t = tt;
break;
}
break;
}
}
//还原
while (top)
{
x = stk[top--];
--tcnt[get(x, V)], --trel[x];
}
return t;
}
IL void modify(int l, int r, int x, int y)
{
if (x == y)
return ;
wi = max(wi, y);
STC int t, tt, xx, yy, ll;
//记录l用于还原
ll = l;
xx = get(x, V), yy = get(y, V);
//还原前缀和
for (t = num, tt = get(ll, B); t >= tt; --t)
{
b[t].cnt[xx] -= b[t - 1].cnt[xx];
b[t].rel[x] -= b[t - 1].rel[x];
b[t].cnt[yy] -= b[t - 1].cnt[yy];
b[t].rel[y] -= b[t - 1].rel[y];
}
//特判同块
if (get(l, B) == (t = get(r, B)))
{
if (b[t].rel[x] > 0)
{
if (!b[t].nd[y])
newnd(b[t].nd[y], y);
for (; l <= r; ++l)
if ((tt = va(l)) == x)
{
fa[l] = b[t].nd[y];
//注意同时跟新大小
++si[b[t].nd[y]];
--si[b[t].nd[x]];
--b[t].cnt[xx], --b[t].rel[x];
++b[t].cnt[yy], ++b[t].rel[y];
}
}
}
else
{
//处理散点
if (get(l - 1, B) == (t = get(l, B)))
{
if (b[t].rel[x] <= 0)
l = LB(t + 1, B);
else
{
if (!b[t].nd[y])
newnd(b[t].nd[y], y);
for (; get(l, B) == t; ++l)
if ((tt = va(l)) == x)
{
fa[l] = b[t].nd[y];
++si[b[t].nd[y]];
--si[b[t].nd[x]];
--b[t].cnt[xx], --b[t].rel[x];
++b[t].cnt[yy], ++b[t].rel[y];
}
}
}
if (get(r + 1, B) == (t = get(r, B)))
{
if (b[t].rel[x] <= 0)
r = RB(t - 1, B);
else
{
if (!b[t].nd[y])
newnd(b[t].nd[y], y);
for (; get(r, B) == t; --r)
if ((tt = va(r)) == x)
{
fa[r] = b[t].nd[y];
++si[b[t].nd[y]];
--si[b[t].nd[x]];
--b[t].cnt[xx], --b[t].rel[x];
++b[t].cnt[yy], ++b[t].rel[y];
}
}
}
//处理整块
for (l = get(l, B), r = get(r, B); l <= r; ++l)
if (b[l].rel[x] > 0)
{
if (!b[l].nd[y])
newnd(b[l].nd[y], y);
merge(b[l].nd[x], b[l].nd[y]);
b[l].cnt[yy] += b[l].rel[x];
b[l].rel[y] += b[l].rel[x];
b[l].cnt[xx] -= b[l].rel[x];
b[l].rel[x] -= b[l].rel[x];
}
}
//再求前缀和
for (t = get(ll, B); t <= num; ++t)
{
b[t].cnt[xx] += b[t - 1].cnt[xx];
b[t].rel[x] += b[t - 1].rel[x];
b[t].cnt[yy] += b[t - 1].cnt[yy];
b[t].rel[y] += b[t - 1].rel[y];
}
}
int main()
{
read(n), read(m);
num = get(n, B), wi = 0;
tot = n;
for (int i = 1, a; i <= n; ++i)
{
read(a);
wi = max(wi, a);
auto &t = b[get(i, B)];
if (!t.nd[a])
newnd(t.nd[a], a);
fa[i] = t.nd[a];
++si[t.nd[a]];
++t.cnt[get(a, V)], ++t.rel[a];
}
for (int j = 1, t = get(wi, V); j <= t; ++j)
for (int i = 1; i <= num; ++i)
b[i].cnt[j] += b[i - 1].cnt[j];
for (int j = 1; j <= wi; ++j)
for (int i = 1; i <= num; ++i)
b[i].rel[j] += b[i - 1].rel[j];
int op, l, r, x, y;
while (m--)
{
read(op), read(l), read(r), read(x);
if (op == 1)
read(y), modify(l, r, x, y);
else
write(ask(l, r, x)), putc('\n');
}
flus();
return 0;
}

其它

我也喜欢我妻由乃!

第二分块

「突刺贯穿的第二分块」

好,我们乘胜追击!

五彩斑斓的世界

维护一个序列,支持区间修改(把所有大于 $x$ 的数减去 $x$ )和查询区间中 $x$ 的出现次数, $n \le 10^5, m \le 5 \times 10^5, a \le 10^5 + 1$ ,时限 $7.5s$ ,空间 $64MB$

做题

不难发现,如果不是那sm的空间,可以用最初分块解决,但这空间, $O(n \sqrt{n})$ 都开不出来

那看看着题和最初分块的不同呢?发现询问是出现次数,而不是排名,这个显然可以直接记录,就不必要前缀和了,注意,这意味着分块后每个块之间的信息毫无关联,并且答案就是区间包含的所有答案的累加于是有一个控制空间的方法:

把操作离线下来,对每个块都按顺序处理一遍所有的操作,然后把询问的答案累加即可,空间是 $O(n + m)$ 的了(也许开 short 也行,没试过)

那么该如何修改呢?注意到由于我们离线了,所以要枚举 $\sqrt{n}$ 块,每一块又要枚举 $m$ 个操作,所以对于每个操作我们要做到 $O(1)$ ,或者所有操作加起来共用 $O(n)$

这……直接瞟一眼出题人的题解,发现可以这样懒标记+并查集维护,具体的,设当前块最大值为 $mx$ :

  • 若 $mx \le 2x$ ,就直接枚举大于 $x$ 的数,让它们减去 $x$
  • 若 $mx > 2x$ ,就直接枚举小于 $x$ 的数,让它们加上 $x$ ,然后区间打上减 $x$ 的标记

设后面的“枚举……”时间复杂度为 $O(t)$ ,发现 $mx$ 的真实值(算上懒标记)是单调下降的,由于 $mx$ 初始最大为 $10^5 + 1$ ,所以所有操作时间加起来是 $O(a) * O(t)$ 的,由于总共只能用 $O(n)$ ,所以必须保证 $O(t) = O(1)$

还是可以开并查集(但这里可不能像最初分块一样开辅助点,空间不够),对每个值开一个并查集,然后记录这个并查集的 $si$ ,整块修改的时候直接合并两个值的并查集,查询的时候直接输出这个并查集的 $si$ ,都是 $O(1)$ 的;如果是散点修改,就直接重构;散点查询,就通过并查集找到范围内每个数的值,暴力统计即可

但对于散点的时间,我们还得从另一个角度分析才能得出结论:假设我们没有离线,空间就开那么大,则 $m$ 次操作最多重构 $2m$ 个块,每个块 $\sqrt{n}$ 个点,同时怼上路径压缩,由于每次会遍历每个叶子,路径压缩的效果极好,散点均摊每个点可以当作 $O(1)$ ,则时间复杂度为 $O(m \sqrt{n})$ ,均摊到每个块和每个操作,就是 $O(1)$ 了(好像严格的 $O(1)$ 应该是用链表代替并查集,重构更快,但不太好打)

代码

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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#include <iostream>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#define STC static
#define IL inline
#define get(x, len) (((x) - 1) / (len) + 1)
#define LB(x, len) (((x) - 1) * (len) + 1)
#define RB(x, len) ((x) * (len))
using namespace std;
namespace Fast
{
const int L = 10000 + 5;
char buf[L], out[L], *iS, *iT;
int l = 0;
#define gh() (iT == iS ? iT = (iS = buf) + fread(buf, 1, L, stdin), (iT == iS ? EOF : *iS++) : *iS++)
template<class T>
IL void read(T &x)
{
x = 0;
char ch = gh(), t = 0;
while (ch < '0' || ch > '9')
t |= ch == '-', ch = gh();
while (ch >= '0' && ch <= '9')
x = x * 10 + (ch ^ 48), ch = gh();
if (t)
x = -x;
}
IL void flus()
{
fwrite(out, 1, l, stdout);
l = 0;
}
IL void putc(char x)
{
out[l++] = x;
if (l == L - 5)
flus();
}
template<class T>
IL void write(T x)
{
if (x < 0)
putc('-'), x = -x;
if (x > 9)
write(x / 10);
out[l++] = x % 10 + 48;
if (l == L - 5)
flus();
}
}
using Fast::flus;
using Fast::putc;
using Fast::read;
using Fast::write;
const int M = 5e5 + 5, A = 1e5 + 5, B = 1000, NB = B + 5, N = 1e6 + B + 5;
struct Question
{
int op, l, r, x, ans;
} ;
struct Block
{
int fa[N], si[N], val[N], rt[A];
int mx, tag, l, r;
IL int find(int x)
{
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
IL int va(int x)
{
return val[find(x)];
}
IL void merge(int x, int y)
{
if (rt[y])
fa[rt[x]] = rt[y];
else
rt[y] = rt[x], val[rt[y]] = y;
si[y] += si[x];
rt[x] = si[x] = 0;
}
IL void build(int x[])
{
mx = 0;
for (int i = l; i <= r; ++i)
{
++si[x[i]];
if (!rt[x[i]])
{
mx = max(mx, x[i]);
rt[x[i]] = i;
fa[i] = i, val[i] = x[i];
}
else
fa[i] = rt[x[i]];
}
}
IL void destroy(int x[])
{
for (int i = l; i <= r; ++i)
{
//一定在摧毁前找到真实值
x[i] = va(i);
rt[x[i]] = si[x[i]] = 0;
x[i] -= tag;
}
for (int i = l; i <= r; ++i)
fa[i] = 0;
tag = 0;
}
IL void modify(int d)
{
if (mx - tag < (d << 1))
{
for (int i = mx; i > d + tag; --i)
if (rt[i])
merge(i, i - d);
//这里最大值不一定要保证存在,反正上面判了rt的,但一定要不多不少
mx = min(mx, d + tag);
}
else
{
for (int i = 1 + tag; i <= d + tag; ++i)
if (rt[i])
merge(i, i + d);
tag += d;
}
}
IL void change(int ll ,int rr, int x[], int d)
{
if (ll > rr)
return ;
destroy(x);
for (int i = ll; i <= rr; ++i)
if (x[i] > d)
x[i] -= d;
build(x);
}
IL int query(int d)
{
if (d + tag > A)
return 0;
return si[d + tag];
}
IL int ask(int ll, int rr, int d)
{
int res = 0;
for (int i = ll; i <= rr; ++i)
if (va(i) - tag == d)
++res;
return res;
}
} blk;
int main()
{
STC int a[N];
STC Question q[M];
int n, m;
read(n), read(m);
for (int i = 1; i <= n; ++i)
read(a[i]);
for (int i = 1; i <= m; ++i)
read(q[i].op), read(q[i].l), read(q[i].r), read(q[i].x), q[i].ans = 0;
for (int i = 1; i <= get(n, B); ++i)
{
blk.l = LB(i, B);
blk.r = RB(i, B);
blk.build(a);
for (int j = 1; j <= m; ++j)
{
if (q[j].op == 1)
{
if (q[j].l <= blk.l && q[j].r >= blk.r)
blk.modify(q[j].x);
else
blk.change(max(q[j].l, blk.l), min(q[j].r, blk.r), a, q[j].x);
}
else
{
if (q[j].l <= blk.l && q[j].r >= blk.r)
q[j].ans += blk.query(q[j].x);
else
q[j].ans += blk.ask(max(q[j].l, blk.l), min(q[j].r, blk.r), q[j].x);
}
}
blk.destroy(a);
}
for (int i = 1; i <= m; ++i)
if (q[i].op == 2)
write(q[i].ans), putc('\n');
flus();
return 0;
}

其它

这是个galgame吗?我好像没玩过……

第三分块

由于lxl觉得它过于简单,被开除块籍了,后面数字断了的就是开除块籍了,不再提

第四分块

「弑尽破净的第四分块」

天降之物

维护一个序列,支持两种操作:

  1. 区间修改,把序列中所有的 $x$ 变成 $y$
  2. 在整个序列中找出一组 $i, j$ 满足 $a_i = x, a_j = y$ ,使 $|i - j|$ 最小,并输出 $i - j$ ,不存在输出Ikaros

强制在线, $n, m, a \le 10^5$ , $500ms, 256MB$

做题

额……操作全都是对序列中进行,分个鬼的块啊!但是毕竟这是“第四分块”,加上出题人lxl,估计还得分,就硬分!

还是考虑对序列分块,同时记录每一块内每一个数的个数,对于询问,如果有一个块同时包含两个数,直接暴力,时间 $O(B)$ ;否则吧每个块当成一个点,点上只有 $x, y, 0$ 三种信息,找到最近的 $x, y$ ,整块直接加,散点暴力,时间为 $O(2B + \frac{n}{B})$ ,修改就并查集维护即可,考虑到每次修改是对于整个序列,可以所有数共用一个并查集,不存在散点的情况,块长取 $B = \sqrt{n}$ 时,时空复杂度为均为 $O(n \sqrt{n})$ ,希望它别卡常……

艹,打到一半意识到这是完全假的,因为可能会有多个块同时包含两个数,而且还有可能出现两个相邻块最优,或者多个跨块最优……总之就是完全假掉了

臭不要脸的康康题解~

原来正解是根号分治(那为啥要叫第四分块啊!不过好像有人分块卡过了),不过既然是学习,当然学更稳当的根号分治了(就是平衡时间复杂度)

在讲正解前,先明确一些东西:

先考虑暴力的做法,当然是对每个数维护一个有序的位置集合,合并时归并排序,查询类似归并用双指针,但不合并,据说这样有 $60pts$ (夭寿了!lxl出的题有暴力分了!)

si[x] 表示 $x$ 在序列中出现的次数,当 si[x] > lim ( $lim$ 是阈值)时称其为大点,否则是小点;定义数组 as[x][y] 有意义当且仅当 $x$ 是大点,其意义为“大点 $x$ 到值 $y$ 的最小距离”;定义附属集合 v[x] 表示“自上一次重构以来,新出现的 $x$ 的位置”

我们要保证所有附属集合的大小不超过 $lim$ ,具体的,每当一个点 $x$ 的附属集合的大小超过 $lim$ ,我们就暴力跟新 as[x][] (此时 $x$ 必为大点),暴力可以做到 $O(n)$ ,我们称这个操作为重构

另外,在修改操作把 $x$ 变成 $y$ 时,可以开一个数组 val[x] 代表值 $x$ 的真实值,这样我们就可以交换 $x, y$ 了,只要记得只要修改一下 $val$ 即可

好,现在来看根号分治:

因为 $x, y$ 可以交换,所以只考虑 si[x] <= si[y] 的情况

对于修改,设把 $x$ 变成 $y$

  • 若 $x$ 是小点,暴力归并二者的附属集合(把 $x$ 并入 $y$ );如果合并后(这是预判,不必真的合并) $y$ 的附属集合大小超过 $lim$ 就重构 $y$
  • 若 $x$ 为大点(此时 $y$ 必为大点),显然把 $x$ 当成 $y$ 直接暴力重构(为啥不合并呢?因为 $x$ 是大点,它的一部分答案信息已经在 as[x][] 里了,附属集合记录信息不完全)

重构发生会把至少 $lim$ 个数从附属集合里除去,它们的信息被记在了 $as$ 中,且它们不会在出现在任何附属集合内了,而当所有附属集合都为空时,显然不会再发生重构,所以重构最多发生 $\frac{n}{lim}$ 次

合并附属集合显然是 $O(lim)$ ,不重构只修改 as[][y] 是 $O(\frac{n}{lim})$ 所以修改的时间复杂度为 $O(m * \max(lim, \frac{n}{lim}) + n * \frac{n}{lim})$

对于查询 $x, y$

先暴力“归并”(不合并)二者附属集合,再与两个数中大点的 $as$ 取 $\min$

查询的时间为 $O(m * lim)$

最后关于空间,最多有 $\frac{n}{lim}$ 个大点,而每个点的附属集合最多有 $lim$ 个数,所以空间为 $O(\frac{n}{lim} * a + n * lim)$ ,另外一定注意 vectorclear 不释放空间,具体写法见代码

明显,因为 $n, m, a$ 同级, $lim = \sqrt{n}$ 时时空都达到最优(然而实际上一般出题人都会卡 $\sqrt{n}$ 的,开大点可能好些,但本题不必)

代码

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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#include <iostream>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <vector>
#define STC static
#define IL inline
using namespace std;
namespace Fast
{
const int L = 10000 + 5;
char buf[L], out[L], *iS, *iT;
int l = 0;
#define gh() (iT == iS ? iT = (iS = buf) + fread(buf, 1, L, stdin), (iT == iS ? EOF : *iS++) : *iS++)
template<class T>
IL void read(T &x)
{
x = 0;
char ch = gh(), t = 0;
while (ch < '0' || ch > '9')
t |= ch == '-', ch = gh();
while (ch >= '0' && ch <= '9')
x = x * 10 + (ch ^ 48), ch = gh();
if (t)
x = -x;
}
IL void flus()
{
fwrite(out, 1, l, stdout);
l = 0;
}
IL void putc(char x)
{
out[l++] = x;
if (l == L - 5)
flus();
}
template<class T>
IL void write(T x)
{
if (x < 0)
putc('-'), x = -x;
if (x > 9)
write(x / 10);
out[l++] = x % 10 + 48;
if (l == L - 5)
flus();
}
}
using Fast::flus;
using Fast::putc;
using Fast::read;
using Fast::write;
const int L = 317, NL = L + 5, N = 1e5 + L + 5;
const int INF = 0x3f3f3f3f;
int n, m, a[N];
//id记录大点编号
int lim, si[N], as[NL][N], id[N], val[N], tot = 0;
vector<int> v[N];
IL void write_Ikaros()
{
putc('I'), putc('k'), putc('a'), putc('r'), putc('o'), putc('s'), putc('\n');
}
IL void build(int x) //重构
{
if (!id[x])
id[x] = ++tot;
int t = id[x];
for (int i = 1; i < N; ++i)
as[t][i] = INF;
for (int i = 1, j = INF; i <= n; ++i)
if (a[i] == x)
j = 0;
else
as[t][a[i]] = min(as[t][a[i]], ++j);
for (int i = n, j = INF; i >= 1; --i)
if (a[i] == x)
j = 0;
else
as[t][a[i]] = min(as[t][a[i]], ++j);
as[t][x] = 0;
//把v和一个空vector交换,开在函数里的会被释放
vector<int> l18qAKIOI;
v[x].swap(l18qAKIOI);
}
IL void merge(int x, int y) //合并
{
int i = 0, j = 0, sx = v[x].size(), sy = v[y].size();
vector<int> t;
while (i < sx && j < sy)
t.push_back(v[x][i] < v[y][j] ? v[x][i++] : v[y][j++]);
while (i < sx)
t.push_back(v[x][i++]);
while (j < sy)
t.push_back(v[y][j++]);
v[y] = t;
}
IL int _merge(int x, int y)
{
int i = 0, j = 0, sx = v[x].size(), sy = v[y].size(), res = INF;
if (!sx || !sy)
return res;
while (i < sx && j < sy)
res = min(res, v[x][i] < v[y][j] ? v[y][j] - v[x][i++] : v[x][i] - v[y][j++]);
while (i < sx)
res = min(res, abs(v[x][i++] - v[y][sy - 1]));
while (j < sy)
res = min(res, abs(v[x][sx - 1] - v[y][j++]));
return res;
}
IL void modify(int x, int y)
{
int _x = val[x], _y = val[y];
if (!si[_x] || _x == _y)
return ;
if (si[_x] > si[_y])
val[y] = _x, swap(_x, _y);
val[x] = 0;
if (!_x || !_y)
return ;
x = _x, y = _y;
if (si[x] > lim)
{
for (int i = 1; i <= n; ++i)
if (a[i] == x)
a[i] = y;
build(y);
}
else
{
for (int i : v[x])
a[i] = y;
if (si[x] + v[y].size() <= lim)
{
//重构y就不必进行这个跟新了,反正as[y][]是准确的
for (int i = 1; i <= tot; ++i)
as[i][y] = min(as[i][y], as[i][x]);
merge(x, y);
}
else
build(y);
}
si[y] += si[x], si[x] = 0;
vector<int> wfyAKNOI;
v[x].swap(wfyAKNOI);
}
IL int ask(int x, int y)
{
x = val[x], y = val[y];
if (!si[x] || !si[y])
return -1;
if (x == y)
return 0;
int res = _merge(x, y);
if (si[y] > lim)
res = min(res, as[id[y]][x]);
if (si[x] > lim)
res = min(res, as[id[x]][y]);
return res;
}
int main()
{
int last = 0, op, x, y;
read(n), read(m);
lim = sqrt(n) + 1;
for (int i = 1; i <= n; ++i)
read(a[i]), ++si[a[i]], v[a[i]].push_back(i), val[i] = i;
for (int i = 1; i <= n; ++i)
if (si[i] > lim)
build(i);
while (m--)
{
read(op), read(x), read(y);
x ^= last, y ^= last;
if (op == 1)
modify(x, y);
else if ((last = ask(x, y)) < 0)
write_Ikaros(), last = 0;
else
write(last), putc('\n');
}
flus();
return 0;
}

其它

天降之物也挺好看的,但可惜 $09$ 年是钢炼的天下

未完待续

在写到这里时,已经 $7000$ 字了,才完成 $\frac{1}{3}$ ,觉定分成几部分(目前想的是三篇,到时候看吧)

已经打了两天的分块了,我都要吐了,缓缓再战,下一篇写好会在这里挂链接的

update:大力分块2