Loading

树套树 分治

本文代码均经过格式化,请放心食用,如有不适,一切责任由小熊猫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;
}

例题

P2617 Dynamic Rankings

模板弱化版

P3759 [TJOI2017] 不勤劳的图书管理员

P12685 [国家集训队] 排队 加强版

P1975 [国家集训队] 排队

逆序对问题 逆序对相关问题,考虑交换两个数只会对两个数之间的逆序对产生影响,

分别求出交换前后这两个数在区间内的逆序对数,进而求出答案变化量

代码先不放,原因待会就知道了

树状数组套权值线段树

最常用,也是理论最快的树套树

原理类似函数式线段树前缀和(主席树),

主席树利用前缀和维护前缀信息,查询时通过在两颗线段树上进行差分操作得到区间答案.

其修改瓶颈在于前缀和修改复杂度爆炸 单次修改可达 \(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';
}

例题

排序解决一维,树套树维护二维前缀和,注意去重.

附效率比较

排队PNG

树套树

SGT

根号

BIT

猫树

一种支持 $O(n\log n) - O(1) $ 回答询问的静态数据结构,可维护信息需满足以下要求

  • 易求前缀/后缀

  • 可快速合并维护的信息

  • 满足结合律

话说不是讲过了吗

-AT_abc223_h [ABC223H] Xor Query/CF1100F Ivan and Burgers

点击查看 已经讲过了,维护前缀/后缀线性基查询时合并即可,代码就不放了.

P4755 Beautiful Pair

点击查看 简要题意

求出有多少数对 \((l,r)\) \((1 \le l \le r \le n)\) 满足

\[a_l \times a_r \le \max_{i=l}^{r} a_i \]

考虑固定式子中的一项,可以根据最值分治,

每次取最值作为分治中点,考虑中点两侧的贡献

最值的位置可以用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 << " ";
}

P7482 不条理狂诗曲

点击查看

考虑 \(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;
}

P8600 [蓝桥杯 2013 省 B] 连号区间数

点击查看 容易发现一个区间是连号区间,当且仅当 $$ \max_{i=l}^{r} a_i - \min_{i=l}^{r} a_i =r - l $$ 最值不好处理,我们可以对最值的位置分讨

我们令 \(m=\min\), \(M=\max\)

可以分以下四种情况

  1. \(m \in [l,mid],M \in [mid+1,r]\)

  2. \(m \in [mid+1,r], M \in [l,mid]\)

  3. \(M,m \in [l,mid]\)

  4. \(M,m \in [mid+1,r]\)

3,4情况比较简单,将相同变量移到同一侧,前缀最值维护, 便可 \(O(1)\) 另一个端点的位置,判断是否合法即可

1,2情况略微复杂,以 2为例 ,将相同变量移至同侧,得

\[maxn_L+L = minn_R+R \]

其中 \(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\) 满足

\[R\in(mid,r],S_R\ge\Delta+S_{i-1} \]

其中 \(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;
}

作业之题

P10833 [COTS 2023] 下 Niz

点击查看

发现当区间最大值确定下来时,区间的长度就固定了,仍然以最值位置为分治中心,枚举较短区间的相应端点,问题就变为判断一个区间是否为 \(1\)\(n\) 的排列。

这是一个经典问题,我们维护一个 \(las\) 数组,维护每个位置上的数上一次出现的位置,当区间内的某个位置的 \(las\) 也在区间内时,这个区间显然不合法。

我们使用线段树或 ST 表维护区间 \(las\) 的最大值,判断它与左端点的关系,即可快速判断区间是否为排列。

时间复杂度 \(O(n\log n)\)

作业题,无代码

P6604 [HNOI2016] 序列 加强版

CF750E New Year and Old Subsequence

P6009 [USACO20JAN] Non-Decreasing Subsequences P

CF526F Pudding Monsters

posted @ 2025-09-17 17:18  Zelensky_Z  阅读(45)  评论(6)    收藏  举报