杂题选讲
G. Periodic RMQ Problem
题意
需要对区间进行两种操作:1、将 [ l , r ] [l,r] [l,r]区间所有 a i a_{i} ai赋值为 x x x,2、求区间 [ l , r ] [l,r] [l,r]内最小值
分析
观察到数据范围可知,区间长度为 n ∗ k n*k n∗k,高达 1 e 9 1e9 1e9,所以不能用普通的线段树进行维护,区间之间重复度很高并且需要支持的操作数只有 1 e 5 1e5 1e5,考虑动态开点线段树。 动态开点线段树不需要提前建树,只需要在操作的时候去访问节点,如果一个点都没有查询到,说明这个区间是没有进行修改的,然后一边修改一边建立新的儿子节点即可,然而对于记录区间最小值,显然不可以暴力的查询最小值,需要提前用 S T ST ST表储存 R M Q RMQ RMQ,可能会存在区间横跨多个数段的情况, S T ST ST表应该存储两个区间段,若横跨了多个区间也能快速得到区间最小值
代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int maxn = 2e5 + 10;
const int maxm = 1e7 + 10;
const int logn = 20;
int n, m, q, a[maxn], lg[maxn], f[maxn][logn];
int tag[maxm], tree[maxm], ls[maxm], rs[maxm], tot, rt;
int qmin(int l, int r) {
if (l > r) {
return 2e9;
}
if (r - l + 1 >= n) {
return f[1][lg[n * 2]];
}
int tmp = (l - 1) / n;
l -= tmp * n;
r -= tmp * n;
int k = lg[r - l + 1];
return min(f[l][k], f[r - (1 << k) + 1][k]);
}
void pushdown(int x) {
if (tag[x] == -1) {
return;
}
if (!ls[x]) {
ls[x] = ++tot;
}
if (!rs[x]) {
rs[x] = ++tot;
}
tag[ls[x]] = tag[rs[x]] = tree[ls[x]] = tree[rs[x]] = tag[x];
tag[x] = -1;
}
void pushup(int x, int l, int r) {
int mid = (l + r) >> 1;
tree[x] = min(ls[x] ? tree[ls[x]] : qmin(l, mid), rs[x] ? tree[rs[x]] : qmin(mid + 1, r));
}
void update(int &rt, int l, int r, int ql, int qr, int x) {
if (!rt) {
rt = ++tot;
}
if (ql <= l && r <= qr) {
tree[rt] = tag[rt] = x;
return;
}
pushdown(rt);
int mid = (l + r) >> 1;
if (ql <= mid) {
update(ls[rt], l, mid, ql, qr, x);
}
if (qr > mid) {
update(rs[rt], mid + 1, r, ql, qr, x);
}
pushup(rt, l, r);
}
int query(int rt, int l, int r, int ql, int qr) {
if (!rt) {
return qmin(max(l, ql), min(r, qr));
}
if (ql <= l && r <= qr) {
return tree[rt];
}
pushdown(rt);
int mid = (l + r) >> 1, res = 2e9;
if (ql <= mid) {
res = min(res, query(ls[rt], l, mid, ql, qr));
}
if (qr > mid) {
res = min(res, query(rs[rt], mid + 1, r, ql, qr));
}
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
memset(tag, -1, sizeof(tag));
lg[0] = -1;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
a[i + n] = a[i];
}
for (int i = 1; i <= n * 2; ++i) {
f[i][0] = a[i];
lg[i] = lg[i >> 1] + 1;
}
for (int j = 1; (1 << j) <= n * 2; ++j) {
for (int i = 1; i + (1 << j) - 1 <= n * 2; ++i) {
f[i][j] = min(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
}
}
cin >> q;
while (q--) {
int op, l, r, x;
cin >> op >> l >> r;
if (op == 1) {
cin >> x;
update(rt, 1, n * m, l, r, x);
} else {
cout << query(rt, 1, n * m, l, r) << '\n';
}
}
return 0;
}
D. Imbalanced Array
题意
求数组中每个子段的最大值与最小值的差的和
分析
因为子段一定是连续的,考虑 a i a_{i} ai什么时候是最大值或最小值,因为只有当 a i a_{i} ai是最大值或最小值的时候才对答案有贡献。
当 a i a_{i} ai是最大值的时候,一定是 a i − x a_{i}-x ai−x,那么它对于答案是正向作用,当 a i a_{i} ai是最小值的时候,一定是 x − a i x-a_{i} x−ai,那么它对于答案是负向作用。因此要找到 i i i左右两边比 a i a_{i} ai大的数的下标,比 a i a_{i} ai小的数的下标,考虑单调栈,然后就是计算贡献了。
代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
LL f(vector<LL> a) {
int n = a.size() - 1;
stack<int> st;
vector<int> l(n + 1), r(n + 1);
for (int i = 1; i <= n; i++) {
while (!st.empty() && a[st.top()] < a[i]) {
st.pop();
}
if (st.empty()) {
l[i] = 0;
} else {
l[i] = st.top();
}
st.push(i);
}
while (!st.empty()) {
st.pop();
}
for (int i = n; i >= 1; i--) {
while (!st.empty() && a[st.top()] <= a[i]) {
st.pop();
}
if (st.empty()) {
r[i] = n + 1;
} else {
r[i] = st.top();
}
st.push(i);
}
LL res = 0;
for (int i = 1; i <= n; i++) {
res += 1LL * a[i] * (i - l[i]) * (r[i] - i);
}
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<LL> a(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
LL ans = 0;
ans += f(a);
for (int i = 1; i <= n; i++) {
a[i] = -a[i];
}
ans += f(a);
cout << ans << '\n';
return 0;
}
F. MEX Queries
题意
给定一个空集合,需要完成三种操作:1、将 [ l , r ] [l,r] [l,r]中所有不存在的数加进去,2、将 [ l , r ] [l,r] [l,r]中所有存在的数删掉,3、将 [ l , r ] [l,r] [l,r]中所有不存在的数加进去并且将所有存在的数删去
分析
根据题意,可以使用类似于珂朵莉树的数据结构,随机数据下时间复杂度为亚线性。对于一个区间,当一个点的值为0时代表这个点已经被删除或者未加入,当值为1时,表示它就在集合里。对于一个区间,只需要记录 l l l点和 r + 1 r+1 r+1点的状态,因为操作 1 1 1和操作 2 2 2相当于把 [ l , r ] [l,r] [l,r]全部设置为 1 1 1,把 [ l , r ] [l,r] [l,r]全部设置为 0 0 0, ( l , r + 1 ) (l,r+1) (l,r+1)之间的状态已经不再需要了,这样每次需要处理的次数就非常少,复杂度接近线性
代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
map<LL, LL> s;
void f(LL x) {
auto it = prev(s.upper_bound(x));
s[x] = it->second;
}
void cover(LL l, LL r, int x) {
f(l);
f(r + 1);
auto it = s.find(l);
while (it->first != r + 1) {
it = s.erase(it);
}
s[l] = x;
}
void rev(LL l, LL r) {
f(l);
f(r + 1);
auto it = s.find(l);
while (it->first != r + 1) {
it->second ^= 1;
it++;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;
cin >> q;
s[1] = 0;
while (q--) {
int op;
LL l, r;
cin >> op >> l >> r;
if (op == 1) {
cover(l, r, 1);
} else if (op == 2) {
cover(l, r, 0);
} else {
rev(l, r);
}
auto it = s.begin();
while (it->second) {
it++;
}
cout << it->first << '\n';
}
return 0;
}
E. Chemistry in Berland
题意
有 n n n种材料,现在 n n n种材料每种有 b i b_{i} bi个,一共需要 a i a_{i} ai个,然后存在一种转换: k i k_{i} ki个 x i x_{i} xi材料可以合成1个 i i i材料,并且1个 i i i材料也可以合成1个 x i x_{i} xi材料,问能否满足全部需求
分析
通过数据范围可知,材料之间的转化过程就是一棵树形结构,那么可以定义节点 1 1 1作为根节点 d f s dfs dfs,倘若现在的材料还有剩余,则把多的都等比例扔给其父节点,若不够,则把需求按比例扔给其父节点,最后判断根节点是否大于等于 0 0 0即可
代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int n;
LL a[100010], b[100010];
double f[100010];
vector<pair<LL, LL> > G[100010];
void dfs(int u) {
f[u] = b[u] - a[u];
for (auto v : G[u]) {
dfs(v.first);
if (f[v.first] >= 0) {
f[u] += f[v.first];
} else {
f[u] += f[v.first] * v.second;
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> b[i];
}
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 2; i <= n; i++) {
LL x, y;
cin >> x >> y;
G[x].push_back(make_pair(i, y));
}
dfs(1);
cout << (f[1] >= 0 ? "YES\n" : "NO\n");
return 0;
}