复习:排序
这节东西有点小多,我打算分几次更完。
排序算法很多,选择排序插入排序冒泡排序堆排序归并排序快速排序等等
所以我准备先展示几个模板代码然后再通过其他题展示排序算法的应用
接下来是三个复杂度为n^2的排序算法
冒泡排序
不断进行相邻数的前后调换最终达到排序的目的,核心代码如下
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n-i;j++)
{
if(a[j]>a[j+1]) swap(a[j],a[j+1]);
}
}
选择排序
每次从未排序的数中选出最大值或最小值放到序列的末尾。这里以升序为例,核心代码如下
for(int i=1;i<=n;i++)
{
int maxx=1;//默认最值为a[1],maxx记录的是下标
for(int j=2;j<=n-i+1;j++)
{
if(a[j]>a[maxx]) maxx=j;
}
swap(a[maxx],a[n-i+1]);
}
插入排序
将没有排序过的数插入到已经排序了的数里面,找到位置后break即可
for(int i=1;i<n;i++)
{
for(int j=i+1;j>=1;j--)
{
if(a[j-1]>a[j]) swap(a[j-1],a[j]);
else break;
}
}
这三个应该是最基础的排序算法了,不过我之前这里面是只会冒泡的,哎呀毕竟有sort也基本用不上......不过到了现在终究还是补上了
快速排序
思想和二分有点类似,。简单来说就是每次从数列中随机取出来一个数x,然后对所有数分为大于x等于x小于x三类。接着再分别对大于x和小于x两类的数列进行相同的操作。直到最终数列只剩一个数为止
至于实现方法,就是开四个数组,一个原数组,三个数组存三类数。每次分类时将数存进三个数组里。在每次遍历完数组后再用三个数组里的数更新原数组,平均而言复杂度是nlogn
这里我卡了挺久最后发现是随机数取错了,唉,铸币。
#include<bits/stdc++.h>
using namespace std;
int n,a[100005],b[100005],c[100005],d[100005];
int randx(int l,int r)
{
return a[rand()%(r-l+1)+l];
}
void qsort(int l,int r)
{
//为了防止a数组在中途就改变,先改变其他数组
//全改变完后再赋值给A
if(l>=r) return;
int x=randx(l,r);
int cntb=0,cntc=0,cntd=0;
//cout<<x<<' ';
for(int i=l;i<=r;i++)
{
if(a[i]<x) b[++cntb]=a[i];
else if(a[i]==x) c[++cntc]=a[i];
else if(a[i]>x) d[++cntd]=a[i];
}
// cout<<x<<endl;
int cnb=0,cnc=0,cnd=0;
for(int i=l;i<=r;i++)
{
if(cnb<cntb) cnb++,a[i]=b[cnb];
else if(cnc<cntc) cnc++,a[i]=c[cnc];
else if(cnd<cntd) cnd++,a[i]=d[cnd];
}
//cout<<l<<' '<<r<<' '<<cnb<<' '<<cnc<<' '<<cnd<<endl;
qsort(l,l+cntb-1);
qsort(l+cnc+cnb,r);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
qsort(1,n);
for(int i=1;i<=n;i++)
{
printf("%d ",a[i]);
}
return 0;
}
然后又看到了一种更简单的算法,不用每次遍历两边区间。
不再取随机数,而是取区间中间的数为x,设置初始下标分别为区间头和区间尾的i和j,顺着找到第一个大于等于x的ai和逆着找到第一个小于等于x的aj并将其交换位置。最后呈现的效果就是将数列分为大于等于x和小于等于x的两部分。而不是上面算法的三部分。
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;
int i = l - 1, j = r + 1, x = q[l + 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);
}
此处的do while是为了保证每次循环都会前进。否则如果i和j处都为x时就会陷入永无止境的循环。
归并排序
归并排序,简单来说就是分治思想,先把整个序列划分至许多长度为2的序列(个数为奇数则会落单一个)后进行排序,然后再每两个序列合为一个序列进行排序,以此类推最终得到一整个排序后的序列。也就是说每次排序都是对两个子序列进行合并。并且这两个子序列都是已经排好序的
具体实现方法就是先找好两个序列的分界点,当然这个我们在一开始划分的时候就已经算好了。然后分别用i和j从两个序列头开始循环,比较两个数大小,放入较小的一方然后下标+1。循环结束条件是某个子序列全部加入新序列。此时已经加入的序列已经排好序了,还剩下一个子序列没有加完,只需要将这部分子序列按顺序加进去即可(因为子序列都是排好序的,且剩下的没加进去说明剩下的数大于新序列中已有的任何数)。
具体代码如下
void msort(int l,int r)
{
if(l>=r) return;
int mid=l+r>>1;
msort(l,mid);
msort(mid+1,r);
int i=l,j=mid+1,sum=0;
while(i<=mid&&j<=r)
{
if(a[i]<a[j]) f[++sum]=a[i++];
else f[++sum]=a[j++];
}
while(i<=mid) f[++sum]=a[i++];
while(j<=r) f[++sum]=a[j++];
for(i=l,j=0;i<=r;i++)
{
a[i]=f[++j];
}
}
本来还想写个堆排序的,不过由于懒了,先鸽着
应用
离散化
CF670C Cinema
#include<bits/stdc++.h>
using namespace std;
map<int,int> film;//存储电影语音, 使用该语音的个数
int f[200005],g[200005],cnt=0;//f储存每个电影的配音
//g储存每个电影的字幕语言
int main()
{
int n,m;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
int a;
scanf("%d",&a);
film[a]++;//记录下每种语言会的人数
}
scanf("%d",&m);
int anss;
int maxn=-1;//maxn为懂配音语言的最大个数
for(int i=1;i<=m;i++)
{
scanf("%d",&f[i]);//配音用的语言
if(film[f[i]]>maxn) maxn=film[f[i]];
}
//maxn此时为配音用的语言会的人数
int maxxn=-1;
for(int i=1;i<=m;i++)
{
scanf("%d",&g[i]);
if(film[f[i]]==maxn)//如果配音语言正确
{
if(film[g[i]]>maxxn) maxxn=film[g[i]],anss=i;
//找出最多人会的字幕语言
}
}
printf("%d",anss);
return 0;
}
本题应该是离散化+排序的,但由于有方便好用的map,以及只需要能听懂语音和看懂字幕的最大值,因此不用排序只需用变量储存最大值即可
求区间和
做这玩意狠狠让我吃了一把我之前没好好练离散化的苦,或者説根本没练
基本思想比较简单,就是将x,l,r离散化,然后预处理一下前缀和,对于每次询问的l和r,找出他们两个离散化后的位置,直接计算出答案即可。
但实现的时候碰了不少壁,我来说一下
首先是数据的储存,x和c肯定要绑定储存所以用pair或者结构体,我用的结构体。同时l和r也要绑定储存所以也需要再开一个。也就是说对于插入和查询我们要开两个数组来储存信息。我用的是vector数组。并且由于位置x,l,和r都需要离散化,因此额外开一个数组来将它们都塞进去。
for(int i=1;i<=n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add.push_back({x,y});
al.push_back(x);
}
for(int i=1;i<=m;i++)
{
int l,r;
scanf("%d%d",&l,&r);
que.push_back({l,r});
al.push_back(l);
al.push_back(r);
}
储存完数据后进行排序并去重,这样做的用处有1.将插入的数据的相对位置排好,便于计算前缀和 2.成为有序序列后可以进行二分查找
然后处理插入的数字,对于每个x都二分查找它在大序列的位置,开一个数组记录他们的值。
插入完成后对于整个大序列进行前缀和操作,虽然里面有无关的l和r但计算前缀和时它们的位置上值都是0所以对前缀和无影响。
最后进行查询操作,对于每个l和r都找出它们在序列中的位置并用前缀和计算出区间和。在查找数x的位置时,我们找的是第一个大于等于x的数的位置。题目给的l和r不一定在插入时出现过,如果均不出现,那查找出的l-1和r的位置必定大于真正的l-1和r的位置,但这些差异的位置上数值均为0,进行前缀和仍然是正确的。
//后续纠错:事实上是不存在不出现l和r的情况的,因为查询的l和r是加入数组进行去重离散化的
完整代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int n,m;
struct num
{
int fir;
int sec;
};
vector<num> add,que;
vector<int> al;
const int maxn=300010;
int sum[maxn],a[maxn];
int find(int x)
{
int l=0,r=al.size()-1;
while(l<r)
{
int mid=l+r>>1;
if(al[mid]>=x) r=mid;
else l=mid+1;
}
return l+1;//l+1是由于前缀和要用到i-1操作,为防止re,初始下标设置为1
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add.push_back({x,y});
al.push_back(x);
}
for(int i=1;i<=m;i++)
{
int l,r;
scanf("%d%d",&l,&r);
que.push_back({l,r});
al.push_back(l);
al.push_back(r);
}
sort(al.begin(),al.end());
al.erase((unique(al.begin(),al.end())),al.end());
for(int i=0;i<add.size();i++)
{
int pos=find(add[i].fir);
a[pos]+=add[i].sec;
}
for(int i=1;i<=al.size();i++)
{
sum[i]=sum[i-1]+a[i];
}
for(int i=0;i<m;i++)
{
int l=find(que[i].fir),r=find(que[i].sec);
printf("%d\n",sum[r]-sum[l-1]);
}
return 0;
}
中位数
洛谷p2803 学校选址
这个是算阶上的题的进化版,不过可以说已经和排序没啥关系了。先说一下算阶上的原题:在一条数轴上有n家商店,他们的坐标分别为a1到an,每天清晨从货仓到每家商店都要运送一车商品。为了提高效率。求把货仓建到何处可以使得货仓到每家商店的距离之和最小。
我们不难想到,当货仓处于两个商店之间时,无论处在哪个位置,到两个商店的距离之和不变且为最小值,值为两个商店的距离。我们把所有商店从外到内两两分组,显然当货仓位置选在最内一组中间的时候总距离最短。如果商店总数是奇数则是处于该商店位置。这样一来就明朗了,我们只需要将位置排序后找到最中间那个数,即中位数即可。
然后回到本题,本题是在楼之间建立学校,不过多了几个条件,便是一共可以建k所学校以及每栋楼都有一定的学生数。我们先讨论在只建立一座学校的情况下的最优解。我们可以看到这时候和原来题目的区别便是每个点都带有权值。这时,对于两个点来说,将学校建在何处能使得总距离最小?设两个点位置为i和j,且他们的权值分别为a和b,学校建立在x处。首先肯定是建立在两点之内,因为当建立在两点之外时显然距离比在两点内要长。然后在两点内时,若a>b且学校位于两点中间不位于点上,总距离为a(x-i)+b(j-x),处于a点时则为b*(j-i),两者相差(x-i)(a-b),显然>0,因此处于a点时绝对优于处于中间,b>a时同理。当有多个点存在时也类似,总之就是最后一定是处于某个点上而不是中间。
那么当k=1时,我们要把学校建在哪个点上?
我们可以把只有两个点的情况推广到多个点,而将其分为左右两部分。众所周知当两个点时哪边权值较高就建在哪个点上,那么分为左右两部分时也是哪边人数较多便在哪一边
至于为什么呢,我们假设学校建在两点中间,并且左边人数多于右边。当学校在这两点中间移动时,比如向右移动了l,总距离增加了(左边人数×l-右边人数×l),那么显然就该再往左建。但当左边人数少于右边时,当往右移动,总距离依然增加(左边人数×l-右边人数×l),但此时这个数是负数,也就是总距离减少。所以应该建在左边恰好大于右边的楼栋处。
那么剩下的就简单了,就是找到刚好左边大于右边的那个点,建在那个点上的距离便为答案。
然后处理建立k所学校的情况。
我们首先将刚才说的处理单所学校的情况处理好,对于n栋楼房,用数组best[i] [j]表示当区间为[i,j]时只建一个学校的最优解,方法就用我上面说到的方法
设dp[i] [k]为在i点之前建立了k个分区的最优解。则要得到k个分区就是在k-1个分区的基础上加一个分区。因此我们需要在i前划定一个点,那个点及其之后的直到i为止的区域均为一个新区域,而之前的区域则为k-1个区域,设这个区域的起始位置是x。
那么就可以得到dp[i] [k]=dp[x-1] [k-1]+best[x] [i]
然后对于每个区域[i,j],枚举断点位置x来算出多个答案并取最小值即可得到该区域在分k个区情况下的最优解
#include<bits/stdc++.h>
using namespace std;
int len[105],peo[105];
int best[105][105],dp[105][105];
int dis(int a,int b)
{
if(a<b) swap(a,b);
return len[a]-len[b];
}
int main()
{
int n,k,x;
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
scanf("%d",&x);
peo[i]=peo[i-1]+x;
}
for(int i=2;i<=n;i++)
{
scanf("%d",&x);
len[i]=len[i-1]+x;
}
//首先求出在(x,y)中只有一个学校的最优解
for(int j=1;j<=n;j++)//右边界
{
for(int i=1;i<=j;i++)//左边界
{
for(int mid=1;mid<=j;mid++)
{
if(mid>j) break;
if(peo[mid]-peo[i-1]>=peo[j]-peo[mid])
{
//最优解为mid处
int sum=0;
for(int ch=i;ch<=j;ch++)
sum+=(peo[ch]-peo[ch-1])*dis(ch,mid);
best[i][j]=sum;
//printf("%d %d %d\n",i,j,best[i][j]);
break;
}
}
}
}
//dp[i][k]=dp[x-1][k-1]+best[x][i]
for(int i=1;i<=n;i++)//终点
{
for(int j=1;j<=k;j++)
{
if(j>=i) break;
if(j==1) {dp[i][j]=best[1][i];continue;}
int minn=0x3f3f3f3f;
for(int x=1;x<=i;x++)
{
minn=min(minn,dp[x-1][j-1]+best[x][i]);
}
dp[i][j]=minn;
}
}
printf("%d",dp[n][k]);
return 0;
}
话说把这题写到排序的博客里真的好吗。。。。。。
第k大数
求一个序列中的第k大数,sort后直接选的复杂度是O(nlogn),采用快速排序思想,快排中每次都会从当前范围中选出一个数作为基准,比它大的放在右边,小的放在左边。要求第k大数时,首先在整个序列内统计大于基准值x的数量为c,若c大于k-1则说明基准值比第k大数要小,因此直接去比x大的区间寻找即可,若小于k-1则去比k小的区间,等于k-1则说明x就是第k大数。
我是用多个数组实现的,用比较简洁的那个快排老是出问题,感觉多数组实现的也比较直观,不容易写错
#include<bits/stdc++.h>
using namespace std;
int n,a[100005],b[100005],c[100005],d[100005],k,boo[100005],len=0;
int randx(int l,int r)
{
return a[rand()%(r-l+1)+l];
}
void qsort(int l,int r,int rank)
{
if(l>=r) return;
int x=randx(l,r);
int cntb=0,cntc=0,cntd=0;
for(int i=l;i<=r;i++)
{
if(a[i]<x) b[++cntb]=a[i];
else if(a[i]==x) c[++cntc]=a[i];
else if(a[i]>x) d[++cntd]=a[i];
}
int cnb=0,cnc=0,cnd=0;
for(int i=l;i<=r;i++)
{
if(cnb<cntb) cnb++,a[i]=b[cnb];
else if(cnc<cntc) cnc++,a[i]=c[cnc];
else if(cnd<cntd) cnd++,a[i]=d[cnd];
}
if(cntb==rank-1) {cout<<x;return;}
if(cntb<rank-1) qsort(l+cntb+cntc,r,rank-cntb-cntc);
if(cntb>rank-1) qsort(l,l+cntb-1,rank);
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
if(!boo[x]) a[++len]=x;
boo[x]=1;
}
if(k>len)
{
cout<<"NO RESULT";
return 0;
}
qsort(1,len,k);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人