思路:
贪心+树状数组
本题是模拟的冒泡排序的流程,首先从直觉上判断,每个小朋友的怒气值既然跟被交换的次数有关,那就肯定跟与其相关的逆序对数量有关,而既然要求与其相关的逆序对,只需求出该小朋友左侧比他高的小朋友的个数和右侧比他矮的小朋友的个数即可。
那么下一个问题是,如何证明直觉做法的正确性,换言之,如何证明这样求出来的怒气值最小。下面我给出证明过程供参考:
- 对于冒泡排序,如果存在k个逆序对,那至少要交换k次才能得到正确的顺序,因为冒泡排序每次交换只交换相邻两个位置的元素,也就是说,每次交换只改变这两个元素的相对位置,所以一共至少交换k次,又因为冒泡排序每次交换必然会把小元素置于大元素的左侧,所以每次交换逆序对数量必然-1,而冒泡排序本身又是正确的,也就是说,假如有k个逆序对,那么一定存在一种排序方法,使交换操作恰好执行k次,且将该数组排序完毕
对于这个序列,假设2左侧比它大的元素的个数为k1,右侧比它小的元素个数为k2,那根据上述证明,1一定要换到2的左边,3一定要换到2的右边,所以2的交换次数至少为k1+k2,由此可推,对于任意元素,其交换次数都至少为k1+k2,又因为所有元素的k1+k2之和为2k,k为整个序列的逆序对数量,上面已经证明了对于全局的排序,交换次数可恰好为k次,且最少为k次,所以任意元素的操作次数均可等于k1+k2。到此,可证贪心思路正确。
所以本题就抽象成了:设某元素左侧比它大的元素的个数为k1,右侧比它小的元素个数为k2,遍历一遍数组,求出每个元素的k1+k2的值,然后算出∑1~k1+k2即可。求单个元素k1和k2值的过程可以用树状数组动态更新和查询
可以看出,贪心的题一般结论很简单,但证明过程较为复杂,我个人建议如果在赛场等仅需要把这道题做出来的话,尽量凭借直觉和经验,等写完程序后多带入几个数验证结果比较好。仔细证明会花费大量时间。
做法如下:
- 开个sum数组,存的是原数组每个下标的k1+k2的值
- 正序遍历数组,利用树状数组在每个元素的sum上加k1
- 倒序遍历数组,利用树状数组在每个元素的sum再加k2
- 用等差数列求和公式算每个小朋友的怒气值,求出总和
代码如下:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1000010;
typedef long long LL;
int tr[N],h[N],sum[N];
int lowbit(int x)
{
return x & -x;
}
void add(int x,int y)
{
for(int i=x;i<=N;i+=lowbit(i)) tr[i]+=y;
}
int query(int x)
{
int res=0;
for(int i=x;i;i-=lowbit(i)) res+=tr[i];
return res;
}
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++) cin>>h[i];
for(int i=0;i<n;i++)
{
sum[i]=query(N)-query(h[i]);
add(h[i],1);
}
memset(tr,0,sizeof tr);//注意需要初始化,不然算k1时的值会影响k2的计算
for(int i=n-1;i>=0;i--)
{
sum[i]+=query(h[i]-1);
add(h[i],1);
}
//for(int i=0;i<n;i++) cout<<sum[i]<<" ";
LL res=0;//注意数据范围,累加的规模是平方级的,会爆int
for(int i=0;i<n;i++) res+=(LL)sum[i]*(sum[i]+1)/2;
cout<<res;
return 0;
}
这里简单说一下,为什么求k2需要倒着遍历数组。因为我的目的是要求该元素右侧***的元素的个数,我用***替换了“小于该元素”,是因为倒着遍历与大于小于无关,不要被误导,重点是与更新顺序有关,只有倒着遍历,才能从右至左动态更新到目标元素
作者:机械之忍
链接:https://www.acwing.com/activity/content/code/content/1997747/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
本人刚开始写题解不久,很多地方还不成熟,为了让读者更好的理解思路和细节,我做这道题时的调试代码片段也保留了,我觉得这样能更直观的表达我的想法,如果各位有疑问之处,或者我写的哪里有错误,欢迎私信我或者在下方留言
我的QQ:2907065305