「Ynoi2018」天降之物

第二道 Ynoi2018 。

Idea:lxl Solution:lxl Std:lxl Data:lxl

“弑尽破净的第四分块” (妈呀这名字好中二啊(谁叫我要用日本轻小说中的东西命名真是作死))

这题就是我想改一个已有的经典根号形式改出来,然后想了想发现可以做

考虑根号分治


存在序列分块做法

对这题的评价:6/11

[Luogu 5397]

[51nod 2046]

题意简述

维护一个长 $n$ 的整数序列 $a$ , $m$ 个操作,可能是以下两种:

  1. 给出 $x,y$ 两个正整数,将序列中所有值为 $x$ 的数变为 $y$ ;
  2. 给出 $x,y$ 两个正整数,查询序列中对于所有 $i,j$ 满足 $a_i=x, a_j=y$ , $|i-j|$ 的最小值,无解输出Ikaros

强制在线, $1\le n,m\le 10^5,0\le a_i,x,y\le 10^5$ 。

主要思路

看到所有操作都只涉及整个序列就感觉应该不是序列分块。

所以考虑根号分治。

设 $x$ 在序列中的个数为 $siz[x]$ 。

称满足 $siz[x] \ge \sqrt{n}$ 的数 $x$ 为大的,否则为小的。

无修做法

把每个数 $i$ 出现的位置按顺序丢到一个数组 $V[i]$ 里。

再预处理每个大的数到所有其他值的答案,显然可以每个大的数 $O(n)$ ,预处理复杂度 $O(n\sqrt{n})$ 。

查询的时候如果 $x$ 与 $y$ 均为小,则可以使用类似归并的方法, $O(\sqrt{n})$ 查询排序后的位置数组得到答案;否则直接 $O(1)$ 取出预处理的答案即可。

所以时间复杂度为 $O(n\sqrt{n} + m\sqrt{n})$ 。

带修做法

假设将所有 $x$ 变为 $y$ 。

由于可以通过一些技巧,使得 $x$ 变为 $y$ 等价于 $y$ 变为 $x$ ,所以不妨先设 $siz[x] \le siz[y]$ 。

假设我们一开始预处理的大的数 $B$ 到任意数 $i$ 的答案为 $ans[B][i]$ 。

如果 $x$ 与 $y$ 均为大,我们可以直接 $O(n)$ 重构 $y$ 到每个数 $i$ 的答案 $ans[y][i]$ 。由于需要满足 $x,y$ 均为大,故这样的重构不会超过 $\sqrt{n}$ 次,总复杂度 $O(n\sqrt{n})$ 。

如果 $x$ 与 $y$ 均为小,并且合并后也为小,我们只需要对于每个大数 $B$ ,使用 $ans[B][x]$ 更新 $ans[B][y]$ ,并将 $ans[B][x]$ 设为 $\inf$ 即可。这样单次复杂度为大数个数,即 $O(\sqrt{n})$ 。若 $x$ 与 $y$ 合并后为大,那么直接如上方 $O(n)$ 重构。易得这样的重构也不会超过 $\sqrt{n}$ 次,所以总复杂度 $O(n\sqrt{n})$ 。

然而 $x$ 为小, $y$ 为大时,仅使用 $ans$ 数组来记录答案不可行。若直接 $O(n)$ 重构,总复杂度上升到 $O(n^2)$ ;若只更新每个大数 $B$ 到 $y$ 的答案 $ans[B][y]$ ,正确性出现问题。

假设将 $x_1, x_2$ (小)分别变为 $y_1, y_2$ (大)。则 $ans[y_1][y_2]$ 记录的是原本的 $y_1$ 到原本的 $y_2, x_2$ 的答案的最小值, $ans[y_2][y_1]$ 记录的是原本的 $y_2$ 到原本的 $y_1, x_1$ 的答案的最小值。若现在 $y_1, y_2$ 的答案实际在原本的 $x_1, x_2$ 之间取得,则无法被更新到。故直接更新正确性错误。

我们对每个大的数 $B$ 都开一个附属集合,表示目前序列的哪些位置原本不是 $B$ 但现在变成了 $B$ ,这些位置就应该是在上方讨论过,未被完全更新的位置。将这些位置按顺序放入数组 $V^\prime[B]$ 中,$|V^\prime[B]|$ 记为 $psz[B]$。

对于 $x$ 为小, $y$ 为大时的修改,仍然对每个大数 $B$ 更新 $ans[B][y]$ ,并将所有 $x$ 出现的位置按顺序加入 $V^\prime[y]$ 中。此时一次的时间复杂度为 $O(\sqrt{n} + psz[y])$ 。故若 $V^\prime[y]$ 在加入 $x$ 的位置后, $psz[y]$ 大小已经比 $\sqrt{n}$ 大,需要像 $x$ 与 $y$ 均为大时一样,直接 $O(n)$ 重构 $ans[y][i]$ 。由于这样的重构次数也不会超过 $\sqrt{n}$ 次,总复杂度也是 $O(n\sqrt{n})$ 。

