【Divide_Conquer】逆序对

1.【洛谷P1908】

1.1 题目描述

输入格式:
第一行,一个数 n,表示序列中有 n个数。
第二行 nn 个数,表示给定的序列。序列中每个数字不超过 10^9。

输出格式:
输出序列中逆序对的数目。

输入样例:

6
5 4 2 6 3 1

输出样例:

11

1.2 暴力解法之冒泡排序

【关键步骤】在冒泡排序代码中,若交换则逆序对加1,就这么简单。
【时间复杂度】O(n^2),肯定超时!!!

//冒泡排序求逆序对
#include<bits/stdc++.h>
using namespace std;

int nixu(vector<int>&nums){
    int n = nums.size();
    int ans = 0;
    for(int i = 0; i< n - 1; i++){
        for(int j = 0; j < n -i - 1; j++){
            if(nums[j] > nums[j + 1]){
                int temp = nums[j];
                nums[j] = nums[j + 1];
                nums[j + 1] = temp;
                ans ++;
            }
        }
    }
    return ans;

}

int main(){
    int n;
    scanf("%d", &n);
    vector<int>nums;
    int x;
    for(int i = 0; i < n; i++){
        scanf("%d", &x);
        nums.push_back(x);
    }
 
    printf("%d\n",nixu(nums));
    return 0;

1.3 优化解法之归并排序

【设计思想】
step1:考虑是否可分,是否可以先求左一半的逆序,再求右一半的逆序?
step2:考虑最简单的case0,{1}逆序对为0;case1{1,2}逆序对为0,{2,1}逆序对为1;
step3:考虑如何从case0求case1,将case1分为{1},{2}两个数组,数组内部逆序对都为0,数组间逆序对如何求解?
step4:考虑一个更一般的例子,{3,4,5,1,0,7},将其分为两半{3,4,5},{1,0,7},左一半和右一半的组间逆序对看起来并不好找,不过我们可以将其排序,变成{3,4,5},{0,1,7},这样看起来好多了。从头开始比较,3>0,说明左一半的所有都与0存在逆序,将结果+3;3>1,说明左一半的所有都与1存在逆序,将结果+3;3<7,不与7逆序;4<7,不与7逆序;5<7不与7逆序;
step5:抽取方法,我们发现这个过程与归并排序的合并过程相同,只需要在排序时记录逆序对即可。

int nixu(vector<int> &nums, int l, int r){
	if(r <= l){
		return 0;
	}
	//将nums数组分为两半
		int mid = (l + r) >> 1; 
	//左一半求逆序对
	   int left = nixu(nums, l, mid);
	//右一半求逆序对
		int right = nixu(nums, mid + 1, r);
	//两部分的逆序对
		int LR = twoMerge(nums, l, mid, r);
	//逆序对加和 
	return left + right + LR;
}

【完整代码】这个代码的归并排序中有一个很大的缺陷,导致通过率为50%,感兴趣的可以找找!!!

//分治法求逆序对
#include<bits/stdc++.h>
using namespace std;
long long twoMerge(vector<int> &nums, int l, int mid, int r){
	int len = nums.size();
	//cout<<"len"<<len<<endl;
	vector<int>temp = nums;
	int i = l;
	int j = mid + 1;
	long long res = 0;
	int k = l;
	for (i, j; i <= mid && j <= r;){
		if(temp[i] > temp[j]){
		res += mid - i + 1;
		nums[k++] = temp[j]; 
		j++;
		}
	else{
		nums[k++] = temp[i];
		i++;
	
		}
	}
	if(j > r){
		while(i <= mid){
			nums[k++] = temp[i];
			i++;
		}
	}
	if(i > mid){
		while(j <= r){
			nums[k++] = temp[j];
			j++;
		}
	}
	
	return res;
}

int nixu(vector<int> &nums, int l, int r){
	if(r <= l){
		return 0;
	}
	//将nums数组分为两半
		int mid = (l + r) >> 1; 
	//左一半求逆序对
	   int left = nixu(nums, l, mid);
	//右一半求逆序对
		int right = nixu(nums, mid + 1, r);
	//两部分的逆序对
		int LR = twoMerge(nums, l, mid, r);
	//逆序对加和 
	return left + right + LR;
}



int main(){
    int n;
    scanf("%d", &n);
    vector<int>nums;
    int x;
    for(int i = 0; i < n; i++){
        scanf("%d", &x);
        nums.push_back(x);
    }
 
    printf("%lld\n",nixu(nums, 0, n-1));
    return 0;
}

【改进代码】
上述代码 twoMerge() 中有一行

	vector<int>temp = nums;

在每次递归的时候都要拷贝整个数组,这真的是有点愚蠢!!!
所以我们改成每次递归只拷贝[l,r]这个区间内的数组,这需要一些下标转换,不要怕麻烦

long long twoMerge(vector<int> &nums, int l, int mid, int r){
	int len = nums.size();
	//cout<<"len"<<len<<endl;
	vector<int>::const_iterator first = nums.begin() + l;
	vector<int>::const_iterator second = nums.begin() + r + 1;
	vector<int>temp ;
	temp.assign (first,second);
	int i = l;
	int j = mid + 1;
	long long res = 0;
	int k = l;
	for (i, j; i <= mid && j <= r;){
		if(temp[i - l] > temp[j - l]){
			res += mid - i + 1;
			nums[k++] = temp[j - l]; 
			j++;
	}
	else{
		nums[k++] = temp[i - l];
		i++;
		}
	}
	if(j > r){
		while(i <= mid){
			nums[k++] = temp[i - l];
			i++;
		}
	}
	if(i > mid){
		while(j <= r){
			nums[k++] = temp[j - l] ;
			j++;
		}
	}

	return res;
}

2 【洛谷P1966】

有两盒火柴,每盒装有 nn 根火柴,每根火柴都有一个高度。 现在将每盒中的火柴各自排成一列, 同一列火柴的高度互不相同, 两列火柴之间的距离定义为:
∑ ( a i − b i ) 2 \sum (a_i-b_i)^2 (aibi)2
其中 a i a_i ai表示第一列火柴中第 i 个火柴的高度, b i b_i bi 表示第二列火柴中第 i 个火柴的高度。每列火柴中相邻两根火柴的位置都可以交换,请你通过交换使得两列火柴之间的距离最小。请问得到这个最小的距离,最少需要交换多少次?如果这个数字太大,请输出这个最小交换次数对 10^8-3取模的结果。
输入格式:
共三行,第一行包含一个整数 n,表示每盒中火柴的数目。
第二行有 n 个整数,每两个整数之间用一个空格隔开,表示第一列火柴的高度。
第三行有 n 个整数,每两个整数之间用一个空格隔开,表示第二列火柴的高度。

输出格式:
一个整数,表示最少交换次数对 10^8-3取模的结果。

输入样例

4
2 3 1 4
3 2 1 4

输出样例

1

【关键知识】
∑ ( a i − b i ) 2 = a i 2 − 2 a b + b i 2 \sum (a_i-b_i)^2 = a_i^2-2ab+b_i^2 (aibi)2=ai22ab+bi2
由于 a i 2 和 b i 2 a_i^2和b_i^2 ai2bi2是常数,因此要想使得上面的式子最小,只要使得 2 a b 2ab 2ab最大即可;
两个长度相同的数列相乘,则 顺序相乘>乱序相乘,因此问题转变成将两个数列排成相同的顺序结构。
【数据结构】
记录这个数和这个数的下标 val(index)
2(1) 3(2) 1(3) 4(4)
3(1) 2(2) 1(3) 4(4)
现在我们将两个数组排序,这个数据结构就为的是排序之后不丢失以前的下标
A[i] 1(3) 2(1) 3(2) 4(4)
B[i] 1(3) 2(2) 3(1) 4(4)
【重要技巧】
做一个数组c,使得c[A[i].index]=B[i].index;
这一步转换非常重要并且不容易想到?那么普通人怎么去想呢?
首先我们的目标是两个数列顺序一致,那么我们可以看上面的例子。

  1. 排好序之后的数列中A[1]和B[1]都在以前的数列中的第三位,这就不用变了
  2. A[2]和B[2]不一样
  3. A[3]和B[3]不一样
  4. A[4]和B[4]一样,也不用变了。
    可以看到我们比较都是对于的元素的下标是否一样,那么我们怎么来表示这个规律呢?
    用一个数组c的下标表示A中元素对应的下标,用这个数组中存的数表示B中元素对应的下标,然后将这个数组c排序,使得这个c数组的下标和元素一一对应,就可以使得A中元素的下标和B中元素的下标一一对应,从而得到答案。 这段话很重要,它揭示了这道题为什么可以转化为逆序对问题,需要仔细的去揣摩。然后对c数组使用逆序对的方法就可以啦!!!
#include <bits/stdc++.h>
using namespace std;
struct node {
    int val;
    int id;
};
bool cmp1(struct node a,struct node b){
          return a.val<b.val;
}
long long twoMerge(vector<int> &nums, int l, int mid, int r){
    int len = nums.size();
    //cout<<"len"<<len<<endl;
    vector<int>::const_iterator first = nums.begin() + l;
    vector<int>::const_iterator second = nums.begin() + r + 1;
    vector<int>temp ;
    temp.assign (first,second);
    int i = l;
    int j = mid + 1;
    long long res = 0;
    int k = l;
    for (i, j; i <= mid && j <= r;){
        if(temp[i - l] > temp[j - l]){
            res += mid - i + 1;
             nums[k++] = temp[j - l]; 
            j++;
        }
        else{
        nums[k++] = temp[i - l];
        i++;

        }
    }
    if(j > r){
        while(i <= mid){
            nums[k++] = temp[i - l];
            i++;
        }
    }
    if(i > mid){
        while(j <= r){
            nums[k++] = temp[j - l] ;
            j++;
        }
    }

    return res;
}

long long nixu(vector<int> &nums, int l, int r){
if(r <= l){
return 0;
}
//将nums数组分为两半
int mid = (l + r) >> 1; 
   //左一半求逆序对
   int left = nixu(nums, l, mid);
  //右一半求逆序对
int right = nixu(nums, mid + 1, r);
  //两部分的逆序对
long long LR = twoMerge(nums, l, mid, r);
  //逆序对加和 
return left + right + LR;
}


int main(){
    int n;
    scanf("%d",&n);
    vector<node>nums1;
    vector<node>nums2;
    vector<int>nums3(n);
    struct node x;
    for(int i = 0; i < n; i++){
        scanf("%d", &x.val);
        x.id = i;
        nums1.push_back(x);
    }
    for(int i = 0; i < n; i++){
        scanf("%d", &x.val);
        x.id = i;
        nums2.push_back(x);
    }
    sort(nums1.begin(),nums1.end(),cmp1);
    sort(nums2.begin(),nums2.end(),cmp1);
    for(int i = 0; i < n; i++){
        nums3[nums1[i].id] = nums2[i].id;
    }
    printf("%lld\n",nixu(nums3, 0, n - 1));
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神仙诙谐代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值