H. Add and Mod
对于一个区间,维护三个信息:
- \(\text{sum}\):区间内所有正数的总和。
- \(\text{suf}\):最后一个 \(A_i=0\) 之后的正数之和;如果区间内没有零,则 \(\text{suf} = \text{sum}\)。
- \(\text{zero}\):区间内是否存在零。
设模数 \(M=998244353\)。
对于查询区间 \([l,r]\),设其聚合信息为 \(s\)。
情况一:区间内没有零
所有操作都是加法,答案为
情况二:区间内存在零
最后一个零之后的元素只会增加当前值,不会再取模。而最后一个零及其之前的操作等价于将初始 \(x\) 加上这些正数的总和后取模。因此答案为
合并左右区间时,如果右区间存在零,那么合并后的 \(\text{suf}\) 就是右区间的 \(\text{suf}\);否则,合并后的 \(\text{suf}\) 等于左区间的 \(\text{suf}\) 加上右区间的 \(\text{sum}\)。
代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
const int mod = 998244353;
struct S {
ll sum, suf;
bool zero;
};
S op(S a, S b) {
S c;
c.sum = a.sum + b.sum;
c.zero = a.zero or b.zero;
if (b.zero) c.suf = b.suf;
else c.suf = a.suf + b.sum;
return c;
}
S e() {
return {0, 0, false};
}
S toS(int x) {
if (x == 0) return {0, 0, true};
return {x, x, false};
}
int main() {
int n, q;
cin >> n >> q;
segtree<S, op, e> seg(n);
rep(i, n) {
int a;
cin >> a;
seg.set(i, toS(a));
}
rep(qi, q) {
int type;
cin >> type;
if (type == 1) {
int i, x;
cin >> i >> x;
--i;
seg.set(i, toS(x));
}
else {
int l, r, x;
cin >> l >> r >> x;
--l;
S s = seg.prod(l, r);
ll ans;
if (!s.zero) ans = x+s.sum;
else ans = (x+s.sum-s.suf)%mod + s.suf;
cout << ans << '\n';
}
}
return 0;
}
I. Subarray
可以用线段树维护一个 \(5\) 阶最大加法矩阵。矩阵乘法满足结合律,因此可以直接作为线段树的合并操作。
矩阵的元素 \(D[i][j]\) 表示:处理完对应区间后,从状态 \(i\) 转移到状态 \(j\) 所能获得的最大元素和。
一共有 \(5\) 个状态,分别表示还没选择区间、正在选择第一个区间、第一个区间结束、正在选择第二个区间、第二个区间结束。矩阵只允许状态编号增大,因此合并两个区间时可以只计算上三角部分:
代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
const ll INF = 1e18;
using A = array<ll, 5>;
using S = array<A, 5>;
S op(S a, S b) {
S c;
for (auto& r : c) r.fill(-INF);
rep(i, 5) {
for (int j = i; j < 5; ++j) {
for (int k = i; k <= j; ++k) {
c[i][j] = max(c[i][j], a[i][k]+b[k][j]);
}
}
}
return c;
}
S e() {
S d;
for (auto& r : d) r.fill(-INF);
rep(i, 5) d[i][i] = 0;
return d;
}
S toS(int x) {
S d;
for (auto& r : d) r.fill(-INF);
d[0][0] = 0;
d[0][1] = x;
d[0][2] = x;
d[1][1] = x;
d[1][2] = x;
d[2][2] = 0;
d[2][3] = x;
d[2][4] = x;
d[3][3] = x;
d[3][4] = x;
d[4][4] = 0;
return d;
}
int main() {
int n, q;
cin >> n >> q;
segtree<S, op, e> seg(n);
rep(i, n) {
int a;
cin >> a;
seg.set(i, toS(a));
}
rep(qi, q) {
int type;
cin >> type;
if (type == 1) {
int i, x;
cin >> i >> x;
--i;
seg.set(i, toS(x));
}
else {
int l, r;
cin >> l >> r;
--l;
S d = seg.prod(l, r);
ll ans = 0;
rep(i, 5) ans = max(ans, d[0][i]);
cout << ans << '\n';
}
}
return 0;
}
J. Long Binary String
可以将所有询问的左右端点离散化,并使用线段树维护区间内 \(1\) 的数量。
代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
// Coodinate Compression
template<typename T=int>
struct CC {
bool initialized;
vector<T> xs;
CC(): initialized(false) {}
void add(T x) { xs.push_back(x);}
void init() {
sort(xs.begin(), xs.end());
xs.erase(unique(xs.begin(),xs.end()),xs.end());
initialized = true;
}
int operator()(T x) {
if (!initialized) init();
return upper_bound(xs.begin(), xs.end(), x) - xs.begin() - 1;
}
T operator[](int i) {
if (!initialized) init();
return xs[i];
}
int size() {
if (!initialized) init();
return xs.size();
}
};
struct S {
ll ones, len;
};
S op(S a, S b) {
return S(a.ones+b.ones, a.len+b.len);
}
S e() {
return S(0, 0);
}
S mapping(bool f, S x) {
if (f) x.ones = x.len - x.ones;
return x;
}
bool composition(bool f, bool g) {
return f ^ g;
}
bool id() {
return false;
}
int main() {
int q;
cin >> q;
vector<tuple<int, ll, ll>> qs;
CC<ll> cc;
rep(i, q) {
int type; ll l, r;
cin >> type >> l >> r;
--l;
qs.emplace_back(type, l, r);
cc.add(l);
cc.add(r);
}
int n = cc.size()-1;
vector<S> init(n);
rep(i, n) {
init[i] = S(0, cc[i+1]-cc[i]);
}
lazy_segtree<S, op, e, bool, mapping, composition, id> seg(init);
for (auto& [type, l, r] : qs) {
l = cc(l);
r = cc(r);
if (type == 1) {
seg.apply(l, r, true);
}
else {
cout << seg.prod(l, r).ones << '\n';
}
}
return 0;
}
K. Multiset
将每个线段树节点维护的信息设为:
- \(\text{cnt}\):区间内元素的个数。
- \(\text{sum}\):区间内所有元素之和。
- \(\text{ans}\):区间内元素按升序排列后,从下标 \(1\) 开始加权求和的结果
设左右子节点分别为 \(a,b\),由于右子节点中的所有元素在合并后的序列中都需要向后移动 \(a.cnt\) 个位置,因此:
将不同的数值离散化后,每个叶子节点维护某个数 \(x\) 的出现次数 \(c\)。该叶子的三个信息分别是:
代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
using P = pair<int, int>;
using mint = modint998244353;
// Coodinate Compression
template<typename T=int>
struct CC {
bool initialized;
vector<T> xs;
CC(): initialized(false) {}
void add(T x) { xs.push_back(x);}
void init() {
sort(xs.begin(), xs.end());
xs.erase(unique(xs.begin(),xs.end()),xs.end());
initialized = true;
}
int operator()(T x) {
if (!initialized) init();
return upper_bound(xs.begin(), xs.end(), x) - xs.begin() - 1;
}
T operator[](int i) {
if (!initialized) init();
return xs[i];
}
int size() {
if (!initialized) init();
return xs.size();
}
};
struct S {
int cnt;
mint sum, ans;
};
S op(S a, S b) {
return S(a.cnt+b.cnt, a.sum+b.sum, a.ans+b.ans+b.sum*a.cnt);
}
S e() {
return S(0, 0, 0);
}
S toS(mint x, int c) {
return S(c, x*c, x*c*(c+1)/2);
}
int main() {
int n, q;
cin >> n >> q;
vector<int> a(n);
rep(i, n) cin >> a[i];
CC cc;
rep(i, n) cc.add(a[i]);
vector<P> qs;
rep(i, q) {
int t, x = 0;
cin >> t;
if (t != 3) {
cin >> x;
cc.add(x);
}
qs.emplace_back(t, x);
}
int m = cc.size();
vector<int> freq(m);
for (int x : a) {
++freq[cc(x)];
}
vector<S> init(m);
rep(i, m) init[i] = toS(cc[i], freq[i]);
segtree<S, op, e> seg(init);
for (auto& [type, x] : qs) {
if (type == 3) {
cout << seg.all_prod().ans.val() << '\n';
}
else {
x = cc(x);
if (type == 1) ++freq[x];
else --freq[x];
seg.set(x, toS(cc[x], freq[x]));
}
}
return 0;
}
L. Swap between Arrays
维护每个区间中 \(A,B,C\) 三个序列各自的区间和与最大前缀和。
区间交换只会交换对应序列的区间信息,而不会改变信息的计算方式;查询则利用 max_right 寻找第一个使前缀和大于 \(0\) 的位置。
代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
const ll INF = 1e18;
struct S {
ll sum[3];
ll pref[3];
};
S op(S a, S b) {
S c;
rep(i, 3) {
c.sum[i] = a.sum[i] + b.sum[i];
c.pref[i] = max(a.pref[i], a.sum[i]+b.pref[i]);
}
return c;
}
S e() {
S s;
rep(i, 3) {
s.pref[i] = -INF;
}
return s;
}
using F = array<int, 3>;
S mapping(F f, S s) {
S t;
rep(i, 3) {
t.sum[i] = s.sum[f[i]];
t.pref[i] = s.pref[f[i]];
}
return t;
}
F composition(F f, F g) {
F h;
rep(i, 3) {
h[i] = g[f[i]];
}
return h;
}
F id() { return F{0, 1, 2}; }
int main() {
int n, q;
cin >> n >> q;
vector<int> a(n), b(n), c(n);
rep(i, n) cin >> a[i];
rep(i, n) cin >> b[i];
rep(i, n) cin >> c[i];
vector<S> d(n);
rep(i, n) {
d[i].sum[0] = d[i].pref[0] = a[i];
d[i].sum[1] = d[i].pref[1] = b[i];
d[i].sum[2] = d[i].pref[2] = c[i];
}
lazy_segtree<S, op, e, F, mapping, composition, id> seg(d);
rep(qi, q) {
int type;
cin >> type;
if (type == 4) {
int i;
cin >> i;
--i;
int r = seg.max_right(i, [](S s) {
return s.pref[0] <= 0;
});
cout << r+1 << '\n';
}
else {
int l, r;
cin >> l >> r;
--l;
F f = id();
if (type == 1) swap(f[0], f[1]);
else if (type == 2) swap(f[1], f[2]);
else swap(f[0], f[2]);
seg.apply(l, r, f);
}
}
return 0;
}
M. NAND
设 \(\text{full}=2^{30}-1\)。对于固定的 \(a\),有
因此,每个操作都能表示成
对于单个元素 \(a\),对应的函数参数为 \((a,\text{full})\);恒等函数对应 \((\text{full},0)\)。
假设左区间对应函数 \(f\),右区间对应函数 \(g\),参数分别是 \((m_1,v_1)\) 和 \((m_2,v_2)\)。合并后执行的是先 \(f\),再 \(g\),即 \(g(f(x))\),其参数为
代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
using P = pair<int, int>;
const int full = (1<<30)-1;
struct S {
int m, v;
};
S op(S a, S b) {
return S(a.m&b.m, (a.v&b.m)^b.v);
}
S e() {
return S(full, 0);
}
int main() {
int n, q;
cin >> n >> q;
vector<int> a(n);
rep(i, n) cin >> a[i];
segtree<S, op, e> seg(n);
rep(i, n) seg.set(i, S(a[i], full));
rep(qi, q) {
int type;
cin >> type;
if (type == 1) {
int p, x;
cin >> p >> x;
--p;
a[p] = x;
seg.set(p, S(x, full));
}
else {
int l, r;
cin >> l >> r;
S f = seg.prod(l+1, r);
int ans = (a[l]&f.m)^f.v;
cout << ans << '\n';
}
}
return 0;
}
N. Times Tables
只需对扫描线求矩形并的板子简单改改就行了
也就是将线段树维护的区间长度改为区间内列坐标之和
代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using mint = modint998244353;
const int INF = 1001001001;
struct S {
int mn;
mint sum;
};
S op(S a, S b) {
if (a.mn < b.mn) return a;
if (a.mn > b.mn) return b;
return S(a.mn, a.sum+b.sum);
}
S e() {
return S(INF, 0);
}
S mapping(int f, S x) {
x.mn += f;
return x;
}
int composition(int f, int g) {
return f+g;
}
int id() {
return 0;
}
mint c2(mint x) {
return x*(x+1)/2;
}
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int n, q;
cin >> n >> q;
vector<vector<tuple<int, int, int>>> qs(n+1);
rep(i, q) {
int a, b, c, d;
cin >> a >> b >> c >> d;
--a; --b;
qs[a].emplace_back(b, d, 1);
qs[c].emplace_back(b, d, -1);
}
lazy_segtree<S, op, e, int, mapping, composition, id> seg(n);
rep(j, n) {
seg.set(j, S(0, j+1));
}
mint tot = c2(n);
mint ans = tot*tot;
rep(x, n+1) {
for (auto& [l, r, sign] : qs[x]) {
seg.apply(l, r, sign);
}
S s = seg.all_prod();
mint now = tot;
if (s.mn == 0) now -= s.sum;
ans -= (x+1)*now;
}
cout << ans.val() << '\n';
return 0;
}
O. Minimum Value
单调栈+历史和线段树
处理右端点 \(i=0,1,\ldots,N-1\)。对于每个左端点 \(j\),维护两个量:
mn[j]:当前最小值 \(\min(A_j,\ldots,A_i)\)。hsum[j]:历史和 \(\sum\limits_{k=j}^{i}\min(A_j,\ldots,A_k)\)。
那么查询 \([l,r]\) 的答案恰好就是
因为 \(\operatorname{hsum}[j]\) 已经统计了左端点为 \(j\)、右端点不超过当前 \(r\) 的所有子数组。
当加入 \(A_i\) 时,随着左端点 \(j\) 从右往左移动,\(\text{mn}[j]\) 单调不减。
使用单调栈维护最小值相同的连续区间。弹出所有满足 栈顶值 \(>= A_i\) 的元素,得到最左边界 \(j'\),然后将区间 \([j',i]\) 内所有位置的当前最小值赋为 \(A_i\) 。
完成当前最小值更新后,需要将所有已经加入的左端点的历史和加上当前最小值。
这里用线段树统一维护当前最小值和历史和。每个线段树节点保存两个区间和以及区间长度,懒标记表示对每个叶子执行
这样,区间赋值和历史累加都可以用同一套懒标记表示。
代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
using P = pair<int, int>;
struct S {
ll mn, hsum;
int len;
};
S op(S a, S b) {
return S(a.mn+b.mn, a.hsum+b.hsum, a.len+b.len);
}
S e() {
return S(0, 0, 0);
}
struct F {
ll p, q, u, v;
};
S mapping(F f, S x) {
return S(f.p*x.mn + f.q*x.len, x.hsum + f.u*x.mn + f.v*x.len, x.len);
}
F composition(F f, F g) {
return F(f.p*g.p, f.p*g.q + f.q, g.u + f.u*g.p, g.v + f.u*g.q + f.v);
}
F id() {
return F(1, 0, 0, 0);
}
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int n, q;
cin >> n >> q;
vector<int> a(n);
rep(i, n) cin >> a[i];
vector<vector<P>> qs(n);
rep(qi, q) {
int l, r;
cin >> l >> r;
--l; --r;
qs[r].emplace_back(l, qi);
}
lazy_segtree<S, op, e, F, mapping, composition, id> seg(n);
rep(i, n) seg.set(i, S(0, 0, 1));
vector<ll> ans(q);
vector<P> st;
rep(i, n) {
int j = i;
while (st.size() and st.back().first >= a[i]) {
j = st.back().second;
st.pop_back();
}
st.emplace_back(a[i], j);
seg.apply(j, i+1, F(0, a[i], 0, 0));
seg.apply(0, i+1, F(1, 0, 1, 0));
for (auto& [l, qi] : qs[i]) {
ans[qi] = seg.prod(l, i+1).hsum;
}
}
rep(i, q) cout << ans[i] << '\n';
return 0;
}
P. Connect the Dots
将圆剪开,把每条曲线看作区间 \([A_i,B_i]\)。
两条曲线相交,当且仅当存在 \(i<j\),满足
因此,询问 \([l,r]\) 合法,当且仅当对应的区间两两不相交(允许包含关系),即构成一个 laminar family。
维护数组 \(C\),初始全部为 \(0\)。
为每条曲线 \([A_i,B_i]\) 随机分配一个 \(64\) 位哈希值 \(h_i\)。加入曲线时,将 \(h_i\) 异或到所有满足
的位置 \(C_k\);删除时执行相同操作即可撤销。
考虑将新区间 \([a,b]\) 加入当前集合。已有区间 \([c,d]\) 对 \(C_a\) 和 \(C_{b-1}\) 的贡献不同,当且仅当它与新区间交错。因此,可以通过
判断新区间能否加入。用线段树维护差分数组,即可在 \(O(\log N)\) 时间内完成区间哈希更新和前缀查询。
定义 \(R_l\) 为以 \(l\) 为左端点的最大合法区间的右端点(半开区间)。
使用双指针维护当前合法区间 \([l,r)\):
- 不断尝试加入第 \(r\) 条曲线;若加入后仍合法,就令 \(r\gets r+1\)。
- 记录 \(R_l=r\)。
- 删除第 \(l\) 条曲线,继续处理下一个左端点。
每条曲线最多加入、删除各一次,因此预处理时间复杂度为 \(O(N\log N)\)。
对于询问 \([l,r)\),答案为
代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ull = unsigned long long;
ull op(ull a, ull b) { return a^b; }
ull e() { return 0; }
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int n, q;
cin >> n >> q;
vector<int> a(n), b(n);
rep(i, n) cin >> a[i] >> b[i];
mt19937_64 rng(58);
vector<ull> h(n);
rep(i, n) h[i] = rng();
segtree<ull, op, e> seg(2*n+1);
auto upd = [&](int i) {
seg.set(a[i], seg.get(a[i])^h[i]);
seg.set(b[i], seg.get(b[i])^h[i]);
};
auto get = [&](int p) {
return seg.prod(0, p+1);
};
auto check = [&](int i) {
return get(a[i]) == get(b[i]-1);
};
vector<int> lim(n);
int r = 0;
rep(l, n) {
while (r < n and check(r)) upd(r++);
lim[l] = r;
upd(l);
}
rep(qi, q) {
int l, r;
cin >> l >> r;
--l;
if (r <= lim[l]) puts("Yes");
else puts("No");
}
return 0;
}
Q. Number of Moves
对于一段连续操作,设初始坐标为 \(x\),最终坐标为 \(f(x)\),获得的钱数为 \(g(x)\)。可以用四个整数 \((l,r,a,b)\) 描述这两个函数:
\[ f(x)= \begin{cases} l+a & x<l,\\ x+a & l\le x\le r,\\ r+a & x>r, \end{cases} \]
\[ g(x)=b-\max(0,l-x)-\max(0,x-r). \]
其中,\([l,r]\) 是最终坐标函数的线性区间,\(a\) 是坐标偏移量,\(b\) 是在该区间内获得的钱数。
设 \(X,Y\) 分别表示前后两段操作,合并后对应函数复合 \(f_Y\circ f_X\)。由于上述函数的形式在复合后仍然保持不变,因此可以继续用四元组表示。
具体地,将后一段的线性区间平移 \(-a_X\),再与前一段的线性区间求交。如果交集非空,新区间端点取交集端点,偏移量和基准收益分别相加;如果交集为空,则合并后的最终坐标为常数,需要根据两区间的相对位置确定新的四元组。
用线段树维护每个位置对应的四元组。修改时更新单点;查询区间时,通过线段树合并得到区间四元组 \((l,r,a,b)\),最终答案为
\[ \boxed{b-\max(0,l-x)-\max(0,x-r)}. \]
代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
struct S {
int l, r, a; ll b;
};
int m;
S op(S x, S y) {
int l = y.l-x.a, r = y.r-x.a;
if (x.r < l) return S(x.r, x.r, y.l+y.a-x.r, x.b+y.b-(l-x.r));
if (r < x.l) return S(x.l, x.l, y.r+y.a-x.l, x.b+y.b-(x.l-r));
return S(max(x.l, l), min(x.r, r), x.a+y.a, x.b+y.b);
}
S e() {
return S(0, m, 0, 0);
}
S toS(int a, char c) {
int d = min(a, m);
if (c == '1') return S(0, m-d, d, d);
return S(d, m, -d, d);
}
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int n, q;
string s;
cin >> n >> m >> q >> s;
vector<int> a(n);
rep(i, n) cin >> a[i];
segtree<S, op, e> seg(n);
rep(i, n) seg.set(i, toS(a[i], s[i]));
rep(qi, q) {
int type;
cin >> type;
if (type == 1) {
int i;
cin >> i;
--i;
s[i] ^= 1;
seg.set(i, toS(a[i], s[i]));
}
else {
int x, l, r;
cin >> x >> l >> r;
--l;
S z = seg.prod(l, r);
ll ans = z.b-abs(x-clamp(x, z.l, z.r));
cout << ans << '\n';
}
}
return 0;
}
浙公网安备 33010602011771号