Minimum Inversion Number HDU - 1394(线段树求逆序数)

Minimum Inversion Number

题目链接: HDU - 1394
求逆序数的方法有多种, 一可以直接暴力求, O(n^2)的复杂度, 二可以用归并排序求, O(nlogn)的复杂度,
现在学了线段树又可以用线段树求, O(nlogn)的复杂度;
前两种方法就不多说了, 就只谈用线段树求逆序数的方法;
9 2 8 3 6 7 5 这个序列的逆序数是12,是怎么出来的呢?
先看9, 前边没有比他大的数, 贡献0,
2, 前边有一个比它大的数, 贡献1,
8, 贡献1;3贡献2;6贡献2;7贡献2;5贡献4,合计12;
由此可看出只需要找出写下每个数之前有几个比他大的数已写即可;
我们可以先建个空的线段树, 节点为1~n, 每次写下一个数之前查询该数所在位置后边有几个数被写, 然后再记录该数;
这个题要求求出所给序列中按不同排列次序最小的逆序数是多少, 如2 8 7 4 3 的不同排列次序可以是8 7 4 3 2 或7 4 3 2 8。。。。。。
若一个序列中是0~n-1时:
当一个数i在序列首时, 序列中比他小的数只有i个, 当他移到序列尾时, 前边比他大得数有n-i-1个;
所以每次转移逆序数变化为sum=sum-i+n-1-i;
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 5010;
int n;
int a[maxn];
struct node{
	int left, right, x;
}tree[maxn<<2];
void build(int m, int l, int r){
	tree[m].left=l;
	tree[m].right=r;
	if(l==r){
		tree[m].x=0;
		return;
	}
	int mid=(l+r)>>1;
	build(m<<1, l, mid);
	build(m<<1|1, mid+1, r);
	tree[m].x=tree[m<<1].x+tree[m<<1|1].x;
}
void update(int m, int a){
	if(tree[m].left==a&&tree[m].right==a){
		tree[m].x++;
		return;
	}
	int mid=(tree[m].left+tree[m].right)>>1;
	if(a<=mid) update(m<<1, a);
	else update(m<<1|1, a);
	tree[m].x=tree[m<<1].x+tree[m<<1|1].x;
}
int query(int m, int l, int r){
	if(tree[m].left==l&&tree[m].right==r){
		return tree[m].x;
	}
	int sum1=0, sum2=0;
	int mid=(tree[m].left+tree[m].right)>>1;
	if(r<=mid) return query(m<<1, l, r);
	else if(l>mid) return query(m<<1|1, l, r);
	return query(m<<1, l, mid)+query(m<<1|1, mid+1, r);
}
int cnt[maxn], sum=0;
int main(){
	while(~scanf("%d", &n)){
		build(1, 1, n);
		sum=0;
		for(int i=1; i<=n; i++){
			scanf("%d", &a[i]);
			sum+=query(1, a[i]+1, n);//先查询再更新;因为输入值为0~n-1, 节点值为1~n, 所以向后错一个位置;
			update(1, a[i]+1);
		}
		int ans=sum;
		for(int i=1; i<=n; i++){
			//printf("i:%d   sum:%d\n", i, sum);
			sum=sum-a[i]+n-a[i]-1;
			ans=min(ans, sum);	
		}
		printf("%d\n", ans);
	}
	return 0;
}



