算法模板(2):数据结构(5)做题积累

数据结构(3)

一、并查集

238. 银河英雄传说

  • N N N 艘战舰,也依次编号为 1 , 2 , . . . , N 1,2,...,N 1,2,...,N,其中第 i i i 号战舰处于第 i i i 列。有 T T T 条指令,每条指令格式为以下两种之一:M i j,表示让第 i i i 号战舰所在列的全部战舰保持原有顺序,接在第 j j j 号战舰所在列的尾部;C i j,表示询问第 i i i 号战舰与第 j j j 号战舰当前是否处于同一列中,如果在同一列中,它们之间间隔了多少艘战舰。

  • 三种并查集的综合运用。注意unite怎么写的。以及find函数千万别设计错了,可能会出现无返回值的情况。

#include<cstdio>
#include<algorithm>
#include<cstdlib>
using namespace std;
const int maxn = 30010;
int p[maxn], d[maxn], sz[maxn];
void init(int N) {
	for (int i = 1; i <= N; i++) {
		p[i] = i;
		sz[i] = 1;
	}
}
int find(int x) {
	if (p[x] != x) {
		int u = find(p[x]);
		d[x] += d[p[x]];
		p[x] = u;
	}
	
	return p[x];
}
void unite(int a, int b) {
	int pa = find(a), pb = find(b);
	if (pa == pb) return;
	d[pa] = sz[pb];
	sz[pb] += sz[pa];
	p[pa] = pb;
}
int main() {
	int T;
	char op[5];
	scanf("%d", &T);
	init(30000);
	while (T--) {
		int a, b;
		scanf("%s%d%d", op, &a, &b);
		if (op[0] == 'M') unite(a, b);
		else {
			int pa = find(a), pb = find(b);
			if (pa != pb) printf("-1\n");
			else {
				if (a == b) printf("0\n");
				else printf("%d\n", abs(d[a] - d[b]) - 1);
			}
		}
	}
	return 0;
}

239. 奇偶游戏

  • 题意:小 A 和小 B 在玩一个游戏。首先,小 A 写了一个由 0 和 1 组成的序列 S,长度为 N。然后,小 B 向小 A 提出了 M 个问题。在每个问题中,小 B 指定两个数 l 和 r,小 A 回答 S[l∼r] 中有奇数个 1 还是偶数个 1。

  • 这个题麻烦在于区间交的问题,但是通过前缀和倒是可以避免掉这个问题

  • 这道题前缀和去写。[l, r] 有奇数个1,可以转化为 S r − S l − 1 S_r - S_{l-1} SrSl1是奇数,然后可知 S r S_r Sr S l − 1 S_{l-1} Sl1奇偶性不同。

  • 用带边权的并查集去做(其实就是类似于大雪菜讲解食物链的那道题):

#include<iostream>
#include<string>
#include<unordered_map>
#include<algorithm>
using namespace std;
const int maxn = 20010;
int p[maxn], d[maxn], N, M, num;
unordered_map<int, int> id;
int get(int x) {
	if (!id.count(x)) id[x] = ++num;
	return id[x];
}
void init() {
	for (int i = 0; i < maxn; i++) p[i] = i;
}
int find(int x) {
	if (p[x] != x) {
		int u = find(p[x]);
		d[x] ^= d[p[x]];
		p[x] = u;
	}
	return p[x];
}
void unite(int a, int b, int t) {
	int pa = find(a), pb = find(b);
	p[pa] = pb;
	d[pa] = d[a] ^ d[b] ^ t;
}
int main() {
	cin >> N >> M;
	int ans = 0;
	init();
	for(int i = 0; i < M; i++) {
		int a, b;
		string type;
		cin >> a >> b >> type;
		a = get(a - 1), b = get(b);
		int t = 0;
		if (type == "odd") t = 1;
		if (find(a) == find(b)) {
			if (d[a] ^ d[b] ^ t && !ans) {
				ans = i;
			}
		}
		else unite(a, b, t);
	}
	if (ans) printf("%d\n", ans);
	else printf("%d\n", M);
	return 0;
}
  • 用邻域的方法去做(其实就是类似于白书讲解的食物链的那个方法)。但是当种类过多的时候,邻域的性能较差。
  • 有几个类别就开几个邻域。1~N表示奇数,N+1 ~ 2N 表示偶数。然后就这么开始写了:
#include<iostream>
#include<string>
#include<unordered_map>
#include<algorithm>
using namespace std;
const int maxn = 40010, N = 20000;
int p[maxn], M, num;
unordered_map<int, int> id;
int get(int x) {
	if (!id.count(x)) id[x] = ++num;
	return id[x];
}
void init() {
	for (int i = 0; i < maxn; i++) p[i] = i;
}
int find(int x) {
	if (p[x] != x) return p[x] = find(p[x]);
	return x;
}
void unite(int a, int b) {
	if (find(a) == find(b)) return;
	p[find(a)] = find(b);
}
int main() {
	int len;
	cin >> len >> M;
	int ans = 0;
	init();
	bool flag = true;
	for(int i = 0; i < M; i++) {
		int a, b;
		string type;
		cin >> a >> b >> type;
		if (!flag) continue;
		a = get(a - 1), b = get(b);
		if (type == "odd") {
			if (find(a) == find(b) || find(a + N) == find(b + N)) flag = false;
			else {
				unite(a, b + N);
				unite(a + N, b);
			}
		}
		else {
			if (find(a) == find(b + N) || find(a + N) == find(b)) flag = false;
			else {
				unite(a, b);
				unite(a + N, b + N);
			}
		}
		if (!flag) ans = i;
		
	}
	if (ans) printf("%d\n", ans);
	else printf("%d\n", M);
	return 0;
}

二、树状数组

1. 243. 一个简单的整数问题2

  • 给定一个长度为 N N N 的数列 A A A,以及 M M M 条指令,每条指令可能是以下两种之一:C l r d,表示把 A [ l ] , A [ l + 1 ] , … , A [ r ] A[l],A[l+1],…,A[r] A[l],A[l+1],,A[r] 都加上 d d dQ l r,表示询问数列中第 l ∼ r l \sim r lr 个数的和。对于每个询问,输出一个整数表示答案。

  • 这道题仍可以用差分去写,但是求区间和的时候,可以转化。假设b是差分数组,p1是b的前缀和数组,p2是 i ∗ b [ i ] i*b[i] ib[i]的前缀和数组。那么求原数组a的前i项和,可以转会为: p 1 [ i ] ∗ ( i + 1 ) − p 2 [ i ] p_1[i]*(i + 1) - p_2[i] p1[i](i+1)p2[i]。因此我们只需要用两个树状数组维护 p 1 p_1 p1 p 2 p_2 p2 即可。

