【题解】随笔

【题解】随笔


#1 『###』arc071d (计算几何)

English

On a two-dimensional plane, there are m m m lines drawn parallel to the x x x axis, and n n n lines drawn parallel to the y y y axis. Among the lines parallel to the x x x axis, the i i i-th from the bottom is represented by y y y = y i y_i yi. Similarly, among the lines parallel to the y y y axis, the i i i-th from the left is represented by x x x = x i x_i xi.
For every rectangle that is formed by these lines, find its area, and print the total area modulo 1 0 9 + 7 10^9+7 109+7.
That is, for every quadruple ( i , j , k , l ) (i,j,k,l) (i,j,k,l) satisfying 1 ⩽ i < j ⩽ n 1\leqslant i < j\leqslant n 1i<jn and 1 ⩽ k < l ⩽ m 1\leqslant k < l\leqslant m 1k<lm, find the area of the rectangle formed by the lines x = x i x=x_i x=xi, x = x j x=x_j x=xj, y = y k y=y_k y=yk and y = y l y=y_l y=yl, and print the sum of these areas modulo 1 0 9 + 7 10^9+7 109+7.

简体中文

给定 n n n 条平行于 y y y 轴的线 x x x,和 m m m 条平行于 x x x 轴的线 y y y,求由这些线组成的矩形的面积之和是多少?

如下图:

在这里插入图片描述

一共有这些矩形:

在这里插入图片描述
朴素算法

这道题如果用朴素算法的话,时间复杂度将会达到 O ( n 2 m 2 ) O(n^2m^2) O(n2m2) 之高,即

∑ 1 ⩽ i ⩽ j ⩽ n ∑ 1 ⩽ k ⩽ l ⩽ n ( x j − x i ) ( y l − y k ) \sum_{1\leqslant i\leqslant j \leqslant n}\sum_{1 \leqslant k \leqslant l \leqslant n}(x_j-x_i)(y_l-y_k) 1ijn1kln(xjxi)(ylyk)

肯定行不通。

优化

我们知道(如果不知道可以自行推导):

∑ ∑ a b = ∑ a ∑ b \sum\sum ab = \sum a\sum b ∑∑ab=ab

则原式可展开为

∑ 1 ⩽ i ⩽ j ⩽ n ( x j − x i ) ∑ 1 ⩽ i ⩽ j ⩽ n ( y l − y k ) \sum_{1 \leqslant i \leqslant j \leqslant n}(x_j-x_i)\sum_{1\leqslant i\leqslant j\leqslant n}(y_l-y_k) 1ijn(xjxi)1ijn(ylyk)

这样,我们就可以分别处理 x x x y y y 坐标,降低时间复杂度。而且又由于两个方向其实做法都是一样的,因此,我们只需要计算这两个中的任意一个就可以了。这里以计算 x x x 坐标为例。

因为直接对这个算式进行暴力求解,时间复杂度也为 O ( n 2 ) O(n^2) O(n2),还是会超时。这里我们仔细观察这个题目,对于这 n n n 个数字 x 1 , x 2 , x 3 , … , x n x_1,x_2,x_3,\dots,x_n x1,x2,x3,,xn 而言,我们在计算的时候其实只是在简单的对这些数字进行相加和相减,那么,我们只需要知道,对于每一个数字而言,它究竟被使用了多少次,我们就可以求出最终的答案。即将 x 1 + x 2 + x 3 + … x_1+x_2+x_3+\dots x1+x2+x3+ 转化成 a 1 x 1 + a 2 x 2 + a 3 x 3 + … ( a 1 , a 2 , a 3 , ⋯ ⩾ 1 ) a_1 x_1 + a_2x_2+a_3x_3+\dots(a_1,a_2,a_3,\dots\geqslant 1) a1x1+a2x2+a3x3+(a1,a2,a3,1)

在枚举过程中会发现:对于一个数字 x k x_k xk 而言,它使用加法的次数其实就相当于比它小的下标的个数,即 ( k − 1 ) (k-1) (k1) 次,它使用减法的次数就相当于比它下标大的个数,即 ( n − k ) (n-k) (nk) 次。

