题目连接 :hdu 1394
Minimum Inversion Number
Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)
Total Submission(s): 25736 Accepted Submission(s): 15158
Problem Description
The inversion number of a given number sequence a1, a2, ..., an is the number of pairs (ai, aj) that satisfy i < j and ai > aj.
For a given sequence of numbers a1, a2, ..., an, if we move the first m >= 0 numbers to the end of the seqence, we will obtain another sequence. There are totally n such sequences as the following:
a1, a2, ..., an-1, an (where m = 0 - the initial seqence)
a2, a3, ..., an, a1 (where m = 1)
a3, a4, ..., an, a1, a2 (where m = 2)
...
an, a1, a2, ..., an-1 (where m = n-1)
You are asked to write a program to find the minimum inversion number out of the above sequences.
Input
The input consists of a number of test cases. Each case consists of two lines: the first line contains a positive integer n (n <= 5000); the next line contains a permutation of the n integers from 0 to n-1.
Output
For each case, output the minimum inversion number on a single line.
Sample Input
10
1 3 6 9 0 8 5 7 4 2
Sample Output
16
在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序。
一个排列中逆序的总数就称为这个排列的逆序数。
这里我要先说一下题意:
1.给定一个元素个数为n的数列,a0~an,每一个元素大小ai 取值范围属于区间[0, n-1],
2.要进行 m=0 -> m=n-1 次循环,每一次循环都要将 a[0] 放到数列的末尾,即a[n-1]的位置,并求出移动后数列的逆序数
3. m次循环后,得出m个不同序列的m个逆序数,输出其中最小的逆序数
例如 :
给出 4 个数范围属于[0,3] : 3 0 2 1
m = 0 不进行移动 3 0 2 1 逆序数为 :2
m = 1 3放在最后 0 2 1 3 逆序数为:1
m = 2 0放在最后 2 1 3 0 逆序数为:4
m = 3 2放在最后 1 3 0 2 逆序数为:3
所有数列中 最小逆序数为 1;
思路:
1.先求出初始数列的逆序数sum,则a[i]移动后的数列的逆序数为 sum = sum-a[i]+n-a[i]-1;
这个规律推出来也不难, 因为 ai 属于[0, n] ,在 ai后面比其大的数有n-a[i]-1 个,比其小的数的个数就是a[i]个。
又因为比a[i]小的数,在a[i]移动到数列末尾后不能与其构成逆序,所以逆序数sum应减去这些数的个数。
相反比a[i]大的数,在a[i]移动到数列末尾后都能与其构成逆序,所以逆序数sum应加上这些数。
所以变化后的 sum 就是新序列的逆序数 。
2. 移动后的逆序数求解问题解决了,后面的问题主要就是初始逆序数的求解
这里采用线段树求解
我们可以利用输入数据的时效,每输入一个元素a[i],就查看一下它前面有多 少个大于a[i]的数,sum进行累加,直到最后一个元素输入,sum就是该序列的逆序数
3.那么要怎么求a[i]前面大于a[i]的个数呢
总共 n个数且属于[0, n],那我们不妨假设有一个区间[0,n],把总区间内的值初始化为0,然后只要把第i个数前出现过的数都在相应的区间的值定为为1,表示相对应的元素已经出现过
那问题就转化为 [a[i], n-1] 区间求和了
例如 :
3个数 1 0 2
把[0,2]区间内所有的数初始化为0
输入1 将[1, 2]的所有子区间的值全加上,求和,因为[1, 1] = 0, [2,2] = 0;所以1的逆序:sum1=0; 同时[1,1]++;表示1已经出现
输入0 将[0, 2]的所有子区间的值全加上,求和,因为[0,0] = 0, [1,1] = 1, [2,2] = 0; 所以0的逆序:sum2 = 1; 同时[0,0]++;
输入2 将[2, 2]的所有子区间的值全加上,求和,因为[2,2] = 0, 所以2的逆序:sum3 = 0; 同时[2,2]++;
所以 逆序数 sum = sum1 + sum2 + sum3 = 0 + 1 + 0 = 1;
AC代码:
#include <iostream>
#include <stdio.h>
#include <algorithm>
using namespace std;
const int Max = 5005;
int str[Max << 2], a[Max];
void pushDate(int rt)
{
str[rt] = str[rt << 1] + str[rt << 1 | 1];
}
void Bulid(int l, int r, int rt)
{
str[rt] = 0; // 为所有区间节点赋初值为0;
if(l == r) return ;
int m = (l + r) >> 1;
Bulid(l, m, rt << 1);
Bulid(m+1, r, rt << 1 | 1);
}
void upDate(int p, int l, int r, int rt)
{
if(l == r)
{
str[rt]++; // 标记 a[i] == p 已经出现过
return ;
}
int m = (l + r) >> 1;
if(p <= m) upDate(p, l, m, rt << 1);
else upDate(p, m+1, r, rt << 1 | 1);
pushDate(rt);
}
int Query(int L, int R, int l, int r, int rt)
{
if(L <= l && r <= R)
return str[rt];
int temp = 0;
int m = (l + r) >> 1;
if(L <= m) temp += Query(L, R, l, m, rt << 1);
if(R > m) temp += Query(L, R, m+1, r, rt << 1 | 1);
return temp;
}
int main()
{
int n;
while(scanf("%d", &n) != EOF)
{
Bulid(0, n-1, 1); // 因为元素a[i] 属于区间[0,n-1],所以叶节点最好从[0,0]开始时
int sum = 0;
for(int i = 0; i < n; i++)
{
scanf("%d", &a[i]);
sum += Query(a[i], n-1, 0, n-1, 1);
upDate(a[i], 0, n-1, 1);
}
int temp = sum;
for(int i = 0; i < n; i++)
{
sum += n-1-a[i] - a[i];
temp = min(temp, sum);
}
printf("%d\n",temp);
}
return 0;
}