算法学习笔记
学习算法的过程中看题解是很重要的一环,但往往题解只提供一种解法,角度难免片面。因此这里主要以题目为中心,整理我看到的各种解法以及一些个人感想。
P1115 最大子段和
这题贪心应该是最直接的方法,也同时可以用前缀和的思想做。
题目要求的是选连续的一段,而且不限制长度,如果我们又枚举起点又枚举长度就会 \(O(n^2)\) .
起点不确定的情况不好控制,我们可以把子段和转化成:第 \(1\) 到 \(x\) 个数之和减去第 \(1\) 到 \(y\) 个数之和,其中 \(x > y\) . 对于每一个 \(x\) ,问题就转换成了求最小前缀和。时间复杂度 \(O(n)\).
贪心就更直接了。设当前以第 \(i\) 个数结尾的子段最大和为 \(dp[i]\) ,我们可以看出若是 \(dp[i - 1] + a[i] < 0\),我们就还不如不选第 \(i\) 个数,直接 \(dp[i] = 0\) 即可。其他情况下就必然是 \(dp[i] = dp[i - 1] + a[i]\). 最后找出最大的 \(dp[i]\) 就可以了。由于 \(dp[i]\) 可以直接由一个变量存储,空间复杂度 \(O(1)\). 时间复杂度 \(O(n)\).
CF1573B Swaps
这题看起来很复杂,其实就一个关键点:\(a\) 都为奇数而 \(b\) 都为偶数,因此这两个数组从第一个数开始就不一样。想让 \(a\) 字典序小于 \(b\) 的话,就需要且仅需要 \(a_1<b_1\). 所以只要找到一对 \(a_i\) 和 \(b_j\) 满足 \(a_i<b_j\) 并一个一个把这两个数分别移到最前面就好了。此时问题就转化为找最小的 \(i+j-2\). (若下标从 \(0\) 开始则为最小的 \(i+j\))
假设我们一定要选某个 \(b\) 中的偶数 \(b_j\),此时我们只需要找满足 \(a_i<b_j\) 的下标最小的 \(a_i\) 即为最优解。因此我们可以这样做:开一个数组 \(p\) 记录某个数 \(n\) 在数组 \(a/b\) 中的位置 \(p_n\). 从大到小遍历所有的数,然后用一个变量 \(l\) 记录当前遍历到的 \(a\) 中最小的下标,若当前数 \(n\) 为奇数则 \(l=min(l, p_n)\),若当前为偶数则更新答案为 \(min(answer, p_n + l)\). (注意这里认为下标从 \(0\) 开始)
ABC223B String Shifting
不知道是不是受到之前做的一道很类似的题的影响,这题想了很久很久。
首先分析问题:题目中的平移可等价为把字符串的前\(n\)个字符移到最后或者把字符串的后\(n\)个字符移到最前。那么我们首先会发现把前\(n\)个字符移动到最后等价于把后\(length-n\)个字符移动到最前,我们只用关心移动到最前(或最后)的情况。由于每次只能操作最后的\(n\)个字符,无论操作多少次都可等价于操作一次。那么问题转化为:
给定非空字符串\(S\),选择\(S\)的后\(n\)个字符移动到首端。分别求移动后使得\(S\)字典序最大和最小的\(n\)。
剩下的就很简单了:鉴于\(n\leq1000\),我们直接枚举所有可能的\(n\),把最大的和最小的找出来就好了。(然而因为没注意到这个题允许\(O(n^2)\)时间复杂度我卡了1hr多)
2019年广东工业大学新生赛 F 失踪的玫瑰
题面可以等价转换为:有\(n\)个山洞,初始状态下每个山洞里都有一只狐狸,每天晚上每只没抓住的狐狸都会分身去两边的洞,每天白天都有一个猎人去查看一个山洞,求抓住全部狐狸的方案数。以下用x代表一定没有狐狸的洞口,用o代表有狐狸的洞口。对于\(n=5\)的情况,我们从右往左依次检查试试:
检查洞口 | 白天抓捕完后 | 过了一晚上后 |
---|---|---|
5 | oooox | ooooo |
4 | oooxo | oooox |
3 | ooxox | oooxo |
2 | oxoxo | xoxox |
可以看出最后一次抓捕的下一个白天,所有奇数洞都没有狐狸。
下面有一个重要的结论:
每天检查之后,被检查的洞的右边不会存在开局位于奇数洞的狐狸。
为什么呢?
因为开局在奇数洞的狐狸,在奇数次抓捕时必定在奇数洞里,在偶数次抓捕时必定在偶数洞里。开局在偶数洞的狐狸正好相反。而从奇数洞走到旁边的奇数洞需要2个晚上,被检查的洞的左边的狐狸若是想走到右边,必定会在移动时遇到抓捕。
这里还应注意到的是,我们第一天检查5号洞口后,4号洞口的狐狸当晚就会分身去5号洞口,也就是说检查两边的洞口是无效的。因此3天即可抓捕完奇数洞狐狸。
如果要抓开局位于偶数洞的狐狸呢?
检查洞口 | 白天抓捕完后 | 过了一晚上后 |
---|---|---|
4 | oooxo | oooox |
3 | ooxox | oooxo |
2 | oxoxo | xoxox |
3天即可抓捕完。
然后呢,抓奇数洞狐狸?诶奇数洞怎么已经没有狐狸了?这是因为我们花了奇数天抓捕,开局偶数洞的狐狸一晚上后都在奇数洞。那么我们能不能把这个当作开局状态,再来一次偶数洞狐狸抓捕?
当然可以!最后答案是\(\{4,3,2,4,3,2\}\),但是这不是字典序最小的方案。因为选择从左到右给洞穴标号还是从右到左给洞穴标号是不重要的,所以答案为\(\{2,3,4,2,3,4\}\).
再经过一些推导,洞穴个数\(n\)为奇数时都为这种情况,即\(\{2,3,4,\cdots ,n-1,2,3,4,\cdots ,n-1\}\).
\(n\)为偶数时,比如\(n=4\)时,因为\(n-1=3\)为奇数,我们就先抓开局奇数洞狐狸:
检查洞口 | 白天抓捕完后 | 过了一晚上后 |
---|---|---|
3 | ooxo | ooox |
2 | oxox | xoxo |
3 | xoxo(?) | oxox |
2 | oxox(?) | xoxo |
诶诶诶等一下!这最后两次都抓了个空气啊!
这是因为\(n\)为偶数时抓奇数洞狐狸所需天数不是奇数天而是偶数天了。开局偶数洞的狐狸一晚上后都在偶数洞。我们要抓偶数洞狐狸,但是不能检查两边的洞口(也就是第1个洞口不能是\(n\)),所以我们调换方向,从2号洞口开始。
检查洞口 | 白天抓捕完后 | 过了一晚上后 |
---|---|---|
2 | oxoo | xooo |
3 | xoxo | oxox |
成功!最后结果是\(\{3,2,2,3\}\),调换一下编号方向就是\(\{2,3,3,2\}\). 所以\(n\)为偶数时答案为\(\{2,3,\cdots n-1,n-1,n-2,\cdots ,1\}\).
CF1584B Coloring Rectangles
刚看到这题的时候没什么想法,就乱试。试了半天发现似乎\(1\times 3\)是最优的切法。然后就开始纠结在特判两条边都不能被\(3\)整除的情况上。现在回想起来,自己的思维还是太窄了。
然后就想了个DP做法。对于\(1\times n\)的长方形而言,只需要贪心尽可能多地切成\(1\times 3\)即可。模拟一下就会发现答案是\(\lceil \frac{n}{3} \rceil\). 于是就可以用DP做了:
1 | for (int i = 1; i < MAXNM; i++) { |
然而看数据范围\(1 \leq n, m \leq 3 \cdot 10^4\),被卡了。所以下面是官方题解:
首先找上色后矩形的性质:若一个矩形按照题目要求上色,则相邻的行/列的上色图案正好相反,相邻两行的上色和未上色方块数相同。若长和宽都为偶数,则每两行和列的上色图案将会完全相同。若一个为偶数一个为奇数,则每两行或列的上色图案将会完全相同,这两行内部互相上色方块数将相差1. 这两种情况下上色和未上色方块数都相同,即上色方块占所有方块的\(\frac{1}{2}\)。但若长和宽都为奇数,上色和未上色方块数将会相差1. 令方块总数(面积)为\(S\), 则上色方块数为\(\frac{S - 1}{2}\), 占所有方块的\(\frac{S - 1}{2 \cdot S}\).
我们的期望是上色方块的比例尽可能小。根据题目条件\(S\geq 2\),因此当\(S=3\)时\(\frac{S - 1}{2 \cdot S}=\frac{1}{3}\)为可能的最小值。因此,这就是对答案的最小估计:\(n \cdot m \cdot \frac{1}{3} \leq answer\).
现在,如果我们构造出一个要求必须给\(cnt\)个方块上色的剪切方案,且\(\frac{n \cdot m}{3} \leq cnt < \frac{n \cdot m}{3} + 1\),那么答案就是\(cnt\). (也即这个剪切方案就是最优方案)因为\(cnt\)是满足我们估计的最小的整数。
如果一条边能被3整除,那么很明显你可以把它全部切成\(1\times 3\)的矩形然后得到最优解。
如果这两条边被3除后余1和1,或者余2和2,那么你可以把它切成一些\(1\times 3\)的矩形,加上一个\(1\times 4\)或\(2\times 2\)的矩形。多出来的这个矩形里要给2个方块上色,显然满足要求。
如果这两条边被3除后余1和2,那么你可以把它切成一些\(1\times 3\)的矩形,加上一个\(1\times 2\)的矩形。多出来的这个矩形里要给1个方块上色,显然满足要求。
由答案必须是整数,可得答案为\(\lceil \frac{n \cdot m}{3} \rceil\).
P5824 十二重计数法
自己做的,好像有些错误。先记着吧。
- 问题可转化成有\(n\)个有编号的格子,每个格子可填入\(1\sim m\)的任意整数。题目正好对应为\(m\)进制下\(1\sim n\)位数的个数,也即\(n^m\).(可重复排列)
- 先从\([1,n]\)中选出\(m\)个数,然后全排列。即\(C_n^m\cdot A_m^m\).
- 可拆分成:先从\([1,n]\)中选出\(m\)个数,使它们装到\(m\)个盒子中,每个盒子至多装一个球,也即问题2;剩下的\(n-m\)个球可以任意装到盒子中,也即问题1. 答案\(C_n^m\cdot A_m^m\cdot (n-m)^m\).
- 盒子全部相同,意味着哪些球在一个盒子里重要,但是具体在哪一个盒子不重要。问题1除以盒子的排列数\(A_m^m\)即可。但是我们也可以用隔板法:\(1,2,\cdots ,n\)编号的小球打乱顺序后之间放上\(m-1\)个隔板。即为\(A_n^n\cdot C_{n+1}^{m-1}\).
- 不可重复组合:\(C_n^m\).
- 先从\([1,n]\)中选出\(m\)个数直接装到盒子中,剩下的\(n-m\)个球任意装。\(C_n^m\cdot A_{n-m}^{n-m}\cdot C_{n-m+1}^{m-1}\).
- 球全部相同,意味着盒子里有多少球重要,但是具体是哪些球不重要。可用隔板法:\(n\)个小球中间放上\(m-1\)个隔板,即\(C_{n+1}^{m-1}\).
- 盒子只有两种状态:装了一个球/没装球。选出装了球的盒子即为\(C_m^n\).
- 每个盒子都装一个球后剩下的球有\(n-m\)个,回到问题7,即\(C_{n-m+1}^{m-1}\).
- 从这开始就有点难了,先模拟一下:设装球最少的盒子装了\(x\)个球,那么装球第二少的盒子可以装\([x,m]\)个,
2021广东工业大学新生赛初赛题解与反思
初赛结果不理想,只出了5个题。
其实回想起来有些题赛时本来能做出来的,但是因为各种原因没做好,在此纪录。也一并为以后的学习指明方向。
A 简单的求零点问题
很显然,用二分找零点。但是我一看到这题就有点犯难,为什么呢?因为我连二分都不能熟练地码出来。
所以今天整理一下二分。(参考这篇文章)
如何快速判断自己的二分程序是否正确
我们知道二分的思想就是每次取一半,想象一下,不管给我们的数组有多长,每次取一半,最终都会被压缩成长度为 1 的数组,然后在这个长度为 1 的数组里判断并返回,所以我们可以直接用长度为 1 的数组来测试程序。
二分查找
给定长度为n的数组(\(n\geq 1\))
问题1:找到值为value的元素的下标
1 | int BinarySearch(int array[], int n, int value) { |
问题2:找到第一个值为value的元素的下标
这时候遇到相等也不能直接返回,只能排除掉右侧的所有数。
数组的长度为 1 或 2 时,middle
为 0. 若array[0]
为要找的数,则right
将被赋值为 -1,循环结束,left
为答案。数组长度为 2 且array[1]
为要找的数时,left
将被赋值为 1,回到数组长度为 1 的情况。
因此最后再判断一下left
是否为要找的数,如果是则返回,否则答案不存在。
1 | int BinarySearch(int array[], int n, int value) { |
问题3:找到最后一个值为value的元素的下标
这就是问题2的倒序版本。改动两个地方即可
if (array[middle] >= value)
中的等号去掉;if (right >= 0 && array[right] == value) {return right;}
问题4:找到第一个大于等于value的下标
在问题2中,我们的策略是让left
刚好为第一个大于等于value的数的下标,而让right
刚好为第一个小于value的数的下标。因此只需要去掉最后判断答案存在的array[left] == value
条件即可。
问题5:找到最后一个小于等于value的下标
与问题4同理,去掉问题3中最后判断答案存在的array[right] == value
条件。
B a+b(easy)
这题能卡我,题面也要背点锅。赛时的样例1输入为1 2
输出为3
. 而我又对位运算不够熟悉,在无论如何也找不到对应的x和y时陷入了自我怀疑之中。
题目本身确实很简单。取或运算可以看成不进位,只把每一位单独加起来。取和运算就看成每一位会不会进位(二进制加法只有进位/不进位两种可能)。那么加起来自然就与这两个数本身加起来相等。
C 百家姓与年龄
这道题题面看起来有点混乱……本来不复杂的描述参杂了3个人进来,还有两个人名字就差一个字母。当时愣是来回看了三四遍才明白意思。下次读题要冷静,快速排除无关信息。
读完题,把式子列出来一算就很显然了。令\(x\)为出生年份,\(y\)为排位。 \[ \begin{aligned} a&=50(2y+5)+1771-x\\ &=100y+2021-x \end{aligned} \] 联想到今年是2021年,\(2021-x\)为年龄,且年龄小于100岁。年龄即为\(a\)的最后两位数,排位为余下的数。
所以这道题的保质期已经不足两个月了。
所以居然是年龄小于100而不是百家姓排位小于100。
D 帮助小鱼
接下来 \(n\) 个数,第 \(i\) 个数代表第 \(i\) 个部长摸了那一条鱼。
又懵逼半天,这到底输入的啥。
最后就是开个桶或者map记录鱼被谁摸,然后顺序输出就好了。
E 上楼方式
整个比赛中做的最错误的决定就是第二个读这道题。
当时自己晚了3分钟开始(定了个开始前20分钟的闹钟,响了以后就坐在电脑前等,然后居然忘记定开场时的闹钟了),然后眼看着其他题都被人AC过了,这道题还没有。再加上有上楼梯这种经典递推板题的存在,我信 心 满 满地准备试一试。
这题怎么也有3个莫名其妙的名字。
然后就读错题了,3步登上教堂我想当然地以为是踩3阶楼梯。其实是指在楼梯上移动三次,比如1-2-3-4.
第一阶必定是1,最后一阶必定是\(n\),所以上楼方案数为 \[ C_{n-2}^2=\dfrac{n\times (n-1)}{2} \] 但是还有一个细节:不能直接对被除数取模。一个 workaround 是判断当前\(n\)的奇偶性然后先除后乘,避免溢出。
F 气球
先看两种买法折算的单价:
3 * a < 2 * b
:两件买法更优。这时最多可能买1个三件,因为买2个三件时显然不如买3个两件。3 * a > 2 * b
:三件买法更优。这时可能买1或2个两件,买3个两件时就必定不如买2个三件。
代码:
1 | int ans = 0; |
G zrgg出题
前\(x\)个数中\(a_i\)的倍数的个数为:\(\left\lfloor\dfrac{x}{a_i}\right\rfloor\),容斥一下就可得到\([l,r]\)之间的倍数个数为\(\left\lfloor\dfrac{r}{a_i}\right\rfloor-\left\lfloor\dfrac{l-1}{a_i}\right\rfloor\).
但是需要注意,三个\(a_i\)之间有公倍数,这些公倍数被重复计算了,所以我们需要减回去。但是减回去的时候同时为三个数的公倍数又被多减了,所以需要再加回去。
1 | int n, l, r, k; |
H a+b(hard)
基于B题的铺垫,可以直接想到把\(b_i\)和\(c_i\)加起来得到 \[ d_i= \begin{cases} a_i+a_{i+1}&1\leq i<n\\ a_i+a_1&i=n \end{cases} \] 当\(2\leq i< n\)时,\(d_i-d_{i-1}=a_{i+1}-a_{i-1}\). 由\(n\)为奇数,得 \[ d_{n-1}-d_{n-2}+d_{n-3}-d_{n-4}+\cdots +d_{2}-d_{1}=a_n-a_1 \] 并且\(d_n=a_n+a_1\),所以可以直接求出\(a_1\). 之后就很简单了。
I 定向越野是世界上最好的运动
这题看起来就很吓人,有点让人无所适从。但是我们注意数据范围:\(2\leq n\leq 12\),时间限制3秒。这数据范围一看就是妥妥的NP问题,不是多项式时间能解决的。所以我们可以直接从比较暴力的思路想。
先看一个简化的问题:如果两人速度相同,那么问题可以转化成:经过所有点位(包括起点终点)且不重复走某个点位的最优方案。这样走出来的路径将是一个环,而我们从起点和终点分割这个环得到的就是两人的行走路线。这就是著名的“旅行商问题(Travelling salesman problem)”。我们可以使用状态压缩动态规划来求解。
啥是状态压缩呢?很简单,本来我们有\(n\)个点位,那么对于每个点位是否经过就一共有一个\(n\)维的状态,每个维度只能取经过/没经过两个值。那么现在我们可以用一个二进制数来对应一种状态,用0和1表示没经过和经过。这样就把\(n\)维的状态压缩成了一维。
定义\(dp(S,i)\)表示当前在点位\(i\),已访问点位集合为\(S\)(用二进制表示)的最短路程,那么状态转移方程为 \[ dp(S,i)=\min\left\{d(S-\{i\},j)+dist(j,i)\mid j\in S\right\} \] 对于这道问题,我们只需要一点点适配:设没经过终点(最后一个点位)之前是第一个人A在走,经过终点回到起点时是第二个人B在走。那么修改求两点距离的函数\(dist(j,i)\)为此人走过这段距离所需时间即可。
CF1603A Di-visible Confusion
首先分析操作:决定一个数能否被删除的只有它的序号,因此删除某个数对它左边的数没有影响,而右边的数也不会关心具体是哪个数被删除了。所以,从右往左贪心删除一定是最优的方案。
题目问的是一个序列能不能被全部删除,所以我们就要找反面:从右往左贪心删除时会遇到所有数都无法被删除的局面。再联系刚才说了删除操作对左边的数没影响可知,如果一个序列前面某几项都被\((i+1)\)整除,那么这个序列必定无法被全部删除。即若第一项被\(2\)整除,整个序列必定无法被全部删除。
继续推广,如果第\(i\)项被\((i+1)!\)整除,那么整个序列必定无法被全部删除。由于阶乘的增长速度是很大的,而\(a_i\leq 10^9\),所以一旦阶乘超过\(10^9\)就不用看后面的了。
写出来一交,Wrong Answer.
上面推广的过程是错误的。应该把\((i+1)!\)换成\(LCM(2,3,\cdots,i+1)\). 把最小公倍数当成乘法这个操作在新生赛初赛中已经搞过一次,现在印象够深刻了。说到底,还是数论相关题目自己做得太少,知识也不牢固。
最大公因数(Greatest Common Divisor)
最大公因数的算法是辗转相除法,基于一个原理:如果\(a>b\)则\(gcd(a,b)=gcd(b,a-b)\). 我们可以验证一下正确性 \[ \begin{aligned} a&=2^2\times 3^2\times 7=252\\ b&=2\times 3^3\times 5=270\\ b-a&=2\times 3^3\times 5-2^2\times 3^2\times 7\\ &=2\times 3^2\times (3\times 5-2\times 7)\\ &=2\times 3^2\times 1 \end{aligned} \] 两个互质的数相减,得到的数也和小的那个数互质(证明)。\(gcd(a,b)=gcd(b,a-b)=1\).
如果\(a-b>b\),那么就继续相减到\(a-b<b\)为止,所以直接\(gcd(a,b)=gcd(b,a\bmod b)\).
代码:
1 | int gcd(int a, int b) { |
最小公倍数(Least Common Multiple)
考虑两个数\(a\),\(b\),将这两个数分解质因数 \[ \begin{aligned} a&=2^2\times 3^2\times 7=252\\ b&=2\times 3^3\times 5=270 \end{aligned} \] 这两个数的最大公因数(Greatest Common Divisor)就是它们质因数的交集的乘积 \[ gcd(252,270)=2\times 3^2 \] 考虑最小公倍数的性质。最小公倍数必须被\(a\)或\(b\)整除,也就是说最小公倍数必须同时包含这两数的所有质因数,所以是它们质因数的并集的乘积。怎样得到这个乘积?\(a\times b\),然后容斥除掉共同的质因数\(gcd(a,b)\)就好了。 \[ \begin{aligned} lcm(252,270)&=\dfrac{252\times 270}{gcd(252,270)}\\ &=\dfrac{2^2\times 3^2\times 7\times 2\times 3^3\times 5}{2\times 3^2}\\ &=3780 \end{aligned} \] 实际编程中一般先除后乘,防止溢出。
代码:
1 | int lcm(int a, int b) { |
CF1601A Array Elimination
拿到题目简单分析一下:按位取AND,任意下标选择。所以其实数组的排列以及二进制下每一位整体的排列都不重要。由于取AND的性质,每次消去操作对于某一位来说要么消去\(k\)个1要么无影响。所以,实际上每一位内部0和1是怎样排布的也无任何影响。
那问题就很简单了,某个\(k\)能够单独消掉每一位的所有1,就等价于能消掉所有位。设第\(i\)位有\(n_i\)个1,则对任意\(i\)需满足\(k\mid n_i\). 也就是说\(k\bmod gcd(\{n\})=0\).
CF1515A Phoenix and Gold
从反面分析,假如现在有长为\(k\)的重量子序列使得\(\sum\limits_{j = 1}^{k}w_j = x\),那么要避免这种情况只需要从余下的重量中找出一个不等于\(w_k\)的重量替换即可。由于该子序列可以任意重新排列,所以只要子序列中任意两项不相等就一定可以找到符合要求的排列。综上,无法避免的情况为整个序列全部相等,且存在\(i\)使得\(\sum\limits_{j = 1}^{i}w_j = x\);或者此时已经没有可用于替换的重量了,也即\(\sum\limits_{j = 1}^{n}w_j = x\).
具体怎么实现呢?我又卡了。因为我没注意到这句话:
It is guaranteed that the weights are pairwise distinct.
所以其实只要看后一种情况是否出现就好了,否则都是可以的。我们从前往后加,如果遇到放不了的就先放它的下一个就好了。
我说为啥800难度的会这么复杂。
CF1515C Phoenix and Towers
不允许任意两个塔楼高度差严格大于\(x\),相当于不允许最高的塔楼和最低的塔楼的高度差严格大于\(x\). 联系\(1 \le h_i \le x\)的条件,如果高度差大于\(x\),超出的部分必定不止一个块。那么此时显然可以把多出来的那些块补到最低的塔楼上面。因此,不存在无法达到要求的情况。
然后就不知道怎么写了。
官方题解:上述那种情况是因为有一次或以上未把块加到当前最矮的塔楼上,因此只需一直贪心把块加到当前最矮的塔楼即可。
CF1606C Banknotes
某面值的钞票数量不能比较它更大一级的一张钞票能表示的还多 \[ 10^{a_{i+1}}>x_i\cdot 10^{a_i} \] 所以 \[ x_i\leq \frac{10^{a_{i + 1}}}{10^{a_i}} - 1 \] 所以我们只需要贪心给到\(x_i=\min(\mathit{left}, \frac{10^{a_{i + 1}}}{10^{a_i}} - 1)\),其中\(left\)是剩余可以分配的钞票数目。
CF1610C Keshi Is Throwing a Party
考虑从穷到富决策是否邀请第\(i\)个人,我们需要知道邀请后是否满足要求。我们从穷到富枚举所以前面有多少比他穷的人已经确定了,但是我们并不知道后面还会邀请多少比他富的人。所以这里还缺个信息:我们一共要邀请多少个人?这就又增加一个变量,整体时间复杂度达到\(O(n^2)\),我们需要用二分优化。
为什么呢?题目要求的是输出最大的能邀请的人数,而如果能邀请人数\(n\),那么也一定能邀请比\(n\)小的人数。所以人数是从0开始连续增大的(单调性),某人数下无法邀请那么比该人数还大时也无法邀请(局部舍弃性)。
P1439 【模板】最长公共子序列
这题虽然是个模板题,但是解法还是挺有趣的。
首先能想到的应该是DP(好吧这就是动态规划模板题)。注意到子序列不要求相邻。那么我们从前往后,分别枚举找两个序列的前 \(i,j\) 位的最长公共子序列,然后加上记忆化试试:
1 |
|
(由于在准备蓝桥杯所以就用 Python 写了)
交上去以后发现不行,显然空间时间都是\(O(n^2)\),太大了。
如果转换成自底向上的for
循环模式写,可以很容易优化一下空间。具体来说就是滚动数组,只把当前时刻需要的状态保留
1 | dp = [[0 for x in range(n+1)] for y in range(2)] |
空间优化成了\(O(n)\),但是时间仍然是\(O(n^2)\). 有一半的测试点超时。
这时候算法本身已经没有什么好优化的了。但是你可能感觉有点奇怪,题目中有一个条件我们还没有用到
给出 \(1,2,\ldots,n\) 的两个排列 \(P_1\) 和 \(P_2\)
这两个数列是排列数,也就是 \(1\) 到 \(n\) 的每个数都出现且仅出现一次。
从我们上面提到的子序列的要求来看,如果 \(P_2\) 是单调递增的数列(也就是它的子序列一定是单调递增的),那么 \(P_1\) 中若有它的公共子序列,这个子序列显然得是单调递增的。结合 \(P_2\) 是排列数的条件,\(P_2\) 一定形如 \(\left\{ 1, 2, 3, \cdots, n \right\}\),所以 \(P_1\) 中单调递增的子序列就一定是这两个序列的公共子序列。
如何把 \(P_2 = \left\{ 1, 2, 3, \cdots, n \right\}\) 推广到更一般的情况呢?回到这道题中,注意到这里面的数字其实只是一个符号,我们完全可以用任何符号代替某个数字。也就是说,我们可以直接拿 \(1, 2, 3, \cdots, n\) 映射到 \(P_2\) 中对应位置的数字,然后把两个数列都按照这种规则替换。通过这种做法,我们人为地引入了单调性。
这时候,问题就转化成了求 \(P_1\) 中的最长的单调递增子序列(longest increasing subsequence, LIS)。
LIS 怎么求解?最简单的方式还是直接 DP
1 |
|
但是时间复杂度仍然是\(O(n^2)\),所以介绍一种\(O(n\log n)\)的算法。
设输入的数组为\(\left\{a_1, a_2, \cdots, a_n\right\}\),令\(A_{i,j}\)为在前\(i\)个数字中,长为\(j\)的上升子序列的末位之中的最小值。那么显然对于任意给定的\(i\)且\(j<J_i\),有 \(A_{i, j}<A_{i, j+1}\).
我们现在枚举数组的第\(i\)位,假设我们要找以\(a_{i+1}\)为结尾的最长的上升子序列。很自然的想法是我们要把\(a_{i+1}\)附到它前面尽可能长的上升子序列的末尾。所以我们需要找到临界值\(j\),使得 \[ A_{i,j} < a_{i+1} \leq A_{i,j+1} (j \leq J_i-1)\\ A_{i,j} < a_{i+1} (j=J_i) \] 则\(a_{i+1}\)刚好可以附到\(A_{i,j}\)所代表的那个子序列末尾了。 \[ A_{i+1,j+1}=a_{i+1} \] 这时我们还发现,剩余的\(A_{i,k}\)要么已经比\(a_{i+1}\)小了,要么比\(a_{i+1}\)大即不满足单调性,因此对于\(k \neq j+1\)有 \[ A_{i+1,k}=A_{i,k} \] 怎样找到临界值\(j\)呢?由于\(A_{i,j}\)的单调性,我们直接二分搜索即可。
1 | from bisect import bisect_left |
P2419 Cow Contest
首先观察题目要求,可得排名可以确定的奶牛的定义为:如果奶牛 \(i\) 的排名确定为 \(r\) 则必定有 \(r-1\) 只奶牛与它直接或间接地比赛并获胜,\(N-r\) 只奶牛与它直接或间接地比赛并失败。
从定义直接想到,我们可以枚举奶牛(节点),然后递归找比它强的和比它弱的,如果两者数目加起来为 \(N-1\) 则满足要求。仔细思考一下,其实不需要递归,我们从入度为 0 和出度为 0 的点开始,累加途中的点并记录即可。