∑ 1 ⩽ i ⩽ n ( ( k − 1 ) x k − ( n − k ) x k ) = ∑ 1 ⩽ i ⩽ n ( ( 2 k − 1 − n ) x k ) \sum_{1\leqslant i\leqslant n}((k-1)x_k-(n-k)x_k)=\sum_{1\leqslant i\leqslant n}((2k-1-n)x_k) 1in((k1)xk(nk)xk)=1in((2k1n)xk)

只需要 O ( n ) O(n) O(n) 的时间复杂度枚举 i i i,最后,再把两边的答案相乘,就可以得到最终答案。

#include <cstdio>
#include <algorithm>
using namespace std;
const int mod = 1e9 + 7;
long long x[100005], y[100005];
int main() {
	int n, m;
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i ++) {
		scanf("%lld", &x[i]);
	}
	for(int i = 1; i <= m; i ++) {
		scanf("%lld", &y[i]);
	}
	long long sx = 0, sy = 0;
	for(int i = 1; i <= n; i ++) {
		sx = (sx + x[i] * (i - 1)) % mod;
		sx = (sx - x[i] * (n - i)) % mod;
	}
	for(int i = 1; i <= m; i ++) {
		sy = (sy + y[i] * (i - 1)) % mod;
		sy = (sy - y[i] * (m - i)) % mod;
	}
	printf("%lld", sx * sy % mod);
	return 0;
}

#2 『性格公交车』(栈 & 队列 & 贪心)

在一个公交车上,有 n n n 排座位,每排座位有个 2 2 2 座位,有个 2 n 2n 2n 车站,每个车站上来一个乘客, 0 0 0 表示性格内向乘客, 1 1 1 表示性格外向乘客, 你的任务是给他们排座位。排座位的要求是:
内向的人总是选择两个座位都是空的一排。在这些空座位排当中,选择一排座位宽度最小的,并占了其中的一个座位;
外向的人总是选择一个内向的人所占的那排座位。在这些座位排当中,选择一排座位宽度最大的,并在其中占据了空位。
现在已知排座位的宽度和 2 n 2n 2n 个乘客是内向还是外向的,以及乘客上车的顺序,你的任务是输出每一位乘客应该坐哪一排?

对于乘客有两种,可以坐 的座位也有两种——空座位 以及 1 1 1 人坐的座位

先说没人的。

没人的座位只要有一个内向的人坐了,该座位就会变成下一种座位。很容易想到用队列来装没人的座位,上来一个内向的人,就坐队头的座位,马上 p o p pop pop 掉,进入下一种座位的处理。

又因为内向的人只坐最窄的座位,所以要先排序再将座位依次压入队列。

再说有 1 1 1 人的。

上面说到有人坐的座位会 p o p pop pop 掉,那么这些座位存在哪里?先说方法:存在一个栈里。为什么?
因为前面已经对座位排过序,且是从小到大,则压入栈后依次取出将是从大到小(FILO),符合外向的人只坐长座位的要求。故上来一个外向的人就让他坐栈顶的座位,随即 p o p pop pop 掉。

#include <cstdio>
#include <algorithm>
#include <queue>
#include <stack>
#include <iostream>
using namespace std;
struct node {
	int length, order;
} a[100005];
bool cmp(node x, node y) {
	return x.length < y.length;
}
int n;
queue<node> q;
stack<node> s;
int main() {
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++) {
		scanf("%d", &a[i].length);
		a[i].order = i; //在排序后下标会改变,要存储下标
	}
	sort(a + 1, a + 1 + n, cmp); //从小到大排序
	for(int i = 1; i <= n; i ++) {
		q.push(a[i]); //压队列
	}
	char ch;
	for(int i = 1; i <= (n << 1); i ++) {
		cin >> ch;
		if(ch == '0') {
			printf("%d ", q.front().order); //坐队头
			s.push(q.front()); //压进栈
			q.pop(); //pop掉
		}
		else {
			printf("%d ", s.top().order); //坐栈顶
			s.pop(); //pop掉
		}
	}
	return 0;
}

#3 『搭配购买』(并查集 & 01背包)

Joe 觉得云朵很美,决定去山上的商店买一些云朵。商店里有 n n n 朵云,云朵被编号为 1 , 2 , ⋯ n 1,2,\cdots n 1,2,n,并且每朵云都有一个价值。但是商店老板跟他说,一些云朵要搭配来买才好,所以买一朵云则与这朵云有搭配的云都要买。
但是 Joe 的钱有限,所以他希望买的价值越多越好。

