算法课7-Divide and Conquer

分治的思想,最经典的两个例子首先是排序的两个例子
一个是快排 一个是归并排序【请看到这里的你,去复习一下排序算法】

快速排序
先总体再局部

int partition(int nums[],int left,int right){
    int i=left;
    int j=right;
    int temp=nums[i];
    while(i<j){//注意下面内层while的顺序
        while(i<j && nums[j]>=temp) {j--;}
        nums[i]=nums[j];
        while(i<j && nums[i]<=temp) {i++;}
        nums[j]=nums[i];
    }
    nums[i]=temp;
    return i;
}
void quickSort(int nums[],int left,int right){
    if(left<right) {
        int p = partition(nums, left, right);
        quickSort(nums, left, p - 1);
        quickSort(nums, p + 1, right);
    }
}


/**使用栈的非递归快速排序**/
template<typename Comparable>
void quicksort2(vector<Comparable> &vec,int low,int high){
    stack<int> st;
    if(low<high){
        int mid=partition(vec,low,high);
        if(low<mid-1){
            st.push(low);
            st.push(mid-1);
        }
        if(mid+1<high){
            st.push(mid+1);
            st.push(high);
        }
        //其实就是用栈保存每一个待排序子串的首尾元素下标,下一次while循环时取出这个范围,对这段子序列进行partition操作
        while(!st.empty()){
            int q=st.top();
            st.pop();
            int p=st.top();
            st.pop();
            mid=partition(vec,p,q);
            if(p<mid-1){
                st.push(p);
                st.push(mid-1);
            }
            if(mid+1<q){
                st.push(mid+1);
                st.push(q);
            }       
        }
    }
}

作业例题 【使用归并排序】
先局部再总体,需要一个临时数组。

F:求逆序对数
总时间限制: 500ms 内存限制: 65536kB

描述
对于一个长度为N的整数序列A,满足i < j 且 Ai > Aj.的数对(i,j)称为整数序列A的一个逆序
请求出整数序列A的所有逆序对个数

输入
输入包含多组测试数据,每组测试数据有两行
第一行为整数N(1 <= N <= 20000),当输入0时结束
第二行为N个整数,表示长为N的整数序列
输出
每组数据对应一行,输出逆序对的个数
样例输入

5
1 2 3 4 5
5
5 4 3 2 1
1
1
0

样例输出

0
10
0


#include
#include
#include
#include
#include
#include
#include 
#include 
using namespace std;

int n;
int num[20005];
int temp[20005];

int res;


void Merge(int low,int mid,int high){
    int i=low;
    int j=mid+1;
    int tmp=low;
    while (i<=mid && j<=high){
        if(num[j]<num[i])
        {
        	//如果aj
            res+=mid-i+1;            
            temp[tmp++]=num[j++];
        }
        else{
            temp[tmp++]=num[i++];
        }
    }
    while (i<=mid) temp[tmp++]=num[i++];
    while (j<=high) temp[tmp++]=num[j++];

    for(int k=low;k<=high;k++){
        num[k]=temp[k];
    }


}

void mergesort(int low,int high){
    if(low<high){
        int mid=(low+high)/2;
        mergesort(low,mid);
        mergesort(mid+1,high);
        Merge(low,mid,high);
    }

}
int main(){
    while(cin>>n && n){
        for(int i=0;i<n;i++){
            cin>>num[i];
        }
        res=0;
        mergesort(0,n-1);
        cout<<res<<endl;

    }
    return 0;
}

拓展
重要逆序对
总时间限制: 10000ms 单个测试点时间限制: 1000ms
内存限制: 65536kB

描述

给定N个数的序列a1,a2,…aN,定义一个数对(ai, aj)为“重要逆序对”的充要条件为 i < j 且 ai > 2aj。求给定序列中“重要逆序对”的个数。

输入

第一行为序列中数字的个数N(1 ≤ N ≤ 200000)。
第二行为序列a1, a2 … aN(0 ≤a ≤ 10000000),由空格分开。

输出

输出一个整数,为给序列中“重要逆序对”的个数。

样例输入

10
0 9 8 7 6 5 4 3 2 1

样例输出

16

提示
如果使用printf输出long long类型,请用%lld数据范围
对于40%的数据,有N ≤ 1000。


