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
∑(ai−bi)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
∑(ai−bi)2=ai2−2ab+bi2
由于
a
i
2
和
b
i
2
a_i^2和b_i^2
ai2和bi2是常数,因此要想使得上面的式子最小,只要使得
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;
这一步转换非常重要并且不容易想到?那么普通人怎么去想呢?
首先我们的目标是两个数列顺序一致,那么我们可以看上面的例子。
- 排好序之后的数列中A[1]和B[1]都在以前的数列中的第三位,这就不用变了
- A[2]和B[2]不一样
- A[3]和B[3]不一样
- 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;
}