垃圾ACMer的暑假训练220708

垃圾ACMer的暑假训练220708

5. 线段树与树状数组

5.1 动态求连续区间和

题意

给定一个整数数组,数组下标从 1 1 1开始,要求支持两种操作:①修改某一元素的值;②求子序列 [ a , b ] [a,b] [a,b]的和.

第一行输入两个整数 n    ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n  (1n1e5) m    ( 1 ≤ m ≤ 1 e 5 ) m\ \ (1\leq m\leq 1\mathrm{e}5) m  (1m1e5),分别表示数组长度和操作个数.第二行输入 n n n个数表示原数组.接下来 m m m行每行输入三个数 k , a , b    ( 1 ≤ a ≤ b ≤ n ) k,a,b\ \ (1\leq a\leq b\leq n) k,a,b  (1abn),当 k = 0 k=0 k=0是表示求子序列 [ a , b ] [a,b] [a,b]的和; k = 1 k=1 k=1时表示第 a a a个数加 b b b.数据保证任意时刻数组内的元素之和在int内.

输出 k = 0 k=0 k=0的操作的结果.

代码I:BIT
const int MAXN = 1e5 + 5;
int n, m;  // 数组长度、操作数
int BIT[MAXN];

void add(int x, int v) {  // arr[x]+=v
	for (int i = x; i <= n; i += lowbit(i)) BIT[i] += v;
}

int query(int x) {  // 求arr[1...x]的前缀和
	int res = 0;
	for (int i = x; i; i -= lowbit(i)) res += BIT[i];
	return res;
}

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		int x; cin >> x;
		add(i, x);
	}

	while (m--) {
		int k, a, b; cin >> k >> a >> b;
		if (k) add(a, b);
		else cout << query(b) - query(a - 1) << endl;
	}
}

代码II:SegTree
const int MAXN = 1e5 + 5;
int n, m;  // 数组长度、操作个数
int nums[MAXN];
struct Node {
	int l, r;
	int sum;
}SegT[MAXN << 2];

void pushup(int u) {
	SegT[u].sum = SegT[u << 1].sum + SegT[u << 1 | 1].sum;
}

void build(int u, int l, int r) {
	if (l == r) SegT[u] = { l,r,nums[r] };
	else {
		SegT[u] = { l,r,0 };
		int mid = l + r >> 1;
		build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
		pushup(u);
	}
}

int query(int u, int l, int r) {
	if (SegT[u].l >= l && SegT[u].r <= r) return SegT[u].sum;
	int mid = SegT[u].l + SegT[u].r >> 1;
	int res = 0;
	if (l <= mid) res += query(u << 1, l, r);
	if (r > mid) res += query(u << 1 | 1, l, r);
	return res;
}

void modify(int u, int x, int v) {  // 在x位置加上v
	if (SegT[u].l == SegT[u].r) SegT[u].sum += v;
	else {
		int mid = SegT[u].l + SegT[u].r >> 1;
		if (x <= mid) modify(u << 1, x, v);
		else modify(u << 1 | 1, x, v);
		pushup(u);
	}
}

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) cin >> nums[i];
	
	build(1, 1, n);

	while (m--) {
		int k, a, b; cin >> k >> a >> b;
		if (k) modify(1, a, b);
		else cout << query(1, a, b) << endl;
	}
}


5.3 数列区间最大值 ( 2   s 2\ \mathrm{s} 2 s)

题意

输入一个长度为 n    ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n  (1n1e5)的序列和 m    ( 1 ≤ m ≤ 1 e 6 ) m\ \ (1\leq m\leq 1\mathrm{e}6) m  (1m1e6)个询问,每次询问输入两整数 l , r    ( 1 ≤ l ≤ r ≤ n ) l,r\ \ (1\leq l\leq r\leq n) l,r  (1lrn),求区间 [ l , r ] [l,r] [l,r]内的最大数.数据保证序列中所有数不超过 2 63 − 1 2^{63}-1 2631.

代码
const int MAXN = 1e5 + 5;
int n, m;  // 数组长度、操作个数
ll nums[MAXN];
struct Node {
	int l, r;
	ll maxnum;
}SegT[MAXN << 2];

void build(int u, int l, int r) {
	if (l == r) SegT[u] = { l,r,nums[r] };
	else {
		SegT[u] = { l,r,0 };
		int mid = l + r >> 1;
		build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
		SegT[u].maxnum = max(SegT[u << 1].maxnum, SegT[u << 1 | 1].maxnum);
	}
}