如上述修改,总复杂度即为 $O(n\sqrt{n} + m\sqrt{n})$ 。


接下来假设查询 $x$ 与 $y$ ,先钦定 $siz[x] \le siz[y]$ 。

如果 $x$ 与 $y$ 均为小,可以通过无修时类似归并的方法, $O(\sqrt{n})$ 做。

如果 $x$ 与 $y$ 均为大,先 $\min(ans[y][x], ans[x][y])$ 得到一个答案,再将两数的附属集合像上面的方法一样归并做,两个答案的最小值就是真正的答案,复杂度也为 $O(\sqrt{n})$ 。

如果 $x$ 为小, $y$ 为大,将 $V[x]$ 和 $V^\prime[y]$ 归并做,再与 $ans[y][x]$ 取最小值即为答案,复杂度仍为 $O(\sqrt{n})$ 。


所以总时间复杂度 $O((n + m)\sqrt{n})$ ,空间复杂度 $O(n\sqrt{n})$ 。

参考代码

然而怎么实现?

由于需要支持在修改中交换 $x, y$ ,需要一个数组 $rn[i]$ 来表示数 $i$ 现在在序列中的真实值。

发现当数 $i$ 为大的时候,存 $V[i]$ 没有用,直接把 $V^\prime[i]$ 存到 $V[i]$ 上去,这样合并两个集合或者附属集合就好写多了。

大数肯定要编个号,注意垃圾回收。然后就是注意 $siz$ 和 $psz$ 的修改不要漏了。

实验证明大数的判定标准定到 $500$ 时跑得最快。加了三行指令集之后,跑到了最优解,纪念一下。

更多细节见代码:
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
#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,avx2,tune=native")

#include<bits/stdc++.h>
namespace my_std{
using namespace std;
#define reg register
#define Rint register int
#define FOR(i,a,b) for(register int i=(a),ed_##i=(b);i<=ed_##i;++i)
#define ROF(i,a,b) for(register int i=(a),ed_##i=(b);i>=ed_##i;--i)
#define FORit(templ,arr,i,a,b) for(register templ *i=(arr)+(a),*ed_##i=(arr)+(b)+1;i!=ed_##i;++i)
#define ROFit(templ,arr,i,a,b) for(register templ *i=(arr)+(a),*ed_##i=(arr)+(b)-1;i!=ed_##i;--i)
#define GO(x,p,e,i,v) for(register int i=p[x].head,v;i;i=e[i].link)
#define MEM(x,v) memset(x,v,sizeof(x))
#define fir first
#define sec second
#define pq priority_queue
#define MP make_pair
typedef long long LL;
typedef double DB;
typedef pair<int,int> PII;
#define Templ(T) template<typename T>
inline int read(){
int ans=0,f=1;char c=getchar();
while(!isdigit(c)){ f^=(c=='-'); c=getchar(); }
for(;isdigit(c);c=getchar()) ans=(ans<<1)+(ans<<3)+(c^48); return f?ans:-ans;
}
Templ(_Tp) inline int chkmin(_Tp &x,_Tp y){ return x>y?x=y,1:0; }
Templ(_Tp) inline int chkmax(_Tp &x,_Tp y){ return x<y?x=y,1:0; }
#define using_mod
const int mod = 998244353, N = 100010, MX = 100000, BN = 500, BM = 210;
#ifdef using_mod
inline void inc(int &x,const int &y){ x+=y; if(x>=mod) x-=mod; }
inline int ksm(int x,LL y){ int res=1; for(;y;y>>=1,x=1ll*x*x%mod) if(y&1) res=1ll*res*x%mod; return res;}
#endif
Templ(_Tp) inline _Tp gcd(_Tp x,_Tp y){ return y?gcd(y,x%y):x; }
#define FILE(s) freopen(s".in","r",stdin),freopen(s".out","w",stdout)
#define PBTXDY
}
using namespace my_std;

inline int my_max(Rint a, Rint b){ return (a > b) ? a : b; }
inline int my_min(Rint a, Rint b){ return (a < b) ? a : b; }
#define max(a,b) my_max(a,b)
#define min(a,b) my_min(a,b)
//inline int abs(Rint x){ return x < 0 ? -x : x; }
//#define abs(x) ((x) < 0 ? -(x) : (x))
#define swap(a,b) (a ^= b ^= a ^= b)
#define inf (114514)
//手写各种东西

vector<int> v[N];
int n, m, bgn, a[N], siz[N], psz[N], rn[N], bnd[N];
//siz[x] : 数 x 在序列中的个数; psz[x] : 数 x 附属集合大小; rn[x] : 数 x 实际上是什么数
//bgn : 目前最大大数序号; bnd[x] : 数 x 对应的大数序号
int ans[BM][N];

int del[N], dt;
const int blo = BN;

inline int new_big_num(){
return dt ? del[dt--] : ++bgn;
}