#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 100010;
typedef long long ll;
ll a[maxn], tri1[maxn], tri2[maxn];
int N, M;
int lowbit(int x) {
	return x & -x;
}
void add(ll tr[], int id, ll c) {
	for (int i = id; i <= N; i += lowbit(i)) tr[i] += c;
}
ll sum(ll tr[], int id) {
	ll res = 0;
	for (int i = id; i; i -= lowbit(i)) res += tr[i];
	return res;
}
ll pre(int i) {
	return sum(tri1, i) * (i + 1) - sum(tri2, i);
}
int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N; i++) {
		scanf("%lld", &a[i]);
	}
	for (int i = 1; i <= N; i++) {
		add(tri1, i, a[i] - a[i - 1]);
		add(tri2, i, i * (a[i] - a[i - 1]));
	}
	while (M--) {
		char op[5];
		int l, r;
		scanf("%s%d%d", op, &l, &r);
		if (op[0] == 'Q') {
			printf("%lld\n", pre(r) - pre(l - 1));
		}
		else {
			ll d;
			scanf("%lld", &d);
			add(tri1, l, d), add(tri1, r + 1, -d);
			add(tri2, l, l * d), add(tri2, r + 1, -(r + 1) * d);
		}
	}
	return 0;
}

2. 244. 谜一样的牛

  • 有一个排列是 1 1 1 ~ n n n(但是不知道具体的排列是什么),已知逆序对,求原排列是什么.
  • 原题意:有 n n n 头奶牛,已知它们的身高为 1 ∼ n 1 \sim n 1n 且各不相同,但不知道每头奶牛的具体身高。现在这 n n n 头奶牛站成一列,已知第 i i i 头牛前面有 A i A_i Ai 头牛比它低,求每头奶牛的身高。
  • 显然最后一头牛的高度 A n + 1 A_n+1 An+1. 因此,倒数第二头牛的高度,就是在去掉 A n + 1 A_n+1 An+1 的数组中找到第 A n − 1 + 1 A_{n-1}+1 An1+1 小的数字. 然后其实这个用树状数组或者平衡树都是可以的。树状数组就是把所有位置初始化为1,扣掉一个位置就把一个数字变成零. 然后二分找满足前多少项和是 A i + 1 A_i+1 Ai+1 的位置。
  • 二分,脑子中一定要想明白求的是 lower_bound 还是 upper_bound 。求 lower_bound,结果保留 u b ub ub;求 upper_bound,结果保留 l b lb lb。不过这个也和 C 函数里面的 return 有关,这里说的是大于等于号。
#include<cstdio>

const int maxn = 100010;
int a[maxn], tr[maxn], N, ans[maxn];
int lowbit(int x) {
	return x & -x;
}
void add(int id, int c) {
	for (int i = id; i <= N; i += lowbit(i)) tr[i] += c;
}
int sum(int id) {
	int res = 0;
	for (int i = id; i; i -= lowbit(i)) res += tr[i];
	return res;
}
int main() {
	scanf("%d", &N);
	for (int i = 2; i <= N; i++) scanf("%d", &a[i]);
	//这里可以做一个小优化。add(i, 1)也可以这么写:
	for (int i = 1; i <= N; i++) tr[i] = lowbit(i);
	for (int i = N; i > 0; i--) {
		int lb = 0, ub = N + 1;
		while (ub - lb > 1) {
			int mid = (lb + ub) / 2;
			if (sum(mid) >= a[i] + 1) ub = mid;
			else lb = mid;
		}
		ans[i] = ub;
		add(ub, -1);
	}
	for (int i = 1; i <= N; i++) printf("%d\n", ans[i]);
	return 0;
}

3. 二维树状数组查询小矩形最值

I. Stock Analysis

法一

应该是求 ( l , l ) (l, l) (l,l) ( r , r ) (r, r) (r,r) 这个矩形的最值。但是并不能看太懂

#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long ll;
const ll INF = 1e18;
const int maxn = 2010, maxm = 200010;
struct node {
	int l, r, id;
	ll val;
};
vector<node> q;
ll w[maxn], tr[maxn][maxn], ans[maxm];
int lowbit(int x) {
	return x & -x;
}
bool cmp(const node& u, const node& v) {
	return u.val < v.val || u.val == v.val && u.id < v.id;
}
int N, M;
void update(int x, int y, ll val) {
	for (int i = x; i <= N; i += lowbit(i)) {
		for (int j = y; j <= N; j += lowbit(j)) {
			tr[i][j] = max(tr[i][j], val);
		}
	}
}
ll query(int x, int y) {
	ll res = -INF;
	for (int i = x; i; i -= lowbit(i)) {
		for (int j = y; j; j -= lowbit(j)) {
			res = max(tr[i][j], res);
		}
	}
	return res;
}
int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N; i++) {
		fill(tr[i], tr[i] + N + 1, -INF);
	}
	for (int i = 1; i <= N; i++) {
		scanf("%lld", &w[i]);
		w[i] += w[i - 1];
	}
	for (int i = 1; i <= N; i++) {
		for (int j = i; j <= N; j++) {
			q.push_back({ i, j, 0, w[j] - w[i - 1] });
		}
	}
	for (int i = 1; i <= M; i++) {
		int l, r;
		ll x;
		scanf("%d%d%lld", &l, &r, &x);
		q.push_back({ l, r, i, x });
	}
	sort(q.begin(), q.end(), cmp);
	for (auto p : q) {
		int l = p.l, r = p.r, id = p.id;
		ll val = p.val;
		if (!id) update(N - l + 1, r, val);
		else ans[id] = query(N - l + 1, r);
	}
	for (int i = 1; i <= M; i++) {
		if (ans[i] != -INF) printf("%lld\n", ans[i]);
		else printf("NONE\n");
	}
	return 0;
}
法二
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long ll;
const ll INF = 1e18;
const int maxn = 2010, maxm = 200010;
struct node {
	int l, r, id;
	ll val;
};
vector<node> q;
ll w[maxn], tr[maxn][maxn], ans[maxm];
int lowbit(int x) {
	return x & -x;
}
bool cmp(const node& u, const node& v) {
	return u.val < v.val || u.val == v.val && u.id < v.id;
}
int N, M;
void update(int x, int y, ll v) {
	while (x) {
		int z = y;
		while (z <= N) {
			tr[x][z] = max(tr[x][z], v);
			z += z & -z;
		}
		x -= x & -x;
	}
}

