题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1394
题意:给定n个数,一个合法操作是每次可以将数列的第一个元素放到数列的尾部,然后问你所有可能操作中的逆序对的个数最少是多少。一个逆序对(ai,aj)的定义是数列a中下标 i 小于 j 同时 ai 大于 aj。
思路:看到数据范围只有5000,一开始就往暴力方面想了。然后模拟一下发现了一个比简单的规律。对于一个n个数的数列a,b,c,d……..,每个数都是0到n-1,设当前的逆序对个数是cnt,那么一次操作后逆序对个数会变成cnt+n-2*a-1个,其实蛮显然的,因为数列中比a大的数有n-1-a个,比a小的数有a个。O(n*n)计算出事的逆序对个数,然后在O(n)遍历一边统计最小值就可以了。
暴力
#include<bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
int main()
{
int n;
int a[5003];
while(~scanf("%d",&n)){
for(int i=0;i<n;i++){
scanf("%d",&a[i]);
}
int cnt=0;
for(int i=0;i<n;i++){
for(int j=i+1;j<n;j++){
if(a[j]<a[i]) cnt++;
}
}
int mi=inf;
for(int i=0;i<n;i++){
cnt+=(n-1-(2*a[i]));
mi=min(cnt,mi);
}
printf("%d\n",mi);
}
return 0;
}
这题的数据范围很小,在大数据的情况暴力可能会T。想了一下优化,有两个思路,一个是归并排序优化,一个是线段树优化。
归并排序
考虑这样一个情况,把n个数的序列A分为一般长度的两个序列L和R,那么A的逆序对个数就等于L、R中自身的逆序对个数之和再加上跨越L和R的逆序对个数。这不正好是归并排序的思想么。R和L中的逆序对个数迭代到1的时候很容易计算,那么如何快速计算跨越L和R的逆序对re呢。基于这样一个事实,我们分别把L、R中的数据进行排序是不会影响re的值的。排序后如果L中下标为ll的值比R中下标为rr的值大,那么re就应该加上L的长度n1-ll。(自己想一下为什么)
#include<bits/stdc++.h>
using namespace std;
const int maxn = 50003;
const int inf = 0x3f3f3f3f;
int n;
int solve(int *a,int l,int r)
{
if(l>=r)
return 0;
int re=0;
int mid=(r+l)>>1;
int sig=solve(a,l,mid)+solve(a,mid+1,r);
int n1=mid-l+1;
int n2=r-mid;
int L[n1],R[n2];
for(int i=0;i<n1;i++)
L[i]=a[i+l];
for(int i=0;i<n2;i++)
R[i]=a[i+mid+1];
L[n1]=R[n2]=inf;
int ll=0,rr=0;
for(int i=l;i<=r;i++){
if(L[ll]>R[rr]){
re+=n1-ll;
a[i]=R[rr++];
}
else
a[i]=L[ll++];
}
/*for(int i=0;i<n;i++)
printf("%d ",a[i]);
printf("\n");*/
return re+sig;
}
int main()
{
int a[maxn],b[maxn];
while(~scanf("%d",&n)){
for(int i=0;i<n;i++){
scanf("%d",&a[i]);
b[i]=a[i];
}
int cnt=solve(b,0,n-1);
int mi=inf;
for(int i=0;i<n;i++){
cnt+=(n-1-(2*a[i]));
mi=min(cnt,mi);
}
printf("%d\n",mi);
}
return 0;
}
线段树
这个是我看别人的题解才理解线段树的做法的,感觉自己的线段树还得再学学。思路是这样的,线段树的每个节点的值初始化为0,插入时把其对应的叶子结点更新为1,每遇到一个a[i]就查询当前树里面比它大的数有多少个,累加一下就是初始的逆序对个数了。由于是计算逆序对,所以查询区间应该是a[i]到n-1。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5003;
const int inf = 0x3f3f3f3f;
int sum[maxn << 2];
void PushUp(int rt)
{
sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
}
void build(int l, int r, int rt)
{
sum[rt]=0;
if (l == r)
return;
int m = (l + r) >> 1;
build(l, m, rt << 1);
build(m + 1, r, rt << 1 | 1);
PushUp(rt);
}
void update(int p,int l, int r, int rt)
{
if (l == r)
{
sum[rt] ++;
return;
}
int m = (l + r) >> 1;
if (p <= m) update(p, l, m, rt << 1);
else update(p, m + 1, r, rt << 1 | 1);
PushUp(rt);
}
int query(int ll, int rr, int l, int r, int rt)
{
if (ll <= l && rr >= r) return sum[rt];
int m = (l + r) >> 1;
int ret = 0;
if (ll <= m) ret += query(ll, rr, l, m, rt << 1);
if (rr > m) ret += query(ll, rr, m + 1, r, rt << 1 | 1);
return ret;
}
int main()
{
int n;
int a[maxn];
while(~scanf("%d",&n)){
int cnt=0;
build(0,n-1,1);
for(int i=0;i<n;i++){
scanf("%d",&a[i]);
cnt+=query(a[i]+1,n-1,0,n-1,1);
update(a[i],0,n-1,1);
}
int mi=inf;
for(int i=0;i<n;i++){
cnt+=(n-1-(2*a[i]));
mi=min(cnt,mi);
}
printf("%d\n",mi);
}
return 0;
}
看了一下status,线段树在时间和空间上都是最优的,归并排序和线段树时间复杂度都是Onlogn,但是归并排序用到了比较多的中间数组,所以内存占用比较大,可能释放掉会比较好,有兴趣可以试一下。总结一下,对这道水题我自己口胡了这么多其实没啥大用,如果是比赛当然是首选暴力的,连剪枝都不会考虑。但是像这样也能拓展一下思路,顺便巩固一下其他知识,权当学习了。