同样的使用归并排序,在归并时先用一个指针扫一次,左边如果有满足条件的,那么它的后面的数字都满足条件。与上题类似,单看merge。
(注意这题的输出是long long类型)

void Merge(int low,int mid,int high){
    int i=low;
    int j=mid+1;
    int tmp=low;
    int pointer=low;
    while (i<=mid && j<=high){
        if(num[j]<num[i])
        {
            while(pointer<=mid && 2*num[j]>=num[pointer]){
                pointer++;
            }
            res+=mid+1-pointer;
//            res+=mid-i+1;
            temp[tmp++]=num[j++];


        }
        else{
            temp[tmp++]=num[i++];
        }
    }
    while (i<=mid) temp[tmp++]=num[i++];
    while (j<=high) temp[tmp++]=num[j++];

    for(int k=low;k<=high;k++){
        num[k]=temp[k];
    }
}

进阶版LeetCode 315. Count of Smaller Number After Self
求每个位置元素对应的逆序对,仍可以用归并排序。
Java merge sort solution
为了保持原数组不动,需要维护一个index数组
注意每次从right中移数,rightcount++;从left中移数,把count数组加上rightcount
如[2,3,4] [1,2,3] 左边是2的时候,rightcount=1,把2移入,对应2的count加上1。


平面最近点对
步骤1:根据点的y值和x值对S中的点排序。
步骤2:找出中线L将S划分为SL和SR
步骤3:将步骤2递归的应用解决SL和SR的最近点对问题,并令d=min(dL,dR)。
步骤4:将L-d ~ L+d内的点以y值排序,对于每一个点(x1,y1)找出y值在y1-d~y1+d内的接下来的7个点,计算距离为d’。如果d’小于d,令d=d’,最后的d值就是答案。
平面最近点对实验

作业题

输入
第一行一个整数T,表示有T组测试数据
每组测试数据第一行一个整数N(2<=N<=1e5)表示平面有N个点
接下来有N行,每行两个整数X Y(-1e9<=X,Y <=1e9)表示点的坐标
输出
输出最近点对的距离,精确到小数点后6位
样例输入

1
3
1 0
1 1
0 1

样例输出

1.000000

#include 
#include 
#include 
#include 
#define INF 1e20
using namespace std;


int t;
int n;
struct point{
    double x;
    double y;
};
point points[100005];
point temp[100005];

double getDistance(point point1,point point2){
    return sqrt((point1.x-point2.x)*(point1.x-point2.x)+(point1.y-point2.y)*(point1.y-point2.y));
}
bool cmpxy(point a,point b)
{
    if(a.x==b.x)
        return a.y<b.y;
    return a.x<b.x;
}
bool cmpy(point a,point b){
    return a.y<b.y;
}

double getClosestPoint(int left,int right){
    double dis=INF;
    if(left==right)
        return dis;
    if(left+1==right)
        return getDistance(points[left],points[right]);
    int mid=(left+right)/2;
    double disLeft=getClosestPoint(left,mid);
    double disRight=getClosestPoint(mid+1,right);
    dis=min(disLeft,disRight);
    int k=0;
    for(int i=left;i<=right;i++){
        if(fabs(points[mid].x-points[i].x)<=dis){
            temp[k++]=points[i]; //存储中间区域的点
        }
    }

    sort(temp,temp+k,cmpy);//按照y升序排列
    for(int i=0;i<k;i++){
        for(int j=i+1;j<k;j++){
            double d=getDistance(temp[i],temp[j]);
            if(dis>d){
                dis=d;
            }
        }
    }
    return dis;

}

int main(){
    cin>>t;
    while(t--){
        cin>>n;
        for(int i=0;i<n;i++){
            int x,y;
            cin>>x>>y;
            points[i].x=x;
            points[i].y=y;
        }
        sort(points,points+n,cmpxy);
        //保留6位小数
        cout<<fixed<<setprecision(6)<<getClosestPoint(0,n-1)<<endl;
    }
    return 0;
}

随机快排
随机选择pivot,可以避免排好序或者全逆序的worst case
三向切分快速排序,可以避免过多的重复元素带来的效率问题
维护两个指针,lo~lt小于pivot gt ~ hi大于pivot 中间等于pivot
算法课7-Divide and Conquer_第1张图片