ll query(int x, int y) {
	ll res = -INF;
	while (x <= N) {
		int z = y;
		while (z) {
			res = max(res, tr[x][z]);
			z -= z & -z;
		}
		x += x & -x;
	}
	return res;
}
int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N; i++) {
		fill(tr[i], tr[i] + N + 1, -INF);
	}
	for (int i = 1; i <= N; i++) {
		scanf("%lld", &w[i]);
		w[i] += w[i - 1];
	}
	for (int i = 1; i <= N; i++) {
		for (int j = i; j <= N; j++) {
			q.push_back({ i, j, 0, w[j] - w[i - 1] });
		}
	}
	for (int i = 1; i <= M; i++) {
		int l, r;
		ll x;
		scanf("%d%d%lld", &l, &r, &x);
		q.push_back({ l, r, i, x });
	}
	sort(q.begin(), q.end(), cmp);
	for (auto p : q) {
		int l = p.l, r = p.r, id = p.id;
		ll val = p.val;
		if (!id) update(l, r, val);
		else ans[id] = query(l, r);
	}
	for (int i = 1; i <= M; i++) {
		if (ans[i] != -INF) printf("%lld\n", ans[i]);
		else printf("NONE\n");
	}
	return 0;
}

三、线段树

1. 245. 你能回答这些问题吗

给定长度为N的数列A,以及M条指令,每条指令可能是以下两种之一:

  1. “1 x y”,查询区间 [x,y] 中的最大连续子段和,即 m a x x ≤ l ≤ r ≤ y ∑ i = l r A [ i ] max_{x≤l≤r≤y}{\sum\limits_{i=l}^rA[i]} maxxlryi=lrA[i]
  2. “2 x y”,把 A[x] 改成 y。

对于每个查询指令,输出一个整数表示答案。

  • 线段树的节点储存:左右儿子、最大连续子段和tmax、最大前缀和lmax、最大后缀和rmax、区间和sum。
  • 最大连续子段和:max{左儿子.tmax,右儿子.tmax,左儿子.rmax + 右儿子.lmax}。
  • 最大前缀和lmax:max{左儿子.lmax,左儿子.sum + 右儿子.lmax}
#include<cstdio
#include<algorithm>
using namespace std;
const int maxn = 500010;
int a[maxn], N, M;
struct node {
	int l, r;
	int tmax, lmax, rmax, sum;
}tr[maxn * 4];
void pushup(node& u, node& l, node& r) {
	u.tmax = max(max(l.tmax, r.tmax), l.rmax + r.lmax);
	u.lmax = max(l.lmax, l.sum + r.lmax);
	u.rmax = max(r.rmax, l.rmax + r.sum);
	u.sum = l.sum + r.sum;
}
void pushup(int u) {
	pushup(tr[u], tr[2 * u], tr[2 * u + 1]);
}
void build(int u, int l, int r) {
	//一定注意,u对应的是线段树的下标,而l和r对应的是原数组的下标。
	if (l == r) tr[u] = { l, r, a[l], a[l], a[l], a[l] };
	else {
		tr[u].l = l, tr[u].r = r;
		int mid = (l + r) / 2;
		build(2 * u, l, mid), build(2 * u + 1, mid + 1, r);
		pushup(u);
	}
}
node query(int u, int l, int r) {
	if (l <= tr[u].l && tr[u].r <= r) return tr[u];
	int mid = (tr[u].l + tr[u].r) / 2;
	if (r <= mid) return query(2 * u, l, r);
	else if (l > mid) return query(2 * u + 1, l, r);
	else {
		node left = query(2 * u, l, r);
		node right = query(2 * u + 1, l, r);
		node res;
        //我突然意识到,这个地方的pushup,不是在修改线段树本身,而是在修改查询结果res.
		pushup(res, left, right);
		return res;
	}
}
void modify(int u, int x, int v) {
	if (tr[u].l == x && tr[u].r == x) tr[u] = { x, x, v, v, v, v };
	else {
		int mid = (tr[u].l + tr[u].r) / 2;
		if(x <= mid) modify(2 * u, x, v);
		else modify(2 * u + 1, x, v);
		pushup(u);
	}
}
int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N; i++) scanf("%d", &a[i]);
	build(1, 1, N);
	while (M--) {
		int type, x, y;
		scanf("%d%d%d", &type, &x, &y);
		if (type == 1) {
			if (x > y) swap(x, y);
			node n = query(1, x, y);
			printf("%d\n", n.tmax);
		}
		else modify(1, x, y);
	}
	return 0;
}

2. 246. 区间最大公约数

给定一个长度为N的数列A,以及M条指令,每条指令可能是以下两种之一:

1、“C l r d”,表示把 A[l],A[l+1],…,A[r] 都加上 d。

2、“Q l r”,表示询问 A[l],A[l+1],…,A[r] 的最大公约数(GCD)。

