众数杂题选做
众数杂题选做
定义:
-
众数:出现次数最多的数。
-
绝对众数:出现次数严格大于不出现次数的数,即出现次数严格大于 \(\frac{n}{2}\) 的数。
众数不满足区间可加性,一般来说众数相关的问题很少是 poly log 的复杂度。可以尝试往根号方面思考(如分块、莫队),或将数字的出现次数进行根号分治。
一些关于众数的结论:
- 合并两个区间时,新众数一定为大区间的众数或小区间的元素。
- 绝对众数若存在,则只有一个。
- 若存在绝对众数,则随机选元素为绝对众数概率为 \(\frac{1}{2}\) ,随机 \(k\) 次判定存在性时出错概率为 \(\frac{1}{2^k}\) 。
求绝对众数的一个常见方法是摩尔投票法:
- 记和当前众数 \(x\) 相等的数权值为 \(+1\) ,不等的数权值为 \(-1\) ,那么总和一定 \(> 0\) ,并且若出现了前 \(i\) 个数的权值和为 \(0\) ,则剩下部分的权值和一定 \(> 0\) 。
- 先选第一个数为绝对众数,遍历区间时若权值和变为 \(0\) 则令下一个数为绝对众数,最后判断一下答案的合法性(出现次数是否 \(> \frac{n}{2}\) )即可。
- 不难发现摩尔投票法具有区间可加性,可以用线段树维护。
P8496 [NOI2022] 众数
给出 \(n\) 个序列,\(q\) 次操作:
- 在 \(x\) 号序列的末尾插入一个数。
- 在 \(x\) 号序列的末尾删除一个数。
- 求将给出的 \(m\) 个标号的序列拼起来组成的新序列的绝对众数。
- 拼接两个序列 \(x_1, x_2\) 为 \(x_3\) 。
\(n, q \leq 5 \times 10^5\)
考虑用链表维护序列,权值线段树维护出现次数。则前两个操作可以直接修改,第三个操作就是在若干个线段树树上二分,第四个操作就是拼接两个链表和线段树,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e6 + 7;
list<int> li[N];
int n, q, tot;
namespace SMT {
const int S = 3e7 + 7;
int rt[N], cnt[S], lc[S], rc[S];
int tot;
void update(int &x, int nl, int nr, int pos, int k) {
if (!x)
x = ++tot;
cnt[x] += k;
if (nl == nr)
return;
int mid = (nl + nr) >> 1;
if (pos <= mid)
update(lc[x], nl, mid, pos, k);
else
update(rc[x], mid + 1, nr, pos, k);
}
int query(int x, int nl, int nr, int pos) {
if (!x)
return 0;
if (nl == nr)
return cnt[x];
int mid = (nl + nr) >> 1;
return pos <= mid ? query(lc[x], nl, mid, pos) : query(rc[x], mid + 1, nr, pos);
}
int search(vector<int> &kp, int nl, int nr, ll k) {
if (nl == nr)
return nl;
int mid = (nl + nr) >> 1;
ll sum = 0;
for (int it : kp)
sum += cnt[lc[it]];
if (sum >= k) {
for (int &it : kp)
it = lc[it];
return search(kp, nl, mid, k);
} else {
for (int &it : kp)
it = rc[it];
return search(kp, mid + 1, nr, k - sum);
}
}
int merge(int a, int b, int l, int r) {
if (!a || !b)
return a | b;
cnt[a] += cnt[b];
if (l == r)
return a;
int mid = (l + r) >> 1;
lc[a] = merge(lc[a], lc[b], l, mid);
rc[a] = merge(rc[a], rc[b], mid + 1, r);
return a;
}
} // namespace SMT
signed main() {
scanf("%d%d", &n, &q);
for (int i = 1; i <= n; ++i) {
int k;
scanf("%d", &k);
li[i].resize(k);
for (int &it : li[i])
scanf("%d", &it), SMT::update(SMT::rt[i], 0, N, it, 1);
}
while (q--) {
int op;
scanf("%d", &op);
if (op == 1) {
int x, k;
scanf("%d%d", &x, &k);
SMT::update(SMT::rt[x], 0, N, k, 1), li[x].emplace_back(k);
} else if (op == 2) {
int x;
scanf("%d", &x);
SMT::update(SMT::rt[x], 0, N, li[x].back(), -1), li[x].pop_back();
} else if (op == 3) {
int k;
scanf("%d", &k);
vector<int> kp(k), tmp;
ll siz = 0, cnt = 0;
for (int &it : kp)
scanf("%d", &it), siz += SMT::cnt[it = SMT::rt[it]];
tmp = kp;
int ans = SMT::search(tmp, 0, N, (siz + 1) / 2);
for (int it : kp)
cnt += SMT::query(it, 0, N, ans);
printf("%d\n", cnt > siz / 2 ? ans : -1);
} else {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
li[z].splice(li[z].end(), li[x]), li[z].splice(li[z].end(), li[y]);
SMT::rt[z] = SMT::merge(SMT::rt[x], SMT::rt[y], 0, N);
}
}
return 0;
}
P5048 [Ynoi2019 模拟赛] Yuno loves sqrt technology III
静态区间求众数出现次数,强制在线。
\(n, m \leq 5 \times 10^5\) ,ML = 62.5 MB(要求空间 \(O(n)\))
取块长 \(B = \sqrt{n}\) 。
若 \(l, r\) 在同一个块或相邻的块,直接暴力统计即可,否则答案一定为左右散块的元素或中间整块的众数。
预处理 \(s_{i, j}\) 表示第 \(i\) 块到第 \(j\) 块的众数出现次数,可以用桶维护做到时空复杂度 \(O(n \sqrt{n}) - O(n)\) ,再用 vector 记录每个元素的出现位置,同时记录每个元素在相应 vector 中的编号 \(p_x\) 。
查询时先令答案为中间整块的众数,记 \(cnt\) 表示当前众数的出现次数。暴力枚举散块的每个元素,以左边的散块为例,右边同理。对于元素 \(x\) ,若 vector 中下标为 \(p_x + cnt\) 的元素 \(\in [l, r]\) ,则令 \(cnt + 1\) 。而 \(cnt\) 最多只会增加 \(2\sqrt{n}\) ,因此复杂度是正确的。
总时间复杂度 \(O(n \sqrt{n})\) ,总空间复杂度 \(O(n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7, B = 711;
vector<int> place[N];
int s[B][B], a[N], buc[N], L[N], R[N], bel[N], p[N];
int n, m, block, tot;
inline void prework() {
block = sqrt(n);
while (++tot) {
L[tot] = R[tot - 1] + 1, R[tot] = min(n, block * tot);
fill(bel + L[tot], bel + R[tot] + 1, tot);
if (R[tot] == n)
break;
}
for (int i = 1; i <= n; ++i)
p[i] = place[a[i]].size(), place[a[i]].emplace_back(i);
for (int i = 1; i <= tot; ++i) {
int mxcnt = 0;
for (int j = i; j <= tot; ++j) {
for (int k = L[j]; k <= R[j]; ++k)
mxcnt = max(mxcnt, ++buc[a[k]]);
s[i][j] = mxcnt;
}
memset(buc, 0, sizeof(buc));
}
}
inline int query(int l, int r) {
if (bel[r] - bel[l] <= 1) {
int ans = 0;
for (int i = l; i <= r; ++i)
ans = max(ans, ++buc[a[i]]);
for (int i = l; i <= r; ++i)
--buc[a[i]];
return ans;
}
int ans = s[bel[l] + 1][bel[r] - 1];
for (int i = l; i <= R[bel[l]]; ++i)
while (p[i] + ans < place[a[i]].size() && place[a[i]][p[i] + ans] <= r)
++ans;
for (int i = L[bel[r]]; i <= r; ++i)
while (p[i] - ans >= 0 && place[a[i]][p[i] - ans] >= l)
++ans;
return ans;
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
prework();
int lstans = 0;
while (m--) {
int l, r;
scanf("%d%d", &l, &r);
l ^= lstans, r ^= lstans;
printf("%d\n", lstans = query(l, r));
}
return 0;
}
P3709 大爷的字符串题
给定序列 \(a_{1 \sim n}\),每次询问一段区间的价值。
一段区间的价值定义为:有一个初始为 \(0\) 的变量 \(k\) 和初始为空的集合 \(S\) ,每次从这个区间中拿出一个数 \(x\) ,并把 \(x\) 从这个区间中删除,直到区间为空。每次做如下判断后将 \(x\) 插入 \(S\) :
- 如果 \(S\) 为空,则令 \(k \gets k - 1\) 。
- 如果 \(S\) 中有一个元素不小于 \(x\) ,则令 \(k \gets k - 1\) ,并清空 \(S\)。
区间的价值定义为最终 \(k\) 的最大值。
\(n, m \leq 2 \times 10^5\)
显然每次将一段严格上升序列插入 \(S\) 是最优的,问题转化为一个区间可以被分配为多少个严格上升的序列,不难发现答案即为众数出现次数。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7, B = 451;
vector<int> place[N];
int s[B][B], a[N], buc[N], L[N], R[N], bel[N], p[N];
int n, m, block, tot;
inline void prework() {
block = sqrt(n);
while (++tot) {
L[tot] = R[tot - 1] + 1, R[tot] = min(n, block * tot);
fill(bel + L[tot], bel + R[tot] + 1, tot);
if (R[tot] == n)
break;
}
for (int i = 1; i <= n; ++i)
p[i] = place[a[i]].size(), place[a[i]].emplace_back(i);
for (int i = 1; i <= tot; ++i) {
int mxcnt = 0;
for (int j = i; j <= tot; ++j) {
for (int k = L[j]; k <= R[j]; ++k)
mxcnt = max(mxcnt, ++buc[a[k]]);
s[i][j] = mxcnt;
}
memset(buc + 1, 0, sizeof(int) * n);
}
}
inline int query(int l, int r) {
if (bel[r] - bel[l] <= 1) {
int ans = 0;
for (int i = l; i <= r; ++i)
ans = max(ans, ++buc[a[i]]);
for (int i = l; i <= r; ++i)
--buc[a[i]];
return ans;
}
int ans = s[bel[l] + 1][bel[r] - 1];
for (int i = l; i <= R[bel[l]]; ++i)
while (p[i] + ans < place[a[i]].size() && place[a[i]][p[i] + ans] <= r)
++ans;
for (int i = L[bel[r]]; i <= r; ++i)
while (p[i] - ans >= 0 && place[a[i]][p[i] - ans] >= l)
++ans;
return ans;
}
signed main() {
scanf("%d%d", &n, &m);
vector<int> vec;
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), vec.emplace_back(a[i]);
sort(vec.begin(), vec.end()), vec.erase(unique(vec.begin(), vec.end()), vec.end());
for (int i = 1; i <= n; ++i)
a[i] = lower_bound(vec.begin(), vec.end(), a[i]) - vec.begin() + 1;
prework();
while (m--) {
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", -query(l, r));
}
return 0;
}
P8330 [ZJOI2022] 众数
给定一个长度为 \(n\) 的序列,可以进行一次区间加或减操作,求最终众数的最大出现次数以及可能的众数。
\(n \leq 2 \times 10^5\) ,保证 \(a_i\) 不全相等
不难将问题转化为选一段区间,最大化区间内众数出现次数与区间外众数出现次数的和。
先处理操作区间为一段前缀和一段后缀的情况,对每个数 vector 维护出现的位置就可以 \(O(1)\) 查询一段前/后缀中该数的出现次数,该部分时间复杂度 \(O(n)\) 。
考虑根号分治,称出现次数 \(\leq B\) 的数为小数,否则为大数。
若区间外众数为大数,枚举这个大数 \(x\) 和区间内众数 \(y\) ,则一段区间的贡献为 \(y\) 的出现次数减去 \(x\) 的出现次数。不难发现最优区间可以调整为两端点均为 \(y\) ,预处理 \(x\) 出现次数的的前缀和后,把所有 \(y\) 出现的位置拿出来,用前缀和拆贡献即可做到单次线性。该部分时间复杂度 \(O(\frac{n^2}{B})\) 。
若区间内众数为大数,做法也是类似的,时间复杂度 \(O(\frac{n^2}{B})\) 。
否则只要考虑区间内外众数均为小数的情况,枚举区间外众数 \(x\) ,则最优区间一定可以调整为:
- \(l = 1\) 或 \(a_{l - 1} = x\) 。
- \(r = n\) 或 \(a_{r + 1} = x\) 。
考虑扫描线,枚举 \(a_i = x\) 出现的所有位置,直接暴力枚举区间的左右端点,问题转化为求区间众数。
如果直接单次 \(O(\sqrt{n})\) 求解,时间复杂度 \(O(nB \sqrt{n})\) ,无法通过。
考虑挖掘一些性质,由于此时考虑的是区间内众数为小数的情况,因此可以直接钦定区间内的众数为出现次数 \(\leq B\) 且出现次数最多的数字。维护 \(s_i\) 表示左端点为 \(i\) 的区间内众数出现次数,则每次暴力枚举当前数之前所有出现位置并更新。注意到 \(s\) 数组是不增的,而每次更新 \(s\) 时会使得 \(s\) 的总和增加 \(1\) ,故这部分复杂度为 \(O(nB)\) 。
取 \(B = \sqrt{n}\) ,时间复杂度 \(O(n \sqrt{n})\) 。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e5 + 7;
vector<int> p[N];
int a[N], b[N], loc[N], L[N], R[N], bel[N], s[N], ans[N];
int n, m, block, tot;
inline void prework() {
memcpy(b + 1, a + 1, sizeof(int) * n);
sort(b + 1, b + n + 1), m = unique(b + 1, b + n + 1) - b - 1;
for (int i = 1; i <= n; ++i)
a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
for (int i = 1; i <= m; ++i)
p[i].clear();
for (int i = 1; i <= n; ++i)
loc[i] = p[a[i]].size(), p[a[i]].emplace_back(i);
for (int i = 1; i <= m; ++i)
ans[i] = p[i].size();
block = sqrt(n), tot = 0;
while (++tot) {
L[tot] = R[tot - 1] + 1, R[tot] = min(n, block * tot);
fill(bel + L[tot], bel + R[tot] + 1, tot);
if (R[tot] == n)
break;
}
}
inline void solve1() {
for (int i = 1, mx = 0; i <= n; ++i) { // [l, r] is a pre
ans[a[i]] = max(ans[a[i]], (int)p[a[i]].size() - loc[i] + mx);
mx = max(mx, loc[i] + 1);
}
for (int i = n, mx = 0; i; --i) { // [l, r] is a suf
ans[a[i]] = max(ans[a[i]], loc[i] + 1 + mx);
mx = max(mx, (int)p[a[i]].size() - loc[i]);
}
}
inline void solve2() {
for (int i = 1; i <= m; ++i) {
if (p[i].size() <= block)
continue;
for (int j = 1; j <= n; ++j)
s[j] = s[j - 1] + (a[j] == i);
for (int j = 1; j <= m; ++j) {
for (int k = 0, mx = -inf; k < p[j].size(); ++k) // i in sides, j in [l, r]
mx = max(mx, s[p[j][k] - 1] - (k - 1)), ans[i] = max(ans[i], mx + s[n] - s[p[j][k]] + k);
for (int k = 0, mx = -inf; k < p[j].size(); ++k) // i in [l, r], j in sides
mx = max(mx, k + 1 - s[p[j][k]]), ans[j] = max(ans[j], mx + s[p[j][k] - 1] + (int)p[j].size() - k);
}
}
}
inline void solve3() {
memset(s + 1, 0, sizeof(int) * n);
for (int i = 1; i <= n; ++i) {
if (p[a[i]].size() > block)
continue;
for (int j = loc[i] - 1; ~j; --j)
ans[a[i]] = max(ans[a[i]], j + 1 + (int)p[a[i]].size() - loc[i] + s[p[a[i]][j] + 1]);
for (int j = 0; j <= loc[i]; ++j) {
auto update = [](int x, int k) {
while (x && s[x] < k)
s[x--] = k;
};
update(p[a[i]][j], loc[i] - j + 1);
}
}
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
prework(), solve1(), solve2(), solve3();
int mx = *max_element(ans + 1, ans + m + 1);
printf("%d\n", mx);
for (int i = 1; i <= m; ++i)
if (ans[i] == mx)
printf("%d\n", b[i]);
}
return 0;
}
CF1446D2 Frequency Problem (Hard Version)
给定序列 \(a_{1 \sim n}\) ,求最长的区间使得区间内众数数量 \(\geq 2\) 。
\(n \leq 2 \times 10^5\)
首先有结论:原序列众数 \(y\) 必为答案区间内两个众数之一,否则可以不断扩张直到 \(y\) 成为区间内众数之一为止。
接下来考虑枚举所有其他数 \(x\) ,则需要找到 \(x, y\) 出现次数相同的最长区间。
考虑对消,将 \(x\) 视为 \(1\) ,\(y\) 视为 \(-1\) ,问题转化为找到区间和为 \(0\) 的最长区间。
一个暴力是把所有 \(x, y\) 的出现位置记录为 \(P\) ,用一个桶存前缀和为 \(i\) 的第一次出现位置 \(vis_i\) ,枚举到 \(p_i\) 时用 \(p_{i + 1} - 1 - vis_{s_i}\) 更新答案即可。时间复杂度 \(O(nc)\) ,其中 \(c\) 为元素种数,可以通过 CF1446D1 Frequency Problem (Easy Version) 。
考虑优化,只枚举所有 \(x\) 的位置,同时维护未被选的 \(y\) 集合 \(S\) ,每次选到一个 \(x\) 后,在 \(S\) 中向前、向后各找一个 \(y\) 加入 \(P\) ,并在 \(S\) 中删掉这两个位置。不难发现这样并不会影响相邻两段 \(x\) 是否能同时被选,而 \(|P|\) 降到了 \(O(|cnt_x|)\) 级别,时间复杂度降为 \(O(n \log n)\) 。
注意如果要整体处理则需要向前、向后各找两个 \(y\) (因为可能出现形如 xyyyx 被简化为 xyyx 的情况,而这两个 x 是不能连在一起的),如果分段处理就找一个就行。
还可以更优,考虑顺序枚举 \(x\) 的出现位置,对每个位置维护二元组 \((a, b)\) 表示向前/向后第一个 \(y\) 。接着考虑相邻的二元组的合并关系,只要两段之间没有间隔一个 \(y\) 就可以拼接,否则重叠的部分需要额外在两侧都补上。
用栈维护这个过程即可做到 \(O(n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7;
vector<int> p[N];
pair<int, int> sta[N];
int a[N], s[N], pre[N], nxt[N], id[N], vis[N << 1];
int n, q;
inline int solve(int x, int y, vector<int> vec) {
vec.emplace_back(n + 1);
int ans = 0;
vis[n] = pre[vec[0] - 1];
for (int i = 0, s = n; i + 1 < vec.size(); ++i) {
s += (a[vec[i]] == x ? 1 : -1);
if (~vis[s])
ans = max(ans, nxt[vec[i] + 1] - 1 - vis[s]);
else
vis[s] = vec[i];
}
for (int i = 0, s = n; i < vec.size(); ++i)
vis[s += (a[vec[i]] == x ? 1 : -1)] = -1;
return ans;
}
inline int query(int x, int y) {
int top = 0;
for (int u : p[x]) {
pair<int, int> w = make_pair(pre[u], nxt[u]);
while (top && id[w.first] <= id[sta[top].second] + 1) {
int l = w.first, nl = sta[top].first, nr = sta[top].second, r = w.second;
if (l > nl)
swap(l, nl);
if (r < nr)
swap(r, nr);
int cnt = s[nr] - s[nl - 1];
--top, w = make_pair(p[y][max(id[l] - cnt, 0)], p[y][min(id[r] + cnt, (int)p[y].size() - 1)]);
}
sta[++top] = w;
}
int ans = 0;
for (int i = 1, l = 0, r = -1; i <= top; ++i, l = r + 1) {
int nl = sta[i].first, nr = sta[i].second;
while (r + 1 < p[x].size() && p[x][r + 1] <= nr)
++r;
vector<int> vec(r - l + 1 + s[nr] - s[nl - 1]);
merge(p[x].begin() + l, p[x].begin() + r + 1, p[y].begin() + s[nl - 1], p[y].begin() + s[nr], vec.begin());
ans = max(ans, solve(x, y, vec));
}
return ans;
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), p[a[i]].emplace_back(i);
int y = 1;
for (int i = 2; i <= n; ++i)
if (p[i].size() > p[y].size())
y = i;
p[y].insert(p[y].begin(), 0), p[y].emplace_back(n + 1);
for (int i = 0; i < p[y].size(); ++i)
id[p[y][i]] = i;
s[0] = 1;
for (int i = 1; i <= n + 1; i++)
s[i] = s[i - 1] + (a[i] == y);
for (int i = 1; i <= n; ++i)
pre[i] = (a[i] == y ? i : pre[i - 1]);
nxt[n + 1] = n + 1;
for (int i = n; i; --i)
nxt[i] = (a[i] == y ? i : nxt[i + 1]);
memset(vis, -1, sizeof(vis));
int ans = 0;
for (int i = 1; i <= n; ++i)
if (!p[i].empty() && i != y)
ans = max(ans, query(i, y));
printf("%d", ans);
return 0;
}
[ABC272G] Yet Another mod M
给定一个长度为 \(n\) 的序列 \(a\) ,其中 \(a_i\) 为互不相同的正整数。
选择一个范围在 \([3, 10^9]\) 的正整数 \(M\) ,并将所有数都模去 \(M\) 。
若能找到一个 \(M\) 使得序列中存在一个数 \(x\) ,且满足 \(a_i = x\) 的数量大于 \(a_i \not = x\) 的数量,输出这个 \(M\) 。
\(3 \leq n \leq 5000\) ,\(a_i \leq 10^9\)
称 \(a_i \bmod M = x\) 的 \(a_i\) 构成答案集合,则对于答案集合中的任意两个数 \(x, y\) ,均有 \(M \mid (x - y)\) 。
由于答案集合大小超过 \(\frac{n}{2}\) ,因此考虑随机钦定两个属于答案集合若干次,再对 \(|a_i - a_j|\) 质因数分解,判断每个质因数是否能成为 \(M\) 即可。
每次成功的概率至少为 \(\frac{1}{4}\) ,随机 \(10\) 次即可通过。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e3 + 7;
int a[N], b[N];
mt19937 myrand(time(0));
int n;
inline int check(int x, int y) {
if (x == y)
return -1;
int k = abs(a[x] - a[y]);
auto check = [](int m, int x) {
if (m < 3)
return false;
int cnt = 0;
for (int i = 1; i <= n; ++i)
cnt += (a[i] % m == x);
return cnt > n / 2;
};
for (int i = 1; i * i <= k; ++i)
if (!(k % i)) {
if (check(i, a[x] % i))
return i;
else if (k / i != i && check(k / i, a[x] % (k / i)))
return k / i;
}
return -1;
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
sort(a + 1, a + 1 + n);
uniform_int_distribution<int> lim(1, n);
for (int i = 1; i <= 10; ++i) {
int ans = check(lim(myrand), lim(myrand));
if (~ans)
return printf("%d", ans), 0;
}
puts("-1");
return 0;
}
P3765 总统选举
给出序列 \(a_{1 \sim n}\) ,\(m\) 次操作,每次给出 \(l, r, s, k\) 以及 \(k\) 个下标,记 \(x\) 为区间 \([l, r]\) 内的绝对众数,若不存在则为 \(s\) ,然后将给出的 \(k\) 个下标的值都改为 \(x\) 。
每次操作后输出 \(x\) ,最后输出序列的绝对众数。
\(n, m \leq 5 \times 10^5\) ,\(\sum k_i \leq 10^6\) ,ML = 125MB
对每个数开一个 fhq-Treap 维护出现下标(由于空间限制不能用动态开点线段树),这样查询出现次数与处理修改操作都比较容易。
求区间众数就用线段树维护摩尔投票,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef unsigned int uint;
using namespace std;
const int N = 5e5 + 7;
int a[N];
mt19937 myrand(time(0));
int n, m;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
namespace SMT {
struct Node {
int ans, cnt;
inline Node operator + (const Node &rhs) const {
Node res;
if (ans == rhs.ans)
res.ans = ans, res.cnt = cnt + rhs.cnt;
else if (cnt > rhs.cnt)
res.ans = ans, res.cnt = cnt - rhs.cnt;
else
res.ans = rhs.ans, res.cnt = rhs.cnt - cnt;
return res;
}
} nd[N << 2];
int ans[N << 2], cnt[N << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
inline void pushup(int x) {
nd[x] = nd[ls(x)] + nd[rs(x)];
}
void build(int x, int l, int r) {
if (l == r) {
nd[x] = (Node) {a[l], 1};
return;
}
int mid = (l + r) >> 1;
build(ls(x), l, mid), build(rs(x), mid + 1, r);
pushup(x);
}
void update(int x, int nl, int nr, int pos, int k) {
if (nl == nr) {
nd[x] = (Node) {k, 1};
return;
}
int mid = (nl + nr) >> 1;
if (pos <= mid)
update(ls(x), nl, mid, pos, k);
else
update(rs(x), mid + 1, nr, pos, k);
pushup(x);
}
Node query(int x, int nl, int nr, int l, int r) {
if (l <= nl && nr <= r)
return nd[x];
int mid = (nl + nr) >> 1;
if (r <= mid)
return query(ls(x), nl, mid, l, r);
else if (l > mid)
return query(rs(x), mid + 1, nr, l, r);
else
return query(ls(x), nl, mid, l, r) + query(rs(x), mid + 1, nr, l, r);
}
} // namespace SMT
namespace fhqTreap {
uint dat[N];
int lc[N], rc[N], siz[N], rt[N];
inline void pushup(int x) {
siz[x] = siz[lc[x]] + 1 + siz[rc[x]];
}
int merge(int a, int b) {
if (!a || !b)
return a | b;
if (dat[a] > dat[b])
return rc[a] = merge(rc[a], b), pushup(a), a;
else
return lc[b] = merge(a, lc[b]), pushup(b), b;
}
void split(int x, int k, int &a, int &b) {
if (!x) {
a = b = 0;
return;
}
if (x <= k)
a = x, split(rc[x], k, rc[a], b);
else
b = x, split(lc[x], k, a, lc[b]);
pushup(x);
}
inline int query(int x, int l, int r) {
int a, b, c;
split(rt[x], r, a, c), split(a, l - 1, a, b);
int cnt = siz[b];
rt[x] = merge(merge(a, b), c);
return cnt;
}
inline void insert(int x, int k) {
int a, b;
split(rt[x], k, a, b), dat[k] = myrand(), rt[x] = merge(merge(a, k), b);
}
inline void remove(int x, int k) {
int a, b, c;
split(rt[x], k, a, c), split(a, k - 1, a, b);
rt[x] = merge(a, c);
}
} // namespace fhqTreap
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), fhqTreap::insert(a[i], i);
SMT::build(1, 1, n);
while (m--) {
int l, r, s, k;
scanf("%d%d%d%d", &l, &r, &s, &k);
vector<int> kp(k);
for (int &it : kp)
scanf("%d", &it);
int x = SMT::query(1, 1, n, l, r).ans;
if (fhqTreap::query(x, l, r) > (r - l + 1) / 2)
s = x;
printf("%d\n", s);
for (int it : kp) {
fhqTreap::remove(a[it], it), fhqTreap::insert(a[it] = s, it);
SMT::update(1, 1, n, it, a[it]);
}
}
printf("%d", fhqTreap::siz[fhqTreap::rt[SMT::nd[1].ans]] > n / 2 ? SMT::nd[1].ans : -1);
return 0;
}
CF643G Choosing Ads
给定 \(a_{1 \sim n}\) 和整数 \(p\) ,\(m\) 次操作,操作有:
- 区间赋值。
- 查询区间内出现次数百分比 \(\geq p\) 的数。
- 输出询问的答案时,可以包含错的数,也可以重复输出,但对的数一定要在答案中,且输出的数的个数 \(\leq \lfloor \frac{100}{p} \rfloor\) 。
\(n, m \leq 1.5 \times 10^5\) ,\(20 \leq p \leq 100\)
先考虑 \(p > 50\) 的情况,答案即为区间绝对众数,不难用线段树维护摩尔投票处理。
当 \(p \leq 50\) 时考虑类似的做法,每次合并区间时只保留 \(\lfloor \frac{100}{p} \rfloor\) 个互不相同的数,这些数一定包含了所有答案。
时间复杂度 \(O((n + m) \lfloor \frac{100}{p} \rfloor^2 \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1.5e5 + 7;
int a[N];
int n, m, p;
namespace SMT {
vector<pair<int, int> > vec[N << 2];
int len[N << 2], tag[N << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
inline vector<pair<int, int> > operator + (vector<pair<int, int> > a, vector<pair<int, int> > b) {
for (auto it : b) {
bool flag = false;
for (auto &x : a)
if (x.first == it.first) {
x.second += it.second, flag = true;
break;
}
if (flag)
continue;
a.emplace_back(it);
if (a.size() > p) {
int mn = inf;
for (auto x : a)
mn = min(mn, x.second);
vector<pair<int, int> > c;
for (auto x : a)
if (x.second != mn)
c.emplace_back(x.first, x.second - mn);
a = c;
}
}
return a;
}
inline void spread(int x, int k) {
vec[x] = {make_pair(k, len[x])}, tag[x] = k;
}
inline void pushdown(int x) {
if (~tag[x])
spread(ls(x), tag[x]), spread(rs(x), tag[x]), tag[x] = -1;
}
void build(int x, int l, int r) {
len[x] = r - l + 1, tag[x] = -1;
if (l == r) {
vec[x] = {make_pair(a[l], 1)};
return;
}
int mid = (l + r) >> 1;
build(ls(x), l, mid), build(rs(x), mid + 1, r);
vec[x] = vec[ls(x)] + vec[rs(x)];
}
void update(int x, int nl, int nr, int l, int r, int k) {
if (l <= nl && nr <= r) {
spread(x, k);
return;
}
pushdown(x);
int mid = (nl + nr) >> 1;
if (l <= mid)
update(ls(x), nl, mid, l, r, k);
if (r > mid)
update(rs(x), mid + 1, nr, l, r, k);
vec[x] = vec[ls(x)] + vec[rs(x)];
}
vector<pair<int, int> > query(int x, int nl, int nr, int l, int r) {
if (l <= nl && nr <= r)
return vec[x];
pushdown(x);
int mid = (nl + nr) >> 1;
if (r <= mid)
return query(ls(x), nl, mid, l, r);
else if (l > mid)
return query(rs(x), mid + 1, nr, l, r);
else
return query(ls(x), nl, mid, l, r) + query(rs(x), mid + 1, nr, l, r);
}
} // namespace SMT
signed main() {
scanf("%d%d%d", &n, &m, &p), p = 100 / p;
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
SMT::build(1, 1, n);
while (m--) {
int op, l, r;
scanf("%d%d%d", &op, &l, &r);
if (op == 1) {
int k;
scanf("%d", &k);
SMT::update(1, 1, n, l, r, k);
} else {
auto ans = SMT::query(1, 1, n, l, r);
printf("%d ", (int)ans.size());
for (auto it : ans)
printf("%d ", it.first);
puts("");
}
}
return 0;
}
P4062 [Code+#1] Yazid 的新生舞会
给定序列 \(a_{1 \sim n}\) ,求存在绝对众数的区间数量。
\(n \leq5 \times 10^5\)
对于数字 \(x\) ,记 \(s_i\) 表示前 \(i\) 个数字中 \(x\) 的出现次数,则 \(x\) 是 \([l + 1, r]\) 的绝对众数当且仅当 \(s_r - s_l > \frac{r - l}{2}\) ,即 \(2s_l - l < 2s_r - r\) 。
考虑对于每个数字分别统计顺序对的数量。记 \(x\) 的相邻两次出现位置为 \(p_{x, i - 1}\) 与 \(p_{x, i}\) ,考虑 \(r \in [p_{x, i - 1}, p_{x, i})\) 时合法的 \(l\) 的数量。
注意到 \(i\) 增加时 \(2s_i - i\) 是一个公差为 \(-1\) 的等差数列,因此问题转化为区间加、区间查询一段连续的前缀和。
考虑树状数组维护差分数组,问题转化为单点加、查询三阶前缀和,不难做到 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 5e5 + 7;
vector<int> p[N];
int a[N];
int n, testid;
namespace BIT {
ll s[N * 3][3];
ll a[N * 3];
inline void modify(int x, int k) {
ll val[3] = {k, 1ll * k * x, 1ll * k * x * x};
for (; x <= n * 3 + 2; x += x & -x)
for (int i = 0; i < 3; ++i)
s[x][i] += val[i];
}
inline void update(int l, int r, int k) {
l += n + 2, r += n + 2;
modify(l, k), modify(r + 1, -k);
}
inline ll ask(int x) {
ll k = x, res = 0;
for (; x; x -= x & -x)
res += s[x][2] - (k * 2 + 3) * s[x][1] + (k + 2) * (k + 1) * s[x][0];
return res;
}
inline ll query(int l, int r) {
l += n + 2, r += n + 2;
return ask(r) - ask(l - 1);
}
} // namespace BIT
signed main() {
scanf("%d%d", &n, &testid);
for (int i = 0; i < n; ++i)
p[i].emplace_back(0);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), p[a[i]].emplace_back(i);
for (int i = 0; i < n; ++i)
p[i].emplace_back(n + 1);
ll ans = 0;
for (int i = 0; i < n; ++i) {
vector<pair<int, int> > upd;
for (int j = 0; j + 1 < p[i].size(); ++j) {
ans += BIT::query(j * 2 - (p[i][j + 1] - 1) - 1, j * 2 - p[i][j] - 1);
BIT::update(j * 2 - (p[i][j + 1] - 1), j * 2 - p[i][j], 1);
upd.emplace_back(j * 2 - (p[i][j + 1] - 1), j * 2 - p[i][j]);
}
for (auto it : upd)
BIT::update(it.first, it.second, -1);
}
printf("%lld", ans / 2);
return 0;
}
P11882 [RMI 2024] 彩虹糖 / Skittlez
有一个 \(n\) 行 \(n\) 列的矩阵,每个位置初始有一个空的可重集。\(q\) 次操作,每次对 \(x \in [x_1, x_2], y \in [y_1, y_2]\) 的子矩阵内的所有位置上的集合加入 \(k\) 个数字 \(c\) 。
所有操作后求每个位置上集合的绝对众数。
\(n \leq 10^3\) ,\(q \leq 5 \times 10^5\)
考虑将 \(c\) 二进制拆分,若一个位置上某一位的数量超过一半且存在绝对众数,则绝对众数的这一位必然为 \(1\) ,否则必然不为 \(1\) 。用二维前缀和处理每个位置上这一位为 \(1\) 的 \(c\) 的数量。
最后再判一下构造出的可能的绝对众数的出现次数是否超过一半,枚举所有 \(c\) 为该值的修改,将所有可能的绝对众数为 \(c\) 的位置拿出来一起判定。这是一个二维数点问题,直接扫描线即可。
时间复杂度 \(O((n^2 + q) \log q)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e3 + 7, M = 5e5 + 7;
struct Update {
int x, y, xx, yy, k;
};
struct Node {
int x, yl, yr, k;
inline bool operator < (const Node &rhs) const {
return x == rhs.x ? yr > rhs.yr : x < rhs.x;
}
};
vector<Update> upd[M];
vector<Node> nd[M];
ll a[N][N], s[N][N];
int ans[N][N];
int n, q;
namespace BIT {
ll c[N];
inline void update(int x, int k) {
for (; x <= n; x += x & -x)
c[x] += k;
}
inline ll query(int x) {
ll res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
} // namespace BIT
signed main() {
scanf("%d%d", &n, &q);
for (int i = 1; i <= q; ++i) {
int x, y, xx, yy, c, k;
scanf("%d%d%d%d%d%d", &x, &y, &xx, &yy, &c, &k);
a[x][y] += k, a[xx + 1][y] -= k, a[x][yy + 1] -= k, a[xx + 1][yy + 1] += k;
upd[c].emplace_back((Update){x, y, xx, yy, k});
nd[c].emplace_back((Node){x, y, yy, k}), nd[c].emplace_back((Node){xx + 1, y, yy, -k});
}
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
a[i][j] += a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1];
for (int i = 0; i <= __lg(q); ++i) {
for (int j = 1; j <= n; ++j)
memset(s[j] + 1, 0, sizeof(ll) * n);
for (int j = 1; j <= q; ++j)
if (j >> i & 1)
for (Update it : upd[j]) {
s[it.x][it.y] += it.k, s[it.xx + 1][it.y] -= it.k;
s[it.x][it.yy + 1] -= it.k, s[it.xx + 1][it.yy + 1] += it.k;
}
for (int j = 1; j <= n; ++j)
for (int k = 1; k <= n; ++k) {
s[j][k] += s[j - 1][k] + s[j][k - 1] - s[j - 1][k - 1];
if (s[j][k] > a[j][k] / 2)
ans[j][k] |= 1 << i;
}
}
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) {
if (1 <= ans[i][j] && ans[i][j] <= q)
nd[ans[i][j]].emplace_back((Node){i, j, -1, 0});
else
ans[i][j] = -1;
}
for (int i = 1; i <= q; ++i) {
sort(nd[i].begin(), nd[i].end());
for (Node it : nd[i]) {
if (~it.yr)
BIT::update(it.yl, it.k), BIT::update(it.yr + 1, -it.k);
else if (BIT::query(it.yl) <= a[it.x][it.yl] / 2)
ans[it.x][it.yl] = -1;
}
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j)
printf("%d ", ans[i][j]);
puts("");
}
return 0;
}
P12426 [BalticOI 2025] BOI acronym
有一个仅由
B、O、I组成的字符串,其中保证B的出现次数严格多于O和I,给出每个子串(共 \(\frac{n(n + 1)}{2}\) 个)中出现次数最多的字符的出现次数 \(c_{l, r}\) ,求所有B出现的位置。\(n \leq 2000\)
首先不难求出最左端和最右端 B 的位置 \(L, R\) ,即前后缀 \(c\) 变化的位置。
接下来考虑从左到右确定每个 B 的位置,枚举到 \(i\) 时,记 \(x, y\) 表示 \([L, i - 1]\) 和 \([i, R]\) 中 B 的出现次数,分类讨论:
- 若
B在 \([L, i - 1]\) 中的出现次数严格多于O和I(即 \(c_{L, i - 1} = x\) 且 \(c_{L + 1, i - 1} = x - 1\)),此时若 \(c_{L, i} = c_{L, i - 1} + 1\) ,则说明 \(i\) 位置为B。 - 若
B在 \([i, R]\) 中的出现次数严格多于O和I(即 \(c_{i, R} = y\) 且 \(c_{i, R - 1} = y - 1\)),此时若 \(c_{i, R} = c_{i + 1, R} + 1\) ,则说明 \(i\) 位置为B。 - 否则
O和I一定分别是两边的众数之一(可能B也是),此时取 \((L, i - 1]\) 和 \([i, R)\) 两段区间,这样O和I在两边的出现次数分别为严格最多,因此可以用上面的方法判定 \(i\) 是否为O或I。
时间复杂度 \(O(n^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 7;
int c[N][N];
int n;
signed main() {
scanf("%d", &n);
if (n == 1)
return puts("1"), 0;
for (int i = 1; i <= n; ++i)
for (int j = i; j <= n; ++j)
scanf("%d", c[i] + j);
int L = 1, R = n;
while (c[L + 1][n] == c[1][n])
++L;
while (c[1][R - 1] == c[1][n])
--R;
printf("%d ", L);
for (int i = L + 1, cnt = 1; i < R; ++i) {
if (c[L][i - 1] == cnt && c[L + 1][i - 1] == cnt - 1) {
if (c[L][i] == c[L][i - 1] + 1)
printf("%d ", i), ++cnt;
} else if (c[i][R] == c[L][R] - cnt && c[i][R - 1] == c[L][R] - cnt - 1) {
if (c[i][R] == c[i + 1][R] + 1)
printf("%d ", i), ++cnt;
} else if (c[L + 1][i] != c[L + 1][i - 1] + 1 && c[i][R - 1] != c[i + 1][R - 1] + 1)
printf("%d ", i), ++cnt;
}
printf("%d", R);
return 0;
}

浙公网安备 33010602011771号