void qsort3way ( int lo,int hi )
{
    if( lo>=hi ) return;  //单个元素或者没有元素的情况
    int lt=lo;
    int i=lo+1;  //第一个元素是切分元素,所以指针i可以从lo+1开始
    int gt=hi;
    int v=a[lo];
    while( i<=gt )
    {
        if( a[i]<v )  //小于切分元素的放在lt左边,因此指针lt和指针i整体右移
            swap( lt++,i++ );  
        else if ( a[i]>v )  //大于切分元素的放在gt右边,因此指针gt需要左移
            swap( i,gt-- );//这里不需要i++
        else
            i++;
    }
    //lt-gt的元素已经排定,只需对it左边和gt右边的元素进行递归求解
    qsort3way( lo,lt-1 );    
    qsort3way( gt+1,hi );
}

对于包含大量重复元素的数组,这个算法将排序时间从O(NlogN)降到了O(N)。


median and selection problem
选取中位数(第k大的数)
Quick-select算法
T(n)=T(n/2)+ θ ( n ) \theta(n) θ(n)

int partition(int nums[],int left,int right){
    int i=left;
    int j=right;
    int temp=nums[i];
    while(i<j){//注意下面内层while的顺序
        while(i<j && nums[j]>=temp) {j--;}
        nums[i]=nums[j];
        while(i<j && nums[i]<=temp) {i++;}
        nums[j]=nums[i];
    }
    nums[i]=temp;
    return i;
}
int quickSelect(int nums[],int left,int right,int k){
   
        int p = partition(nums, left, right);
        if(p==k-1)  return nums[p];
        else if(p<k-1){
         return quickSelect(nums, p + 1, right);      
        }
        else if(p>k-1){
         return quickSelect(nums, left, p - 1);
        }                        
}


LeetCode 类似题
可以用quickselect,也可以用堆
kth-largest-element in an array


书上例题以及拓展

  1. 找peak find peak element
    选取一个切分点mid,这个点三种情况
    ①右边比它小,则向左边找②右边比它大,则向左边找③直到两个指针碰撞,则输出。 二分查找的思想。

  2. 排过序的旋转数组的最小值 find minimum in Rotated Sorted array
    相当于找到那个断崖
    选取一个切分点,和最后一个元素比,如果比最后一个元素大到左边,如果比最后一个元素小到右边
    终止条件是元素个数和右边大于左边

  3. 股票买一次收益最大 best time to buy and sell stock
    动态规划可以是线性时间复杂度的,使用分治法,可以达到nlogn

public int helper(int[]prices,int low,int high){
        int max=Integer.MIN_VALUE;
        if(low==high) return 0;
        int mid=(low+high)/2;
        int leftMax=helper(prices,low,mid);
        int rightMax=helper(prices,mid+1,high);
        int subMax=Math.max(leftMax,rightMax);
        int leftMIN=Integer.MAX_VALUE;
        for(int i=low;i<=mid;i++){
            leftMIN=Math.min(leftMIN,prices[i]);
        }
        int rightMAX=Integer.MIN_VALUE;
        for(int i=mid+1;i<=high;i++){
            rightMAX=Math.max(rightMAX,prices[i]);
        }
        return Math.max(Math.max(0,rightMAX-leftMIN),subMax);
    }
  1. 与上题类似的连续子数组序列最大和,同样可以用分治法
public int helper(int[]nums,int left,int right){
        if(left==right) return nums[left];
        int mid=(left+right)/2;
        int leftmax=helper(nums,left,mid);
        int rightmax=helper(nums,mid+1,right);
        int crossmax=Integer.MIN_VALUE;
        int temp=0;
        for(int i=mid;i>=left;i--){
            temp+=nums[i];
            crossmax=Math.max(crossmax,temp);
        }
        temp=crossmax;
        for(int i=mid+1;i<=right;i++){ //不能再忘记等于号了==
            temp+=nums[i];
            crossmax=Math.max(crossmax,temp);
        }
        return Math.max(Math.max(leftmax,rightmax),crossmax);
    }

LeetCode 例题
Merge k Sorted Lists
也可以用优先队列做,和上面那个leetcode有点类似。

你可能感兴趣的:(LeetCode,数据结构)