对于每个询问,输出一个整数表示答案。

  • gcd(a, b, c, d) = gcd(a, b - a, c - b, d - c)。这样子,就转化成了差分数组,求区间的最大公约数也转化成了求单点的最大公约数。
  • 那么,只需求一下gcd(sum(l), gcd(a[l + 1] - a[l], … g[r] - g[r - 1]))。不过,按照大雪菜的意思,如果gcd求出负数,只需要加一个绝对值就可以。
  • long long让蒟蒻留下两行泪。
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 500010;
typedef long long ll;
int N, M;
ll a[maxn];
struct node {
	int l, r;
	ll sum, d; //区间和和最大公约数
}tr[maxn * 4];
ll gcd(ll a, ll b) {
	if (b == 0) return a;
	return gcd(b, a % b);
}
void pushup(node& u, node& l, node& r) {
	u.sum = l.sum + r.sum;
	u.d = gcd(l.d, r.d);
}
void pushup(int u) {
	pushup(tr[u], tr[2 * u], tr[2 * u + 1]);
}
void build(int u, int l, int r) {
	if (l == r) tr[u] = { l, r, a[l] - a[l - 1], a[l] - a[l - 1] };
	else {
		tr[u].l = l, tr[u].r = r;
		int mid = (l + r) / 2;
		build(2 * u, l, mid), build(2 * u + 1, mid + 1, r);
		pushup(u);
	}
}
node query(int u, int l, int r) {
	if (l <= tr[u].l && tr[u].r <= r) return tr[u];
	int mid = (tr[u].l + tr[u].r) / 2;
	if (r <= mid) return query(2 * u, l, r);
	else if (l > mid) return query(2 * u + 1, l, r);
	else {
		node left = query(2 * u, l, r);
		node right = query(2 * u + 1, l, r);
		node res;
		pushup(res, left, right);
		return res;
	}
}
void modify(int u, int x, ll d) {
	if (tr[u].l == x && tr[u].r == x) tr[u] = { x, x, tr[u].sum + d, tr[u].sum + d };
	else {
		int mid = (tr[u].l + tr[u].r) / 2;
		if (x <= mid) modify(2 * u, x, d);
		else modify(2 * u + 1, x, d);
		pushup(u);
	}
}
int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N; i++) scanf("%lld", &a[i]);
	build(1, 1, N);
	while (M--) {
		char op[5];
		int l, r;
		scanf("%s%d%d", op, &l, &r);
		if (op[0] == 'Q') {
			node left = query(1, 1, l), right;
			if (l + 1 <= r) right = query(1, l + 1, r);
			else right = left;
			ll ans = abs(gcd(left.sum, right.d));
			printf("%lld\n", ans);
		}
		else {
			ll d;
			scanf("%lld", &d);
			modify(1, l, d);
			if (r + 1 <= N) modify(1, r + 1, -d);
		}
	}
	return 0;
}

3. 247. 亚特兰蒂斯

  • 扫描线算法
  • 题意:给很多矩形(可能有些矩形相交),求矩形的并的面积。
  • 操作一:将某个区间 [ l , r ] + k [l, r] + k [l,r]+k。操作二:计算整个区间长度大于0的区间总长。
  • 线段树的信息:
  1. c n t cnt cnt:当前区间被完全覆盖的次数。
  2. l e n len len:不考虑祖先节点的前提下, c n t > 0 cnt > 0 cnt>0 的区间总长。
  • 终于有点看懂了。这道题其实是这样的:第一步,线段树中存的是区间( y 0 y_0 y0 ~ y 1 y_1 y1, y 1 y_1 y1 ~ y 2 y_2 y2…),不过这里的 y y y 是离散化之后的。第二步,每次把 t r [ 1 ] . l e n ∗ ( x 2 − x 1 ) tr[1].len * (x_2 - x_1) tr[1].len(x2x1),而 l e n len len 就是当前 x 1 x_1 x1 ~ x 2 x_2 x2​ 区间的矩形高度之和。

其实这里的 c n t cnt cnt 相当于懒标记。因为这里查询的区间每次都是整个数组,因此就省去了懒标记下传操作,即 p u s h d o w n pushdown pushdown

#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn = 100010;
struct seg {
	double x, y1, y2;
	int k;
	bool operator < (const seg& rhs) {
		return x < rhs.x;
	}
}segs[maxn * 2];
struct node {
	int l, r, cnt;
	double len;
}tr[maxn * 8];
int N, kase;
vector<double> ys;

void pushup(int u) 
{
	if (tr[u].cnt) tr[u].len = (ys[tr[u].r + 1] - ys[tr[u].l]);
	else if (tr[u].l != tr[u].r) 
    {
		tr[u].len = tr[2 * u].len + tr[2 * u + 1].len;
	}
	else tr[u].len = 0;
}

void build(int u, int l, int r) {
	tr[u] = { l, r, 0, 0 };
	if (l != r) 
    {
		int mid = (l + r) / 2;
		build(2 * u, l, mid), build(2 * u + 1, mid + 1, r);
	}
}

void modify(int u, int l, int r, int k) {
	if (l <= tr[u].l && tr[u].r <= r) {
		tr[u].cnt += k;
		pushup(u);
	}
	else {
		int mid = (tr[u].l + tr[u].r) / 2;
		if (l <= mid) modify(2 * u, l, r, k);
		if (r > mid) modify(2 * u + 1, l, r, k);
		pushup(u);
	}
}

int find(double y) {
	return lower_bound(ys.begin(), ys.end(), y) - ys.begin();
}

int main() {
	while (scanf("%d", &N), N) {
        
		double x1, x2, y1, y2;
		for (int i = 0, j = 0; i < N; i++) {
			scanf("%lf%lf%lf%lf", &x1, &y1, &x2, &y2);
			segs[j++] = { x1, y1, y2, 1 };
			segs[j++] = { x2, y1, y2, -1 };
			ys.push_back(y1), ys.push_back(y2);
		}
        
		sort(ys.begin(), ys.end());
		ys.erase(unique(ys.begin(), ys.end()), ys.end());
		sort(segs, segs + 2 * N);
		build(1, 0, ys.size() - 2);
		double ans = 0;
        
		for (int i = 0; i < 2 * N; i++) {
			if (i > 0) ans += tr[1].len * (segs[i].x - segs[i - 1].x);
			modify(1, find(segs[i].y1), find(segs[i].y2) - 1, segs[i].k);
		}
		printf("Test case #%d\nTotal explored area: %.2f\n\n", ++kase, ans);
	}
	return 0;
}

4. 1277. 维护序列

老师交给小可可一个维护数列的任务,现在小可可希望你来帮他完成。

有长为 N N N 的数列,不妨设为 a 1 , a 2 , … , a N a_1,a_2,…,a_N a1,a2,,aN