ll query(int u, int l, int r) {
	if (SegT[u].l >= l && SegT[u].r <= r) return SegT[u].maxnum;
	
	int mid = SegT[u].l + SegT[u].r >> 1;
	ll res = -INFF;
	if (l <= mid) res = max(res, query(u << 1, l, r));
	if (r > mid) res = max(res, query(u << 1 | 1, l, r));
	return res;
}

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) cin >> nums[i];

	build(1, 1, n);

	while (m--) {
		int l, r; cin >> l >> r;
		cout << query(1, l, r) << endl;
	}
}


5.4 小朋友排队

题意

n n n个小朋友排成一排,现要将他们按身高升序排列,要求每次只能交换相邻的小朋友.若两个小朋友身高相等,则他们谁在前都可以.每个小朋友有一个不高兴程度,初始为 0 0 0.若某个小朋友第一次被要求交换,则他的不高兴程度 + 1 +1 +1;若他第二次被要求交换,则他的不高兴程度 + 2 +2 +2,以此类推.问将他们按身高升序排列后他们的不高兴程度之和的最小值.

第一行输入整数 n    ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n  (1n1e5),表示小朋友个数.第二行输入 n n n个整数 h 1 , ⋯   , h n    ( 1 ≤ h i ≤ 1 e 6 ) h_1,\cdots,h_n\ \ (1\leq h_i\leq 1\mathrm{e}6) h1,,hn  (1hi1e6),表示小朋友的身高.

思路

类似于冒泡排序,考察序列的逆序对数.若序列有 k k k个逆序对,每次交换至多使逆序对数 − 1 -1 1,故完成排序需交换 k k k次.

考察如何分配小朋友的交换次数使得他们的不高兴程度之和最小.

最优解中,每个小朋友交换的次数固定.

[] 对序列中的某个数 i i i,设其前面有 k 1 k_1 k1个比它大的数,其后面有 k 2 k_2 k2个比它小的数,则将 i i i放在正确的位置至少需交换 ( k 1 + k 2 ) (k_1+k_2) (k1+k2)次.

设序列有 k k k个逆序对.考察每个小朋友的 ( k 1 + k 2 ) (k_1+k_2) (k1+k2)之和,显然每个逆序对都被算了两次,故和为 2 k 2k 2k.

故每个小朋友交换 ( k 1 + k 2 ) (k_1+k_2) (k1+k2)次即可将序列升序排列,且每个小朋友贡献的不高兴程度为 1 + 2 + ⋯ + ( k 1 + k 2 ) 1+2+\cdots+(k_1+k_2) 1+2++(k1+k2).

用BIT求逆序对数.对每个新数,查询它前面有几个比它大的数,再将新数插入BIT.

注意BIT以身高为下标,要开 1 e 6 1\mathrm{e}6 1e6.注意答案可能爆int.

代码
const int MAXN = 1e6 + 5;
int n;
int h[MAXN];
int BIT[MAXN];
int sum[MAXN];  // 每个小朋友的交换次数

void add(int x, int v) {
	for (int i = x; i < MAXN; i += lowbit(i)) BIT[i] += v;
}

int query(int x) {
	int res = 0;
	for (int i = x; i; i -= lowbit(i)) res += BIT[i];
	return res;
}

int main() {
	cin >> n;
	for (int i = 0; i < n; i++) cin >> h[i], h[i]++;  // BIT下标从1开始

	for (int i = 0; i < n; i++) {  // 对每个数,统计前面有几个比它大
		sum[i] += query(MAXN - 1) - query(h[i]);
		add(h[i], 1);
	}

	memset(BIT, 0, so(BIT));  // 清空BIT

	for (int i = n - 1; i >= 0; i--) {  // 对每个数,统计后面有几个比它小
		sum[i] += query(h[i] - 1);
		add(h[i], 1);
	}

	ll ans = 0;
	for (int i = 0; i < n; i++) ans += (ll)sum[i] * (sum[i] + 1) / 2;
	cout << ans;
}


5.5 油漆面积

题意

