AcWing算法基础课
1 基础算法
1.1 排序
1.1.1 快排(不稳定)平均O(nlogn) 最坏O(n)
基于分治(随意分)
假设要排序的数组为q
- 确定分界点x:q[l]或q[(l+r)/2]或q[r]或随机 随机一个数组里的数值
- 调整区间:保证左边所有的数都小于等于x,右边所有的数都大于等于x(分界点处不一定是x,x在任意位置)
- 递归处理左右两段
调整区间:
(1)暴力
开两个数组a,b
遍历数组q,<=x的放入a,>x的放入b
a放入q,b放入q
(2)优美
i指向q[0],每次向右移动一位,当i>x时,i停止移动
j指向q[n-1],每次向左移动一位,当j<x时,j停止移动
当i,j均停止移动时,交换q[i],q[j]
继续移动i,j重复上述过程,直至i,j相遇
//快排模板
void quick_sort(int q[], int l, int r)
{
if(l>=r)return; //只有一个数或者没有数直接返回
int x = q[l], i = l - 1, j = r + 1;
while(i<j){
do i++; while(q[i]<x);
do j--; while(q[j]>x);
if(i<j)swap(q[i],q[j]);
}
quick_sort(q,l,j);
quick_sort(q,j+1,r);
}
/*
执行完while循环后 ……ji……
若递归时取j、j+1,上面分界点就不能选r,否则会出现边界问题,一直递归下去,造成死循环
若递归时取i-1、i,上面分界点就不能选l,否则会出现边界问题,一直递归下去,造成死循环
背一个模板就行
*/
//AcWing785快速排序
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int n;
int q[N];
void quick_sort(int q[], int l, int r)
{
if(l>=r)return;
int x = q[l],i = l-1, j = r+1;
while(i<j){
do i++; while(q[i]<x);
do j--; while(q[j]>x);
if(i<j)swap(q[i],q[j]);
qucik_sort(q,l,j);
qucik_sort(q,j+1,r);
}
}
int main()
{
scanf("%d",&n);
for(int i = 0; i<n; i++)scanf("%d",&q[i]);
qucik_sort(q,0,n-1);
for(int i = 0; i<n; i++)printf("%d",q[i]);
return 0;
}
1.1.2 归并排序(稳定)O(nlogn)
基本思想——分治(中间位置分)
- 确定分界点:mid = (l + r)/2 下标的中间值
- 递归排序left,right
- 归并——合二为一 O(n)
双指针算法
//归并排序模板
void merge_sort(int q[], int l, int r)
{
if(l>=r)return;
//确定分界点
int mid = (l+r) >> 1;
//递归
merge_sort(q,l,mid);
meege_sort(q,mid+1,r);
//归并
int k = 0,i = l, j = mid + 1; //i左半边的起点,j右半边的起点
while(i <= mid && j <= r){
if(q[i] <= q[j])temp[k++] = q[i++];
else temp[k++] = q[j++];
}
while(i <= mid)temp[k++] = q[i++];
while(j <= r)temp[k++] = q[j++];
for(int i = l, j = 0; i<r; i++, j++)q[i] = tmp[j];
}
//AcWing787归并排序
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int n;
int q[N],temp[N];
void merge_sort(int q[], int l, int r)
{
if(l>=r)return;
//确定分界点
int mid = (l+r) >> 1;
//递归
merge_sort(q,l,mid);
meege_sort(q,mid+1,r);
//归并
int k = 0,i = l, j = mid + 1; //i左半边的起点,j右半边的起点
while(i <= mid && j <= r){
if(q[i] <= q[j])temp[k++] = q[i++];
else temp[k++] = q[j++];
}
while(i <= mid)temp[k++] = q[i++];
while(j <= r)temp[k++] = q[j++];
for(int i = l, j = 0; i<r; i++, j++)q[i] = tmp[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.2 二分
1.2.1 整数二分
有单点调性一定可以二分,没有单调性也有可能可以二分,单调性不是二分的本质
本质——找到一个性质,将整个区间一分为二,一半满足该性质,一半不满足
二分可以寻找边界,可以寻找左半边的右边界,也可以寻找右半边的左边界
二分一定有解,题目可能没解
答案落在区间内
-
二分出左半边的右边界
mid = (l+r+1)/2
if(check(mid))//check函数检验mid是否满足左半边的性质
true [mid,r] l = mid
false [l,mid-1] r = mid - 1
-
二分出右半边的左边界
mid = (l+r)/2
if(check(mid))//check函数检验mid是否满足右半边的性质
true [l,mid] r = mid
false [mid+1,r] l = mid +1
如何选择上述两种
看l = mid (mid = (l+r+1)/2)
还是 r = mid (mid = (l+r)/2)
为什么要+1?
当 r = l+1 时,mid = l, true的情况更新l = mid = l相当于没更新,会出现死循环
补上+1,mid = r true的情况更新l = mid = r,新区间为[r,r],停止循环
因为要更新l = mid,所以不允许一开始mid = l,会出现死循环
//二分出左半边的右边界
//区间[l,r]被划分成[l,mid-1]和[mid,r]时使用
while(l<r){
int mid = l+r+1 >>1;
if(check(mid))l = mid;
else r = mid-1;
}
//二分出右半边的左边界
//区间[l,r]被划分成[l,mid]和[mid+1,r]时使用
while(l<r){
int mid = l+r >>1;
if(check(mid))r = mid;
else l = mid +1;
}
//AcWing789数的范围
#include<iostream>
using namespace std;
const int N = 100010;
int n,m;
int q[N];
int main()
{
scanf("%d",&n);
for(int i = 0; i<n; i++)scanf("%d", &q[i]);
while(m--){
int x;
scanf("%d",&x);
int l = 0,r = n-1;
while(l<r){
int mid = l+r >>1;
if(q[mid]>=x)r = mid;
else l = mid +1;
}
}
if(q[l] != x)cout<<"-1 -1"<<endl;
else{
cout<<l<<' ';
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;
}
cout<<l<<endl;
}
return 0;
}
1.2.2 浮点数二分
没有整除不需要处理边界问题
当r-l大于一个很小的数的时候认为找到了答案
//AcWing
//开平方
#include<iostream>
using namespace std;
int main()
{
double x;
cin>>x;
double l = 0, r = x;
while(r-l>1e-8){
double mid = (l+r)/2;
if(mid * mid >=x)r = mid;
else l = mid;
}
cout<<l<<endl;
return 0;
}
//AcWing790开三次方根