有如下三种操作形式:

  1. 把数列中的一段数全部乘一个值;
  2. 把数列中的一段数全部加一个值;
  3. 询问数列中的一段数的和,由于答案可能很大,你只需输出这个数模 P P P 的值。
  • 用了两个懒标记:add和mul。线段树维护五个值:l, r, sum, add, mul。而sum * mul + add,先乘再加,可以正确地表示这个值。
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 100010;
typedef long long ll;
struct node {
	int l, r;
	ll sum, add, mul;
}tr[maxn * 4];
int a[maxn], N, M, P;
void pushup(int u) {
	tr[u].sum = (tr[2 * u].sum + tr[2 * u + 1].sum) % P;
}
void eval(node& u, ll add, ll mul) {
	u.sum = (u.sum * mul % P + (u.r - u.l + 1) * add % P) % P;
	u.mul = u.mul * mul % P;
	u.add = (u.add * mul % P + add) % P;
}
void pushdown(int u) {
	eval(tr[2 * u], tr[u].add, tr[u].mul);
	eval(tr[2 * u + 1], tr[u].add, tr[u].mul);
	tr[u].add = 0, tr[u].mul = 1;
}
void build(int u, int l, int r) {
	if (l == r) tr[u] = { l, r, a[l], 0, 1 };
	else {
		tr[u] = { l, r, 0, 0, 1 };
		int mid = (l + r) / 2;
		build(2 * u, l, mid), build(2 * u + 1, mid + 1, r);
		pushup(u);
	}
}
void modify(int u, int l, int r, ll add, ll mul) {
	if (l <= tr[u].l && tr[u].r <= r) eval(tr[u], add, mul);
	else {
		pushdown(u);
		int mid = (tr[u].l + tr[u].r) / 2;
		if (l <= mid) modify(2 * u, l, r, add, mul);
		if (r > mid) modify(2 * u + 1, l, r, add, mul);
		pushup(u);
	}
}
ll query(int u, int l, int r) {
	if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum;
	pushdown(u);
	ll sum = 0;
	int mid = (tr[u].l + tr[u].r) / 2;
	if (l <= mid) sum = query(2 * u, l, r);
	if (r > mid) sum = (sum + query(2 * u + 1, l, r)) % P;
	return sum % P;
}
int main() {
	scanf("%d%d", &N, &P);
	for (int i = 1; i <= N; i++) scanf("%d", &a[i]);
	build(1, 1, N);
	scanf("%d", &M);
	for (int i = 0; i < M; i++) {
		int t, l, r;
		scanf("%d%d%d", &t, &l, &r);
		if (t == 1) {
			ll c;
			scanf("%lld", &c);
			modify(1, l, r, 0, c);
		}
		else if (t == 2) {
			ll c;
			scanf("%lld", &c);
			modify(1, l, r, c, 1);
		}
		else printf("%lld\n", query(1, l, r));
	}
	return 0;
}

四、可持久化数据结构

Easy DP Problem

  • 题意:给一个数组 a,询问 ∑ i = 1 r − l + 1 i 2 \sum\limits_{i = 1}^{r - l + 1}i^2 i=1rl+1i2 加上 a[l ~ r] 这个区间前 k 个大的数之和

  • 这个题重点是如何求前 K 个大的数之和。用主席树修改 insert 和 query 部分的代码即可。但是要小心两个地方

  1. 初始化的时候要注意
  2. 因为可能有重复数字,因此在 query 的时候,不可以直接加上 tr[q].sum - tr[p].sum,要这样子处理 (tr[q].sum - tr[p].sum) / (tr[q].cnt - tr[p].cnt) * k;
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
#include<functional>
using namespace std;
const int maxn = 100010;
typedef long long ll;
int N, M, a[maxn];
ll sum2[maxn];
vector<int> nums;
struct node {
	int l, r;
	int cnt;
	ll sum;
}tr[maxn * 4 + maxn * 17];
//空间开到 4N + N * log(N)
int root[maxn], idx;
int find(int x) {
	return lower_bound(nums.begin(), nums.end(), x, greater<int>()) - nums.begin();
}
int build(int l, int r) {
	int p = ++idx;
	tr[p] = { l, r };
	if (l == r) return p;
	int mid = (l + r) / 2;
	tr[p].l = build(l, mid), tr[p].r = build(mid + 1, r);
	return p;
}
//p 为原来的线段树的结点,q 为复制的线段是的结点
int insert(int p, int l, int r, int x) {
	int q = ++idx;
	tr[q] = tr[p];
	if (l == r) {
		tr[q].cnt++;
		tr[q].sum += nums[x];
		return q;
	}
	int mid = (l + r) / 2;
	if (x <= mid) tr[q].l = insert(tr[p].l, l, mid, x);
	else tr[q].r = insert(tr[p].r, mid + 1, r, x);
	tr[q].cnt = tr[tr[q].l].cnt + tr[tr[q].r].cnt;
	tr[q].sum = tr[tr[q].l].sum + tr[tr[q].r].sum;
	return q;
}
ll query(int q, int p, int l, int r, int k) {
	//这个地方要小心,因为有重复数字的话,需要这样处理,不然结果会偏大。
	if (l == r) return (tr[q].sum - tr[p].sum) / (tr[q].cnt - tr[p].cnt) * k;
	int cnt = tr[tr[q].l].cnt - tr[tr[p].l].cnt;
	int mid = (l + r) / 2;
	if (k <= cnt) return query(tr[q].l, tr[p].l, l, mid, k);
	else return tr[tr[q].l].sum - tr[tr[p].l].sum + query(tr[q].r, tr[p].r, mid + 1, r, k - cnt);
}
int main() {
	for (ll i = 1; i <= 100000; i++) {
		sum2[i] = i * i + sum2[i - 1];
	}
	int T;
	scanf("%d", &T);
	while (T--) {
		//看看如何初始化的
		nums.clear();
		for (int i = 0; i <= idx; i++) {
			tr[i] = { 0, 0, 0, 0 };
		}
		fill(root, root + N + 1, 0);
		idx = 0;

		scanf("%d", &N);
		for (int i = 1; i <= N; i++) {
			scanf("%d", &a[i]);
			nums.push_back(a[i]);
		}
		sort(nums.begin(), nums.end(), greater<int>());
		nums.erase(unique(nums.begin(), nums.end()), nums.end());
		root[0] = build(0, nums.size() - 1);

		for (int i = 1; i <= N; i++) {
			root[i] = insert(root[i - 1], 0, nums.size() - 1, find(a[i]));
		}

		scanf("%d", &M);
		while (M--) {
			int l, r, k;
			scanf("%d%d%d", &l, &r, &k);
			int m = r - l + 1;
			printf("%lld\n", sum2[m] + query(root[r], root[l - 1], 0, nums.size() - 1, k));
		}
	}
	return 0;
}

五、平衡树

