排序与二分

一、算法讲解

1.1 快速排序——分治法

1、思路

通过分治法,假设有n个数需要从小到大排序,l,r分别为当前序列的左右边界点,我们每次取中间位置(l+r)/2的值x做为依据,将序列分为2部分,左边的<=x,右边的>=x,然后分别对左右2个序列进行排序,以次类推,一直递归下去。

时间复杂度为O(nlogn),最坏情况为n^2

当然,也可以直接使用sort排序。

实现过程

1、每次确定分界点,q[l],q[r],q[(l+r)/2]

2、调整区间,用双指针的思想,i从左边界往右遍历,j从右边界往左遍历,当左边遇到>=x的元素,右边遇到<=x的元素时,将元素交换。直到i,j相遇结束。

此时i=j,j左边的所有元素都<=x,j右边的元素都>=x,即
q[l],q[l+1],q[l+2]......q[i]<=x
q[j],q[j+1],q[j+2]......q[r]>=x

3、递归处理左右两端

quick_sort(q,l,j);
quick_sort(q,j+1,r);

2、代码
#include<iostream>

using namespace std;

const int N=1e5+10;

int n,q[N];

void quick_sort(int q[],int l,int r)
{
    if(l>=r)    return ;//只有一个元素
    int i=l-1,j=r+1,x=q[(l+r)/2];//由于ij每次交换完都会向x移动一次,所以先让i=l-1,j=r+1 
    while(i<j)//从小到大排序
    {
        do  i++;while(q[i]<x);//找到x左边大于x的位置(x的左边应该小于等于x)
        do  j--;while(q[j]>x);//找到x右边小于x的位置(x的右边应该大于等于x)
        if(i<j)     swap(q[i],q[j]);//找到符合条件的两个位置,当i在j左边时交换值
    }
    quick_sort(q,l,j);//分界写j的话x不能取q[r],要不然会无限递归     没懂:分界点写i为什么不行
    quick_sort(q,j+1,r);
}

int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
        scanf("%d",&q[i]);
    quick_sort(q,0,n-1);
    for(int i=0;i<n;i++)
        printf("%d ",q[i]);
    
    return 0;
}

1.2 归并排序——分治法

1、思路

1、确定分界点,从中间位置(l+r)/2分开

2、递归处理好左右2个序列,形成2个有序的序列

3、归并处理,蒋2个序列合成为新序列res,i从左到右遍历第一个序列,j遍历第二个序列,每次比较,将较小的元素放到res中,然后较小元素序列的指针往后移一位,以此类推,如果元素相同,取第一个序列的元素。这一步的时间复杂度为O(n)

2、代码
#include<iostream>

using namespace std;

const int N=1e5+10;

int n,q[N],tem[N];
void merge_sort(int q[],int l,int r)
{
    if(l>=r)    
        return ;
    int mid=l+r>>1;//先取中间位置
    merge_sort(q,l,mid);//递归将左右两个区间排好
    merge_sort(q,mid+1,r);
    int i=l,j=mid+1,cnt=0;//i记录左边区间的位置,j记录右边区间的位置
    while(i<=mid&&j<=r)
    {
        if(q[i]<=q[j])      
            tem[cnt++]=q[i++];//记录两者中的较小值,并且记录后后移一位
        else                
            tem[cnt++]=q[j++];
    }
    while(i<=mid)//记录两个区间剩下的部分
        tem[cnt++]=q[i++];
    while(j<=r)
        tem[cnt++]=q[j++];
    for(i=l,j=0;i<=r;i++,j++)//将排好的tem数组赋给原数组
        q[i]=tem[j];
}
int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
        scanf("%d",&q[i]);
    merge_sort(q,0,n-1);
    for(int i=0;i<n;i++)
        printf("%d ",q[i]);
    return 0;
}

1.3 二分排序

二分的适用条件

1、确定上下界

2、区间具有一种单调性,根据某种性质可以将序列分为2部分,一边满足这个性质,一边不满足。

1.4 整数二分

一般整数二分会有4种情况,求满足性质的范围,或者

例如在一个序列中,求 值为3的起始位置和终止位置

3.1.1、左边界
1、思路

要求满足条件的左边界(箭头位置):

1、确定区间,取中间位置mid=(l+r)/2

2、根据性质写出check函数,这个随题目而变化

