分治法
具体操作: 把原问题分成 k 个较小规模的子问题,对这 k 个子问题分别求解。如果子问题不够小,那么把每个子问题再划分为规模更小的问题。这样一直分解下去,直到问题足够小,很容易求出这些小问题的解为止
求解的问题特征:
- 平衡子问题:子问题的规模大致相同,能把问题划分成大小差不多相等的 k 个子问题,最好 k=2,即分成两个规模相等的子问题。子问题规模相等的处理效率比子问题规模不等的处理效率要高
- 独立子问题:子问题之间相互独立
分治法的思想几乎就是递归的过程
用分治法建立模型时,解题步骤:
- 分解:把问题分解成独立的子问题
- 解决:递归解决子问题
- 合并:把子问题的结果合并成原问题的解
归并排序
具体操作:
- 分解。把初始序列分成长度相同的左右两个子序列,然后把每个子序列在分成更小的两个子序列,直到子序列只包含 1 个数。这个过程用递归实现,如下图中的第 1 行是初始序列,每个数是一个子序列,可以看成递归到达的最底层
- 求解子问题,对子序列排序。最底层的子序列只包含 1 个数,其实不用排序
![](https://img-blog.csdnimg.cn/20201014203333478.jpg)
- 合并。归并两个有序的子序列,这是归并排序的主要操作,过程如下图所示。例如在图(a)中,i 和 j 分别指向子序列 { 13, 94, 99 } 和 { 34, 56 } 的第一个数,进行第 1 次比较,发现 a[i] < a[j],把 a[i] 放到临时空间 b[] 中。总共经过 4 次比较,得到了 b[] = { 13, 34, 56, 94, 99 }
![](https://img-blog.csdnimg.cn/20201014214715256.jpg)
计算复杂度: 对 n 个数进行归并排序:(1)需要 log(2)n 趟归并; (2)在每一趟归并中有很多次合并操作,一共需要 O(n) 次比较。 所以计算复杂度是 O(nlog(2)n)
空间复杂度:由于需要一个临时的 b[] 存储结果,所以空间复杂度是 O(n)
逆序对问题
问题描述
![](https://img-blog.csdnimg.cn/20201014214407636.jpg)
分析:
当 k=0 时,就是求原始序列中有多少个逆序对
考察图 6.7 所示的一次合并过程发现,可以利用这个过程记录逆序对。观察到以下现象:
(1)在子序列内部,元素都是有序的,不存在逆序对;逆序对只存在于不同的子序列之间
(2)在合并两个子序列时,如果前一个子序列的元素比后面子序列的元素小,那么不产生逆序对,如图 6.7(a) 所示;如果前一个子序列的元素比后面子序列的元素大,就会产生逆序对,如图 6.7(b) 所示。不过,在一次合并中,产生的逆序对不止一个,例如在图 6.7(b) 中把 34 放到 b[] 中,它与 94、99 产生了两个逆序对。在下面程序中,相关代码是 " cnt+=mid-i+1; "
根据以上观察,只要在归并排序过程中记录逆序对就行了
以上解决了 k=0 时原始序列的问题,现在考虑,当 k!=0 时(即把序列中任意两个相邻数交换不超过 k 次)逆序对最少有多少?注意:不超过 k 次的意思是可以少于 k 次,而不一定要 k 次
在所有相邻数中,只有交换那些逆序的才会影响逆序对的数量。设原始序列有 cnt 个逆序对,讨论以下两种情况:
- 如果 cnt <= k ,总逆序数量不够交换 k 次。所以进行 k 次交换之后,最少的逆序对数为 0
- 如果 cnt > k,让 k 次交换都发生在逆序的相邻数上,那么剩余的逆序对是 cnt - k
import java.util.*;
public class Main
{
final int MAXN = 100005;
long[] a = new long[MAXN];
long[] b = new long[MAXN];
long cnt;
void Merge(int L, int mid, int r){
int i=L, j=mid+1, t=0;
while(i<=mid && j<=r){
if(a[i] > a[j]){
b[t++] = a[j++];
cnt += mid-i+1; //记录逆序对的数量,**除去此句,便是完整的纯归并排序**
}
else b[t++] = a[i++];
}
//一个子序列中的数都处理完了,另一个还没有,把剩下的直接复制过来
while (i<=mid)
b[t++] = a[i++];
while(j<=r)
b[t++] = a[j++];
for(i=0; i<t; i++) //把排好序的 b[] 复制回 a[]
a[L+i] = b[i];
return;
}
void Mergesort(int L, int r){
if(L < r){
int mid = (L+r)/2; //平分成两个子序列
this.Mergesort(L,mid);
this.Mergesort(mid+1,r);
this.Merge(L,mid,r); //合并
}
return;
}
public static void main(String args[]){
int n;
long k;
Main m = new Main();
Scanner sc = new Scanner(System.in);
while (!sc.hasNext("#")){ //以 # 字符串结束输出
n = sc.nextInt();
k = sc.nextLong();
m.cnt = 0;
for(int i=0; i<n; i++)
m.a[i] = sc.nextLong();
m.Mergesort(0,n-1); //归并排序
if(m.cnt <= k) System.out.println(0);
else System.out.println(m.cnt-k);
}
}
}
快速排序
思路:把序列分成左、右两部分,使得左边所有的数都比右边的数小;递归这个过程,直到不能再分为止
最简单的办法是设定两个临时空间 X,Y 和一个基准数 t;检查序列中所有的元素,比 t 小的放在 X 中,比 t 大的放在 Y 中。但其实可以直接在原序列上操作,不需要临时空间 X、Y
以下介绍一种很容易操作的方法:
![](https://img-blog.csdnimg.cn/20201014222626253.jpg)
以上方法的实现如下:
import java.util.*;
public class Main{
final int N = 10010;
int[] data = new int[N];
private void swap(int a, int b){ //交换
int temp = data[a];
data[a] = data[b];
data[b] = temp;
}
private int partition(int left, int right){ //划分成左右两部分,以 i 指向的数为界
int i = left;
int temp = data[right]; //把尾数看成基准数
for(int j=left; j<right; j++){
if(data[j] < temp){ //比基准数小的放在左边
swap(i, j);
i++;
}
}
swap(i, right); //把基准数放在中间
return i; //返回基准数的位置
}
private void quicksort(int left, int right){
if(left < right){
int i = partition(left, right); //划分
quicksort(left, i-1); //分治:i 左边的继续递归划分
quicksort(i+1, right); //分治:i 右边的继续递归划分
}
return;
}
public static void main(String[] args) {
Main m = new Main();
Scanner sc = new Scanner(System.in);
int n;
n = sc.nextInt();
for(int i=0; i<n; i++)
m.data[i] = sc.nextInt();
m.quicksort(0,n-1); //快速排序
for(int i=0; i<n; i++)
System.out.print(m.data[i]+","); //输出排序后的序列
System.out.print('\n');
System.out.print(m.data[n/2]);
}
}