265. 营业额统计

  • f i f_i fi的最大值: f i = m i n 1 ≤ j < i ∣ a i − a j ∣ f_i=min_{1≤j<i}|a_i−a_j| fi=min1j<iaiaj
  • 这道题转化过来,就是找到不超过 a i a_i ai的最大值和不小于 a i a_i ai的最小值。这就是平衡树嘛。但是,这个用set可以做到,不过这个题还是手写平衡树演示一下。
  • 这道题不用在rank和key之间转化。所以不用维护sz,也不用pushup函数。按照题目的意思也不用维护cnt。
#include<cstdio>
#include<algorithm>
#include<cstdlib>
using namespace std;
const int maxn = 33010, INF = 1e7;
int N;
struct node {
	int l, r;
	int key, val;
}tr[maxn];
int root, idx;
int get_node(int key) {
	tr[++idx].key = key;
	tr[idx].val = rand();
	return idx;
}
void zig(int& p) {
	int q = tr[p].l;
	tr[p].l = tr[q].r, tr[q].r = p, p = q;
}
void zag(int& p) {
	int q = tr[p].r;
	tr[p].r = tr[q].l, tr[q].l = p, p = q;
}
void build() {
	get_node(-INF), get_node(INF);
	root = 1, tr[1].r = 2;
	if (tr[1].val < tr[2].val) zag(root);
}
void insert(int& p, int key) {
	if (!p) p = get_node(key);
	else if (tr[p].key > key) {
		insert(tr[p].l, key);
		if (tr[tr[p].l].val > tr[p].val) zig(p);
	}
	else if (tr[p].key < key) {
		insert(tr[p].r, key);
		if (tr[tr[p].r].val > tr[p].val) zag(p);
	}
}
int get_prev(int p, int key) {
	if (!p) return -INF;
	//注意,这里一定要写成>,因为这个并非找的严格小于key的,而是找不大于key的.
	if (tr[p].key > key) return get_prev(tr[p].l, key);
	return max(tr[p].key, get_prev(tr[p].r, key));
}
int get_next(int p, int key) {
	if (!p) return INF;
	if (tr[p].key < key) return get_next(tr[p].r, key);
	return min(tr[p].key, get_next(tr[p].l, key));
}
int main() {
	build();
	scanf("%d", &N);
	int res = 0, x;
	for (int i = 0; i < N; i++) {
		scanf("%d", &x);
		if (!i) res += x;
		else res += min(x - get_prev(root, x), get_next(root, x) - x);
		insert(root, x);
	}
	printf("%d\n", res);
	return 0;
}

六、AC自动机

1. 1053. 修复DNA

  • 题意:给定一族只由 A , G , T , C A,G,T,C A,G,T,C 组成的字符串集合 S S S,然后给一个只由 A , G , T , C A,G,T,C A,G,T,C 组成的主串,问至少需要修改主串的几个字母,才能使主串中不含有 S S S 中的任意一个字符串作为其子串.

2. 1285. 单词

  • 题意:某人读论文,一篇论文是由许多单词组成的。但他发现一个单词会在论文中出现很多次,现在他想知道每个单词分别在论文中出现多少次。
  • 一个单词出现的次数,就是满足这样的前缀:后缀等于该单词. 因此我们计算的是,对于每一个前缀,有多少个后缀满足要求(即后缀为一个合法单词). 比如以 i i i 结点结尾的前缀数量可以叠加到 n e [ i ] ne[i] ne[i] 上面去.
  • 因此,我们可以把整个 T r i e Trie Trie 图当作一个 D A G DAG DAG,有向边就是由 i → n e [ i ] i \rightarrow ne[i] ine[i] 这些边组成的。由于是求满足要求的前缀的数量,其实就是求到合法终点(就是单词的最后一个字母的节点编号)的路径的数量. 那么所有起点的初始值为有多少个单词可以以它结尾,沿着拓扑序往根节点叠加即可.
  • n e [ i ] ne[i] ne[i] 的值就是以下标以 i i i 结尾的后缀对应的前缀的结尾的下标。此时的这个后缀与前缀相同,且长度是最长的。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1000010;
int N;
int tr[maxn][26], f[maxn], idx;
int q[maxn], ne[maxn];
char str[maxn];
int id[210];
void insert(int x) {
	int p = 0;
	for (int i = 0; str[i]; i++) {
		int t = str[i] - 'a';
		if (!tr[p][t]) tr[p][t] = ++idx;
		p = tr[p][t];
		f[p]++;
	}
	id[x] = p;
}
void build() {
	int hh = 0, tt = -1;
	for (int i = 0; i < 26; i++) {
		if (tr[0][i]) q[++tt] = tr[0][i];
	}
	while (hh <= tt) {
		int t = q[hh++];
		for (int i = 0; i < 26; i++) {
            //这里使用引用和之前的那个题是一样的效果。
			int& p = tr[t][i];
			if (!p) p = tr[ne[t]][i];
			else {
				ne[p] = tr[ne[t]][i];
				q[++tt] = p;
			}
		}
	}
}
int main() {
	scanf("%d", &N);
	for (int i = 0; i < N; i++) {
		scanf("%s", str);
		insert(i);
	}
	build();
	for (int i = idx - 1; i >= 0; i--) f[ne[q[i]]] += f[q[i]];
	for (int i = 0; i < N; i++) printf("%d\n", f[id[i]]);
	return 0;
}

七、 Splay

1. 2437. Splay

  • 题意:给定一个长度为 n n n 的整数序列,初始时序列为 { 1 , 2 , . . . , n − 1 , n } \{1,2,...,n-1,n\} {1,2,...,n1,n}。序列中的位置从左到右依次标号为 1 ∼ n 1 \sim n 1n。我们用 [ l , r ] [l,r] [l,r] 来表示从位置 l l l 到位置 r r r 之间(包括两端点)的所有数字构成的子序列。现在要对该序列进行 m m m 次操作,每次操作选定一个子序列 [ l , r ] [l,r] [l,r],并将该子序列中的所有数字进行翻转。例如,对于现有序列 1 3 2 4 6 5 7,如果某次操作选定翻转子序列为 [ 3 , 6 ] [3,6] [3,6],那么经过这次操作后序列变为 1 3 5 6 4 2 7。请你求出经过 m m m 次操作后的序列。
  • 这个题就是把第 l − 1 l - 1 l1 个点转到根节点下面,把第 r + 1 r + 1 r+1 个点转到第 l − 1 l-1 l1 个点下面(如图),这样子的话, l l l ~ r r r 部分所形成的子树就成了第 r + 1 r+1 r+1 个点的左子树。然后把这个左子树反转即可。