给定 n    ( 1 ≤ n ≤ 1 e 4 ) n\ \ (1\leq n\leq 1\mathrm{e}4) n  (1n1e4)个矩形的左下角和右上角坐标 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) ( x 2 , y 2 )    ( 0 ≤ x 1 , x 2 , y 1 , y 2 ≤ 1 e 4 , x 1 < x 2 , y 1 < y 2 ) (x_2,y_2)\ \ (0\leq x_1,x_2,y_1,y_2\leq 1\mathrm{e}4,x_1<x_2,y_1<y_2) (x2,y2)  (0x1,x2,y1,y21e4,x1<x2,y1<y2),求矩形覆盖的面积.

思路

扫描线.矩形的每条纵边构成一个三元组 ( x , y 1 , y 2 ) (x,y_1,y_2) (x,y1,y2),且左边的边权值为 1 1 1,右边的边权值为 − 1 -1 1.线段树维护 y y y轴上的坐标,每个节点表示 y y y轴上相邻两整点间的线段,对根节点统计至少被覆盖一次的区间长度之和.节点记录区间左右边界 l , r l,r l,r、不考虑父节点的信息的情况下当前区间被覆盖的次数 c n t cnt cnt、不考虑父节点的信息的情况下至少被覆盖一次的区间长度.

查询时无需下传懒标记,因为权值 + 1 +1 +1 − 1 -1 1成对出现,且先 + 1 +1 +1 − 1 -1 1,故 c n t ≥ 0 cnt\geq 0 cnt0,且只需对根节点统计信息.

代码
const int MAXN = 1e5 + 5;
int n;
struct Segment {  // 矩形的纵边
	int x, y1, y2;
	int k;  // 权值,左边为1,右边为-1

	bool operator<(const Segment& p)const { return x < p.x; }
}segs[MAXN << 1];
struct Node {
	int l, r;
	int cnt;  // 不考虑父节点信息的情况下当前区间被覆盖的次数
	int len;  // 不考虑父节点的信息的情况下至少被覆盖一次的区间长度
}SegT[MAXN << 4];

void pushup(int u) {
	if (SegT[u].cnt) SegT[u].len = SegT[u].r - SegT[u].l + 1;  // 区间至少被覆盖一次
	else if (SegT[u].l == SegT[u].r) SegT[u].len = 0;  // 叶子节点
	else SegT[u].len = SegT[u << 1].len + SegT[u << 1 | 1].len;
}

void build(int u, int l, int r) {
	SegT[u] = { l,r };
	if (l == r) return;  // 叶子节点
	
	int mid = l + r >> 1;
	build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}

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

int main() {
	cin >> n;
	int m = 0;  // 纵边数
	for (int i = 0; i < n; i++) {
		int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2;
		segs[m++] = { x1,y1,y2,1 };  // 左边
		segs[m++] = { x2,y1,y2,-1 };  // 右边
	}

	sort(segs, segs + m);

	build(1, 0, 1e4);

	int ans = 0;
	for (int i = 0; i < m; i++) {
		if (i)  // 从第2条纵边起计算面积
			ans += SegT[1].len * (segs[i].x - segs[i - 1].x);  // 根节点的Δy * Δx
		modify(1, segs[i].y1, segs[i].y2 - 1, segs[i].k);  // 更新区间[y1,y2-1]被覆盖的次数
	}
	cout << ans;
}


1. 递推与递归

1.1 递归实现指数型枚举 ( 5   s 5\ \mathrm{s} 5 s)

题意

1 ∼ n 1\sim n 1n n    ( 1 ≤ n ≤ 15 ) n\ \ (1\leq n\leq 15) n  (1n15)个数中任意选取若干个,输出所有可能的选择方案.每种方案输出一行,每行内的数升序排列,对选 0 0 0个数的方案输出空行.SPJ,不同方案间的顺序任意.

代码
const int MAXN = 16;
int n;
int state[MAXN];  // 记录每个数的状态,0表示未考虑,1表示选,2表示不选

void dfs(int pos) {
	if (pos > n) {
		for (int i = 1; i <= n; i++) 
			if (state[i] == 1) cout << i << ' ';
		cout << endl;
		return;
	}

	// 该位置不选
	state[pos] = 2;
	dfs(pos + 1);
	state[pos] = 0;

	// 该位置选
	state[pos] = 1;
	dfs(pos + 1);
	state[pos] = 0;
}

int main() {
	cin >> n;

	dfs(1);
}


1.2 递归实现排列型枚举 ( 5   s 5\ \mathrm{s} 5 s)

题意

