【题解】随笔
#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 1⩽i<j⩽n and 1 ⩽ k < l ⩽ m 1\leqslant k < l\leqslant m 1⩽k<l⩽m, 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) 1⩽i⩽j⩽n∑1⩽k⩽l⩽n∑(xj−xi)(yl−yk)
肯定行不通。
优化
我们知道(如果不知道可以自行推导):
∑ ∑ a b = ∑ a ∑ b \sum\sum ab = \sum a\sum b ∑∑ab=∑a∑b
则原式可展开为
∑ 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) 1⩽i⩽j⩽n∑(xj−xi)1⩽i⩽j⩽n∑(yl−yk)
这样,我们就可以分别处理 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) (k−1) 次,它使用减法的次数就相当于比它下标大的个数,即 ( n − k ) (n-k) (n−k) 次。
即
∑ 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) 1⩽i⩽n∑((k−1)xk−(n−k)xk)=1⩽i⩽n∑((2k−1−n)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} x⩽h⋅HD 时,
过点
C
C
C 作
C
I
⊥
B
G
CI\bot BG
CI⊥BG,过点
B
B
B 作
B
E
⊥
A
F
BE \bot AF
BE⊥AF。
∴
B
E
/
/
C
I
\therefore BE \:\ /\kern -0.9em / \:\:CI
∴BE //CI
∴
△
A
E
B
∼
△
B
I
C
\therefore \triangle AEB \sim \triangle BIC
∴△AEB∼△BIC
∴
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=H−h,FG=D−x
∴ H − h B I = D − x x \therefore \dfrac{H- h}{BI}=\dfrac{D-x}{x} ∴BIH−h=xD−x
∴ B I = x ( H − h ) D − x \therefore BI=\dfrac{x(H-h)}{D-x} ∴BI=D−xx(H−h)
∵
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+GH−BI=h+x−D−xx(H−h).
否则:
H − h h = D − x C G \dfrac{H-h}{h}=\dfrac{D-x}{CG} hH−h=CGD−x
∴ C G = h ( D − x ) H − h \therefore CG=\dfrac{h(D-x)}{H-h} ∴CG=H−hh(D−x)
∴ L = h ( D − x ) H − h . \therefore L=\dfrac{h(D-x)}{H-h}. ∴L=H−hh(D−x).
综上,
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+x−D−xx(H−h)H−hh(D−x),,x⩽h⋅HDx>h⋅HD
要想分析函数图像可以通过求导等方法,这里用 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 1⩽i,j⩽1e9。
如此大的范围,硬开数组肯定不行。别说提交后铁定的 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
i−1,相当于左端点。注意:这里待处理的区间为
(
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=i−j 个元素。区间中能用优惠券就用,所以在优惠券的存储中,可以使用
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[i−j] 有值,就说明有匹配的优惠券。因为优惠券只省最便宜的,而数组在排序后已经从小到大,所以能省的物品就是区间的前几个。
即得优惠的价格为使用优惠券的物品的价格总和(黄色的)。而对于区间和很容易想到前缀和优化时间复杂度,所以这些物品总和为
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[i−j]]−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 数组,时间复杂度会飙升。
(未完结)