在这里插入图片描述

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 100010;
int N, M;
struct node {
	//子结点,父结点,关键字
	int s[2], p, v;
	//子树大小,是否反转
	int size, flag;
	void init(int v_, int p_) {
		v = v_, p = p_, size = 1;
	}
}tr[maxn];

int root, idx;

void pushup(int x) {
	tr[x].size = tr[tr[x].s[0]].size + tr[tr[x].s[1]].size + 1;
}
//将翻转子树的操作向下传递
void pushdown(int x) {
	if (tr[x].flag) {
		swap(tr[x].s[0], tr[x].s[1]);
		tr[tr[x].s[0]].flag ^= 1;
		tr[tr[x].s[1]].flag ^= 1;
		tr[x].flag = 0;
	}
}

//旋转分为左旋和右旋,但是经过多年的改善,已经统一成一个函数
//巧妙背诵:按照 z, y, x 的顺序从上到下,先修改 z 的儿子,再修改谁指向 z.
void rotate(int x) {
	int y = tr[x].p, z = tr[y].p;
	int k = (tr[y].s[1] == x); // k 是 0 的话,x 是 y 的左儿子
	tr[z].s[tr[z].s[1] == y] = x, tr[x].p = z;
	tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y;
	tr[x].s[k ^ 1] = y, tr[y].p = x;
	//z 的子树大小并没有变化,因此不用pushup(z)了
	pushup(y), pushup(x);
}
//将 splay 下标第 x 个结点反转到第 k 个结点下面
void splay(int x, int k) {
	while (tr[x].p != k) { 
		int y = tr[x].p, z = tr[y].p;
		if (z != k) {
            //判断 x, y, z 是否在一条直线上
			if ((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x);
			else rotate(y);
		}
		rotate(x);
	}
	if (!k) root = x;
}

void insert(int v) {
	int u = root, p = 0;
	while (u) p = u, u = tr[u].s[v > tr[u].v];
	u = ++idx;
	if (p) tr[p].s[v > tr[p].v] = u;
	tr[u].init(v, p);
	splay(u, 0);
}

// 找第 k 个数在 splay 中的下标
int get_k(int k) {
	int u = root;
	while (u) {
		pushdown(u);
		if (tr[tr[u].s[0]].size >= k) u = tr[u].s[0];
		else if (tr[tr[u].s[0]].size + 1 == k) return u;
		else {
			k -= tr[tr[u].s[0]].size + 1, u = tr[u].s[1];
		}
	}
	return -1;
}

void output(int u) {
	pushdown(u);
	if (tr[u].s[0]) output(tr[u].s[0]);
	if (tr[u].v >= 1 && tr[u].v <= N) printf("%d ", tr[u].v);
	if (tr[u].s[1]) output(tr[u].s[1]);
}

int main() {
	scanf("%d%d", &N, &M);
	//为了防止越界,splay会在开头和结尾加两个哨兵
    //一般不是这样建splay树的,建树看下一道题
	for (int i = 0; i <= N + 1; i++) insert(i);
	while (M--) {
		int l, r;
		scanf("%d%d", &l, &r);
		// 要找到第 l - 1 和第 r + 1 个点。因为加了哨兵,所以下标都往右平移一个。
		l = get_k(l), r = get_k(r + 2);
		splay(l, 0), splay(r, l);
		//将整个左子树反转,也就是将 flag 标记一下
		tr[tr[r].s[0]].flag ^= 1;
	}
	//将当前的整个树输出
	output(root);
	return 0;
}

八、树链剖分

918. 软件包管理器 - AcWing题库

  • 题意:给一个 n n n 个点的树,一开始每个点的权值都是 0 0 0. 进行两种操作:install x:把从 x x x 到根节点的一条路径上的的权值全部变成1;uninstall x:把以 x x x 为根节点的子树的所有结点全部置为0. 每进行一个操作,输出有多少个点的权值发生了改变。
#include<bits/stdc++.h>
using namespace std;

const int N = 100010, M = 200010;
int h[N], e[M], ne[M], idx;
int top[N], id[N], cnt;

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

int dep[N], fa[N], sz[N], son[N];
void dfs1(int u, int father, int depth)
{
    dep[u] = depth, sz[u] = 1, fa[u] = father;
    for(int i = h[u]; i != -1; i = ne[i])
    {
        int v = e[i];
        if(v == father) continue;
        dfs1(v, u, depth + 1);
        sz[u] += sz[v];
        if(sz[son[u]] < sz[v]) son[u] = v;
    }
}

void dfs2(int u, int t)
{
    id[u] = ++cnt, top[u] = t;
    if(!son[u]) return;
    dfs2(son[u], t);

    for(int i = h[u]; i != -1; i = ne[i])
    {
        int v = e[i];
        if(v == fa[u] || son[u] == v) continue;
        dfs2(v, v);
    }
}

struct node
{
    int l, r;
    //flag = 0表示没有懒标记,flag = 1 表示整个区间都置为1,flag = 2 表示整个区间都置为0.
    int flag, sum;
}tr[N * 4];



void pushup(int u)
{
    tr[u].sum = tr[2 * u].sum + tr[2 * u + 1].sum;
}

void pushdown(int u)
{
    auto& root = tr[u], &left = tr[2 * u], &right = tr[2 * u + 1];
    if(root.flag)
    {
        left.flag = right.flag = root.flag;
        if(root.flag == 1)
        {
            left.sum = left.r - left.l + 1;
            right.sum = right.r - right.l + 1;
        }
        else left.sum = right.sum = 0;
    }
    root.flag = 0;
}

void build(int u, int l, int r)
{
    tr[u] = {l, r};
    if(l == r) return;
    int mid = (l + r) / 2;
    build(2 * u, l, mid), build(2 * u + 1, mid + 1, r);
}

void update(int u, int l, int r, int flag)
{
    if(l <= tr[u].l && tr[u].r <= r)
    {
        tr[u].flag = flag;
        if(flag == 1) tr[u].sum = tr[u].r - tr[u].l + 1;
        else tr[u].sum = 0;
    }
    else{
        pushdown(u);
        int mid = (tr[u].l + tr[u].r) / 2;
        if(l <= mid) update(2 * u, l, r, flag);
        if(r > mid) update(2 * u + 1, l, r, flag);
        pushup(u);
    }
}

int query(int u, int l, int r)
{
    if(l <= tr[u].l && tr[u].r <= r)
    {
        return tr[u].sum;
    }
    else{
        pushdown(u);
        int mid = (tr[u].l + tr[u].r) / 2;
        int res = 0;
        if(l <= mid) res += query(2 * u, l, r);
        if(r > mid) res += query(2 * u + 1, l, r);
        return res;
    }
}

void update_path(int u, int v)
{
    int res = 0;  //忘记初始化res了...
    while(top[u] != top[v])
    {
        if(dep[top[u]] < dep[top[v]]) swap(u, v);
        res += (id[u] - id[top[u]] + 1) - query(1, id[top[u]], id[u]);
        update(1, id[top[u]], id[u], 1);
        u = fa[top[u]];
    }
    if(dep[u] < dep[v]) swap(u, v);

    //中间的-写成+了
    res += (id[u] - id[v] + 1) - query(1, id[v], id[u]);
    update(1, id[v], id[u], 1);

    printf("%d\n", res);
}

void update_tree(int u)
{
    printf("%d\n", query(1, id[u], id[u] + sz[u] - 1));
    update(1, id[u], id[u] + sz[u] - 1, 2);
}

int main()
{
    int n;
    scanf("%d", &n);
    memset(h, -1, sizeof h);

    for(int i = 2; i <= n; i++)
    {
        int p;
        scanf("%d", &p);
        add(i, p + 1), add(p + 1, i);
    }

    dfs1(1, -1, 1);
    dfs2(1, 1);
    build(1, 1, n);
    
    int q;
    scanf("%d", &q);
    while(q--)
    {
        static char op[15];
        int x;
        scanf("%s%d", op, &x);
        if(*op == 'i')
        {
            update_path(1, x + 1);
        }
        else
        {
            update_tree(x + 1);
        }
    }
    return 0;
}

九、动态树

十、分块

  • 分块是一种很灵活的思想,相较于树状数组和线段树,分块的优点是通用性更好,可以维护很多树状数组和线段树无法维护的信息。

分块数组

  • 块状数组,即把一个数组分为几个块,块内信息整体保存, 若查询时遇到两边不完整的块直接暴力查询。 一般情况下,块的长度为 O ( n ) O(\sqrt n) O(n ).

分块链表

莫队算法

  • 莫队算法可以解决一类离线区间询问问题,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作。
  • 离线后排序,顺序处理每个询问,暴力从上一个区间的答案转移到下一个区间答案(一步一步移动即可)。
  • 对于区间 [ l , r ] [l,r] [l,r], 以 l l l 所在块的编号为第一关键字, r r r 为第二关键字从小到大排序。
  • 复杂度: O ( n ∗ n ) . O(n*\sqrt n). O(nn ).

1. D. Powerful array

  • 题意:给定一个数列: a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an。定义 K a K_a Ka 为区间 [ l , r ] [l,r] [lr] a a a 出现的次数。 t t t 个查询,每个查询l,r,对区间内所有 a [ i ] a[i] a[i] ,求 ∑ K 2 ∗ a [ i ] \sum{K^2*a[i]} K2a[i]
  • 用到了莫队算法:离线+分块。将n个数分成sqrt(n)块。对所有询问进行排序,排序标准:
  1. Q[i].left /block_size < Q[j].left / block_size (块号优先排序)
  2. 如果1相同,则 Q[i].right < Q[j].right (按照查询的右边界排序)
  • 如果一个数已经出现了x次,那么需要累加 ( 2 ∗ x + 1 ) ∗ a [ i ] (2*x+1)*a[i] (2x+1)a[i],因为 ( x + 1 ) 2 ∗ a [ i ] = ( x 2 + 2 ∗ x + 1 ) ∗ a [ i ] (x+1)^2*a[i] = (x^2 +2*x + 1)*a[i] (x+1)2a[i]=(x2+2x+1)a[i] x 2 ∗ a [ i ] x^2*a[i] x2a[i] 是出现x次的结果, ( x + 1 ) 2 ∗ a [ i ] (x+1)^2 * a[i] (x+1)2a[i] 是出现 x + 1 x+1 x+1 次的结果。

  • 看了代码之后,妙啊!

  • 除以,看到 10^6 时,千万别用 c i n , c o u t cin, cout cin,cout,我这次差一点点超时。换成 s c a n f , p r i n t f scanf, printf scanf,printf 后就特别快。

  • 似乎, GNU C++17 (64) 比 GNU C++17 快了不少。

#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
const int maxa = 1000010, maxn = 200010;
typedef long long ll;
struct query {
	int l, r, id;
}Q[maxn];


//sum[i] 是当前查到的 i 这个数字有多少个。
ll a[maxn], sum[maxa], ans[maxn];
int N, T, block_size, L, R;  //当前的左边界、右边界。
ll now;  //当前的答案

bool cmp(query a, query b) {
	if (a.l / block_size == b.l / block_size) return a.r < b.r;
	return a.l / block_size < b.l / block_size;
}

void modify(int l, int r) {
	//若当前 L 在 l 的左边
	while (L < l) {
		now -= (2 * sum[a[L]] - 1) * a[L];
		sum[a[L]]--;
		L++;
	}
	//若当前 L 在 l 的右边
	while (L > l) {
		L--;
		now += (2 * sum[a[L]] + 1) * a[L];
		sum[a[L]]++;
		
	}
	//若当前 R 在 r 的左边
	while (R < r) {
		R++;
		now += (2 * sum[a[R]] + 1) * a[R];
		sum[a[R]]++;
		
	}
	//若当前 R 在 r 的右边
	while (R > r) {
		now -= (2 * sum[a[R]] - 1) * a[R];
		sum[a[R]]--;
		R--;
	}
}
int main() {
	scanf("%d%d", &N, &T);
	
	for (int i = 1; i <= N; i++) scanf("%I64d", &a[i]);
	for (int i = 1; i <= T; i++) {
		scanf("%d%d", &Q[i].l, &Q[i].r);
		Q[i].id = i;
	}
	
	block_size = sqrt(N);

	sort(Q + 1, Q + T + 1, cmp);
	for (int i = 1; i <= T; i++) {
		modify(Q[i].l, Q[i].r);
		ans[Q[i].id] = now;
	}
	for (int i = 1; i <= T; i++) {
		printf("%I64d\n", ans[i]);
	}
	return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值