3、判断mid是否满足check函数

        (1)true满足,代表mid在红色区域里,这时箭头就在我们mid的左边,我们可以将中间位置mid取的更从而靠近箭头,将区间缩小为[l,mid],取r=mid

        (2)false不满足,代表mid在绿色区域中,这时箭头在我们mid的右边,我们的中间位置mid偏,缩小左边界为[mid+1,r],取l=mid+1

4、最终,i的值即为所求的左边界

2、代码
int l=0,r=n-1;//区间范围刚开始为整个区间 
while(l<r)//当l与r没有相遇时一直进行二分,先求左分界点,这点的后面都是大于等于x的 
{
    int mid=(l+r)>>1;//位运算>> 向右移动一位相当于除2 
    if(q[mid]>=x)//当所取的中间值大于等于x时, 
        r=mid;//缩小右边 
    else
        l=mid+1;//缩小左边 
}
3.1.2右边界
1、思路

要求满足条件的右边界(箭头位置):

1、确定区间,取中间位置mid=(l+r+1)/2 ,+1是为例防止死循环

2、根据性质写出check函数,这个随题目而变化

3、判断mid是否满足check函数

        (1)true满足,代表mid在红色区域里,mid在箭头的左边,我们可以将中间位置mid变一点,缩小左边界为[mid,r],取l=mid

        (2)false不满足,代表mid在绿色区域中,mid在箭头的右边,我们的中间位置mid偏,缩小右边界为[l,mid-1],取r=mid-1

4、最终i的值即为所求的右边界。

2、代码
int l=0,r=n-1;
while(l<r)
{
    int mid=(l+r+1)>>1;
    if(q[mid]<=x)
        l=mid;
    else
        r=mid-1;
}
3.1.3 最大值 最小值

还有一种题目例如,问小于3的最大值的位置,或者大于3的最小值的位置,

我们还是按上面的二分找出3的左右边界,然后左边界-1即l-1,就能找出小于3的最大值的位置

同理将求出来的右边界l+1,就能找出大于3的最小值的位置

1.5 浮点数二分

1、思路

与整数二分的思路差不多,由于浮点数没有明确界限,我们默认当区间长度小于题目要求的保留小数的位数+1时。可以认为是找到了我们要找的数字。

2、代码

求一个数三次方根

#include<iostream>

using namespace std;
int main()
{
    double n;
    scanf("%lf",&n);
    double eps=1e-8;//一般都是保留小数位数+1
    double l=-10000,r=10000;
    while(r-l>eps)
    {
        double mid=(l+r)/2;
        if(mid*mid*mid>=n)
            r=mid;
        else
            l=mid;
    }
    printf("%.6lf",l);
    return 0;
}

二、题目练习

2.1 排序

2.1.1 归并排序求逆序对
题目

题目链接:P1908 逆序对 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路

我们在通过分治法,使用归并排序过程中,分为左右2个序列分别递归排好序,然后合并,我们就是在合并的过程中计算逆序对数量,假设在已经递归排序好的左右序列中(左右排序过程中逆序对已经计算完成),进行合并,如果当前左序列的数较小,不是逆序,如果当前右序列的数较小,则产生的逆序对数量就是mid - i + 1(参考代码),即左序列还未合并的数,因为左序列已经合并的数都比当前值小,只有未合并的数大于当前数。

代码模板
#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

typedef long long ll;

const int N = 5e5 + 10;
typedef long long ll;

int n, q[N], tem[N];

ll merge_sort(int l, int r)
{
	if(l >= r)
		return 0;
	int mid = l + r >> 1; //取中点 
	ll sum = merge_sort(l, mid) + merge_sort(mid + 1, r);// 递归排序左右序列并计算和 
	int i = l, j = mid + 1, cnt = 0;
	while(i <= mid && j <= r)
	{
		if(q[i] <= q[j])
			tem[++cnt] = q[i++];
		else
		{
			tem[++cnt] = q[j++];
			sum += mid - i + 1;// 加上逆序对 
		}
	}
	while(i <= mid)
		tem[++cnt] = q[i++];
	while(j <= r)
		tem[++cnt] = q[j++];
	for(int i = l, j = 1; i <= r; i++, j++)
		q[i] = tem[j];
	return sum;
}

int main()
{
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)
		scanf("%d", &q[i]);
	printf("%lld", merge_sort(1, n));
	return 0;
}
2.1.2 字符串排序比较
题目

题目链接:P1012 [NOIP1998 提高组] 拼数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路

