垃圾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 (1≤n≤1e5)和 m ( 1 ≤ m ≤ 1 e 5 ) m\ \ (1\leq m\leq 1\mathrm{e}5) m (1≤m≤1e5),分别表示数组长度和操作个数.第二行输入 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 (1≤a≤b≤n),当 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 (1≤n≤1e5)的序列和 m ( 1 ≤ m ≤ 1 e 6 ) m\ \ (1\leq m\leq 1\mathrm{e}6) m (1≤m≤1e6)个询问,每次询问输入两整数 l , r ( 1 ≤ l ≤ r ≤ n ) l,r\ \ (1\leq l\leq r\leq n) l,r (1≤l≤r≤n),求区间 [ l , r ] [l,r] [l,r]内的最大数.数据保证序列中所有数不超过 2 63 − 1 2^{63}-1 263−1.
代码
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 (1≤n≤1e5),表示小朋友个数.第二行输入 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 (1≤hi≤1e6),表示小朋友的身高.
思路
类似于冒泡排序,考察序列的逆序对数.若序列有 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 (1≤n≤1e4)个矩形的左下角和右上角坐标 ( 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) (0≤x1,x2,y1,y2≤1e4,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 cnt≥0,且只需对根节点统计信息.
代码
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 1∼n这 n ( 1 ≤ n ≤ 15 ) n\ \ (1\leq n\leq 15) n (1≤n≤15)个数中任意选取若干个,输出所有可能的选择方案.每种方案输出一行,每行内的数升序排列,对选 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) 1∼n (1≤n≤9)的全排列.
思路
DFS,枚举每个位置填的数.
搜索树共 n n n层.根节点有 n n n个子节点,第 2 2 2层的每个节点有 ( n − 1 ) (n-1) (n−1)个子节点,即 2 2 2层共 n ( n − 1 ) n(n-1) n(n−1)个节点, ⋯ \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(n−1)+n(n−1)(n−2)+⋯+n!]=n⋅S,显然 S ≥ n ! S\geq n! S≥n!.
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!+1⋅2n!+⋯+(n−1)!n!+n!n!≤n!(1+1+21+41+⋯)≤3n!,故总时间复杂度 O ( n ⋅ n ! ) O(n\cdot n!) O(n⋅n!).
代码
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 1∼n的 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 (0≤m≤n,n+(n−m)≤25)个数,按字典序输出所有方案,每行输出一个方案,同个方案内的数升序排列.
思路
DFS枚举时规定选择的数按从小到大排序,只需保证所有相邻的两数都是后一个比前一个大.
因 C n m = C n n − m C_n^m=C_n^{n-m} Cnm=Cnn−m,则组合数最大值 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 pos−1+(n−start+1)<m,即 p o s + m − s t a r t < m pos+m-start<m pos+m−start<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开始枚举
}