很明显是属于有依赖的背包问题。对于一类的物品,可以用并查集维护。将一类的物品放在一起,在购买时只需将根节点(并查集的祖先)买下,等价于将全部物品一起买下。将一个集合里的价格与价值在并查集合并时逐层累加至根节点,做法便和 01 背包无异——一个集合看做一个物品。

注意在将根节点存到数组时,集合总数并不是 n n n,而是枚举 1 1 1 n n n,当 f i n d ( i ) = i find(i)=i find(i)=i 时( i i i 为根节点),集合总数加 1 1 1

#include <cstdio>
#include <algorithm>
using namespace std;
int Father[10005], Cost[10005], Get[10005];
int n, m, w;
void MakeSet() {
	for(int i = 1; i <= n; i ++) {
		Father[i] = i;
	}
}
int FindSet(int x) {
	if(Father[x] == x) return x;
	int root = FindSet(Father[x]);
	Cost[x] += Cost[Father[x]]; //从下往上的过程中累加价格,存于根节点
	Get[x] += Get[Father[x]]; //价值同理
	return Father[x] = root;
}
void UnionSet(int a, int b) {
	int x = FindSet(a), y = FindSet(b); 
	if(x == y) return;
	Father[x] = y;
	Cost[y] += Cost[x]; //合并时,一个集合接在另一个后面,则该集合的信息累加给合并后的集合
	Get[y] += Get[x];
}
int dp[10005], v[10005], p[10005];
int main() {
	scanf("%d%d%d", &n, &m, &w);
	MakeSet();
	for(int i = 1; i <= n; i ++) {
		int x, y;
		scanf("%d%d", &x, &y);
		Cost[i] = x; //初始化
		Get[i] = y;
	}
	for(int i = 1; i <= m; i ++) {
		int a, b;
		scanf("%d%d", &a, &b);
		UnionSet(a, b); //捆绑的物品放在一个集合里
	} 
	int cnt = 0;
	for(int i = 1; i <= n; i ++) {
		if(FindSet(i) == i) { //是根节点
			cnt ++; //cnt即为总的集合数
			p[cnt] = Cost[i];
			v[cnt] = Get[i];
		}
	}
	for(int i = 1; i <= cnt; i ++) {
		for(int j = w; j >= p[i]; j --) {
			dp[j] = max(dp[j], dp[j - p[i]] + v[i]); //01背包
		}
	}
	printf("%d", dp[w]);
	return 0;
}

#4 『灯泡』(三分 & 计算几何)

相比 wildleopard 的家,他的弟弟 mildleopard 比较穷。他的房子是狭窄的而且在他的房间里面仅有一个灯泡。每天晚上,他徘徊在自己狭小的房子里,思考如何赚更多的钱。有一天,他发现他的影子的长度随着他在灯泡和墙壁之间走到时发生着变化。一个突然的想法出现在脑海里,他想知道他的影子的最大长度。
在这里插入图片描述

我们可以枚举人到墙的距离长度,设为 x x x

x ⩽ h ⋅ D H x \leqslant h \cdot\cfrac{D}{H} xhHD 时,

在这里插入图片描述

过点 C C C C I ⊥ B G CI\bot BG CIBG,过点 B B B B E ⊥ A F BE \bot AF BEAF
∴ B E     / /    C I \therefore BE \:\ /\kern -0.9em / \:\:CI BE //CI
∴ △ A E B ∼ △ B I C \therefore \triangle AEB \sim \triangle BIC AEBBIC
∴ A E B I = B E C I \therefore \dfrac{AE}{BI}=\dfrac{BE}{CI} BIAE=CIBE

∵ A F = H , B G = h , G H = x , H F = D \because AF=H,BG=h,GH=x,HF=D AF=H,BG=h,GH=x,HF=D
∴ A E = H − h , F G = D − x \therefore AE=H-h,FG=D-x AE=Hh,FG=Dx

∴ H − h B I = D − x x \therefore \dfrac{H- h}{BI}=\dfrac{D-x}{x} BIHh=xDx

∴ B I = x ( H − h ) D − x \therefore BI=\dfrac{x(H-h)}{D-x} BI=Dxx(Hh)

∵ L = C H + G H \because L=CH+GH L=CH+GH
∴ L = I G + G H = B G + G H − B I = h + x − x ( H − h ) D − x . \therefore L=IG+GH=BG+GH-BI=h+x-\dfrac{x(H-h)}{D-x}. L=IG+GH=BG+GHBI=h+xDxx(Hh).

