分治算法

  • 大纲:
    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;
}
#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)
  • 实现:参考
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值