补题 + 区间$mex$模板
题目 题目描述 给定一个序列,$m$次查询区间的$mex$值,$mex$表示最小的没出现过的自然数,$n,m ≤ 10^5$。显然可以使用经典算法轻易解决这个经典问题。
现在蒟蒻 djy 魔改了一下它,给定一个$0$到$n - 1$的排列,$m$次查询区间的$mex$值,但是$n, m$变大了,你能帮帮他吗?
$n,m ≤ 10^7$,输入数据在程序内生成。
输入描述 第一行给出$n, m$
以下为数据生成模板
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 int a[10000001 ],c[10000002 ][2 ],n,m;unsigned int rd;inline int rnd (int mod,int seed) { rd^=(1 <<13 ); rd^=(rd>>7 ); rd^=(rd<<20 ); rd^=(rd>>3 ); rd^=(rd<<10 ); rd^=(rd<<5 ); rd^=(1 <<2 ); return rd%mod+seed; } void make_sequence () { for (int i=1 ;i<=n;i++) { a[i]=i-1 ; } for (int i=1 ;i<=n;i++) { swap (a[i],a[rnd (n,1 )]); } } pair<int ,int > make_query () { int l=rnd (n,1 ); int r=rnd (n+1 -l,l); return make_pair (l,r); }
输出描述 一行一个整数, 表示所有答案的异或和
样例 输入
5 6
输出
6
题解 思路 一看到数据随机, 以为是什么规律题, 或者考察了区间$mex$的某种性质, 或者关于随机的某种技巧
但是没想到其实是个正经题, 和数据随不随机没有关系
注意题目中给出的是$0$到$n - 1$的排列, 这是解决本题的关键。 区间$mex$定义为区间内没有出现过的最小自然数, 那么对于一个询问$[l, r]$, 我们要找的就是$a[1, l - 1], a[r + 1, n]$中最小的数, 因为这是一个排列, $0$到$n - 1$中所有的数都在$a$中出现了一次, 所以这个算法是正确的
代码 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 #include <iostream> #include <cstdio> #include <map> int pn[10000007 ], ln[10000007 ];int ans;int a[10000001 ],c[10000002 ][2 ],n,m;unsigned int rd;inline int rnd (int mod,int seed) { rd^=(1 <<13 ); rd^=(rd>>7 ); rd^=(rd<<20 ); rd^=(rd>>3 ); rd^=(rd<<10 ); rd^=(rd<<5 ); rd^=(1 <<2 ); return rd%mod+seed; } void make_sequence () { for (int i=1 ;i<=n;i++) { a[i]=i-1 ; } for (int i=1 ;i<=n;i++) { std:: swap (a[i],a[rnd (n,1 )]); } } std:: pair<int ,int > make_query () { int l=rnd (n,1 ); int r=rnd (n+1 -l,l); return std:: make_pair (l,r); } int main () { scanf ("%d%d" , &n, &m); make_sequence (); pn[0 ] = n; for (int i = 1 ; i <= n; i ++) pn[i] = std:: min (pn[i - 1 ], a[i]); ln[n + 1 ] = n; for (int i = n; i >= 1 ; i --) ln[i] = std:: min (ln[i + 1 ], a[i]); for (int i = 1 ; i <= m; i ++) { std:: pair<int , int > tmp = make_query (); ans ^= std:: min (pn[tmp.first - 1 ], ln[tmp.second + 1 ]); } printf ("%d\n" , ans); return 0 ; }
区间$mex$ 如上题所描述的, 是一个经典问题, 这里采用的例题为洛谷P4137 Rmq Problem / mex
通常来说求解区间$mex$有两种方法: 离线和在线做法。 离线又可以用莫队 + 权值分块或者线段树, 在线好像只能用权值主席树
莫队 + 权值分块 思路 经典的莫队操作, 将所有询问$[l, r]$按$\frac{l}{\sqrt n}$为第一关键字, $r$为第二关键字从小到大排序然后依次对每个询问移动$l, r$, 过程中维护两个数组:
$cnt[x]: $ $[l, r]$中$x$出现的次数
$bs[y] : $ $[l, r]$中属于第$y$个权值块的$x$的个数。 假设$max_{a_i} = mx$, 我们按照权值分块后, $bs[0]$维护的就是$[l, r]$中属于$[0, \sqrt {mx} - 1]$的$x$个数, $bs[1]$维护的就是$[l, r]$中属于$[\sqrt {mx}, 2 \times \sqrt {mx} - 1]$的$x$个数, 以此类推3
这样对于一个询问, 我们移动完$[l, r]$并维护好两个数组后, 我们只需要用$O(\sqrt {mx})$的时间遍历一下所有的$bs[]$, 找到第一个$bs[y] \neq \sqrt {mx}$的块号$y$, 然后花$O(\sqrt {mx})$的时间找到这个块内第一个没有出现也就是$cnt[x] = 0$的$x$, 这次询问的答案就是$x$,一次询问查找答案的时间复杂度是$O(2 \times \sqrt {mx}) = O(\sqrt {mx})$, 总查找答案的时间复杂度为$O(m \times \sqrt {mx})$
算法的总时间复杂度要在加上莫队时间复杂度, $2 \times 10^5$肯定是没有问题的
代码 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 #include <iostream> #include <cstdio> #include <cmath> #include <algorithm> int n, m;int a[210005 ];int sqn; struct Q { int l, r, num; bool operator < (Q tmp) const { int b1 = l / sqn, b2 = tmp.l / sqn; if (b1 < b2) return true ; else if (b1 > b2) return false ; else return r < tmp.r; } } q[210005 ]; int bs[210005 ], cnt[210005 ];int ans[210005 ];void Modui () { std:: sort (q + 1 , q + m + 1 ); int l = 0 , r = 0 ; cnt[0 ] = bs[0 ] = 1 ; for (int i = 1 ; i <= m; i ++) { int ql = q[i].l, qr = q[i].r, num = q[i].num; if (l < ql) for (int j = l; j < ql; j ++) cnt[a[j]] --, bs[a[j] / sqn] -= (cnt[a[j]] == 0 ); else if (l > ql) for (int j = l - 1 ; j >= ql; j --) bs[a[j] / sqn] += (cnt[a[j]] == 0 ), cnt[a[j]] ++; if (r < qr) for (int j = r + 1 ; j <= qr; j ++) bs[a[j] / sqn] += (cnt[a[j]] == 0 ), cnt[a[j]] ++; else if (r > qr) for (int j = r; j > qr; j --) cnt[a[j]] --, bs[a[j] / sqn] -= (cnt[a[j]] == 0 ); l = ql; r = qr; for (int j = 0 ; ; j ++) if (bs[j] != sqn) { for (int k = j * sqn; ; k ++) if (! cnt[k]) { ans[num] = k; break ; } break ; } } } int main () { scanf ("%d%d" , &n, &m); sqn = sqrt (n); for (int i = 1 ; i <= n; i ++) scanf ("%d" , &a[i]); for (int i = 1 ; i <= m; i ++) scanf ("%d%d" , &q[i].l, &q[i].r), q[i].num = i; Modui (); for (int i = 1 ; i <= m; i ++) printf ("%d\n" , ans[i]); return 0 ; }
对于本题, 数据中可能会出现$n$小, $max_{a_i}$比较大的情况, 这样的话$sqn$大小应该不能代表权值分块的块大小
但是我们注意到, $a_i > n$的情况下$a_i$是无意义的, 因为对于$n$个数的序列而言, 其$mex$最大值也就能到$n$, 不可能比$n$要大, 所以权值分块的块大小是根据$n$的大小定的, 但是$bs[]$的大小一定要开的和$max_{a_i}$一样
当然也可以将$bs[]$开的和$n$一样, 这个时候只需要把所有的$a_i > n$的$a_i$都变成$n + 1$就可以了, 所以实则查找答案的时间复杂度为$O(m \sqrt n)$
莫队 莫队是一种优秀的处理区间询问的离线算法, 这里主要讨论一下算法的时间复杂度
众所周知, 莫队的时间复杂度来源于$l, r$的移动次数
$r: $在询问的每一个块中, 由于$r$是严格递增的, 所以对于每个块$r$最多移动$n$次, 由于一共有$\sqrt n$个块, 所以这一部分的时间复杂度为$O(n \sqrt n)$; 对于不在同一个块的询问, 即从第$i - 1$个询问到第$i$个询问时左端点所属的块变化了, 这时右端点也最多移动$n$次, 但是这种变化最多只有$ \sqrt n - 1$次(因为只有$\sqrt n$个询问块), 所以这一部分的时间复杂度也是$O(n \sqrt n)$
所以$r$的时间复杂度为$O(2 \times n \sqrt n)$, 也就是$O(n \sqrt n)$
$l: $ 对于不在同一个块的询问, $l$最多移动$2 \sqrt n$次, 这种变化最多只有$\sqrt n - 1$次, 所以这部分时间复杂度为$O(n)$; 在同一个块的内的询问最多有$m$个, 而在每个块内$l$每两个询问之间最多移动$\sqrt n$次(因为块大小只有$\sqrt n$), 所以这部分时间复杂度为$O(m \sqrt n)$
所以$l$的时间复杂度为$O(m \sqrt n + n) = O(m \sqrt n)$
所以算法的总时间复杂度为$O((m + n) \sqrt n)$
权值主席树 思路 第$i$棵主席树的叶子节点表示的是$a_1, a_2, …, a_i$中的所有权值最后一次出现的位置, 每个节点维护区间最小值
当我们要查询$[l, r]$的$mex$时, 我们在第$r$棵主席树上查询, 二分找到最后出现位置$ < l$的最小权值, 这个权值就是答案
代码 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 #include <iostream> #include <cstdio> #include <vector> int n, m;struct Node { int ls, rs; int val, dat; }; struct St { std:: vector<Node> vct; int tot, root[210005 ]; void Init () { tot = 0 ; for (int i = 0 ; i <= 200000 ; i ++) root[i] = 0 ; vct.clear (); vct.push_back ((Node){0 , 0 , 0 , 0 }); } int Addnode () { tot ++; vct.push_back ((Node){0 , 0 , 0 , 0 }); return tot; } int Modify (int l, int r, int rt, int p, int v) { int now = Addnode (); vct[now] = vct[rt]; if (l == r) { vct[now].dat = vct[now].val = v; return now; } int mid = (l + r) >> 1 ; if (p <= mid) vct[now].ls = Modify (l, mid, vct[rt].ls, p, v); else vct[now].rs = Modify (mid + 1 , r, vct[rt].rs, p, v); vct[now].dat = std:: min (vct[vct[now].ls].dat, vct[vct[now].rs].dat); return now; } int Query (int l, int r, int rt, int ql) { if (!rt || l == r) return l; int mid = (l + r) >> 1 ; if (vct[vct[rt].ls].dat < ql) return Query (l, mid, vct[rt].ls, ql); else return Query (mid + 1 , r, vct[rt].rs, ql); } }; int main () { St st; st.Init (); scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i ++) { int a; scanf ("%d" , &a); st.root[i] = st.Modify (0 , 200001 , st.root[i - 1 ], a, i); } for (int i = 1 ; i <= m; i ++) { int ql, qr; scanf ("%d%d" , &ql, &qr); printf ("%d\n" , st.Query (0 , 200001 , st.root[qr], ql)); } return 0 ; }
这份代码在洛谷上只能用c++17或者20才能过, 低于这个版本会RE, 但是在我本地和Acwing就不会出问题, 不知道为什么, 我还Debug了好久
下面这个数组版的就可以在c++11的条件下通过
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 #include <iostream> #include <cstdio> #include <vector> int n, m;struct Node { int ls, rs; int val, dat; }; struct St { Node vct[10000007 ]; int tot, root[210005 ]; void Init () { tot = 0 ; for (int i = 0 ; i <= 200001 ; i ++) root[i] = 0 ; } int Addnode () { tot ++; vct[tot] = (Node){0 , 0 , 0 , 0 }; return tot; } int Modify (int l, int r, int rt, int p, int v) { int now = Addnode (); vct[now] = vct[rt]; if (l == r) { vct[now].dat = vct[now].val = v; return now; } int mid = (l + r) >> 1 ; if (p <= mid) vct[now].ls = Modify (l, mid, vct[rt].ls, p, v); else vct[now].rs = Modify (mid + 1 , r, vct[rt].rs, p, v); vct[now].dat = std:: min (vct[vct[now].ls].dat, vct[vct[now].rs].dat); return now; } int Query (int l, int r, int rt, int ql) { if (!rt || l == r) return l; int mid = (l + r) >> 1 ; if (vct[vct[rt].ls].dat < ql) return Query (l, mid, vct[rt].ls, ql); else return Query (mid + 1 , r, vct[rt].rs, ql); } }; int main () { St st; st.Init (); scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i ++) { int a; scanf ("%d" , &a); st.root[i] = st.Modify (0 , 200001 , st.root[i - 1 ], a, i); } for (int i = 1 ; i <= m; i ++) { int ql, qr; scanf ("%d%d" , &ql, &qr); printf ("%d\n" , st.Query (0 , 200001 , st.root[qr], ql)); } return 0 ; }