排序
链和反链
对于偏序集S,称S的子集A是链,当且仅当A组成一个全序集。若S的子集B中元素两两不可比较,那么称B是反链。
最小链覆盖是指S的一个子集族,每个元素都是链,所有元素的并为V,但是不同元素的交集为空。同样最小反链覆盖也是S的一个子集族,每个元素都是反链,所有元素的并为V,但是不同元素的交集为空。
Dilworth定理:偏序集的最小链覆盖大小等于S的最长反链的长度
证明:
记最小链覆盖的大小为n,最长反链长度为m。
设L是S的一个最长反链,很显然多个L中的元素不能同时属于相同的链,因此最小链覆盖至少包含m个链,即n≥m。
之后利用数学归纳法证明n≤m。
当S的大小为0或1的时候,命题显然成立。
我们记s是S的一个极大元,记S′=S−{s}。由归纳法知,S′的最小链覆盖的大小与最长反链的长度相同,记作k。设S′的最长链覆盖为C1,C2,…,Ck,而S′的所有长度为k的反链为R1,R2,…,Rr。很显然,对于Ri,必定是由最长链覆盖中每个集合取一个元素组成的,我们记Ri与Cj的唯一公共元素为xij。对于j=1,2,…,k,记mj=maxi(xij)。现在考虑集合M=m1,m2,…,mk,如果其中有任意两个不同元素mi,mj可以比较,不妨设mi≤mj,记mi所在的反链为Rp,而mj所在的反链为Rq,那么可以推出xqj=Mj≥xpi=Mi≥xqi,即Rq包含两个可比较元素,这是不可能的,因此我们知道M是反链。
接下来我们考虑集合S,即向S′加入元素s。考虑两种情况:
1) 如果M与s中元素都不可比较,那么我们考虑集合M∪s,这是一个长度为k+1的反链。而向集合中加入一个元素,最多会增加其最长反链长度1,以及最小链覆盖大小1。因此有m≥k+1≥n。
2) 如果M与s中某个元素可以比较,假设s与Mi可以比较,设Mi所在的反链为Rj,那么我们可以推出s大于等于Rj中的所有元素,即D=Rj∪s也是一个链。现在考虑集合S″=S−D,由于S中的所有反链都少了一个元素,因此S″的最长反链的长度为k−1,同样利用归纳法知其最小链覆盖为{C′1,C′2,…,C′k−1}。我们将D加入得到新的最小链覆盖{C′1,C′2,…,C′k−1,D}。因此S的最小链覆盖大小一定是k,故可以得到m≥k=n。
Dilworth对偶定理:偏序集的最小反链覆盖大小等于S的最长链长度
证明:
记最小反链覆盖的大小为n,最长链长度为m。
设L为S的一个最长链,而L中多个元素肯定不能同时属于一个反链,因此最小反链覆盖至少含有m个反链,即我们推出了n≥m。
我们记X1为S中的极小元的集合,并从S中移除X1的元素。之后继续定义X2为S中极小元的集合,并同样从中移除。重复这个过程,直到S为空,假设过程中我们得到了k个非空集合X1,X2,…,Xk。极小元的集合一定是S的一个反链,我们得到了一个大小为k的反链覆盖。同样我们从X1,X2,…,Xk中分别取一个元素x1,x2,…,xk,满足x1≤x2≤…≤xk,这样我们就得到了长度为k的一个链。得到关系式n≥k≥m,结合之前得到的n≥m,可以推出n=m。
求最长反链方法:
我们先利用最小相交路径覆盖将图建成二分图并求得最大匹配。设n为顶点数,最大匹配数为m,记l(i)表示二分图左侧第i个顶点,r(i)表示二分图右侧第i个顶点。
之后我们可以找到最小顶点覆盖,利用最小顶点覆盖得到最大独立集。对于1≤i≤n,如果l(i)和r(i)同时属于最大独立集,我们就将它放入到集合A中,集合A初始时为空集。在上述操作后,我们得到的集合A就是最长反链条。上面的算法的时间复杂度为O(n2.5)。
用反证法证明,如果A中任意两个元素u、v可比较,那么不妨设u≤v。这意味着l(u)与r(v)之间有边,而我们知道l(u),r(u),l(v),r(v)都属于最大独立集,因此不可能发生。
现在我们证明得到的反链长度正好为n−m。由于二分图共有2n个顶点,而最大独立集中有2n−m个顶点。|最大独立集|=|A|+|{i|l(i)与r(i)有且仅有一个属于最大独立集}|。而后者最多为n,因此推出前者至少为n−m。当然反链长度是不可能超过n−m的,因此得到的反链的长度恰好为n−m。
整除偏序关系
对于两个正整数a,b,如果a∣b,则认为a≺b。很显然这是一个偏序关系。
题目1:要求找到1到n的所有整数的最长链。
由算术基本定理可知,最长链等价于找到一个数x,满足1≤x≤n,并且其分解后素因子的幂次和最大。我们可以直接找最大的数2m,满足2m≤n,则最长链的长度为m+1,且最长链为20,21,…,2m。
题目2:要求找到1到n的所有整数的最小反链覆盖。
提供一道题目。
记m=⌊log2n⌋,很显然由于20,21,…,2m的存在,它们需要分到不同的组中,因此至少需要分成m+1组。
下面我们给出一个将n个数正好分成m+1组的算法。根据算术基本定理,我们知道对于任意正整数x,有x=pc11pc22…pckk,我们可以记deg(x)=∑ici。之后我们将数x分类到第deg(x)+1组即可。由于对于任意数1≤x≤n,都一定有0≤deg(x)≤m,因此最多只需要分成m+1组。
这里顺带一提,贪心算法得出的分组结果和上面的方案是相同的。
上面的做法是O(nlnn)的,但是如果我们将x分组到⌊log2x⌋+1组的话,可以O(n)进行分组。
字符串无穷偏序
对于字符串S,记S+表示无穷个S前后拼接得到的无穷长的字符串。
命题1:对于任意字符串A,B,A+≤B+⇔AB≤BA,这里的比较规则是字典序。
证明:
如果A与B相互没有前缀关系,则命题很显然成立。下面不妨假设A是B的前缀,换言之,B=AC。
考虑到A+和B+的大小关系由其长度为m=|A||B|的前缀共同确认,因此我们只需要保证这一段前缀中A+和B+的关系即可。
如果AB≤BA成立。那么容易推出AC≤CA,之后我们有
B+=(AC)+=A(CA)+≥A(AC)+=A2(CA)+…≥A|B|(AC)+由于B+的长度为m的前缀字典序不小于A+,因此有A+≤B+。
同理如果AB>BA成立,可以推出A+>B+。
至此命题得证。
题目1:给定n个字符串,要求将这些字符串按照一定的顺序拼接起来,且要求拼接得到的字符串字典序最小。
提供一道题目。
假设结果中A和B相邻,且A在B之前,则很显然有AB≤BA成立(否则我们可以交换两者的顺序得到更小字典序的结果)。考虑到AB≤BA等价于A+≤B+,而后者是全序关系,因此可以直接上排序算法。时间复杂度为O(nmlog2n),其中m为字符串长度。
拓扑序问题
给定一副有向无环图,并给所有顶点排序,满足如果存在边(u,v),则u的序号大于v的序号,那么称这个序列满足拓扑序。
题目1:生成某个n个顶点m条边的有向无环图的任意拓扑序。
可以不断地删除出度为0的顶点,这样就能的到满足条件的拓扑序。可以用队列维护所有度数为0且未被删除的顶点,时间复杂度为O(n+m)。
题目2:生成某个n个顶点m条边的有向无环图的字典序最小的拓扑序。
在题目1的基础上,不断从队列中删除编号最小的顶点即可。可以将队列替换为优先队列,这样就可以得到O(nlog2n+m)的时间复杂度性能。
题目3:生成某个n个顶点m条边的有向无环图的某个拓扑序,要求保证1的序号最小,如果有多个,则保证2的序号最小,如此循环下去。
提供一道题目。
考虑生成最小字典序的时候,我们贪心删除编号最小的字典序。这时候实际上等价于我们要让编号较大的顶点序号尽可能大。现在我们希望让编号较小的顶点序号尽可能小,我们可以尝试生成一个反向拓扑序,且要求这个反向拓扑序的字典序最大即可。可以用类似题目2的做法来做,时间复杂度为O(nlog2n+m)。
排序操作
给定长度为n的序列,以及k个函数f1,…,fn,其中每个函数会对输入序列的某个子序列进行排序。要求判断对于任意给定序列x,能否保证fn(fn−1(⋯f1(x)⋯))返回的序列是有序的。
问题1:给定长度n和m个命令,判断是否对于所有长度为n的序列,执行上述m个命令后都能得到有序序列。命令从前往后执行,每个命令指定若干个位置,将这些位置的数从小到大进行重排。其中n≤15,m≤100。
我们不能枚举所有的n种排列,因为数量级太大。但是我们可以这样玩,我们考虑一个长度为n的01序列,其中部分为1,部分为0。如果命令能保证对所有长度为n的序列正确排序,那么自然也能对我们的01序列进行排序。
现在我们断定如果所有可能的长度为n的二进制序列经过排序后都是有序的,那么一定能正确排序所有的长度为n的序列。用反证法,假设存在一个序列a不能被正确排序,那么考虑在对a应用m个命令后得到的新序列为b,考虑最小的数x,满足存在某个更小的数在b中排在x后面。接下来,我们将a中所有大于等于x的数改写为1,而所有小于x的数改写为0,那么经过m个命令后,得到的新序列实际上是将b中所有大于等于x的数改成1,而其余数改写为0,这样我们就发现一个长度为n的二进制序列不能被正确排序,这与前提相悖。
用这种方式做校验,时间复杂度为O(mn2n)。
提供一道题目:SRM761 SortArray。
问题2:给定长度n和m个命令,判断是否对于所有长度为n的序列,执行上述m个命令后都能得到有序序列。命令从前往后执行,每个命令指定若干个位置,将这些位置的数从小到大进行重排。其中n≤60,m≤10。
提供一道题目。
题目1的做法在这里不适用,但是我们可以借鉴它通过对所有二进制串排序的校验方式。
如果n=1,很显然所有指定都是能保证正确排序的。下面考虑n>1的情况。
令N={1,2,…,n}。
记第i个命令指定的下标集合为Ii,记Si=⋃ij=1Ij。很显然指令能正确重排的前提是Sm=N。
我们考虑一个函数f:Si→{0,1}。我们称函数f是i可达的,当且仅当存在一个01序列b1,…,bn,经过前i个命令处理后,对于j∈Si,有f(j)=bj。
那么我们可以得到我们排序算法成立的充要条件,所有m可达的函数都是递增函数,而这样的函数最多只有n+1个。我们可以通过枚举所有可能的m可达函数来校验我们的排序算法是否有效。
我们记Di=Si∖Si−1,令S0=∅。那么当我们执行完前i−1个命令后,要处理第i个命令时,实际上真正会影响结果的只有输入二进制串Di位置上的1的数目,它只有|Di|+1种可能。
因此我们暴力枚举的时间复杂度为O(∏ni=m|Di|+1)。由于|D1|+1+…+|Dm|=n+m,因此有上界O((n+mm)m)。
这里我们需要使用二进制来表示集合,以及利用它快速计算交集并集等操作,否则时间复杂度需要额外乘上一个n。
基础排序算法
冒泡排序
要排序大小为N的数组A。循环下面流程直到整个数组有序
- 顺序枚举i=0,1,…,N−2,如果Ai>Ai+1,则交换两个数。
很显然冒泡排序一定会结束(每次都至少减少一个逆序对),且最终数组一定有序。
下面分析一下冒泡排序的原理:
考虑另外一个大小为N的数组B,其中Bi表示A0,…,Ai−1中比Ai大的数的个数。很显然Bi≤i。如果A是一个置换,不难发现A与B有唯一对应关系(可以O(NlogN)的时间复杂度内相互转换。当A有序的时候,B的所有元素都是0。
考虑一次冒泡排序的循环体,其本质上做的事情是:
- 对于i=0,…,N−1,将Bi修改为max(Bi−1,0)。
- 将数组B循环左移一步,即新的数组为B1,…,BN−1,B0。
因此我们直接操作B的话,可以快速模拟出A经过k次冒泡后的排列。同时只需要经过maxB次冒泡整个数组有序。