排序与贪心知识点小结
前言
其实最初的打算是将贪心排序全部一股脑地塞到基础算法小结中去,但是注意到这两块知识点的重要性,几乎可以独立存在,而且加之基础算法小结写的太长了(其实是因为我太菜了,这些东西不能被我归结到基础算法中),而为什么把两块知识点合并来写,在于这两块知识点联系紧密,后者大部分题目基于前者,所以就做了这个蠢动作。
排序
\(O(n^2)\)
选择排序
插入排序
冒泡排序
注:算法过水,不解释
\(O(nlogn)\)
快速排序
参考代码
template<class free>
il void Swap(free &a,free &b){
free t(a);a=b,b=t;
}
void fsort(int l,int r,int a[]){
int mid(a[l+r>>1]),i(l),j(r);
do{
while(a[i]<mid)++i;
while(mid<a[j])--j;
if(i<=j)Swap(a[i],a[j]),++i,--j;
}while(i<=j);
if(i<r)fsort(i,r);
if(l<j)fsort(l,j);
}
解释
对于序列\(\{a_i\}\)的区间\([l,r]\)进行排序,确定一个中间点\(a[mid]\)(一般令\(mid=l+r>>1\)),声明两个变量\(i,j\),开始\(i=l,j=r\),从区间两端向中间枚举,如果i对应的位置上的数字大于mid,j对应位置上的数字小于mid,则交换两者位置,一直到i,j相等,停止程序,然后序列就因为i,j划分成两个部分,一个部分都比mid小,一个部分都比mid大,对这两个部分进行递归同样的处理,就可以让整个序列达到有序。
归并排序
参考代码
int t[100001];
void mergesort(int l,int r,int a[]){
if(l==r)return;
int mid(l+r>>1),i(l),j(mid+1),k(l);
mergesort(l,mid,a),mergesort(j,r,a);
while(i<=mid&&j<=r)
if(a[i]<=a[j])t[k++]=a[i++];
else t[k++]=a[j++];
while(i<=mid)t[k++]=a[i++];
while(j<=r)t[k++]=a[j++];
for(i=l;i<=r;++i)a[i]=t[i];
}
解释
对于两个有序的序列我们可以使用归并(具体操作代码中有),于是二分地处理下去,即可以到达\(nlog^n\)。
桶排序
解释
桶排序的时间复杂度与数字的范围有关,它是根据每个数字出现多少进行统计,从而达到排序。
应用范围
- 数字范围小的排序
- 统计每个数字出现了多少次
排序的应用
离散化
使用范围
- 对于数字特别大的数据,但是数据的数量有很少,可以通过排序,将每个元素建立对一个较小的数字范围的映射,从而可以实现与数字大小有关的数据范围。
- 如果题目只关心元素的大小关系,而不关心元素具体的大小,可以通过离散化维护序列的单调性,常用于优化递推,比如离散化+线段树可以优化大量递推题目
- 维护一个序列的单调性的同时维护它的最值,离散化+线段树or树状数组
参考代码
struct lsh{
int a[Size],b[Size],n;
il void prepare(int size,int ar[]){n=size;
for(ri int i(1);i<=n;++i)a[i]=ar[i];sort(a+1,a+n+1);
for(ri int i(1);i<=n;++i)b[i]=dfs(ar[i]);
}
il int dfs(int x){
int l(1),r(n),mid;
while(l<=r){
mid=l+r>>1;
if(a[mid]<x)l=mid+1;
else r=mid-1;
}if(a[l]==x)return l;
return 0;
}
};
逆序对
定义
- 对于序列\(\{a_i\}\)中,如果有一对数\(a_i,a_j\)满足\(i<j,a_i>a_j\),这称\((a_i,a_j)\)为一对逆序对。
- 如果一对数是逆序对,则称它们具有逆序关系,反之,则称具有非逆序关系
性质
-
对于序列其中一个数i从它的位置中拿出来,插入到另外一个位置,只有对这个数i跨过的数与i的逆序关系会发生改变,而且原来的逆序关系变为非逆序关系,非逆序关系变为逆序关系
-
对于冒泡排序使任意一个序列变为递增序列的次数,即逆序对的对数,因为一个交换一对数后面比前面打,根据性质一,减少一堆逆序对,应用
应用
序列的一个性质,对于一些偏僻的题目,可以考虑逆序对
求逆序对数
归并排序中,假设在进行归并过程时(所有变量照参考代码),对于\(a_j<a_i\)来说,我们容易知道右边的一段是单调递增的,于是\(a_{i\sim mid}\)到要比\(a_j\)大,于是逆序对数直接累加\(mid-i+1\)即可。
注:逆序对数有爆int的风险,一定要计算是否要开long long,考场上最好全部开long long
参考代码
#define ll long long
ll lxd(int l,int r,int a[]){
if(l==r)return 0;
int mid(l+r>>1),i(l),j(mid+1),k(l);
ll ans(lxd(l,mid,a)+lxd(j,r,a));
while(i<=mid&&j<=r)
if(a[i]<=a[j])te[k++]=a[i++];
else te[k++]=a[j++],ans+=mid-i+1;
while(i<=mid)te[k++]=a[i++];
while(j<=r)te[k++]=a[j++];
for(i=l;i<=r;++i)a[i]=te[i];
return ans;
}
中位数问题
货仓寻址
给出长度为n序列\(\{a_i\}\),寻找一个数字x,\(\sum_{i=1}^n|a_i-x|\)最小,当\(x=a_{[\frac{1+n}{2}]}\)时。
\(O(n)\)求第k大数
可以在快速排序中顺便记录哪些数比中间点大,从而确定要到那个区间去寻找第k大数,另外一个区间就不必递归处理了,这样只要\(O(n+n/2+n/4+...+1)\approx O(n)\)。
\(nlog(n)\)动态维护区间第k大数
应用范围:动态维护中位数,货仓寻址的答案
做法:双堆对顶
建立两个堆,一个小根堆\(s\),大根堆\(d\),大根堆堆顶存放第k大数,当新加入的数大于d的堆顶,意味着不会影响第k大数的位置,将其加入小根堆,如果比d的堆顶小,说明现在记录的数字为第k+1大的数,将它加入小根堆s,然后弹出d的堆顶,把该数加入d。
八数码问题
\({\large n\times m}\)
-
现在有一个\(n\times m\)的网格图,网格上的数字由\(0\sim n-1\)组成,第i行第j列的数字为\(a[i][j]\),现在你可以将其中的0与其相邻的网格上的数字交换位置,给出两网格图的初始局面,询问它们是否能够到达。
-
解:
- 先将两个局面网格图拆行成列,写成一行数字,第一行放在第二行前,第二行放在第三行前...以此类推,分别把两个局面所形成的序列记做\(\{a_i\},\{b_i\}\)。
- 如果m为奇数,如果\(\{a_i\}\)的逆序对数a的奇偶性与\(\{b_i\}\)的逆序对数b的奇偶性相同(不考虑0的逆序对),那么两个局面能够到达,即\((a\&1)==(b\&1)\),简要说明一下必要性(充分性网上资料太少,唯一一篇我还看不懂),显然0的左右交换不会改变逆序对的数量,而当0上下交换,会导致一个数c跨过m-1个数插入到原来0所在的位置,显然m-1为偶数,根据逆序对的性质,容易知道只有该数c跨过的数的与c的逆序关系会发生改变,而且其中的逆序关系变为非逆序关系,非逆序关系变为逆序关系,当原来的逆序关系有奇数对时,原来的非逆序关系则会有奇数对,于是两者之差的绝对值,即逆序对数的变化数必然是一个偶数,当原来的逆序关系有偶数对时,原来的非逆序关系也会有偶数对,同理,逆序对的变化数为一个偶数。于是我们知道无论怎么变化,数列的逆序对数奇偶性不会发生改变,当两个数列逆序对数奇偶性不同时,那么必然无法到达
- 如果m为偶数,同1设两个序列的逆序对数的变量,设\(c\)为两个序列中间0所在的行位置之差的绝对值,当c为奇数,a,b奇偶性不同,则两个局面可以互相到达,当c为偶数的时候,a,b奇偶性相同,两个局面可以互相到达,简单地就是\((a\&1)==(b\&1)\wedge(c\&1)\),简要说明一下必要性(理由同1),首先当你把0左右移动,不会造成任何影响,而当你将0向上交换,跨过了m-1个数,且这个数为奇数,于是逆序对数的变化数会是一个奇数,当你将0向上交换在向下交换,奇偶性不改变,推广了讲,也就是在每一行有自己独特的逆序对数的奇偶性,因此如果c为奇数那么必然会导致逆序对奇偶性的改变奇数次,最终由奇奇的偶,奇偶得奇知道最终逆序对数的奇偶性从一个局面到达另外一个局面会发生改变。同理当c为偶数意味着,逆序对数的奇偶性要变换偶数次才能到达另外一个局面,所以最终逆序对数的奇偶性不会发生改变。
- 该解法针对于\(n,m\geq 2\),如果不在这个范围中,需要特判,但特判只是\(O(n)\)。
- 此外,本解并未证明其充分性,也就是两个局面只要满足以上条件,那么必然可以到达,但是如果考试的时候是在遇到这种情况,最好相信它是对的,猜结论,伟大的定理诞生与胡乱的猜想。
\({\large a\times b\times c}\)
-
现在有一个\(a\times b\times c(z,y,x)\)的空间网格图,网格空间上的数字中由数字\(0\sim abc-1\)组成,现在你可以将0与上下左右前后任意一个数字交换位置,给出两个初始局面,询问它们是否到达。
-
解
讲清楚太难写,说明方法,给出一张表,意会一下即可(毕竟这不是重要知识点,而且了解以上已经足够,目的在于提升素养)
(x,y,z分别为两个局面下,x,y,z坐标之差的绝对值,d,e为分别两个序列的逆序对数)
b | c | bc-1 | b-1 | 有解表达式 |
---|---|---|---|---|
奇 | 奇 | 偶 | 偶 | \((d\&1)==(e\&1)\) |
奇 | 偶 | 偶 | 偶 | \((d\&1)==(e\&1)\) |
偶 | 奇 | 偶 | 奇 | \((d\&1)==(e\&1)\wedge y\) |
偶 | 偶 | 奇 | 奇 | \((d\&1)==(e\&1)\wedge y+z\) |
贪心
基本转换思想
- 转换模型(最难的)
- 转为为区间问题
- 无序序列要排序
- 分组问题考虑一个元素放那些组,也可以逆向思维考虑一个组放那些元素
- 由简单到困难(通常对于2个研究,再对3个研究,最后推广)
基本思路
基本证明办法
- 微扰(证明最优解交换顺序对结果没有影响或者不会更优,或者反证不优解交换顺序后更优;常用与证明排序,但角度不至于相邻,还可以不相邻;此外使用微扰时一定注意后面的决策是否全面;与排序的联系)
- 决策包容(一个决策包含了另外一个决策,必然前者更优)
- 数学归纳法
- 反证法
区间问题再讲
切入点
- 端点
- 长度
切入关系
- 包含
- 交叉
- 相离相邻
切入方法
- 「首要」分组问题一定要表现组的信息(否则你怎么分组?)
- 「首要」按左端点排序(再按右端点排序)(或者按右端点排序(再按左端点排序)),灵活转换,不要死扛一个方向。
- 「简化」如果包含关系对问题没有影响,去除包含关系,简化问题(利用栈实现)
- 「算法」常用暴力,枚举左端点再枚举右端点,或者枚举右端点,再枚举左端点,具有对称中心的时候,还可以枚举对称中心(如求最长回文子串的二分做法),然后再寻求优化
- 「算法」递推解决,注意无后效性,因此先要排序
- 「优化」对于一段区间信息的维护,采取将信息存储到左右端点附近,然后用前缀和处理出信息
切入递推状态角度
- 区间作为一个整体
- 区间所包含的位置
数列贪心
对于数列\(\{a_i\}\)
切入性质
- 逆序对个数和奇偶性与问题的关系
- 构造新的数列\(\{b_i\}\)按\(a_i\)排序,\(\{c_i\}\)表示数列中\(b_i\)在原数列出现的位置
- 构造新的数列\(\{b_i\}\),令\(b_i=a_i-i\)
- 最长上升子序列的个数以及长度
切入点
-
第几个位置(数列中第几个数);前几个位置(前缀和)
-
选什么,选择顺序
切入方法
-
「数列顺序最优」利用微扰法,看相邻两个元素,寻找关键字来排序(加减乘除随便猜)(依据是冒泡排序是相邻的交换,最终可以使得使得整个序列有序)
-
「数字移动最优」根据前i个数字,推出第i+1个数字的最优移动次数,从而推出全部的最优移动次数。
-
集合问题排序转数列问题
树上贪心
研究点
- 叶子
- 子树
- 根节点
套路
- 无根转有根,维护点到根的性质
- 合并性贪心
网格图问题
考虑对象
- 行列对角线矩形轮廓线
- 矩形为\(1times 1\)该输出什么
玄学方法
- 行列独立
- 拆行成列
基本模型
均分纸牌
有n堆纸牌,保证纸牌总数为n的倍数,每次操作可以把一堆牌的数张牌移动到附近的牌堆,询问牌的最少移动次数。
解
这是一个数列的贪心问题,不妨把纸牌数记做一个数列\(\{a_i\}\),现在问题就变成把数字移动旁边的数字上,问移动的数字之和最小值。
对于每一个位置考虑,记\(\{a_i\}\)前缀和为\(\{s_i\}\),显然前i个数字需要第i+1个数字移动\(|s_i-s_n/n|\)个数字进来,于是经过第i+1个数字移动的数字必然只有\(|s_i-s_n/n\times i|\),同理对于每一个位置都是这样,记\(g_i=s_i-s_i/n\times i\),那么么答案就是\(\sum_{i=1}^n|g_i|\)。
额外有用的性质\(g_n=0\)
删数问题
有一个n位的数字r,第i位上的数字为\(\{a_i\}\),求从中删去m位,剩下组成的数字的最小值。
解
显然删去m位数字,可以转化为保留\(n-m\)位数字,剩下的\(n-m\)位数字,必然是保证最高位最小的前提下后面的位上的数字尽可能小,于是有了一个贪心策略,就是从原数字r最高位开始选择剩下的数字l的位数(也是从高位选到低位),l的每一位数字在它能够选择地范围内选择最小的靠近r高位的数字,这样就可做到最优。
拦截导弹
给出一个长度为n的序列\(\{a_i\}\),求将其划分成最少的子序列数,使得每个子序列都是单调递增。
解
贪心分组,自然考虑要保存组的状态,不妨记\(b_i\)为第i个子序列的最后一个数字的大小,于是对于a中的第k个数字考虑,如果没有任何子序列可以作为它的前接,显然开一个新的子序列即可,但是在能够成为它的前接的子序列中,选择哪一个作为前接?
凭借朴素的自然感觉,是选择\(b_i\)最大的子序列,原因在与对于k能够后接的子序列的\(b_i,b_j\),有\(b_i>b_j\),对于后面的数字而言,要么是能够选择i,又能选j,此时对于k,选哪个都无所谓,要么是能选j,不能选i,显然不选i白不选,反正后面不能选,显然不选,要么是即不能选j,有不能选i,显然也是不选白不选。
活动选择
给出n个区间,第i个区间为\([l_i,r_i]\),询问从中能够选出的最多的区间数满足每个区间相离或者相邻。
解
区间贪心问题,显然首要考虑排序,不妨按照右端点排序,记前面选择的区间的右端点为R,对于第i个区间而言,如果\(R\leq l_i\),显然这个区间就可选择,但是问题是一定要选这个区间吗,采取决策包容的办法,假设不选这个区间,选后面的区间j,然而右端点是单调递增的,有\(r_j\geq r_i\),于是对于选i而言,后面能够选择的区间有左端点的范围在\([r_i,+\infty)\),而选j范围在\([r_j,+\infty]\),显然前者包含了后者,于是选i不会更差。
整数区间
给出n个区间,第i个区间记做\([l_i,r_i]\),现在询问最少的点的数目,能够被所有区间包含。
解
显然是区间问题,不妨按照右端点排序,记选择的上一个点的坐标为p,对第i个区间考虑,如果\(l_i\leq p\)的话,显然直接被已有的点包含,向后处理,如果\(p<l_i\),显然需要新开一个点,但是点的位置在哪里?凭借朴素的直觉,应该是放在\(r_i\),可以利用决策包容来证明,假设放在位置q,显然有\(q\leq p\),那么接下来的区间右端点必然是递增的,于是右端点必然会在p,q右方,现在只要考虑左端点是否能在p,q左边,因此对于q的接下来的区间的l的取值范围为\((-\infty,q]\),而p为\((-\infty,p]\),显然后者包含前者,后者不会更差,得证。
其他
- 集合\(\{a_i\}\)中,任意选出m对数字,使一对数中的差的平方的和最小化。(排序后,最大和最小配对,次大和次小配对...以此类推)
- 求长度为n数列\(\{a_i\}\)中,最大的连续子段和,从前往后扫描,如果新加入的数使子段和小于0,新开一段,否则累加答案(可以推广到2维,即枚举是第i列到第j列,把它们看做一个整体,然后各行是一个数字,就是一维的问题
- 判断两个局面是否能够互达:最小表示法(如树型地铁系统);奇偶性(奇数码问题).
尾声
但无论如何,这些都只能归结到简单的知识当中(毕竟还是被别人归在基础算法里了,但是我太菜了,只能单独提出来,作为很难的知识点处理),同机房的人如hl都在刷ctsc,隔壁机房已经进了两个国家集训队,看看机房里强者的刷题记录(都在我之上!),那我也只能看看这些简单的东西,写一些无聊的东西,聊以自慰了,但愿对于各位强者有所帮助了)