一、算法讲解
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、确定分界点,从中间位置分开
2、递归处理好左右2个序列,形成2个有序的序列
3、归并处理,蒋2个序列合成为新序列res,i从左到右遍历第一个序列,j遍历第二个序列,每次比较,将较小的元素放到res中,然后较小元素序列的指针往后移一位,以此类推,如果元素相同,取第一个序列的元素。这一步的时间复杂度为
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、确定区间,取中间位置
2、根据性质写出check函数,这个随题目而变化
3、判断mid是否满足check函数
(1)true满足,代表mid在红色区域里,这时箭头就在我们mid的左边,我们可以将中间位置mid取的更小从而靠近箭头,将区间缩小为,取
(2)false不满足,代表mid在绿色区域中,这时箭头在我们mid的右边,我们的中间位置mid偏小了,缩小左边界为,取
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、确定区间,取中间位置 ,+1是为例防止死循环
2、根据性质写出check函数,这个随题目而变化
3、判断mid是否满足check函数
(1)true满足,代表mid在红色区域里,mid在箭头的左边,我们可以将中间位置mid变大一点,缩小左边界为,取
(2)false不满足,代表mid在绿色区域中,mid在箭头的右边,我们的中间位置mid偏大了,缩小右边界为,取
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即,就能找出小于3的最大值的位置
同理将求出来的右边界,就能找出大于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;
}