P8496 [NOI2022] 众数
1 P8496 [NOI2022] 众数
2 题目描述
对于一个序列,定义其众数为序列中出现次数严格大于一半的数字。注意该定义与一般的定义有出入,在本题中请以题面中给出的定义为准。
一开始给定 \(n\) 个长度不一的正整数序列,编号为 \(1 \sim n\),初始序列可以为空。这 \(n\) 个序列被视为存在,其他编号对应的序列视为不存在。
有 \(q\) 次操作,操作有以下类型:
- \(1 \ x \ y\):在 \(x\) 号序列末尾插入数字 \(y\)。保证 \(x\) 号序列存在,且 \(1 \le x, y \le n + q\)。
- \(2 \ x\):删除 \(x\) 号序列末尾的数字,保证 \(x\) 号序列存在、非空,且 \(1 \le x \le n + q\)。
- \(3 \ m \ x_1 \ x_2 \ x_m\):将 \(x_1, x_2, \ldots, x_m\) 号序列顺次拼接,得到一个新序列,并询问其众数。如果不存在满足上述条件的数,则返回 \(-1\)。数据保证对于任意 \(1 \le i \le m\),\(x_i\) 是一个仍然存在的序列,\(1 \le x_i \le n + q\),且拼接得到的序列非空。注意:不保证 \(\boldsymbol{x_1, \ldots, x_m}\) 互不相同,询问中的合并操作不会对后续操作产生影响。
- \(4 \ x_1 \ x_2 \ x_3\):新建一个编号为 \(x_3\) 的序列,其为 \(x_1\) 号序列后顺次添加 \(x_2\) 号序列中数字得到的结果,然后删除 \(x_1, x_2\) 对应的序列。此时序列 \(x_3\) 视为存在,而序列 \(x_1, x_2\) 被视为不存在,在后续操作中也不会被再次使用。保证 \(1 \le x_1, x_2, x_3 \le n + q\)、\(x_1 \ne x_2\)、序列 \(x_1, x_2\) 在操作前存在、且在操作前没有序列使用过编号 \(x_3\)。
输入格式
输入的第一行包含两个正整数 \(n\) 和 \(q\),分别表示数列的个数和操作的次数,保证 \(n \le 5 \times {10}^5\)、\(q \le 5 \times {10}^5\)。
接下来 \(n\) 行,第 \(i\) 行表示编号为 \(i\) 的数列。每一行的第一个非负整数 \(l_i\) 表示初始第 \(i\) 号序列的数字个数,接下来有 \(l_i\) 个非负整数 \(a_{i,j}\) 按顺序表示数列中的数字。假定 \(C_l = \sum l_i\) 代表输入序列长度之和,则保证 \(C_l \le 5 \times {10}^5\)、\(a_{i,j} \le n + q\)。
接下来 \(q\) 行,每行若干个正整数,表示一个操作,并按照题面描述中的格式输入。
假定 \(C_m = \sum m\) 代表所有操作 \(3\) 需要拼接的序列个数之和,则保证 \(C_m \le 5 \times {10}^5\)。
输出格式
对于每次询问,一行输出一个整数表示对应的答案。
样例 #1
样例输入 #1
2 8
3 1 1 2
3 3 3 3
3 1 1
3 1 2
4 2 1 3
3 1 3
2 3
3 1 3
1 3 1
3 1 3
样例输出 #1
1
3
-1
3
-1
样例 #2
样例输入 #2
4 9
1 1
1 2
1 3
1 4
3 4 1 2 3 4
1 1 2
3 2 1 2
2 3
3 3 1 2 3
1 4 4
1 4 4
1 4 4
3 4 1 2 3 4
样例输出 #2
-1
2
2
4
提示
【样例解释 #1】
第一次询问查询序列 \(1\) 的众数。由于序列包含两个 \(1\),超过序列长度的一半,因此众数为 \(1\)。
第二次询问查询序列 \(2\) 的众数。由于序列只包含 \(3\),因此众数为 \(3\)。
第三次询问询问序列 \(3\) 的众数。此时序列 \(3\) 为 \((3, 3, 3, 1, 1, 2)\),不存在出现次数大于 \(3\) 次的数,因此输出为 \(-1\)。
【样例解释 #2】
第一次询问查询序列 \(1, 2, 3, 4\) 拼接后得到的序列的众数。拼接的结果为 \((1, 2, 3, 4)\),不存在出现次数大于两次的数,因此输出为 \(-1\)。
第四次询问查询序列 \(1, 2, 3, 4\) 拼接后得到的序列的众数。拼接的结果为 \((1, 2, 2, 4, 4, 4, 4)\),众数为 \(4\)。
【数据范围】
对于所有测试数据,保证 \(1 \le n, q, C_m, C_l \le 5 \times {10}^5\)。
3 题解
首先不考虑 \(2\) 操作。容易发现,此时序列内数的相对顺序并不重要,可以看作若干集合。
考虑 \(1, 4\) 操作,我们用线段树合并维护这 \(n + q\) 个集合即可,由于合并时 \(x_1 \ne x_2\),故复杂度有保障。
如何求解一个集合的绝对众数呢?
容易想到在线段树上每个权值处维护出现次数,然后直接全局最大值即可求出众数,判断是否大于总大小一半即可。
这个做法显然不能扩展到多个集合一起求众数的情况。
考虑性质:如果在线段树上某一区间所有值的出现次数之和小于等于 \(\lfloor \dfrac{n}{2} \rfloor\),那么这个区间的所有值的出现次数最大值也一定小于等于 \(\lfloor \dfrac{n}{2} \rfloor\)。
因此,我们维护线段树上数值出现次数的和,查询时在 \(m\) 棵线段树上一起二分:
如果 \(m\) 棵线段树的左区间的和大于 \(\lfloor \dfrac{\sum n}{2} \rfloor\),那么右区间的和一定小于等于 \(\lfloor \dfrac{\sum n}{2} \rfloor\),故直接向左区间走。
否则就判断 \(m\) 棵线段树的右区间的和是否大于 \(\lfloor \dfrac{\sum n}{2} \rfloor\),如果是则向右区间走,否则直接返回 \(-1\)。
考虑 \(2\) 操作,对每个序列维护一个链表,\(1\) 操作是在末端插入,\(2\) 操作是在末端删除,\(4\) 操作是将 \(x_1\) 的末端与 \(x_2\) 的前端连接。
复杂度 \(O(\sum m \log V)\)。
4 代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#include <set>
#include <cstdlib>
#include <cmath>
#include <deque>
using namespace std;
#define pii pair<int,int>
#define mp make_pair
const int N = 1e6 + 10;
#define int long long
typedef long long ll;
int read()
{
int x = 0, f = 1;
char c = getchar();
while (c < '0' || c > '9')
{
if (c == '-') f = -1;
c = getchar();
}
while (c >= '0' && c <= '9')
{
x = (x << 1) + (x << 3) + (c ^ 48);
c = getchar();
}
return x * f;
}
int n, q, cnt, tot;
int a[N], rt[N];
ll siz[N];
int head[N << 1], nxt[N << 1], tail[N << 1], h[N << 1], pre[N << 1];
struct sgt
{
int tot;
struct node
{
int ls, rs;
ll sum;
}t[N * 37];
int build()
{
tot++;
t[tot].ls = t[tot].rs = t[tot].sum = 0;
return tot;
}
void pushup(int p) {t[p].sum = t[t[p].ls].sum + t[t[p].rs].sum;}
void modify(int p, int l, int r, int pos, ll d)
{
if (l == r)
{
t[p].sum += d;
return ;
}
int mid = (l + r) >> 1;
if (pos <= mid)
{
if (!t[p].ls) t[p].ls = build();
modify(t[p].ls, l, mid, pos, d);
}
else
{
if (!t[p].rs) t[p].rs = build();
modify(t[p].rs, mid + 1, r, pos, d);
}
pushup(p);
}
int merge(int p, int q, int l, int r)
{
if (!p || !q) return p + q;
if (l == r)
{
t[p].sum += t[q].sum;
return p;
}
int mid = (l + r) >> 1;
t[p].ls = merge(t[p].ls, t[q].ls, l, mid);
t[p].rs = merge(t[p].rs, t[q].rs, mid + 1, r);
pushup(p);
return p;
}
ll S() {ll res = 0; for (int i = 1; i <= cnt; i++) res += t[t[a[i]].ls].sum; return res;}
ll S2() {ll res = 0; for (int i = 1; i <= cnt; i++) res += t[t[a[i]].rs].sum; return res;}
int query(int l, int r, int d)
{
if (l == r) return l;
int mid = (l + r) >> 1;
if (S() > d / 2)
{
for (int i = 1; i <= cnt; i++) a[i] = t[a[i]].ls;
return query(l, mid, d);
}
if (S2() > d / 2)
{
for (int i = 1; i <= cnt; i++) a[i] = t[a[i]].rs;
return query(mid + 1, r, d);
}
return -1;
}
}T;
signed main()
{
n = read(), q = read();
for (int i = 1; i <= n; i++)
{
siz[i] = read(); rt[i] = T.build();
for (int j = 1; j <= siz[i]; j++)
{
int x = read();
if (!head[i]) head[i] = ++tot, tail[i] = tot, h[tot] = x;
else
{
nxt[tail[i]] = ++tot;
pre[tot] = tail[i];
tail[i] = tot;
h[tot] = x;
}
T.modify(rt[i], 0, n + q, x, 1);
}
}
for (int i = 1; i <= q; i++)
{
int opt = read();
if (opt == 1)
{
int x = read(), y = read(); siz[x]++;
if (!head[x]) head[x] = ++tot, tail[x] = tot, h[tot] = y;
else
{
nxt[tail[x]] = ++tot;
pre[tot] = tail[x];
tail[x] = tot;
h[tot] = y;
}
T.modify(rt[x], 0, n + q, y, 1);
}
if (opt == 2)
{
int x = read(); siz[x]--;
T.modify(rt[x], 0, n + q, h[tail[x]], -1);
if (!siz[x]) head[x] = tail[x] = 0;
else
{
tail[x] = pre[tail[x]];
nxt[tail[x]] = 0;
}
}
if (opt == 3)
{
int m = read();
ll sum = 0;
cnt = 0;
for (int j = 1; j <= m; j++)
{
int x = read();
a[++cnt] = rt[x];
sum += (ll)siz[x];
}
printf("%lld\n", T.query(0, n + q, sum));
}
if (opt == 4)
{
int x = read(), y = read(), z = read();
if (!siz[x]) head[z] = head[y], tail[z] = tail[y];
else if (!siz[y]) head[z] = head[x], tail[z] = tail[x];
else
{
head[z] = head[x]; tail[z] = tail[y];
nxt[tail[x]] = head[y]; pre[head[y]] = tail[x];
}
rt[z] = T.merge(rt[x], rt[y], 0, n + q);
siz[z] = siz[x] + siz[y];
}
}
return 0;
}