<think>嗯,用户现在想用递归函数来计算逆序数逆序数通常指的是在一个序列中,前面的数比后面的数大的对数,比如在数组[3,1,2]中,逆序数是2,因为3>1和3>2。那如何用递归来实现呢? 首先,我得回忆一下递归的基本思想。递归通常是把大问题分解成小问题,然后合并结果。对于逆序数,可能的分治方法就是类似于归并排序中的分治策略。归并排序在合并的时候可以统计逆序数,这个方法应该可行。用户之前的问题是关于组合数的计算,现在转向逆序数,可能需要不同的递归方法。不过用户提供的引用里提到了用递归逆序栈,可能用户对递归的应用有误解,或者想将栈逆序的方法应用到逆序数计算中。但逆序数和栈逆序其实是两个不同的问题。我需要先确定用户的需逆序数的计算通常有两种方法:暴力法和归并分治法。暴力法是O()复杂度,而分治法则可以达到O(n logn)。递归在这里更适合分治法,因为分治自然符合递归的结构。 那如何用递归实现归并排序中的逆序数统计呢?基本思路是,将数组分成左右两半,分别计算左半和右半的逆序数,再计算跨越左右的逆序数,三者相加就是总数。这需要修改归并排序的合并过程,在合并时统计逆序数。 用户可能希望得到具体的递归函数实现。比如,一个递归函数负责分割数组,另一个负责合并并统计逆序数。代码结构可能类似于归并排序,但需要额外的计数器。然后考虑递归的终止条件,当数组长度为1时,逆序数为0。分割到最小单位后,合并时开始统计。合并过程中,当左半的元素大于右半的元素时,逆序数增加左半剩余元素的数量,因为左半剩余的元素都会比当前右半元素大。 现在需要把这个思路转化为递归的代码。比如,在C++中,可以写一个函数mergeSortCount,返回逆序数,同时进行排序。递归分割数组,合并时统计逆序数,并返回左、右和合并时的逆序数总和。 用户提供的引用中,递归用于逆序栈,可能涉及到多次递归调用获取栈底元素。但逆序数的递归方法不同,需要分治策略。这时候需要明确两者的区别,避免混淆。 另外,用户可能对递归的理解不够深入,需要解释清楚递归的分解步骤和合并过程。例如,在归并排序中,如何通过递归分解数组,再逐层合并并统计逆序数。 可能还需要考虑数组的索引处理,比如在合并过程中,如何正确计算逆序数。例如,当左半的当前元素大于右半的当前元素时,逆序数增加mid- leftIndex+1,其中mid是左半的结束位置,leftIndex是当前左半的指针位置。 测试案例也很重要,比如数组[4,3,2,1]的逆序数应该是6,每个元素都比后面的所有元素大。递归方法应该能够正确计算这个结果。 最后,总结递归方法的时间复杂度,说明其优于暴力法的地方,以及可能的应用场景。同时,提醒用户注意递归深度可能导致栈溢出的问题,特别是对于大规模数组的情况。</think>### 递归实现逆序数计算的原理与方法 逆序数Inversion Count)指序列中满足$i < j$且$a_i > a_j$的元素对数。递归实现的核心思想基于分治法,与归并排序结合,时间复杂度为$O(n \log n)$。以下是详细实现步骤: --- #### 一、数学原理 设数组$A$的逆序数为$C(A)$,将其分为左半部分$L$和右半部分$R$,则: $$C(A) = C(L) + C(R) + C_{cross}$$ 其中: - $C(L)$: 左半部分内部的逆序数 - $C(R)$: 右半部分内部的逆序数 - $C_{cross}$: 跨越左右两部分的逆序数 --- #### 二、递归实现步骤 1. **分割数组**:递归地将数组分成左右两半 2. **合并统计**:在合并过程中统计跨越左右两部分的逆序数[^1] --- #### 三、代码实现(C++) ```cpp #include <vector> using namespace std; long long mergeAndCount(vector<int>& arr, vector<int>& temp, int left, int mid, int right) { int i = left, j = mid + 1, k = left; long long inv_count = 0; while (i <= mid && j <= right) { if (arr[i] <= arr[j]) { temp[k++] = arr[i++]; } else { temp[k++] = arr[j++]; inv_count += (mid - i + 1); // 统计跨越逆序数 } } while (i <= mid) temp[k++] = arr[i++]; while (j <= right) temp[k++] = arr[j++]; for (i = left; i <= right; ++i) arr[i] = temp[i]; return inv_count; } long long mergeSortCount(vector<int>& arr, vector<int>& temp, int left, int right) { long long inv_count = 0; if (left < right) { int mid = left + (right - left) / 2; inv_count += mergeSortCount(arr, temp, left, mid); inv_count += mergeSortCount(arr, temp, mid + 1, right); inv_count += mergeAndCount(arr, temp, left, mid, right); } return inv_count; } long long countInversions(vector<int>& arr) { vector<int> temp(arr.size()); return mergeSortCount(arr, temp, 0, arr.size() - 1); } ``` --- #### 四、算法分析 | 指标 | 值 | 说明 | |--------------|------------------|-----------------------------| | 时间复杂度 | $O(n \log n)$ | 与归并排序相同 | | 空间复杂度 | $O(n)$ | 需要临时数组存储合并结果 | | 适用场景 | 大规模数据 | 比暴力法$O(n^2)$效率更高 | --- #### 五、应用示例 ```cpp int main() { vector<int> arr = {3, 1, 2, 4, 5}; cout << "逆序数:" << countInversions(arr) << endl; // 输出2 return 0; } ``` --- #### 六、注意事项 1. **递归深度**:当$n \geq 10^5$时可能导致栈溢出,需改用迭代版归并排序 2. **元素范围**:需处理整数溢出(代码中使用`long long`类型) 3. **稳定性**:归并排序是稳定排序,不影响逆序数统计[^2] --- ### 相关问题 1. 分治算法在排序问题中的其他典型应用? 2. 如何优化递归实现的栈空间使用? 3. 逆序数在现实中的具体应用场景有哪些? 4. 如何处理浮点数数组的逆序数计算?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值