树套树 分治
本文代码均经过格式化,请放心食用,如有不适,一切责任由小熊猫DEV承担
By Zelensky
树套树
顾名思义,二维数据结构
线段树套线段树
此处指两个区间线段树嵌套,可用于解决二维平面上的矩阵问题
需满足条件:
-
修改操作可标记永久化或无需下传懒标记
-
不需要push_up操作
-
操作范围为连续矩阵
时间复杂度为 \(O(q\log{n}\log{m})\) 其中 \(q\) , \(n\) , \(m\) 分别指操作数,行数,列数.
具体实现
点击查看
先在外层线段树上找到要处理的 \(\log n\) 个区间,对每个区间维护的内层线段树进行对应区间的操作.
\(Code :\)
#include<bits/stdc++.h>
#define ls (p<<1)
#define rs ((p<<1)|1)
#define mid ((s+t)>>1)
const int M = 1e3 + 5;
using namespace std;
inline int read() {
int x(0), op(0);
char ch = getchar();
while (ch < '0' || ch > '9')op |= (ch == 45), ch = getchar();
while (ch >= '0' && ch <= '9')x = (x << 3) + (x << 1) + (ch ^ 48), ch = getchar();
return op ? -x : x;
}
int n, m, q;
struct SGT {
int T[M << 2], tag[M << 2];
void modify(int l, int r, int x, int p, int s, int t) {
T[p] = max(T[p], x);
if (l <= s && t <= r) {
tag[p] = max(x, tag[p]);
return;
}
if (l <= mid)modify(l, r, x, ls, s, mid);
if (r > mid)modify(l, r, x, rs, mid + 1, t);
}
int query(int l, int r, int p, int s, int t) {
if (l <= s && t <= r)return T[p];
int ans = tag[p];
if (l <= mid)ans = max(query(l, r, ls, s, mid), ans);
if (r > mid)ans = max(query(l, r, rs, mid + 1, t), ans);
return ans;
}
} T[M << 2], tag[M << 2];
void modify(int l, int r, int d, int u, int x, int p, int s, int t) {
T[p].modify(d, u, x, 1, 1, m);
if (l <= s && t <= r) {
tag[p].modify(d, u, x, 1, 1, m);
return;
}
if (l <= mid)modify(l, r, d, u, x, ls, s, mid);
if (r > mid)modify(l, r, d, u, x, rs, mid + 1, t);
}
int query(int l, int r, int d, int u, int p, int s, int t) {
if (l <= s && t <= r)return T[p].query(d, u, 1, 1, m);
int ans = tag[p].query(d, u, 1, 1, m);
if (l <= mid)ans = max(query(l, r, d, u, ls, s, mid), ans);
if (r > mid)ans = max(query(l, r, d, u, rs, mid + 1, t), ans);
return ans;
}
int main() {
n = read(), m = read(), q = read();
while (q--) {
int d = read(), s = read(), w = read(), x = read(), y = read();
int add = query(x + 1, x + d, y + 1, y + s, 1, 1, n) + w;
modify(x + 1, x + d, y + 1, y + s, add, 1, 1, n);
}
printf("%d\n", T[1].query(1, n, 1, 1, m));
return 0;
}
线段树套平衡树
比较常见的平衡树,用于维护区间内有关值域的信息,线段树负责维护区间,平衡树维护值域
常见操作见 P3380 【模板】树套树
点击查看
考虑普通平衡树如何在全局进行操作
修改是平凡的.
考虑排名实际上是求给定元素在值域上的前缀和,维护子树大小后即可在 \(\log n\) 时间内求得
k小值在维护子树大小后也可简单求得
前驱后继是上面两种操作的结合
考虑将如何将操作与线段树结合
修改操作对修改位置到根的 \(\log n\) 个节点所维护的平衡树进行修改即可,时间复杂度 \(\log^2 n\)
排名操作先在线段树上找到查询区间维护的平衡树,对这些平衡树进行值域前缀和,最后答案是所有区间的和+1,时间复杂度 \(\log^2 n\)
k小值可以外层二分答案,用排名函数check,时间复杂度 \(\log^3 n\)
复杂度 \(O(n\log^3 n)\)
我的内层树用了替罪羊树,常熟小,好写
\(Code :\)
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, opt, x, q, lans, ans;
double A = 0.725;
// SGT begin
struct SGT {
int rt[(int)5e6], cur, val[(int)5e6], lc[(int)5e6], rc[(int)5e6], cnt[(int)5e6], siz[(int)5e6], tot[(int)5e6], sum[(int)5e6];
int t, g[(int)5e6];
void aray(int x) {
if (!x)return ;
aray(lc[x]);
if (cnt[x])g[++t] = x;
aray(rc[x]);
}
void up(int x) {
siz[x] = siz[lc[x]] + siz[rc[x]] + (cnt[x] ? 1 : 0);
tot[x] = tot[lc[x]] + tot[rc[x]] + 1;
sum[x] = sum[lc[x]] + sum[rc[x]] + cnt[x];
}
int build(int l, int r) {
if (l > r)return 0;
if (l == r) {
lc[g[l]] = rc[g[l]] = 0;
up(g[l]);
return g[l];
}
int mid = (l + r) >> 1;
lc[g[mid]] = build(l, mid - 1);
rc[g[mid]] = build(mid + 1, r);
up(g[mid]);
return g[mid];
}
void rebuild(int &x) {
t = 0;
aray(x);
x = build(1, t);
}
bool check(int x) {
return sum[x] && (A * tot[x] <= max(tot[lc[x]], tot[rc[x]]) || A * tot[x] >= siz[x]); //判断是否重构
}
void insert(int &x, int v) {
if (!x) {
x = ++cur;
val[x] = v;
cnt[x] = 1;
up(x);
return;
}
if (v == val[x])cnt[x]++;
else if (v < val[x])insert(lc[x], v);
else insert(rc[x], v);
up(x);
if (check(x))rebuild(x);
}
void erase(int &x, int v) {
if (!x)return ;
if (v == val[x])cnt[x]--;
else if (v < val[x])erase(lc[x], v);
else erase(rc[x], v);
up(x);
if (check(x))rebuild(x);
}
int rnk(int x, int v) {
if (!x)return 1;
if (v == val[x])return sum[lc[x]] + 1;
if (v < val[x])return rnk(lc[x], v);
return sum[lc[x]] + cnt[x] + rnk(rc[x], v);
}
int kth(int x, int k, int opt) {
if (!x) {
if (opt == 1)return -INT_MAX;
else return INT_MAX;
}
if (sum[lc[x]] < k && k <= sum[lc[x]] + cnt[x])return val[x];
if (k <= sum[lc[x]])return kth(lc[x], k, opt);
return kth(rc[x], k - sum[lc[x]] - cnt[x], opt);
}
int pre(int x, int v) {
return kth(x, rnk(x, v) - 1, 1);
}
int nxt(int x, int v) {
return kth(x, rnk(x, v + 1), 2);
}
} t;
// SGT end
int a[(int)5e6];
// SEG begin
struct SEG {
int cnt = 0;
int ls[(int)5e6], rs[(int)5e6], siz[(int)5e6];
void add(int &i, int l, int r, int x, int old, int k) {
if (!i)i = ++cnt;
t.erase(t.rt[i], old), t.insert(t.rt[i], k);
if (l == r)return ;
int mid = (l + r) >> 1;
if (x <= mid)add(ls[i], l, mid, x, old, k);
else add(rs[i], mid + 1, r, x, old, k);
}
int get_pre(int i, int l, int r, int x, int y, int k) {
if (!i)return -INT_MAX;
if (x <= l && y >= r)return t.pre(t.rt[i], k);
int ans = -INT_MAX, mid = (l + r) >> 1;
if (x <= mid)ans = max(ans, get_pre(ls[i], l, mid, x, y, k));
if (y > mid) ans = max(ans, get_pre(rs[i], mid + 1, r, x, y, k));
return ans;
}
int get_nxt(int i, int l, int r, int x, int y, int k) {
if (!i)return INT_MAX;
if (x <= l && y >= r)return t.nxt(t.rt[i], k);
int ans = INT_MAX, mid = (l + r) >> 1;
if (x <= mid)ans = min(ans, get_nxt(ls[i], l, mid, x, y, k));
if (y > mid) ans = min(ans, get_nxt(rs[i], mid + 1, r, x, y, k));
return ans;
}
int get_rnk(int i, int l, int r, int x, int y, int k) {
if (!i)return 0;
if (x <= l && y >= r)return t.rnk(t.rt[i], k) - 1;
int ans = 0, mid = (l + r) >> 1;
if (x <= mid)ans += get_rnk(ls[i], l, mid, x, y, k);
if (y > mid) ans += get_rnk(rs[i], mid + 1, r, x, y, k);
return ans;
}
int get_kth(int x, int y, int k) {
int l = 0, r = 1e8 + 10;
while (l <= r) {
int mid = (l + r) >> 1;
if (get_rnk(1, 1, n, x, y, mid) + 1 <= k)l = mid + 1;
else r = mid - 1;
}
return l - 1;
}
} T;
// SGE end
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> q;
for (int i = 1; i <= n; i++)cin >> a[i], T.add(T.ls[0], 1, n, i, -1, a[i]);
while (q--) {
int opt;
cin >> opt;
if (opt == 1) {
int l, r, k;
cin >> l >> r >> k;
cout << T.get_rnk(1, 1, n, l, r, k) + 1 << '\n';
} else if (opt == 2) {
int l, r, k;
cin >> l >> r >> k;
cout << T.get_kth(l, r, k) << '\n';
} else if (opt == 3) {
int x, k;
cin >> x >> k;
T.add(T.ls[0], 1, n, x, a[x], k);
a[x] = k;
} else if (opt == 4) {
int l, r, k;
cin >> l >> r >> k;
cout << T.get_pre(1, 1, n, l, r, k) << '\n';
} else {
int l, r, k;
cin >> l >> r >> k;
cout << T.get_nxt(1, 1, n, l, r, k) << '\n';
}
}
return 0;
}
例题
模板弱化版
逆序对问题
逆序对相关问题,考虑交换两个数只会对两个数之间的逆序对产生影响,分别求出交换前后这两个数在区间内的逆序对数,进而求出答案变化量
代码先不放,原因待会就知道了
树状数组套权值线段树
最常用,也是理论最快的树套树
原理类似函数式线段树前缀和(主席树),
主席树利用前缀和维护前缀信息,查询时通过在两颗线段树上进行差分操作得到区间答案.
其修改瓶颈在于前缀和修改复杂度爆炸 单次修改可达 \(O(n\log n)\)
考虑有无支持快速修改,求前缀和的数据结构
发现树状数组完美契合这一特点(线段树常熟太大)
具体操作
点击查看
每次操作用树状数组找出涉及的 \(\log n\) 棵线段树,在找出的线段树上操作即可
与线段树套平衡树相比
树状数组的常熟更小,线段树上二分与平衡树相比省去了一个二分的 $\log $
复杂度 \(O(n\log^2 n)\)
\(Code :\)
#include<bits/stdc++.h>
#define ls tree[i].lss
#define rs tree[i].rss
const int N = 1e5 * 2 + 1000;
const int T = 4e7 + 1000;
const int inf = 2147483647;
using namespace std;
int rt[N], b[N * 2], a[N], opt[N], x[N], y[N], k[N], len, cntl, cntr, L[N], R[N], n, m;
struct ccc {
int lss, rss, siz; //左右儿子
} tree[T];
int cnt;
int tot;
int copy(int old) { //创建新节点
tree[++cnt] = tree[old];
return cnt;
}
int low_bit(int x) {
return x & -x;
}
int id(int x) { //离散化
return lower_bound(b + 1, b + len + 1, x) - b;
}
int build(int i, int l, int r) {
i = ++cnt;
if (l == r)return i;
int mid = (l + r) >> 1;
ls = build(ls, l, mid);
rs = build(rs, mid + 1, r);
return i;
}
int add(int i, int l, int r, int x, int k) {
i = copy(i);
if (l == r) {
tree[i].siz += k;
return i;
}
int mid = (l + r) >> 1;
if (x <= mid)ls = add(ls, l, mid, x, k);
else rs = add(rs, mid + 1, r, x, k);
tree[i].siz = tree[ls].siz + tree[rs].siz;
return i;
}
void change(int rot, int id, int v) {
while (rot <= n) { //利用树状数组的特性,找出需要修改的log棵线段树
rt[rot] = add(rt[rot], 1, len, id, v); //修改
rot += low_bit(rot);
}
}
void pre_get(int x, int y) { //预处理出与询问区间有关的log棵线段树
cntl = cntr = 0;
for (int i = x - 1; i; i -= low_bit(i))L[++cntl] = rt[i];
for (int i = y; i; i -= low_bit(i))R[++cntr] = rt[i];
}
int get(int l, int r, int k) { //与普通主席树一致,不过之前利用前缀和的部分用树状数组代替
if (l == r)return l;
int si = 0;
for (int i = 1; i <= cntl; i++)si -= tree[tree[L[i]].lss].siz;
for (int i = 1; i <= cntr; i++)si += tree[tree[R[i]].lss].siz;
int mid = (l + r) >> 1; //视情况分别转移到子树
if (k <= si) {
for (int i = 1; i <= cntl; i++)L[i] = tree[L[i]].lss;
for (int i = 1; i <= cntr; i++)R[i] = tree[R[i]].lss;
return get(l, mid, k);
} else {
for (int i = 1; i <= cntl; i++)L[i] = tree[L[i]].rss;
for (int i = 1; i <= cntr; i++)R[i] = tree[R[i]].rss;
return get(mid + 1, r, k - si);
}
}
int rk(int l, int r, int k) { //不同之处同k小操作
if (l == r)return 1;
int si = 0;
for (int i = 1; i <= cntl; i++)si -= tree[tree[L[i]].lss].siz;
for (int i = 1; i <= cntr; i++)si += tree[tree[R[i]].lss].siz;
int mid = (l + r) >> 1;
if (k <= mid) {
for (int i = 1; i <= cntl; i++)L[i] = tree[L[i]].lss;
for (int i = 1; i <= cntr; i++)R[i] = tree[R[i]].lss;
return rk(l, mid, k);
} else {
for (int i = 1; i <= cntl; i++)L[i] = tree[L[i]].rss;
for (int i = 1; i <= cntr; i++)R[i] = tree[R[i]].rss;
return rk(mid + 1, r, k) + si;
}
}
int get_pre(int x, int y, int k) { //查出k的排名,减1后为前驱的排名,再查询
pre_get(x, y);
int rak = rk(1, len, k) - 1;
if (rak == 0)return 0;
pre_get(x, y);
return get(1, len, rak);
}
int get_nxt(int x, int y, int k) { //查出k+1的排名,为前驱的排名,再查询
if (k == len)return len + 1;
pre_get(x, y);
int rak = rk(1, len, k + 1);
if (rak == y - x + 2)return len + 1;
pre_get(x, y);
return get(1, len, rak);
}
int main() {
// freopen("bi.in","r",stdin);
// freopen("bi.out","w",stdout);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
b[++tot] = a[i];
}
for (int i = 1; i <= m; i++) {
cin >> opt[i];
cin >> x[i] >> y[i];
if (opt[i] == 1) {
cin >> k[i];
b[++tot] = k[i];
} else if (opt[i] == 2) {
cin >> k[i];
} else if (opt[i] == 3) {
b[++tot] = y[i];
} else if (opt[i] == 4) {
cin >> k[i];
b[++tot] = k[i];
} else {
cin >> k[i];
b[++tot] = k[i];
}
}
// puts("Caohao AK IOI");
sort(b + 1, b + tot + 1);
len = unique(b + 1, b + tot + 1) - b - 1; //离散化
// rt[0]=build(0,1,len);
b[0] = -inf;
b[len + 1] = inf; //方便前驱后继无解的查询
// puts("Caohao AK IOI");
for (int i = 1; i <= n; i++)change(i, id(a[i]), 1);
// puts("Caohao AK IOI");
for (int i = 1; i <= m; i++) {
if (opt[i] == 1) {
pre_get(x[i], y[i]);
cout << rk(1, len, id(k[i])) << endl;
} else if (opt[i] == 2) {
pre_get(x[i], y[i]);
cout << b[get(1, len, k[i])] << endl;
} else if (opt[i] == 3) {
change(x[i], id(a[x[i]]), -1);
a[x[i]] = y[i];
change(x[i], id(y[i]), 1);
} else if (opt[i] == 4)cout << b[get_pre(x[i], y[i], id(k[i]))] << endl;
else cout << b[get_nxt(x[i], y[i], id(k[i]))] << endl;
}
return 0;
}
邪教来临:莫队套分块
高贵的 \(O(n\sqrt{n})\) ,反正我不用.
点击查看
用莫队离线处理区间询问,另一种数据结构维护值域,发现如果用 $\log$ 的数据结构,复杂度会变成 $O(n\sqrt{n}\log{n})$考虑用值域分块平衡复杂度,发现指针移动次数达到了 \(O(n\sqrt{n})\) 次,所以选用 \(O(1)\) 修改, \(O(\sqrt{n})\) 查询的值域分块维护即可.
#include<bits/stdc++.h>
using namespace std;
int Len, ans[(int)5e6];
struct value_block {
int siz[(int)500], blg[(int)5e6], L[(int)500], R[(int)500], cnt[(int)5e6];
void block() {
int len = sqrt(1e5);
for (int i = 0; i <= Len; i++) {
blg[i] = i / len + 1;
if (!L[blg[i]])L[blg[i]] = i;
R[blg[i]] = i;
}
L[1] = 0;
}
void add(int k) {
siz[blg[k]]++;
cnt[k]++;
}
void del(int k) {
siz[blg[k]]--;
cnt[k]--;
};
int get_rk(int x) {
int ans = 0;
for (int i = 1; i < blg[x]; i++)ans += siz[i];
for (int i = L[blg[x]]; i < x; i++)ans += cnt[i];
return ans + 1;
}
int get_val(int k) {
for (int i = 1; i <= blg[Len]; i++) {
if (k > siz[i])k -= siz[i];
else {
for (int j = L[i]; j <= R[i]; j++) {
k -= cnt[j];
if (k <= 0)return j;
}
}
}
return Len;
}
} B;
struct qry {
int l, r, t, id, typ, x;
} q[(int)5e6];
struct cag {
int pos, k;
} c[(int)5e6];
int blg[(int)5e6], n, m;
void block() {
int len = pow(n, 0.66666);
for (int i = 1; i <= n; i++)blg[i] = i / len + 1;
}
bool cmp(qry x, qry y) {
if (blg[x.l] != blg[y.l])return x.l < y.l;
else if (x.r != y.r)return x.r < y.r;
return x.t < y.t;
}
int b[(int)5e6], tot, a[(int)5e6], Q, T;
int id(int x) {
return lower_bound(b + 1, b + Len + 1, x) - b;
}
int opt[(int)5e6], l[(int)5e6], r[(int)5e6], k[(int)5e6];
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++)cin >> a[i], b[i] = a[i];
tot = n;
for (int i = 1; i <= m; i++) {
cin >> opt[i];
if (opt[i] == 1 || opt[i] == 5 || opt[i] == 4)cin >> l[i] >> r[i] >> k[i], b[++tot] = k[i];
else if (opt[i] == 3)cin >> l[i] >> k[i], b[++tot] = k[i];
else cin >> l[i] >> r[i] >> k[i];
if (r[i])Q++, q[Q] = {l[i], r[i], T, Q, opt[i], k[i]};
else c[++T] = {l[i], k[i]};
}
sort(b + 1, b + tot + 1);
Len = unique(b + 1, b + tot + 1) - b - 1;
b[0] = -INT_MAX, b[++Len] = INT_MAX;
block(), B.block();
sort(q + 1, q + Q + 1, cmp);
int l = 1, r = 0, t = 0;
for (int i = 1; i <= Q; i++) {
int ql = q[i].l, qr = q[i].r, qt = q[i].t;
while (l > ql)B.add(id(a[--l]));
while (r < qr)B.add(id(a[++r]));
while (l < ql)B.del(id(a[l++]));
while (r > qr)B.del(id(a[r--]));
while (t < qt) {
t++;
int pos = c[t].pos;
if (pos >= l && pos <= r) {
B.del(id(a[pos]));
B.add(id(c[t].k));
}
swap(a[pos], c[t].k);
}
while (t > qt) {
int pos = c[t].pos;
if (pos >= l && pos <= r) {
B.del(id(a[pos]));
B.add(id(c[t].k));
}
swap(a[pos], c[t].k);
t--;
}
if (q[i].typ == 1)ans[q[i].id] = B.get_rk(id(q[i].x));
else if (q[i].typ == 2)ans[q[i].id] = b[B.get_val(q[i].x)];
else if (q[i].typ == 4)ans[q[i].id] = b[B.get_val(B.get_rk(id(q[i].x)) - 1)];
else ans[q[i].id] = b[B.get_val(B.get_rk(id(q[i].x) + 1))];
}
for (int i = 1; i <= Q; i++)cout << ans[i] << '\n';
}
例题
排序解决一维,树套树维护二维前缀和,注意去重.
附效率比较
猫树
一种支持 $O(n\log n) - O(1) $ 回答询问的静态数据结构,可维护信息需满足以下要求
-
易求前缀/后缀
-
可快速合并维护的信息
-
满足结合律
话说不是讲过了吗
题
-AT_abc223_h [ABC223H] Xor Query/CF1100F Ivan and Burgers
点击查看
已经讲过了,维护前缀/后缀线性基查询时合并即可,代码就不放了.点击查看
简要题意求出有多少数对 \((l,r)\) \((1 \le l \le r \le n)\) 满足
考虑固定式子中的一项,可以根据最值分治,
每次取最值作为分治中点,考虑中点两侧的贡献
最值的位置可以用ST表或线段树维护
最值已经固定,采用启发式分治的思想
枚举区间长度较小的一侧的元素,
计算出另一侧有多少元素可以与此元素组成合法数对
即求另一侧有多少元素满足 $\le
\lfloor a_{mid}/a_l \rfloor $
发现可以使用主席树维护,时间复杂度 \(O(n \log^2 n)\)
#include<bits/stdc++.h>
using namespace std;
inline int read() {
int x = 0, f = 1;char ch = getchar();
while (ch < '0' || ch > '9') {if (ch == '-') f = -1;ch = getchar();}
while (ch >= '0' && ch <= '9') {x = (x << 3) + (x << 1) + ch - 48;ch = getchar();}
return x * f;
}
int rt[(int)5e5];
long long ans, n, len;
int a[(int)5e6], b[(int)5e6], lg[(int)5e5];
struct HJT {
int siz[(int)5e6], cnt, ls[(int)5e6], rs[(int)5e6];
int copy(int old) {
return ls[++cnt] = ls[old], rs[cnt] = rs[old], siz[cnt] = siz[old], cnt;
}
void add(int &i, int l, int r, int x, int k) {
i = copy(i);
siz[i] += k;
if (l == r)return ;
int mid = (l + r) >> 1;
if (x <= mid)add(ls[i], l, mid, x, k);
else add(rs[i], mid + 1, r, x, k);
}
int query(int L, int R, int l, int r, int x, int opt) {
if (l == r)return (siz[R] - siz[L]) * opt;
int sl = siz[ls[R]] - siz[ls[L]], mid = (l + r) >> 1;
if (x <= mid)return query(ls[L], ls[R], l, mid, x, opt);
else return query(rs[L], rs[R], mid + 1, r, x, opt) + sl;
}
} T;
struct ST {
int f[(int)5e5][30];
void build(int n) {
lg[1] = 0;
for (int i = 2; i <= n; i++)lg[i] = lg[i >> 1] + 1, f[i][0] = i;
f[1][0] = 1;
for (int i = 1; (1 << i) <= n; i++) {
for (int j = 1; j + (1 << i) - 1 <= n; j++) {
int lp = f[j][i - 1], rp = f[j + (1 << (i - 1))][i - 1];
f[j][i] = (a[lp] > a[rp] ? lp : rp);
}
}
}
int query(int l, int r) {
int k = lg[r - l + 1], lp = f[l][k], rp = f[r - (1 << k) + 1][k];
return a[lp] > a[rp] ? lp : rp;
}
} s;
int id(int x) {
return lower_bound(b + 1, b + len + 1, x) - b;
}
struct Cat {
void solve(int l, int r) {
if (l > r)return ;
if (l == r)return ans += a[l] == 1, void();
int mx = 0, mid;
mid = s.query(l, r);
mx = a[mid];
solve(l, mid - 1), solve(mid + 1, r);
if (mid - l <= (r - mid - 1)) {
for (int i = l; i <= mid; i++) {
int x = mx / a[i], D;
if (b[id(x)] == x) ans += D = T.query(rt[mid - 1], rt[r], 1, len, id(x), 1);
else ans += D = T.query(rt[mid - 1], rt[r], 1, len, id(x), 0);
if (D < 0)puts("O_o");
}
} else {
for (int i = mid; i <= r; i++) {
int x = mx / a[i], D;
if (b[id(x)] == x) ans += D = T.query(rt[l - 1], rt[mid], 1, len, id(x), 1);
else ans += D = T.query(rt[l - 1], rt[mid], 1, len, id(x), 0);
if (D < 0)puts("O_o");
}
}
}
} t;
signed main() {
cin >> n;
for (int i = 1; i <= n; i++)a[i] = read(), b[i] = a[i];
s.build(n);
sort(b + 1, b + n + 1);
len = unique(b + 1, b + n + 1) - b - 1;
b[++len] = 1e9 + 7;
T.add(rt[0], 1, len, b[len], 1);
for (int i = 1; i <= n; i++)rt[i] = rt[i - 1], T.add(rt[i], 1, len, id(a[i]), 1);
t.solve(1, n);
cout << ans << " ";
}
点击查看
考虑 \(f\) 的求法,令 \(g\) 为强制不选中点的最大值
\(f_{l,r} = \max(f_{l,mid}+g_{mid+1,r},g_{l,mid}+f_{mid+1,r})\)
\(\max\) 不好处理,分情况考虑贡献
考虑 $f_{l,mid}+g_{mid+1,r} \le g_{l,mid}+f_{mid+1,r} $ 的情况
移项得到 $f_{l,mid}-g_{l,mid} \le f_{mid+1,r}-g_{mid+1,r} $
我们可以预处理出左区间 \(f\) , \(g\) 的前缀和,
对于每个右区间的 \(i\) ,我们二分找出在左区间合法的编号区间,通过前缀和计算贡献
另一种情况类似,略.
#include<bits/stdc++.h>
using namespace std;
const int mod = 1e9 + 7;
struct ccc {
long long val;
int id;
} t[(int)2e5];
int a[(int)2e5], n;
bool cmp(ccc x, ccc y) {
return x.val < y.val;
}
long long ans, f[(int)2e5], g[(int)2e5], sf[(int)2e5], sg[(int)2e5];
void solve(int l, int r) {
if (l == r)return ans = (ans + a[l]) % mod, void();
int mid = (l + r) >> 1;
solve(l, mid), solve(mid + 1, r);
for (int i = mid; i >= l; i--) {
if (i == mid)f[i] = a[i], f[i + 1] = g[i] = 0;
else f[i] = max(f[i + 1], f[i + 2] + a[i]);
if (i == mid - 1)g[i] = a[i], g[i + 1] = 0;
else if (i != mid)g[i] = max(g[i + 1], g[i + 2] + a[i]);
t[i] = {max(f[i] - g[i], 0ll), i};
}
sort(t + l, t + mid + 1, cmp);
sf[l - 1] = sg[l - 1] = 0;
for (int i = l; i <= mid; i++)sf[i] = sf[i - 1] + f[t[i].id], sg[i] = sg[i - 1] + g[t[i].id];
for (int i = mid + 1; i <= r; i++) {
if (i == mid + 1)f[i] = a[i], f[i - 1] = g[i] = 0;
else f[i] = max(f[i - 1], f[i - 2] + a[i]);
if (i == mid + 2)g[i] = a[i];
else if (i != mid + 1)g[i] = max(g[i - 1], g[i - 2] + a[i]);
int k = lower_bound(t + l, t + mid + 1, ccc{f[i] - g[i], 0}, cmp) - t;
ans = (ans + (sf[mid] - sf[k - 1]) % mod + (mid - k + 1) * (g[i] % mod) % mod) % mod;
ans = (ans + (sg[k - 1] % mod) + (k - l) * (f[i] % mod) % mod) % mod;
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++)cin >> a[i];
solve(1, n);
cout << ans % mod;
return 0;
}
点击查看
容易发现一个区间是连号区间,当且仅当 $$ \max_{i=l}^{r} a_i - \min_{i=l}^{r} a_i =r - l $$ 最值不好处理,我们可以对最值的位置分讨我们令 \(m=\min\), \(M=\max\)
可以分以下四种情况
-
\(m \in [l,mid],M \in [mid+1,r]\)
-
\(m \in [mid+1,r], M \in [l,mid]\)
-
\(M,m \in [l,mid]\)
-
\(M,m \in [mid+1,r]\)
3,4情况比较简单,将相同变量移到同一侧,前缀最值维护, 便可 \(O(1)\) 另一个端点的位置,判断是否合法即可
1,2情况略微复杂,以 2为例 ,将相同变量移至同侧,得
其中 \(L \in [l,mid], R \in [mid+1,r]\) , \(maxn,minn\) 为维护的到中点的前/后缀最值
且满足 \(maxn_L > maxn_R\) , \(minn_L > minn_R\)
我们枚举 \(R\) , 发现 \(maxn_R\) , \(minn_R\) ,随 \(i\) 增加而增大/减小
左区间满足条件的区间的端点也只会单向移动,可以类似双指针的维护一下,用桶统计答案
#include<bits/stdc++.h>
using namespace std;
int a[(int)2e5], n, M, ans, mx[(int)5e6], mn[(int)5e6], t[(int)5e6];
void solve(int l, int r) {
if (l == r)return ans++, void();
int mid = (l + r) >> 1;
solve(l, mid), solve(mid + 1, r);
mx[mid] = mn[mid] = a[mid], mx[mid + 1] = mn[mid + 1] = a[mid + 1];
for (int i = mid - 1; i >= l; i--)mx[i] = max(mx[i + 1], a[i]), mn[i] = min(mn[i + 1], a[i]);
for (int i = mid + 2; i <= r; i++)mx[i] = max(mx[i - 1], a[i]), mn[i] = min(mn[i - 1], a[i]);
int L = mid + 1, R = mid;
for (int i = mid + 1; i <= r; i++) {
while (L > l && mn[L - 1] > mn[i])L--, t[mx[L] + L]++;
while (R >= L && mx[R] < mx[i])t[mx[R] + R]--, R--;
ans += t[mn[i] + i];
}
for (int i = l; i <= mid; i++)t[mx[i] + i] = 0;
for (int i = mid + 1; i <= r; i++) {
int L = mn[i] + i - mx[i];
if (L < l || L > mid)continue;
ans += (mx[L] < mx[i] && mn[L] > mn[i]);
}
L = mid + 1, R = mid;
for (int i = mid + 1; i <= r; i++) {
while (L > l && mx[L - 1] < mx[i])L--, t[mn[L] - L + M]++;
while (R >= L && mn[R] > mn[i])t[mn[R] - R + M]--, R--;
ans += t[mx[i] - i + M];
}
for (int i = mid; i >= l; i--) {
int R = mx[i] - mn[i] + i;
if (R > r || R <= mid)continue;
ans += (mx[R] < mx[i] && mn[R] > mn[i]);
}
for (int i = l; i <= mid; i++)t[mn[i] - i + M] = 0;
}
int main() {
cin >> n;
M = n;
for (int i = 1; i <= n; i++)cin >> a[i];
solve(1, n);
cout << ans;
return 0;
}
P12624 [NAC 2025] Humans vs AI
点击查看
一个序列计数问题,把题意转化一下,发现如果没有 AI 操作,就是要求有多少个区间的 \(\sum_{i=l}^r(h_i-a_i)\ge0\),其中负数的权值要乘上 \(k\) ,为求简便,之后的区间不加说明则为 \(h_i-a_i\) 所构成的序列中的区间。
考虑 AI 的操作,如果区间修改前为人类胜利,发现 AI 的最优决策一定是选中区间中最大的数交换,这样获得的收益最大,否则 AI 不需要修改操作。
这个启发我们根据最值分治,选取当前分治区间的最大值作为分治中心,这样跨越中心的区间 AI 一定会选择分治中心作为操作对象,我们成功地固定了式子中的一项。
容易计算出操作后的变化量 \(\Delta=Mx\times(k+1)\) ,问题转化为有多少区间满足 \(\sum_{i=l}^r(h_i-a_i)-\Delta\ge0\) ,考虑启发式分治,枚举区间长度较小的一边,当枚举左区间时,问题转化为以下形式,询问有多少 \(R\) 满足
其中 \(S\) 为前缀和数组。右区间同理。
显然的一个二维数点问题,可以离线下来扫描线,也可以在线主席树维护,注意一下整形的溢出和边界的处理即可。
#include<bits/stdc++.h>
#define int long long
typedef long long ll;
using namespace std;
const int M=5e6+10;
long long a[(int)5e6],h[(int)5e6];
int len,rt;
ll b[(int)5e6],tot;
int id(int x){return lower_bound(b+1,b+len+1,x)-b;}
struct ST{
int f[(int)5e5][30],lg[(int)5e6];
void build(int n){
lg[1]=0;
for(int i=2;i<=n;i++)lg[i]=lg[i>>1]+1,f[i][0]=i;
f[1][0]=1;
for(int i=1;(1<<i)<=n;i++){
for(int j=1;j+(1<<i)-1<=n;j++){
int lp=f[j][i-1],rp=f[j+(1<<(i-1))][i-1];
f[j][i]=(h[lp]>h[rp]?lp:rp);
}
}
}
int query(int l,int r){
int k=lg[r-l+1],lp=f[l][k],rp=f[r-(1<<k)+1][k];
return h[lp]>h[rp]?lp:rp;
}
}s;
struct pre_sum{
ll s[(int)5e6];
void build(int n){for(int i=1;i<=n;i++)s[i]=s[i-1]+h[i];}
int query(int l,int r){return s[r]-s[l-1];}
}t;
struct qry{ll v;int opt;};
vector<qry>q1[(int)5e6],q2[(int)5e6];
int n,k;long long ans;
void solve(int l,int r){
if(l>r)return ; if(l==r)return ans+=h[l]==0,void();
int mid=s.query(l,r);
solve(l,mid-1),solve(mid+1,r);
if(h[mid]<0)return ;
if(mid-l+1<=r-mid){
for(int i=mid;i>=l;i--){
ll val=h[mid]*(k+1)+t.s[i-1];
q1[r].push_back({val,1}),q1[mid-1].push_back({val,-1});
b[++tot]=val;
}
}
else {
for(int i=mid;i<=r;i++){
ll val=t.s[i]-h[mid]*(k+1);
q2[mid].push_back({val,1}),q2[l-1].push_back({val,-1});
b[++tot]=val;
}
}
}
struct SEG{
int cnt=0;
int siz[(int)5e6],ls[(int)5e6],rs[(int)5e6];
void add(int &i,int l,int r,int x){
if(!i)i=++cnt;siz[i]++;if(l==r)return ;int mid=(l+r)>>1;
x<=mid?add(ls[i],l,mid,x):add(rs[i],mid+1,r,x);
}
int query(int i,int l,int r,int x,int opt){
if(l==r)return siz[i]*opt;int mid=(l+r)>>1;
return x<=mid?query(ls[i],l,mid,x,opt):(query(rs[i],mid+1,r,x,opt)+siz[ls[i]]);
}
}T;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++)cin>>h[i];
int cnt=0;
for(int i=1;i<=n;i++){
cin>>a[i],h[i]-=a[i];
}
for(int i=1;i<=n;i++)if(h[i]<0)h[i]*=k;
s.build(n),t.build(n);
solve(1,n);
for(int i=0;i<=n;i++)b[++tot]=t.s[i];
sort(b+1,b+tot+1);len=unique(b+1,b+tot+1)-b-1;
for(int i=0;i<=n;i++){
for(auto x:q2[i])ans+=T.query(rt,1,M,id(x.v)+1,1)*x.opt;
T.add(rt,1,M,id(t.s[i])+1);
for(auto x:q1[i])ans+=(i-T.query(rt,1,M,id(x.v)+1,0))*x.opt;
}
cout<<ans<<endl;
}
作业之题
点击查看
发现当区间最大值确定下来时,区间的长度就固定了,仍然以最值位置为分治中心,枚举较短区间的相应端点,问题就变为判断一个区间是否为 \(1\) 到 \(n\) 的排列。
这是一个经典问题,我们维护一个 \(las\) 数组,维护每个位置上的数上一次出现的位置,当区间内的某个位置的 \(las\) 也在区间内时,这个区间显然不合法。
我们使用线段树或 ST 表维护区间 \(las\) 的最大值,判断它与左端点的关系,即可快速判断区间是否为排列。
时间复杂度 \(O(n\log n)\)
作业题,无代码
CF750E New Year and Old Subsequence