- 大纲:
1.mergesort归并排序
2.counting inversions统计逆序数
3.closest pair of points最近点对算法
4.randomized quicksort随机快速排序
5.median and selection中位数 - 分治
1.将问题分解为几个子问题
2.递归(recursively)的解决每个子问题
3.把子问题的答案组合,成为整个问题的答案 - 最常使用
在线性时间分解和组合问题n->n/2
Brute force暴力法O(n2),分治法O(nlogn)。
一、归并排序
- 思想:
递归的排序左半段
递归的排序右半段
合并两段 - 目标:合并有序的listA、B成为整体C。
A、B分别设置一个指针,从左到右遍历,若ai<=bj,把ai append to C;否则相反。 - 一个有用的归并关系
令T(n)=有n个元素的list中,使用mergesort排序,比较的最大次数。(T单调非减)
T(n)=T(n/2)+T(n/2)+n=O(nlogn) - 实现一:自顶向下(递归)
实现二:自底向上(迭代)
#include<iostream>
#include<algorithm>
using namespace std;
const int len = 5;
int a[len]={ 0,5,4,3,2 },b[len];
void merge(int low, int mid, int high){
int i = low, j = mid + 1,k=0;
while (k <= high - low + 1){
if (i > mid)
b[k++] = a[j++];
else if (j > high)
b[k++] = a[i++];
else if (a[i] > a[j]){
//统计重要逆序数
//ans += mid + 1 - binarySearch(i, mid, 2 * a[j]);
//统计逆序数
//ans += mid + 1 - i;
b[k++] = a[j++];
}
else
b[k++] = a[i++];
}
for (int t = 0; t <k; t++)
a[low+t] = b[t];
}
void mergeSort2(){//ButtonUpMergeSort
for (int i = 1; i <= len; i += i){
for (int low = 0; low < len; low += i + i){
int high = min(low + i + i - 1, len - 1);
int mid = low + i - 1;
merge(low, mid, high);
}
}
}
void mergeSort1(int low, int high){//TopDownMergeSort
if (low >= high) return;
int mid = (low + high) >> 1;
mergeSort1(low, mid);
mergeSort1(mid+1, high);
merge(low, mid, high);
}
int main(){
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
mergeSort1(0, 4);
for (int i = 0; i < len; i++){
cout << a[i] << " ";
}
return 0;
}
二、统计逆序数
-
逆序数:i < j, but a i > a j
暴力法:O(n2)
分治法:O(nlogn)。 -
分治思路:
分:把list分两半A和B。
治:递归统计每个list的逆序数。
合:统计(a,b) 其中a ∈ A and b ∈ B的逆序数个数。
(将A、B排好序,二分查找b在A中的位置,b在A中时,A右边元素个数=逆序数)
(将A、B排好序,两个指针i、j分别从左到右指向A、B。若 a i <= a j :i++ ;a i > a j,i及右边的元素对于a j都是逆序数 :count+=n-1;i++。
每次现将较小的元素进入数组C)
ans=三个数之和。 -
实现:
输入:list L
输出:L中的逆序数和排好序的L`
只需要在merge中加一句:if (b[i] > b[j]) ans += mid - i + 1; -
变体:统计重要逆序对
#include<iostream>
using namespace std;
#define LL long long
const int maxn = 2e5 + 5;
int a[maxn], b[maxn];
LL ans;
int binarySearch(int low,int high,int val) {
int cnt = high + 1;
while (low <= high){
if (low == high){
if (a[low] > val)
cnt = low;
break;
}
int mid = (low + high) >> 1;
if (a[mid] > val){
cnt = mid;
high = mid;
}
else{
low = mid + 1;
}
}
return cnt;
}
void merge(int low, int mid, int high){
int i = low, j = mid + 1,k=0;
while (k <= high - low + 1){
if (i > mid)
b[k++] = a[j++];
else if (j > high)
b[k++] = a[i++];
else if (a[i] > a[j]){
ans += mid + 1 - binarySearch(i, mid, 2 * a[j]);
b[k++] = a[j++];
}
else
b[k++] = a[i++];
}
for (int t = 0; t <k; t++)
a[low+t] = b[t];
}
void mergeSort(int l, int r) {
if (l == r) return;
int mid = (l + r) >> 1;
mergeSort(l, mid);
mergeSort(mid + 1, r);
merge(l, mid, r);
}
int main() {
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int n;
while (cin >> n && n) {
ans = 0;
for (int i = 0; i < n; i++)
cin >> a[i];
mergeSort(0, n - 1);
cout << ans << endl;
}
return 0;
}
总结:写代码时可以用举例法。
三、 closest pair of points最近点对算法
- Closest pair problem:有n个点,找到距离最短的两个点。
- 解法
暴力法:O(n2) 。
1d 版本:若点在一条线上,为O(nlogn)。
法1,排序解法:以x坐标排序,比较邻接点;以y坐标排序,比较邻接点。
法2,把图分为4个部分,但不能保证每部分有n/4个点。 - 分治算法思路:
分:画一条竖线L,使两边各有n/2个点。
治:递归的找到两部分最近的一对点,距离分别为d1、d2。
合:找到(a,b) 其中a ∈ A and b ∈ B,距离最小。
(方法:令distance d=min(d1,d2),只需要考虑线L两侧的距离d的点;以点的y坐标排序;只需要检测排序11个位置内的点。检测到的距离d3,d=min(d,d3)。
最后return d)
时间复杂度 O(n log 2 n)
可以优化为 O(n logn) (在平方型(x 1 - x 2 ) 2 + (y 1 - y 2 ) 2树型模型中,任何找最近一对的算法需要Ω(n log n) ) - 如何优化?
Each recursive returns two lists: all points sorted by x-coordinate,and all points sorted by y-coordinate.
Sort by merging two pre-sorted lists. - 实现
#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
const int maxn = 2000 + 5;//每组点的个数
const long long inf = 0x3f3f3f3f;//表示无穷大的数
//点的结构体
struct node{
double x, y;//点的坐标
}pre[maxn], f[maxn];//读入的点都放在pre数组中,在两条线距离d的范围内的点存放在f[]中
bool cmp_x(node a, node b);
bool cmp_y(node a, node b);
double close_pair(int low, int high);
int main()
{
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int t; cin >> t;//number of test case
while (t--){
//读入数据
int n; cin >> n;//number of nodes
for (int i = 1; i <= n; i++){
cin >> pre[i].x >> pre[i].y;
}
//处理
sort(pre + 1, pre + n, cmp_x);//对所有点根据x坐标排序
//输出
printf("%.3f\n", close_pair(1, n));//%f 是输出bai float 型变量du;%f 是输出 double 型变量;%Lf 是输出 long double 型变量;long long 为lld
}
return 0;
}
bool cmp_x(node a, node b){//根据点的x坐标从大到小排序,若x坐标相同根据y坐标从大到小排序
return a.x < b.x;
}
bool cmp_y(node a, node b){
return a.y < b.y;
}
double abso(double x){//绝对值
return x < 0 ? -x : x;
}
double dis(node a, node b){//两个点的距离公式
return sqrt(pow(a.x - b.x, 2) + pow(a.y - b.y, 2));
}
//计算距离最近的两个点
double close_pair(int low, int high){
double d = 1.0*inf;//d初始为无穷大
//递归出口:有1个点(d为无穷大)OR 有2个点(若为同组,d为无穷大;若为异组,d为两点距离计算公式结果)
if (low == high)
return d;
if (low + 1 == high)
return dis(pre[low], pre[high]);
//分治法:
//分:在中间画一条线,左右分别找两边最近的两个点,距离为d1、d2
//治:取d=min(d1,d2),遍历所有点找与中间线(点)距离<=d的点,存入f[]中,对f[]中的点y坐标排序
//合:检测排序11个位置内的点。检测到的距离d3,d=min(d,d3)
int mid = (low + high) >> 1;
double d1 = close_pair(low, mid);
double d2 = close_pair(mid + 1, high);
d = min(d1, d2);
int k = 1;
for (int i = low; i <= high; i++){
if (abso(pre[i].x - pre[mid].x) <= d)
f[k++] = pre[i];
}
sort(f + 1, f + k, cmp_y);
for (int i = 1; i < k; i++){
for (int j = i + 1; j < k && (f[j].y - f[i].y) < d; j++){
d = min(d, dis(f[i], f[j]));
}
}
return d;
}
- 变体:POJ 3 B
#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
const int maxn = 2000 + 5;//每组点的个数
const long long inf = 0x3f3f3f3f;//表示无穷大的数
//点的结构体
struct node{
double x, y;//点的坐标
int pt;//点的组号,只有有1和2两种标记
}pre[maxn], f[maxn];//读入的点都放在pre数组中
bool cmp_x(node a, node b);
bool cmp_y(node a, node b);
double close_pair(int low, int high);
int main()
{
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int t; cin >> t;//number of test case
while (t--){
//读入数据
int n; cin >> n;//number of pair of nodes
for (int i = 1; i <= n; i++){//first pair
cin >> pre[i].x >> pre[i].y;
pre[i].pt = 1;
}
for (int i = n + 1; i <= 2 * n; i++){//second pair
cin >> pre[i].x >> pre[i].y;
pre[i].pt = 2;
}
//处理
sort(pre + 1, pre + 1 + 2 * n, cmp_x);//对所有点根据x坐标排序
//输出
printf("%.3f\n", close_pair(1, 2 * n));//%f 是输出bai float 型变量du;%f 是输出 double 型变量;%Lf 是输出 long double 型变量;long long 为lld
}
return 0;
}
bool cmp_x(node a, node b){//根据点的x坐标从大到小排序,若x坐标相同根据y坐标从大到小排序
return a.x < b.x;
}
bool cmp_y(node a, node b){
return a.y < b.y;
}
double abso(double x){//绝对值
return x < 0 ? -x : x;
}
double dis(node a, node b){//两个点的距离公式
return sqrt(pow(a.x - b.x, 2) + pow(a.y - b.y, 2));
}
//计算距离最近的两个点
double close_pair(int low, int high){
double d = 1.0*inf;//d初始为无穷大
//递归出口:有1个点(d为无穷大)OR 有2个点(若为同组,d为无穷大;若为异组,d为两点距离计算公式结果)
if (low == high)
return d;
if (low + 1 == high){
if (pre[low].pt == pre[high].pt)
return d;
else
return dis(pre[low], pre[high]);
}
//分治法:
//分:在中间画一条线,左右分别找两边最近的两个点,距离为d1、d2
//治:取d=min(d1,d2),遍历所有点找与中间线(点)距离<=d的点,存入f[]中,对f[]中的点y坐标排序
//合:检测排序11个位置内的点。检测到的距离d3,d=min(d,d3)
int mid = (low + high) >> 1;
double d1 = close_pair(low, mid);
double d2 = close_pair(mid + 1, high);
d = min(d1, d2);
int k = 1;
for (int i = low; i <= high; i++){
if (abso(pre[i].x - pre[mid].x) <= d)
f[k++] = pre[i];
}
sort(f + 1, f + k, cmp_y);
for (int i = 1; i < k; i++){
for (int j = i + 1; j < k && (f[j].y - f[i].y) < d; j++){
if (f[i].pt != f[j].pt){
d = min(d, dis(f[i], f[j]));
}
}
}
return d;
}
总结:定义绝对值函数时,起名为abs会报错(改成了abso)。所以函数名太短,有可能与库函数重名。
四、randomized quicksort随机快速排序
- 思路:选一个Pivot, p,小于p的放在p左边,大的右边。再递归的处理左右两个list。
- 时间复杂度:O(n log n)。(pf.每个元素只与它的祖先和孩子比较(二叉树中))
- 实现:
void quickSort(int *arr,int low,int high){
if(low>=high)//递归出口
return;
//1.选基准
int p=arr[low],i=low,j=high;
//2.左右移,中间放
while(i<j){
while(i<j&&arr[j]>p)
j--;
arr[i]=arr[j];
while(i<j&&arr[i]<p)
i++;
arr[j]=arr[i];
}
arr[i]=p;
//3.左右递归排序
quickSort(arr,low,i-1);
quickSort(arr,i+1,high);
return;
}
五、median and selection选择中位数
- 问题描述:在有n个元素的list中,找到第k个最小的数。
- 比较得出最大、最小值使用 O(n)
方法1、
- 通过排序比较 O(nlogn)
方法2、
- 用堆取第k个最小的数
- 时间复杂度 :O(nlogk)
建堆O(klogk),对于k个元素的堆,每次调整O(logk),比较(n-k)次,总=O(klogk+(n-k)*logk)=O(nlogk) - 思路:取这组数据中的k个元素,建立一个大顶堆。再将剩下的元素与堆顶元素比较,若大于堆顶元素,继续取剩下的元素的下一个元素循环;若小于堆顶元素,则将堆顶元素与此元素交换,调整堆,继续取剩下的元素的下一个元素循环。最终,堆顶是第k个最小的数,整个堆是最小的k个数。
参考 - 实现:
#include <iostream>
#include <cstring>
using namespace std;
void heaptify(int *arr, int i, int n);
void buildHeap(int *arr, int n);
void swap(int *arr, int i, int j);
int findMinKth(int *arr, int n, int k);
void heapSort(int *arr, int n);
int main(){//test case:找a[]中,第4小元素
int a[10] = { 10, 16, 18, 12, 11, 13, 15, 17, 14, 19 };
cout << findMinKth(a, 10, 4) << endl;
//for (int i = 0; i < 10; i++)
//cout << a[i] << " ";
return 0;
}
void heapSort(int *arr, int n){
buildHeap(arr, n);
for (int i = n - 1; i >= 0; i--){
swap(arr, 0, i);//将堆顶元素与最后一个元素交换
heaptify(arr, 0, i);//从堆顶开始调整有i个元素的堆
}
}
int findMinKth(int *arr, int n, int k){
buildHeap(arr, k);
for (int i = k; i < n; i++){
if (arr[i] < arr[0]){
swap(arr, i, 0);
heaptify(arr, 0, k);
}
}
return arr[0];
}
void buildHeap(int *arr, int n){
int last_node = n - 1;
int parent = (last_node - 1) >> 1;
for (int i = parent; i >= 0; i--)
heaptify(arr, i, n);
}
void heaptify(int *arr, int i, int n){
if (i >= n)
return;
int lchild = i * 2 + 1, rchild = i * 2 + 2;
int max = i;
if (lchild<n && arr[max] < arr[lchild])
max = lchild;
if (rchild<n && arr[max] < arr[rchild])
max = rchild;
if (max != i){
swap(arr, max, i);
heaptify(arr, max, n);
}
return;
}
void swap(int *arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
- 总结:以O(nlogk)找第k小/前k个最小的数,建k个元素的大顶堆;找第k大/前k个最大的数,建k个元素的小顶堆。
方法3、
- Quickselect
- 时间复杂度 O(n)
- 思路:选择一个pivot p(at random),比p小的全部放p左边,大的右边,相等的放中间,产生了三个list:L、M、R。在包含k th smallest element的subarray中再递归:若k<=L.size(),k在L中,在L中递归找第k小的元素;若k>L.size() && k<=L.size()+M.size() ,p就是第k小的数;若k>L.size()+M.size(),k在R中,递归找R中第 k-L.size()-M.size()小的数。
- 目标:选择pivot,保证分割出的每个list,<7/10n个元素。
方法:循环计算<2/10n个元素的中位数。把所有元素,分成5个元素一组(一共n/5组),找每组的中位数,找中位数(n/5个元素中的)中位数,用它作为pivot。
momSelection(arr,k)
b为每组中位数
p=momSelection(b,n/10) - 实现:参考