😊😊 😊😊
不求点赞,只求耐心看完,指出您的疑惑和写的不好的地方,谢谢您。本人会及时更正感谢。希望看完后能帮助您理解算法的本质
😊😊 😊😊
题目描述:
小白到进阶各种解法:
一、暴搜:待更新😊
代码:
二、记忆化搜索:待更新😊
代码:
三、本题考察算法:树状数组求逆序对,已更新完毕😊
思路:
做题的时候第一步永远是模拟样例,然后思考答案的子集,即答案是从哪里来的?
-
对于本题给出的样例:3, 2, 1。模拟其交换过程:
模拟如图,其中每个数都交换了两次,那么根据题意可知,不高兴程度是由交换造成的,具体大小取决于:交换了 几 次,那么当前这个小朋友的不高兴程度就会增加 几。所以说一个小朋友总的不高兴程度 = 1 k 1~k 1 k 的累加, k k k 表示交换了 k k k 次。
u n h a p p y [ i ] = k + k − 1 + k − 2 + . . . + 1 ; unhappy[i] = k + k-1 + k-2 + ... + 1; unhappy[i]=k+k−1+k−2+...+1; -
问题给到:怎么知道一个小朋友交换了几次?
其实本题的规律很简单:
比如 3 交换了 2次,为什么交换两次?
因为 3 > 2,2在3的后面,所以交换一次。
交换后,3 > 1,1在3的后面,所以交换一次,总共交换了2次!
总之,统计3后面有多少个数比3小,即为3交换的次数。2,1 分别与 3 构成逆序对。比它小的数在它的后面,这种逆序对称为正逆序对,3有2个正逆序对 ⇒ 交换2次。
再来看2:
先看 2 正逆序对:后面有一个 1比 2小,可得 2 有一个正逆序对,所以交换一次,但是图中,2 交换了两次。对于区间内的点和区间的端点,分开看,区间端点只看一个方向,区间内的点有两个方向,现在 2 往前看,2 的前面有一个 3 比它大,那么对于2而言,是一个反逆序对。所以 区间内的点的交换次数 = 该点的反逆序对数量 + 该点的正逆序对的数量。
最后对于 1,而言,1 有两个反逆序对,0个正逆序对,所以说1也交换了2次。
总结:将上述三种情况,看作一种情况:即统计每个点的正反逆序对的数量,即为该点的交换次数!
- 接下来问题给到了如何统计每个点的正反逆序对的数量。通俗来说:就是如何统计每个点左边比它大的数的个数,右边比它小数的个数。
有同学会说:二重循环暴力枚举:
统计每个数左边比它大的数的个数:
int Greater[100010];
for (int i=1; i <= n; i ++)
{
for (int j=1; j < i; j ++)
{
if (a[j] < a[i])
Greater[i] ++; 表示第i个数左边比它大的数的个数!
}
}
int lower[100010];
再统计每个数右边比它小的数的个数:
for (int i=n; i >= ; i--)
{
for (int j=n; j > i; j --)
{
if (a[i] > a[j])
lower[i] ++; 表示第 i 个数右边比它小的数的个数!
}
}
n
≤
100000
n \leq 100000
n≤100000,双重循环直接超时。
所以现在要想一种比较简单的统计方式!统计某个数在一个序列中的逆序对的数量,可以采用树状数组的方式。(树状数组可以统计某个数在序列中的逆序对的数量!
)。
首相要明确树状数组维护的底层数组是什么:维护的是原数组中每个数出现的次数。即以 a[i]作为下标索引 [ a[i] ] = 0/1/2…表示它出现的次数!
-
利用树状数组统计每个数左边比它大的数或者比它小的数的个数:
从左往右逐个访问元素,每次访问一个元素都将它在树状数组中出现的次数+1,那么由于从左往右枚举,比如枚举到 第3个数时,第一个数,第二个数已经被访问过,且一定是插入到了树状数组中。这两个数都是 3 左边的数!至于是比第三个数大的数还是怎么的,就具体分析了! -
同理:统计每个数右边的比它大的数的个数和比它小的数的个数:从右往左枚举即可!
-
细节
:当用一个树状数组去统计某个数左右两边的含有某种性质的数的时候,注意,由于是分左右两边的,所以两边是独立地统计的,而不是一起统计的。假如我先用树状数组统计左边的数,然后完了是不是还要用树状数组统计右边的数的个数,但是记得初始化树状数组,因为此时树状数组中沾有之前的痕迹。!
代码:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 1000010;
int tr[N];
int h[N];
int n;
int qt[N];
int lowbit(int x)
{
return x & (-x);
}
void add (int pos, int v)
{
for (int i=pos; i <= N-1; i += lowbit(i)) //为什么是小于等于N-1,而不是小于等于n?
tr[i] += v;
}
int sum (int pos)
{
int res=0;
for (int i=pos; i > 0; i -= lowbit(i))
res += tr[i];
return res;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n;
for (int i=1; i <= n; i ++)
cin >> h[i], h[i] ++; //为什么会防止 h[i]=0的情况?
for (int i=1; i <= n; i ++)
{
int y = h[i];
qt[i] = sum(N-1) - sum(y);
add (y, 1);
}
memset (tr, 0, sizeof (tr));
for (int i=n; i >= 1; i--)
{
int y = h[i];
qt[i] += sum (y-1);
add (y, 1);
}
LL res=0;
for (int i=1; i <= n; i ++)
res += (LL)qt[i]*(qt[i] + 1)/2;
cout << res << endl;
return 0;
}
代码中的两个注释问题的解释:
- 为什么每次输入 h[i] 之后,又要使得 h[i] ++ 呢?
在代码中, h [ i ] h[i] h[i] 数组中的元素加1后作为树状数组的下标。如果没有对h[i]数组中的元素进行加1操作,当h[i]=0
时,会导致程序出现越界访问的情况,因为树状数组的下标必须大于0。 因此,对于防止越界访问的问题,可以将h[i]数组中的元素全部加1,这样即使h[i]=0,加1后也可以作为树状数组的下标使用,避免了越界访问的问题。
以下是针对这段代码:
void add (int pos, int v)
{
for (int i=pos; i <= N-1; i += lowbit(i)) //为什么是小于等于N-1,而不是小于等于n?
tr[i] += v;
}
-
为什么是小于等于N-1,而不是小于等于n?
其实我觉得应该是树状数组下标的对象的问题, n n n 只是表示当前有几个小朋友。而我们的树状数组维护的底层数组是:某个数x,出现的次数,而这里x代指的最大范围是 1e6,所以说应该取到1e6级别!但是由于N初始化为1e6+10,而C++数组的下标是从0开始的,所以是取不到 1e6+10,最大取到 1e6+9; -
为什么不是小于等于N,而是小于等于N-1?
树状数组的实现需要利用数组的下标,因此需要先给数组分配内存空间。在这段代码中,树状数组的大小是N,即tr数组的大小为N。由于C++数组下标是从0开始的,因此当需要访问数组的最后一个元素时,下标应该是N-1而不是N。因此在for循环中,i从pos开始增加,每次增加的步长为lowbit(i),如果i最后增加到N,则意味着当前的pos已经到达了数组的最后一个位置N-1,因此for循环中的终止条件为i<=N-1。这样可以避免越界访问数组的问题。 -
树状数组维护的底层数组是什么?就是它维护的对象是个数还是?
这段代码中维护的底层数组是一个计数数组count,其中count[i]表示数值为i出现的次数。
对于每个输入的数值h[i],首先通过 h[i]++ 的方式将数值范围映射到 [1, N] 区间内,然后将这个数值加入到树状数组中,即执行 add(y, 1) 操作,其中 y=h[i]。这个操作相当于在 count 数组中将 count[y] 的值加1,表示数值 y 出现了一次。
之后,在第一个循环中,通过 sum(N-1) - sum(y) 的方式统计了数值 y 右侧的数值中比 y 小的数值出现的次数。由于 count 数组维护的是所有数值的出现次数,因此 sum(N-1) 表示了所有数值出现的总次数,而 sum(y) 表示数值 y 出现的次数。因此,sum(N-1) - sum(y) 就表示了数值 y 右侧的数值中比 y 小的数值出现的次数。这个操作相当于统计了当前数值 h[i] 的逆序对个数,并将其存储到数组 qt 中。
在第二个循环中,按照与第一个循环相反的顺序遍历数值,这样可以统计每个数值左侧的逆序对个数。具体地,通过 sum(y-1) 的方式统计了数值 y 左侧的数值中比 y 大的数值出现的次数,并将其加到之前在 qt 中存储的逆序对个数上。最终,将 qt 中所有元素的值累加起来,就可以得到整个序列的逆序对个数。
因此,这段代码中维护的树状数组并不是直接维护原始数值,而是通过对数值的转换,维护了一个计数数组,以便快速统计逆序对个数。