莫队
考虑一种暴力,维护 \(cnt_i\) 表示 \(i\) 当前出现了多少次。
对于所有询问 \(i\) 保存询问 \(i-1\) 的 \(cnt\),暴力删除上一次询问有,这一次没有的数,并加入上一次询问没有,这一次询问有的数。
加入、删除一个数的代码
void add(int x) {
cnt[x]++;
if (cnt[x] == 1)
ans++;
}
void del(int x) {
cnt[x]--;
if (!cnt[x])
ans--;
}
查询代码
while (l > ql)
add(a[--l]);
while (r < qr)
add(a[++r]);
while (l < ql)
del(a[l++]);
while (r > qr)
del(a[r--]);
cout << ans << '\n';
利用分块的思想对上述算法进行优化。
把询问离线下来,并对整个序列分块,左端点在同一个块的一起处理,左端点所在块相同的询问在按右端点从小到大处理。
这样,左端点最多移动 \(m\sqrt{n}\) 次,右端点最多移动 \(m\sqrt{n}\) 次,总时间复杂度为 \(m\sqrt{n}\)。
#include <bits/stdc++.h>
using namespace std;
int n, m, L, now, a[100005], b[100005], cnt[100005], ans[100005];
struct query {
int l, r, id;
} q[100005];
bool cmp(query x, query y) {
if (b[x.l] == b[y.l])
return x.r < y.r;
return b[x.l] < b[y.l];
}
void add(int x) {
cnt[x]++;
if (cnt[x] == 1)
now++;
}
void del(int x) {
cnt[x]--;
if (!cnt[x])
now--;
}
int main() {
cin >> n;
L = sqrt(n);
for (int i = 1; i <= n; i++)
cin >> a[i], b[i] = (i - 1) / L + 1;
cin >> m;
for (int i = 1; i <= m; i++)
cin >> q[i].l >> q[i].r, q[i].id = i;
sort(q + 1, q + m + 1, cmp);
for (int i = 1, l = 1, r = 0; i <= m; i++) {
while (l > q[i].l)
add(a[--l]);
while (r < q[i].r)
add(a[++r]);
while (l < q[i].l)
del(a[l++]);
while (r > q[i].r)
del(a[r--]);
ans[q[i].id] = now;
}
for (int i = 1; i <= m; i++)
cout << ans[i] << '\n';
return 0;
}
整体二分
对于一些有多次询问,且答案具有单调性的题目,对于单个询问二分复杂度为 \(O(n\log v)\),总时间复杂度达到 \(O(qn\log v)\),其中 \(v\) 表示答案上限,直接二分不可做。
整体二分的核心思路:枚举答案区间 \([l,r]\),确定那些询问的答案在 \([l,r]\) 中。
对于每个答案,直接枚举所有询问显然是不可取的。
可以利用分治的思想,先确定答案区间 \([1,v]\)、询问区间 \([1,q]\),然后考虑将答案区间分为两个长度为 \(\dfrac{v}{2}\) 级别的答案区间 \([1,\dfrac{v}{2}],[\dfrac{v}{2}+1,v]\),划分时分别确定那些询问在答案区间 \([1,\dfrac{v}{2}],[\dfrac{v}{2}+1,v]\) 中,再将这些答案区间分别划为两半,以此类推。
最后就得到 \(v\) 个长度为 \(1\) 的答案区间,也同时得到了每个答案区间包含了那些询问。
这是整体二分递归时下传的几个参数:
-
\(l,r\) 表示当前考虑的答案区间。
-
\(ql,qr\) 表示答案在 \([l,r]\) 中的询问编号为 \(ql\sim qr\)。(整体二分在划分区间后会重新排列询问顺序)
整体二分代码框架大致如下:
void solve(int l, int r, int ql, int qr) {
if (l == r) {
for (int i = ql; i <= qr; i++)
ans[q[i]] = l; //答案确定,记录区间
return;
}
int mid = l + r >> 1, cnt1 = 0, cnt2 = 0;
//......
for (int i = ql; i <= qr; i++) {
if (/*......*/)
q1[++cnt1] = q[i]; //确定哪些询问的答案在 [l,mid] 中
else
q2[++cnt2] = q[i]; //确定哪些询问的答案在 [mid+1,r] 中
}
for (int i = ql; i <= ql + cnt1 - 1; i++)
q[i] = q1[i - ql + 1]; //将答案小的放在前面,以便往下递归时快速找到查询区间
for (int i = ql + cnt1; i <= qr; i++)
q[i] = q2[i - ql - cnt1 + 1];
solve(l, mid, ql, ql + cnt1 - 1); //继续划分答案区间
solve(mid + 1, r, ql + cnt1, qr);
}
思路
例题
题目给定的是一个环,第一步肯定是断环成链,将 \(o_i\) 复制一份到 \(o_{i+m}\)。
显然,答案具有单调性,对于每个国家二分一次的做法复杂度不能接受,考虑整体二分。
考虑快速区分某个询问的答案是在 \([l,mid]\) 还是 \([mid+1,r]\)。
对于一次划分操作,直接执行一遍 \(l\sim mid\) 操作,用差分树状数组维护每个下标的值。
对于国家 \(i\),如果它在操作完之后总共收集到的陨石达到 \(p_i\),那么它的答案在 \([l,mid]\),否则在 \([mid+1,r]\)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, k, o[1000005], q[1000005], q1[1000005], q2[1000005];
long long c[1000005], p[1000005], ans[1000005];
vector<int> s[1000005];
struct data {
int l, r;
long long a;
} d[1000005];
void add(int x, int k) {
while (x <= m) {
c[x] += k;
x += (x & -x);
}
}
long long query(int x) {
long long ret = 0;
while (x) {
ret += c[x];
x -= (x & -x);
}
return ret;
}
long long sum(int x) {
long long ret = 0;
for (auto i : s[x]) {
ret += query(i);
if (ret > p[x])
return ret;
}
return ret;
}
void solve(int l, int r, int ql, int qr) {
if (l == r) {
for (int i = ql; i <= qr; i++)
ans[q[i]] = l;
return;
}
int mid = l + r >> 1, cnt1 = 0, cnt2 = 0;
for (int i = l; i <= mid; i++)
add(d[i].l, d[i].a), add(d[i].r + 1, -d[i].a);
for (int i = ql; i <= qr; i++) {
long long tmp = sum(q[i]);
if (tmp >= p[q[i]])
q1[++cnt1] = q[i];
else
q2[++cnt2] = q[i], p[q[i]] -= tmp;
//下面会复原 l~mid 的操作,在后续递归中 p[q[i]] 会少算 sum(q[i]),因为后续计算只在乎大小,可以直接把它减去 sum(q[i])
}
for (int i = l; i <= mid; i++) //复原 l~mid 的操作
add(d[i].l, -d[i].a), add(d[i].r + 1, d[i].a);
for (int i = ql; i <= ql + cnt1 - 1; i++)
q[i] = q1[i - ql + 1];
for (int i = ql + cnt1; i <= qr; i++)
q[i] = q2[i - ql - cnt1 + 1];
solve(l, mid, ql, ql + cnt1 - 1);
solve(mid + 1, r, ql + cnt1, qr);
}
int main() {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
cin >> o[i];
o[i + m] = o[i]; //破环为链
s[o[i]].push_back(i);
s[o[i]].push_back(i + m);
}
for (int i = 1; i <= n; i++)
cin >> p[i];
cin >> k;
for (int i = 1; i <= k; i++) {
cin >> d[i].l >> d[i].r >> d[i].a;
if (d[i].l > d[i].r)
d[i].r += m;
}
for (int i = 1; i <= n; i++)
q[i] = i;
d[++k] = {1, k, (long long)1e18}, m *= 2;
//这里还要补上一个一定能使任意国家最终收获 p[i] 陨石的操作,用于判断无解,防止溢出
solve(1, k, 1, n);
for (int i = 1; i <= n; i++) {
if (ans[i] == k)
cout << "NIE\n";
else
cout << ans[i] << '\n';
}
return 0;
}