用string类型数组s[N]将其全部存下,由于我们要生成最大的整数,肯定数字大的优先,所有我们需要按位比较每个字符串,这里可以写一个cmp比较函数然后用sort直接排序即可。

代码模板
#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int N = 25;

int n;
string s[N];

bool cmp(string A, string B)//比较函数
{
	return A + B > B + A;
}

int main()
{
	cin.tie(0);
	ios::sync_with_stdio(false);
	
	cin>>n;
	for(int i = 1; i <= n; i++)
		cin>>s[i];
		
	sort(s + 1, s + n + 1, cmp);
	
	for(int i = 1; i <= n; i++)
		cout<<s[i];
		
	return 0;
}

2.2 二分练习

2.2.1 差分与二分——借教室
题目描述

题目链接:P1083 [NOIP2012 提高组] 借教室 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

思路

首先每一个订单,都需要把区间[l, r]上加上d,这里可以很容易想到用差分来实现。但是如果我们遍历订单,把每个订单区间进行差分,由于我们每次都需要判断当前是否有一天教室不够用,我们就要把差分数组求前缀和,然后遍历检查,这样的时间复杂度是O (n^2)的。

由于当我们遇到第一个不合理订单就可以停下,意味着我们其实并不用遍历依次求,我们可以把二分查找订单的位置,找到第一个不合法的订单,这样我们的时间复杂度就能降到O(nlogn)。

在二分检查的时候,还是需要依次差分1~mid订单然后求前缀和检查,时间复杂度是O(n)的。

代码模板
#include<iostream>
#include<cstring>
#include<algorithm>

#define x first
#define y second

using namespace std;

typedef long long ll;
typedef pair<int, int> PII;
typedef pair<PII, int> PIII;

const int N = 1e6 + 10;

int n, m;
int a[N], b[N];
PIII p[N];

void insert(int l, int r, int c)
{
	b[l] += c;
	b[r + 1] -= c;
}

bool check(int x)
{
	memset(b, 0, sizeof b);
	
	for(int i = 1; i <= x; i++)// 差分 
		insert(p[i].x.x, p[i].x.y, p[i].y);

	for(int i = 1; i <= n; i++)//前缀和检查 
	{
		b[i] += b[i - 1];
		if(a[i] < b[i])
			return 1;
	}
	return 0;
}

int main()
{
	scanf("%d %d", &n, &m);
	
	for(int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
	for(int i = 1; i <= m; i++)
		scanf("%d %d %d", &p[i].y, &p[i].x.x, &p[i].x.y);
	int l = 1, r = m + 1;//+1是为了返回m+1时确保是全部合法,而不是第m天不合法 
	while(l < r)//二分查找订单 
	{
		int mid = l + r >> 1;
		if(check(mid))
			r = mid;
		else
			l = mid + 1;
	}
	if(l == m + 1)
		printf("0");
	else
	{
		printf("-1\n");
		printf("%d", l);
	}
	return 0;
}

2.2.2 旅途的终点

题目描述

题目链接:G-旅途的终点_河南萌新联赛2024第(一)场:河南农业大学 (nowcoder.com)

思路

二分枚举城市数量,时间复杂度是O(logn),每次枚举mid时,我们要判断这一次是否合法。假如用w数组存下每个城市需要的代价,将w[1~mid]存到一个临时数组tem中,从小到大排序,然后计算前mid - k个城市的和sum,然后看sum与m的关系即可,时间复杂度是O(n)的。

总时间复杂度是O(nlogn)。

二分的答案应该是取右边界。

代码模板
#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

typedef long long ll;

const int N = 2e5 + 10;

ll n, m, k;
ll w[N], t[N];

bool check(int x)
{
    
    ll sum = 0;
    for(int i = 1; i <= n; i++)
        t[i] = w[i];
    sort(t + 1, t + x + 1);
    for(int i = 1; i <= x - k; i++)
    {
        sum += t[i];
        if(sum >= m)
            return 0;
    }
    return 1;
}

int main()
{
    scanf("%lld %lld %lld", &n, &m, &k);
    for(int i = 1; i <= n; i++)
        scanf("%lld", &w[i]);
    int l = 0, r = n; //二分
    while(l < r)
    {
        int mid = l + r + 1 >> 1;
        if(check(mid))
            l = mid;
        else
            r = mid - 1;
    }
    printf("%d", l);
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值