inline void init_big_num(Rint x){//O(n) 预处理 ans[bnd[x]]][i]
Rint id = bnd[x], now = inf;
FOR(i, 1, MX) ans[id][i] = inf;
FOR(i, 1, n){
if(a[i] == x) now = 0;
else chkmin(ans[id][a[i]], ++now);
}
now = inf;
ROF(i, n, 1){
if(a[i] == x) now = 0;
else chkmin(ans[id][a[i]], ++now);
}
}

inline void build_big_num(Rint x){//建新大数
bnd[x] = new_big_num();
init_big_num(x), ans[bnd[x]][x] = 0;
}

inline void rebuild_big_num(Rint x, Rint y){//将 x 合并到大数 y 并重构
if(bnd[x]) del[++dt] = bnd[x];
FOR(i, 1, n) if(a[i] == x) a[i] = y;
psz[y] = 0;
init_big_num(y);
}

inline void merge(Rint x, Rint y){//将 x 的集合合并到 y 的集合中
Rint sx = siz[x], sy = siz[y] < blo ? siz[y] : psz[y], ss = sx + sy;
vector<int> tmp = v[y];
v[y].resize(ss + 10);
while(sx && sy) v[y][ss--] = ((v[x][sx] > tmp[sy]) ? v[x][sx--] : tmp[sy--]);
while(sx) v[y][ss--] = v[x][sx--];
while(sy) v[y][ss--] = tmp[sy--];
FOR(i, 1, bgn) chkmin(ans[i][y], ans[i][x]);
FOR(i, 1, siz[x]) a[v[x][i]] = y;
return;
}

inline int merge_num(Rint x, Rint y){//求两个集合间的答案
Rint sx = siz[x] < blo ? siz[x] : psz[x], sy = siz[y] < blo ? siz[y] : psz[y], res = inf;
if(!sx || !sy) return inf;
while(sx && sy) chkmin(res, (v[x][sx] > v[y][sy]) ? v[x][sx--] - v[y][sy] : v[y][sy--] - v[x][sx]);
//这两行不需要是因为通过这两行得出的答案一定不会比上一行得出的答案更优
// ROF(i, sx, 1) chkmin(res, v[y][1] - v[x][i]);
// ROF(i, sy, 1) chkmin(res, v[x][1] - v[y][i]);
return res;
}

inline void update(Rint xx, Rint yy){
Rint x = rn[xx], y = rn[yy];
if(x == y || !siz[x]) return;
if(siz[x] > siz[y]) swap(x, y), swap(xx, yy), rn[yy] = 0, rn[xx] = y;//保证 siz[x] <= siz[y]
else rn[xx] = 0;
if(!x || !y) return;//没有直接跳了
//siz[x] <= siz[y] < blo
if(siz[y] < blo){
if(siz[x] + siz[y] < blo){
merge(x, y);
siz[y] += siz[x], siz[x] = 0;
}
else{//变大重构
bnd[y] = new_big_num();
FOR(i, 1, siz[x]) a[v[x][i]] = y;
init_big_num(y);
siz[y] += siz[x], siz[x] = 0;
}
return;
}
//siz[x] < blo <= siz[y]
if(siz[x] < blo){
if(psz[y] + siz[x] < blo){
merge(x, y);
psz[y] += siz[x], siz[y] += siz[x], siz[x] = 0;
}
else{//psz[y] 过大重构
psz[y] += siz[x], siz[y] += siz[x], siz[x] = 0;
rebuild_big_num(x, y);
}
return;
}
//blo <= siz[x] <= siz[y]
siz[y] += siz[x], siz[x] = 0;
rebuild_big_num(x, y);//直接重构
}

inline int query(Rint xx, Rint yy){
Rint x = rn[xx], y = rn[yy];
if(x == y) return siz[x] ? 0 : -1;
if(!siz[x] || !siz[y]) return -1;//判掉无解情况
if(siz[x] > siz[y]) swap(x, y), swap(xx, yy);
//siz[x] <= siz[y] < blo
if(siz[y] < blo) return merge_num(x, y);
//siz[x] < blo <= siz[y]
if(siz[x] < blo) return min(ans[bnd[y]][x], merge_num(x, y));
//blo <= siz[x] <= siz[y]
return min(min(ans[bnd[y]][x], ans[bnd[x]][y]), merge_num(x, y));
}

int main(){
n = read(), m = read();
FOR(i, 1, n) ++siz[a[i] = read()], rn[i] = i;
FOR(i, 1, MX){
if(siz[i] >= blo) build_big_num(i), v[i].resize(10);
else v[i].resize(siz[i] + 10), siz[i] = 0;
}
FORit(int, a, i, 1, n){
if(siz[*i] < blo) v[*i][++siz[*i]] = i - a;
}
Rint lastans = 0, opt, x, y;
while(m --){
opt = read(), x = read() ^ lastans, y = read() ^ lastans;
// opt = read(), x = read(), y = read();
if(opt == 1) update(x, y);
else{
lastans = query(x, y);
if(lastans == -1) puts("Ikaros"), lastans = 0;
else printf("%d\n", lastans);
}
}
return 0;
}

对比一下自己写「Ynoi2018」未来日记时的码风发现变化有点大……

参考资料

lxl 原博客

foreverlasting 的博客