否则:

在这里插入图片描述

H − h h = D − x C G \dfrac{H-h}{h}=\dfrac{D-x}{CG} hHh=CGDx

∴ C G = h ( D − x ) H − h \therefore CG=\dfrac{h(D-x)}{H-h} CG=Hhh(Dx)

∴ L = h ( D − x ) H − h . \therefore L=\dfrac{h(D-x)}{H-h}. L=Hhh(Dx).

综上,
L = { h + x − x ( H − h ) D − x , x ⩽ h ⋅ D H h ( D − x ) H − h , x > h ⋅ D H L=\left\{ \begin{aligned} h+x-\dfrac{x(H-h)}{D-x} & , & x \leqslant h \cdot \cfrac{D}{H} \\ \dfrac{h(D-x)}{H-h} & , & x > h\cdot \cfrac{D}{H} \end{aligned} \right. L= h+xDxx(Hh)Hhh(Dx),,xhHDx>hHD

要想分析函数图像可以通过求导等方法,这里用 geogebra。函数图像大概是这样:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
明显用二分是不可行的。对于存在多种单调性的函数,选用三分。

#pragma GCC optimize(2)
#include <cstdio>
#include <algorithm>
using namespace std;
int T;
double H, h, D;
double check(double variable) {
	return variable <= h * D / H ? variable + h - variable * (H - h) / (D - variable) : (D - variable) / (H - h) * h;
}
int main() {
	scanf("%d", &T);
	while(T --) {
		scanf("%lf%lf%lf", &H, &h, &D);
		double l, r;
		l = 0.0;
		r = D;
		while(r - l > 1e-12) { //三分模板
			double midl = l + (r - l) / 3;
			double midr = r - (r - l) / 3;
			if(check(midl) >= check(midr)) {
				r = midr;
			}
			else {
				l = midl;
			}
		}
		printf("%.3lf\n", check(l));
	}
	return 0;
}

三分原理可参见 这篇博客(三分查找,博主pi9nc)


#5 『程序自动分析』(并查集 & 离散化)

在实现程序自动分析的过程中,常常需要判定一些约束条件是否能被同时满足。
考虑一个约束满足问题的简化版本:假设 x 1 , x 2 , x 3 , … x_1,x_2,x_3,\ldots x1,x2,x3, 代表程序中出现的变量,给定 n n n 个形如 x i = x j x_i=x_j xi=xj x i ≠ x j x_i \not= x_j xi=xj 的变量相等/不等的约束条件,请判定是否可以分别为每一个变量赋予恰当的值,使得上述所有约束条件同时被满足。例如,一个问题中的约束条件为: x 1 = x 2 , x 2 = x 3 , x 3 = x 4 , x 1 ≠ x 4 x_1=x_2,x_2=x_3,x_3=x_4,x_1\not=x_4 x1=x2,x2=x3,x3=x4,x1=x4,这些约束条件显然是不可能同时被满足的,因此这个问题应判定为不可被满足。 现在给出一些约束满足问题,请分别对它们进行判定。

这是属于并查集题型里对逻辑命题的判断。将描述相等关系的命题看做条件,而描述不等关系的命题看做待判断真伪的命题。故将前者放入并查集中(即集合中的所有元素相等),当出现后者时,判断两数是否在统一集合中,如是,则证明两数相等,矛盾,命题错误。如否,则命题正确。

值得注意的是,数据范围为:

对于所有的数据, 1 ⩽ i , j ⩽ 1 e 9 1\leqslant i,j\leqslant 1e9 1i,j1e9

如此大的范围,硬开数组肯定不行。别说提交后铁定的 MLE,首先 DEV 这关都过不了。

在这里插入图片描述

引入新概念:离散化

离散化是一种将无限变成有限的方法,在数据本身没有意义,只是数据间的大小关系有意义时,将数据缩小,保持大小关系不变,可以达到一样的效果。

529745484 7534274 982532748 37542 37542685537
(With Discretization)
3 2 4 1 5

可参见 oi-wiki

综上,

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
struct node {
	int a, b, op;
} s[1000005];
int father[10000005];
bool cmp(node x, node y) {
	return x.op > y.op;
}
void MakeSet(int n) { for(int i = 1; i <= n; i ++) father[i] = i; }
int FindSet(int x) {
	if(father[x] == x) return x;
	else return father[x] = FindSet(father[x]);
}
void UnionSet(int a, int b) {
	int x = FindSet(a), y = FindSet(b);
	if(x == y) return;
	father[x] = y;
} 
int t, n, a[1000005], b[1000005], aux[1000005], task[1000005];
int main() {
	scanf("%d", &t);
	while(t --) {
		memset(aux, 0, sizeof(aux)); //离散化数组
		int cnt = 1;
		bool flag = true;
		scanf("%d", &n);
		for(int i = 1; i <= n; i++) {
			scanf("%d%d%d", &s[i].a, &s[i].b, &s[i].op);
			aux[cnt ++] = s[i].a;
			aux[cnt ++] = s[i].b;
		} 
		cnt --;
		sort(aux + 1, aux + 1 + cnt); //排序
		int len = unique(aux + 1, aux + 1 + cnt) - aux; //去重
		for(int i = 1; i <= n; i++) {
			s[i].a = lower_bound(aux, aux + len, s[i].a) - aux; //赋值
			s[i].b = lower_bound(aux, aux + len, s[i].b) - aux;
		}
		MakeSet(len);
		sort(s + 1, s + 1 + n, cmp);
		for(int i = 1; i <= n; i++) {
			if(s[i].op) UnionSet(s[i].a, s[i].b);
			else {
				if(FindSet(s[i].a) == FindSet(s[i].b)) { 
					flag = false;
					break; //见好就收,找到假命题就结束
				}
			}
		}
		if(flag) printf("YES\n");
		else printf("NO\n");
	}
	return 0;
}

#6 『雪铲商店』(线性DP)

在附近的商店有 𝑛 𝑛 n 把铲子。第 i i i 把铲的价格为 a i a_i ai 元。
Misha 需要购买 𝑘 𝑘 k 铲子。每把铲子只能买一次。
Misha 可以买几把铲子。在一次购买期间,他可以选择剩余(未购买)铲子的任何子集并购买这个子集。
在商店里也有 𝑚 𝑚 m 张优惠券,这意味着如果 Misha 使用优惠券 ( x j , y j ) (x_j,y_j) (xj,yj),那么在她买的 x j x_j xj 把铲子中,其中 y j y_j yj 把最便宜的铲子免费。
Misha可以使用任意次数的优惠券(在优惠券数量的范围之内,可能是零次),一张优惠券可以被使用多次。
你的任务是计算购买 𝑘 𝑘 k 把铲子的最小花费。

要想花费最少,换言之,是要求得最大的优惠。所以说定义 d p i dp_i dpi 为枚举到第 i i i 个元素时的最大优惠(省的最多的钱)。

要想省钱,东西首先要便宜,所以一开头就排序(从小到大)。但同时产生了一个新的问题:使用优惠券时应该是越贵的东西省了越划算,即应当从大到小排序。这里的两个贪心原则相互冲突,不过仔细想一想,使用优惠券时,并不是所有的都省掉,该付的还是要付,所以排序规则应该是从小到大。

对于如何动态规划,首先枚举 i i i ,再枚举 j j j,这是什么意思?

在这里插入图片描述
i i i 1 1 1 n n n,相当于右端点; j j j 0 0 0 i − 1 i - 1 i1,相当于左端点。注意:这里待处理的区间为 ( j , i ] (j,i] (j,i],即上图中括号括起来的部分 [ j + 1 , i ] [j+1,i] [j+1,i] 。故区间中有 i − ( j + 1 ) + 1 = i − j i - (j+1)+1=i-j i(j+1)+1=ij 个元素。区间中能用优惠券就用,所以在优惠券的存储中,可以使用 b u y [ x ] = y buy[x]=y buy[x]=y 的方式,其中 x x x 为长度, y y y 为优惠券能省的物品个数。所以只要 b u y [ i − j ] buy[i-j] buy[ij] 有值,就说明有匹配的优惠券。因为优惠券只省最便宜的,而数组在排序后已经从小到大,所以能省的物品就是区间的前几个。

在这里插入图片描述
即得优惠的价格为使用优惠券的物品的价格总和(黄色的)。而对于区间和很容易想到前缀和优化时间复杂度,所以这些物品总和为 p r e f [ j + b u y [ i − j ] ] − p r e f [ j ] pref[j+buy[i-j]]-pref[j] pref[j+buy[ij]]pref[j]

完了吗?很明显,没有。在输入时如果出现长度一样,省的物品个数不同的优惠券:

 5 2
 5 3
 5 4
 ...

怎么办?肯定选省的个数最多的。所以输入时优惠券需要去重,比 max ⁡ \max max

注意:我们求的是最大优惠,所以答案为总价值减去最大优惠。

#include <cstdio>
#include <algorithm>
using namespace std;
int n, m, k, a[200005], bought[200005], pref[200005], dp[200005];
int main() {
	scanf("%d%d%d", &n, &m, &k);
	for(int i = 1; i <= n; i ++) {
		scanf("%d", &a[i]);
	}
	for(int i = 1; i <= m; i ++) {
		int x, y;
		scanf("%d%d", &x, &y);
		bought[x] = max(bought[x], y); //去重比max
	}
	sort(a + 1, a + 1 + n); //从小到大排序
	for(int i = 1; i <= k; i ++) {
		pref[i] = pref[i - 1] + a[i]; //前缀和
	}
	for(int i = 1; i <= k; i ++) {
		for(int j = 0; j <= i - 1; j ++) {
			dp[i] = max(dp[i], dp[j] + pref[j + bought[i - j]] - pref[j]); //方程
		}
	}
	printf("%d", pref[k] - dp[k]); //总和减最大优惠
	return 0;
}

#7 『字母 Letters』POI2012(树状数组)

给定两个长度相同且由大写英文字母组成的字符串 A 和 B,保证 A 和 B 中每种字母出现的次数相同。
每次可以交换 A 中相邻两个字符,求最少需要交换多少次可以使得 A 变成 B。

把这道题题目改一下:

给定 n n n 个数,求最少需要多少次才能将其变得有序。

是不是很熟悉?这道题主要的思想就是将字符串看做数组,求逆序对。因此问题可以看做两问:一、将字符串转数组;二、求逆序对。

1.字符串转数组

输入中包含两个字符串:原字符串和目标字符串。在对数组求逆序对时,似乎只有一个原数组。那么另外一个目标数组呢?

有序,这就是另一个数组。

要求有序不就等价于目标数组为一个有序的数组吗?想通这一点,就很简单了。我们把目标字符串看做有序的(尽管并不是有序),并把其想象成一串有序的数。

ADBC -> 1234

在目标字符串里的一个字符,一定可以在原字符串里找到。所以字符 c c c 在目标字符串中被分配到什么数,它在原字符串中就是什么数。

target: ADBC -> 1234
source: BCDA -> 3421
map<char, int> g;
for(int i = 1; i <= n; i++) {
	g[target[i]] = i;
}
for(int i = 1; i <= n; i++) { //a为字符串转化后的数组
	a[i] = g[source[i]]
}

有问题吗?

有。

如果字符串中有重复的字符,那么它们都对应着同一个数(尽管它们下标不同)。怎么解决?既然对应同一个数,那就偏偏给每一个数对应不同的值。

map<char, queue<int> > g; //额(map套queue,有点小骚)
for(int i = 1; i <= n; i++) {
	g[target[i]].push(i);
}
for(int i = 1; i <= n; i++) {
	a[i] = g[source[i]].front(); //先进先出,先分配到的先出队,保证下标顺序不变
	g[source[i]].pop();
} 

2.求逆序对

法一:二路归并(不细说)

#include <cstdio>
int a[100005], r[100005];
long long sum;
void msort(int s, int e) {
	if(s == e) {
		return;
	}
	int mid = (s + e) / 2;
	msort(s, mid);
	msort(mid + 1, e);
	int i = s, j = mid + 1, k = s;
	while(i <= mid && j <= e) {
		if(a[i] <= a[j]) {
			r[k] = a[i];
			k ++;
			i ++;
		}
		else {
			r[k] = a[j];
			k ++;
			j ++;
			sum += mid - i + 1; //sum即为逆序对数
		}
	}
	while(i <= mid) {
		r[k] = a[i];
		k ++;
		i ++;
	}
	while(j <= e) {
		r[k] = a[j];
		k ++;
		j ++;
	} 
	for(i = s; i <= e; i ++) {
		a[i] = r[i];
	}
}

法二:树状数组

懒得写了,用 GM 的话说:

就是统计当前元素的前面有几个比它大的元素的个数,然后把所有元素比它大的元素总数垒加就是逆序对总数。

for(int i = 1; i <= n; i++) {
	update(a[i], 1); //占位子,a[i]位上多一个
	ans += i - sum(a[i]); //前面有几个比它大的
}

综上:

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <map>
#include <queue>
using namespace std;
#define int long long
int n;
map<char, queue<int> > g;
char str[1000005], trg[1000005];
int a[1000005], BIT[1000005];
int lowbit(int x) { return x & -x; }
void update(int k, int x) {
	for(int i = k; i <= n; i += lowbit(i)) BIT[i] += x;
}
long long sum(int k) {
	long long ans = 0;
	for(int i = k; i; i -= lowbit(i)) ans += BIT[i];
	return ans;
}
long long ans = 0;
signed main() {
	scanf("%lld%s%s", &n, str + 1, trg + 1);
	for(int i = 1; i <= n; i ++) g[trg[i]].push(i);
	for(int i = 1; i <= n; i++) {
		a[i] = g[str[i]].front();
		g[str[i]].pop();
	}
	for(int i = 1; i <= n; i++) {
		update(a[i], 1);
		ans += i - sum(a[i]);
	}
	printf("%lld", ans);
	return 0;
}

#8 『火神之友』(树状数组)

火神是一个非常单纯的人,他的好朋友风神给他一个有 n n n 个自然数的数组,然后对他进行 Q Q Q 次查询.
每一次查询包含两个正整数 l , r l, r l,r,表示一个数组中的一个区间 [ l , r ] [l,r] [l,r],火神需要回答在这个区间中有多少个值刚好出现 2 2 2 次。

讨论一种特殊情况:区间内全为同一个数。

在这里插入图片描述
从区间左端点枚举指针 j j j

在这里插入图片描述

在上图中的红色区域 [ l , j ] [l, j] [l,j] 中,数字 4 4 4 出现了两次,因此满足条件的数数量加 1 1 1。难点在于 1 1 1 加在哪里。事实上,将 1 1 1 加在 j j j 前一个数上是可行的,因为在求前缀和时,总数会因此加 1 1 1

在这里插入图片描述
j j j 继续向右移动。此时同上一次一样, j j j 前一个数加 1 1 1,前缀和为 2 2 2。但是 4 4 4 已经出现了 3 3 3 次,所以区间内应该没有符合条件的数。因而我们要想办法将 2 2 2 变为 0 0 0。在 j j j 前面的前面的数上减 2 2 2 可以达到该效果,区间前缀和为 0 0 0

在这里插入图片描述
j j j 移动的过程中,始终贯穿一个原则:当 j j j 移动超过一次时(枚举过第二个元素),让区间内的前缀和恒为零。上图中为达到目的,需要在 j j j 的前一个元素的前一个元素的前一个元素加 1 1 1

似乎出现了规律。那么下一步是不是在 j j j 的前一个元素的前一个元素的前一个元素的前一个元素减 2 2 2 呢?

在这里插入图片描述
并不是。我们可以发现,最多只需要在 j j j 的前一个元素的前一个元素的前一个元素减 2 2 2 即可。

所以在 j j j 自左向右枚举的时候,做三步操作:

1.在 j j j 的前一个元素加 1 1 1
2.在 j j j 的前一个元素的前一个元素减 2 2 2。(只要存在前一个元素的前一个元素)
3.在 j j j 的前一个元素的前一个元素的前一个元素加 1 1 1。(只要存在前一个元素的前一个元素的前一个元素)

注意在单点修改和区间查询的过程中,用树状数组维护。

上面我们考虑的是同数的情况,但事实上一个区间并不会这么巧全是一个数。其实,只需要将相同的数放在一起,每一个数都记录着前一个相同的数的下标,就等同于把相同的数放在一起。

for(int i = 1; i <= n; i++) {
	if(g.find(a[i]) != g.end()) front[i] = g[a[i]]; //i前面有相同的数
	else front[i] = -1; //没有就把前面的数看成-1
	g[a[i]] = i;
}

同时,我们只考虑了一个区间的情况。但实际上有多组询问区间。对于每一组区间,都要重置 BIT 数组,时间复杂度会飙升。

在这里插入图片描述

(未完结)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值