按字典序升序输出 1 ∼ n    ( 1 ≤ n ≤ 9 ) 1\sim n\ \ (1\leq n\leq 9) 1n  (1n9)的全排列.

思路

DFS,枚举每个位置填的数.

搜索树共 n n n层.根节点有 n n n个子节点,第 2 2 2层的每个节点有 ( n − 1 ) (n-1) (n1)个子节点,即 2 2 2层共 n ( n − 1 ) n(n-1) n(n1)个节点, ⋯ \cdots ,第 n n n层共 n ! n! n!个节点.

树中节点每个节点都需枚举 n n n个数,叶子节点每个节点需输出方案,故每个节点的时间复杂度都为 O ( n ) O(n) O(n).

总时间复杂度 n [ 1 + n + n ( n − 1 ) + n ( n − 1 ) ( n − 2 ) + ⋯ + n ! ] = n ⋅ S n\left[1+n+n(n-1)+n(n-1)(n-2)+\cdots+n!\right]=n\cdot S n[1+n+n(n1)+n(n1)(n2)++n!]=nS,显然 S ≥ n ! S\geq n! Sn!.

S = n ! + n ! 1 + n ! 1 ⋅ 2 + ⋯ + n ! ( n − 1 ) ! + n ! n ! ≤ n ! ( 1 + 1 + 1 2 + 1 4 + ⋯   ) ≤ 3 n ! S=n!+\dfrac{n!}{1}+\dfrac{n!}{1\cdot 2}+\cdots+\dfrac{n!}{(n-1)!}+\dfrac{n!}{n!}\leq n!\left(1+1+\dfrac{1}{2}+\dfrac{1}{4}+\cdots\right)\leq 3n! S=n!+1n!+12n!++(n1)!n!+n!n!n!(1+1+21+41+)3n!,故总时间复杂度 O ( n ⋅ n ! ) O(n\cdot n!) O(nn!).

代码
const int MAXN = 15;
int n;
int state[MAXN];  // 记录每个位置放的数,0表示未放
bool used[MAXN];  // 记录每个数是否被用过

void dfs(int pos) {
	if (pos > n) {
		for (int i = 1; i <= n; i++) cout << state[i] << ' ';
		cout << endl;
		return;
	}

	for (int i = 1; i <= n; i++) {
		if (!used[i]) {
			state[pos] = i, used[i] = true;
			dfs(pos + 1);
			state[pos] = 0, used[i] = false;
		}
	}
}

int main() {
	cin >> n;

	dfs(1);
}


1.3 递归实现组合型枚举

题意

1 ∼ n 1\sim n 1n n    ( n > 0 ) n\ \ (n>0) n  (n>0)个整数中随机选 m    ( 0 ≤ m ≤ n , n + ( n − m ) ≤ 25 ) m\ \ (0\leq m\leq n,n+(n-m)\leq 25) m  (0mn,n+(nm)25)个数,按字典序输出所有方案,每行输出一个方案,同个方案内的数升序排列.

思路

DFS枚举时规定选择的数按从小到大排序,只需保证所有相邻的两数都是后一个比前一个大.

C n m = C n n − m C_n^m=C_n^{n-m} Cnm=Cnnm,则组合数最大值 C 18 7 C_{18}^7 C187,计算知不会超时.

剪枝:因需保证选择的数升序,若将比当前位置填的数大的所有数都选上也不足 m m m个树,则该分支无解.设当前位置为 p o s pos pos,当前位置可填的数的最小值为 s t a r t start start,则 p o s − 1 + ( n − s t a r t + 1 ) < m pos-1+(n-start+1)<m pos1+(nstart+1)<m,即 p o s + m − s t a r t < m pos+m-start<m pos+mstart<m时无解

代码
const int MAXN = 30;
int n, m;  // C(n,m)
int state[MAXN];  // 记录每个位置填的数,0表示未填

void dfs(int pos, int start) {  // 当前填的位置、当前位置可填的数的最小值
	if (pos + n - start < m) return;  // 剪枝

	if (pos > m) {
		for (int i = 1; i <= m; i++) cout << state[i] << ' ';
		cout << endl;
		return;
	}

	for (int i = start; i <= n; i++) {
		state[pos] = i;
		dfs(pos + 1, i + 1);
		state[pos] = 0;
	}
}

int main() {
	cin >> n >> m;

	dfs(1, 1);  // 从第1个位置填1开始枚举
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值