算法竞赛入门学习,本文习题来自牛客网教程。
优化枚举的基本思路,减少枚举次数
例一:在一个N*N(N<=100)矩阵中求一个最大的正方形使得该正方形的四个顶点都是由字符“#”构成。
示例
#*#***
******
#*#*#*
******
#*****
***#**
思路:枚举正方形的对角两个顶点即可。
例二:给你一个数列{an}(1≤n≤100000),有q(1≤q≤100000)次询问,每次询问数列的第li个元素到第ri个元素的和。
思路:将对区间的查询转化为对区间端点的查询,采用前缀和的方法
例三:给你一个数列{an}(1≤n≤100000),有q(1≤q≤100000)次修改,每次把数列中的第li到第ri的每个元素都加上一个值ki,求所有的修改之后每个数的值。
思路:修改之后,第 l i − 1 − r i l_{i-1}-r_i li−1−ri之间的数字与前一个数字之差保持不变,故只需要修改 l i l_i li 与 r i + 1 r_{i+1} ri+1 即可,采用差分法
对于数列修改问题,由于时间复杂度的限制,我们要考虑将对区间上的操作转化到对于端点上的操作。因此通常可以考虑前缀和和差分的方法,这两个操作是对称操作。对数组前缀和进行差分可以得到原数组,对差分进行前缀和也可以得到原数组。
扩展: 某校大门外长度为L的马路上有一排树,每两棵相邻的树之间的间隔都是1米。我们可以把马路看成一个数轴,马路的一端在数轴0的位置,另一端在L的位置;数轴上的每个整数点,即0,1,2,…,L都种有一棵树。由于马路上有一些区域要用来建地铁。这些区域用它们在数轴上的起始点和终止点表示。已知任一区域的起始点和终止点的坐标都是整数,区域之间可能有重合的部分。现在要把这些区域中的树(包括区域端点处的两棵树)移走。你的任务是计算将这些树都移走后,马路上还有多少棵树?
思路:随着数据量的不同,可以采用三种不同的方法。数据量较小时可以使用布尔数组,数据量较大时可以使用差分法,数据量很大时采用离散化的方法。
例四: 给定长度为n的正整数数列以及整数S,求出总和不小于S的连续子串的长度的最小值,如果解不存在,输出0
思路:如果使用一般情况下的枚举,则其时间复杂度为 O ( n 3 ) O(n^3) O(n3) ,因此对于此题,需要考虑采用尺取法。考虑我们遇到的第一个数列和不小于 S 子数列 A,其左右下标分别为 s 和 t,此时我们需要先记录总和,然后继续遍历,得到第二个数列和不小于 S 的子数列。如果 t 向右移动,我们会发现是完全没有必要的,因为该数列为正整数数列,当 t 向右移动时,所得新的子数列的数列和必然大于刚才的子数列的和,若 t 向左移动时,所得数列和则必然小于 S,因此我们需要保持 t 的位置不变,而使 s 的位置向右移动一位。若得到的新数列和小于 S,我们需要将 t 向右移动,以增加序列和;若新得到的数列和大于 S ,记录总和,然后我们需要将 s 向右移动一位。为什么不将 t 左移一位呢,因为此时若 t 左移一位,所得到的序列B 是 A 的子序列,我们可以判断出 B 的和一定小于 S,因此 t 不能左移。
例五:有一个N*M的灯泡,(N<=10,M<=100),每次按某一个点可以使得其本身以及其上下左右共五个的灯的开关反向。给定初始状态(每个灯泡的亮或者灭),问:能否把所有灯都灭掉
思路:枚举第一列灯泡的按灭状态即可。先枚举第一列的每个灯泡是否按下开关,从而得到第一列灯泡与第二列灯泡的亮灭状态(因为第一列灯泡的开关会影响到第二列灯泡的亮、灭),此时为使得第一列灯泡全部处于明亮状态,第二列灯泡的开关实际上已经固定,即前一列灯泡的明暗控制着下一列灯泡的开关是否应该按下。因此第一列灯泡的明暗状态控制着后面所有灯泡的开关是否应该按下,由于最后一列灯泡无法被后续灯泡开关调暗,因此只需考查最后一列灯泡是否全暗即可判断能否将所有开关关闭。
一定要注意位运算的优先级问题。加减乘除的优先级要高于移位运算,即2 + 3<<1 的运算过程是先计算 2 + 3,然后再将运算结果左移一位;加减乘除与移位运算的优先级均高于比较运算符,即我们可以从容的先进行运算再比较大小。但是位运算符的优先级要低于比较运算符,即 2|1 != 3,会先进行 1!=3的运算,然后再将运算结果与2进行位运算(即 2 | 0)。最后,短路运算符 “||” 和 “&&”要低于前面所有运算符,即在 if 判断语句中,“||” 和 “&&” 会最后执行。综上,在一个表达式中,会优先计算加减乘除,随后进行移位运算,然后再进行大于小于等于不等于之类的运算,然后再进行位运算,最后进行短路运算符的布尔运算。
位运算基础
位运算 | 功能 | 位运算 | 功能 |
---|---|---|---|
x >> 1 | 去掉最后一位 | x << 1 | 在最后一位加0 |
(x << 1) + 1 | 在最后一位加1 | x | 1 | 把最后一位变成1 |
(x |1) - 1 | 把最后一位变成0 | x ^ 1 | 最后一位取反 |
x | (1 << (k-1)) | 把右数第 k 位变成1 | x & (~(1 << (k-1))) | 把右数第 k 位变成0 |
x ^ (1 << (k-1)) | 右数第 k 位取反 | a = a^b; b = a^b; a=a^b; | 交换a、b的值 |
贪心算法:在堆问题求解时,总是做出在当前看来最好的选择。即不从整体最优上加以考虑,而是做出局部最优解。
使用贪心算法的问题都是能严格证明贪心出的局部最优解就是所求的全局最优解。
例一:(排队接水)有n个人在一个水龙头前排队接水,假如每个人接水的时间为Ti,请编程找出这n个人排队的一种顺序,使得n个人的平均等待时间最小。(n<=1000,ti<=1e6)
解:选择接水时间越短的人越靠前。第一个人的等待时间为 0,第二个人的等待时间为 t 1 t_1 t1,第三个人的等待时间为 t 1 + t 2 t_1+t_2 t1+t2,…,所有人的等待时间为 t 1 + ( t 1 + t 2 ) + ( t 1 + t 2 + t 3 ) + . . . t_1 + (t_1+t_2) + (t_1+t_2+t_3) + ... t1+(t1+t2)+(t1+t2+t3)+...,不难看出,若要求平均等待时间最小,则需要总体等待时间最小,而当 t 1 、 t 2 、 t 3 、 . . . t_1、t_2、t_3、... t1、t2、t3、...依次取最短接水时间时,总时间最小。
例二(连数问题)设有n个正整数,将它们连接成一排,组成一个最大的多位整数。
• 例如:n=3时,3个整数13,312,343,连成的最大整数为34331213
• 又如:n=4时,4个整数7,13,4,246,连成的最大整数为7424613
• (n<=1000,每个数都在int范围内)
解:在某个位置上,我们需要选择一个正整数,候选的数字有a、b、c、d,那么为保持多位整数最大,我们的第一想法是取字典序最大的整数。对于 37、358,明显 37 的字典序较大,因此选择 37。但是当某个数是其他数的前缀时,我们会遇到问题,考虑3、31、37,按照字典序的大小来获得最大整数,我们可以得到 37313,但实际的最大整数是 37331,如何解决这个问题?考虑 3、31 与 31、3,我们可以把他们连起来比较,即比较 331 与 313 的字典序,这样我们就可以得到正确的组合。因此我们可以按照类似冒泡排序的思想来从候选数字中应该首先选择的数字,先假设当前位置应放置 a、随后连接 a、b,得到 ab 和 ba,比较两者字典序大小,假设 ab 字典序较大,当前位置仍然应该放置 a,则再用 a 与 c 进行相同的比较,若 ca 的字典序大于 ac,则当前位置放置 c 要优于放置 a,以此类推,最后筛选得到该位置应该放置的数字。
例三(区间覆盖)在0到L的数轴上有n个区间[li,ri],现在需要你选出其中尽量多个区间,使得其两两不相交。(n<=100000)
解:假设有位置 pos,我们已经选择了位置 pos 之前的区间已保证区间数量的最大化,我们要选择 pos 开始之后的区间。那么在以 pos 为左边界的区间中,我们应选择右边界尽量小的区间,这样做可以保证没有比起更优的选择。
例四(活动安排)给n个活动,每个活动需要一段时间Ci来完成,并且有一个截止时间Di,当完成时间ti大于截
止时间完成时,会扣除ti-Di分,让你找出如何使自己所扣分的最大值最小。(n<=100000)
解:我们先选定一个序列 a 1 , a 2 , . . . , A , B , . . . a n a_1,a_2,...,A,B,...a_n a1,a2,...,A,B,...an ,交换A,B的位置,A之前的任意活动扣分不变,B之后的任意活动扣分不变,而A,B的扣分会发生改变,因此我们需要考虑A,B的前后顺序,来使得A,B中扣分较大的那个所扣分数相对较小,假设 A、B 之前活动所用总时间为 C,当 A 活动位于 B 之前时,则 A所扣分数为 m a x ( 0 , C + C A − D A ) max(0, C+C_A-D_A) max(0,C+CA−DA), B 所扣分数为 m a x ( 0 , C + C A + C B − D B ) max(0, C+C_A+C_B-D_B) max(0,C+CA+CB−DB);当 A 活动位于 B 之后时,则 A 所扣分数为 m a x ( 0 , C + C B + C A − D A ) max(0, C+C_B+C_A-D_A) max(0,C+CB+CA−DA),B 所扣分数为 m a x ( 0 , C + C B − D B ) max(0, C+C_B-D_B) max(0,C+CB−DB)。如果最后结果是 A 活动位于 B 活动之前,则需要满足 $ max(max(0, C+C_A-D_A), max(0, C+C_A+C_B-D_B)) < max(max(0, C+C_B+C_A-D_A), max(0, C+C_B-D_B)) , 化 简 该 不 等 式 , 即 有 ,化简该不等式,即有 ,化简该不等式,即有max(0, C+C_A+C_B-D_B)) < max(0, C+C_B+C_A-D_A) $,也即 $ D_A < D_B $,因此,若 D A < D B D_A < D_B DA<DB,则排列顺序为AB,否则为 BA。因此我们按照 D i D_i Di 对 a i a_i ai 进行排序即可。
例五 (国王游戏)
恰逢 H 国国庆,国王邀请 n 位大臣来玩一个有奖游戏。首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。然后,让这 n 位大臣排成一排,国王站在队伍的最前面。排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。
• 国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序,使得获得奖赏最多的大臣,所获奖赏尽可能的少。注意,国王的位置始终在队伍的最前面。
• 求获得奖赏最多的大臣获得的奖赏。
• 1≤ n ≤1,000, 0< a , b <10000。
解:与上一题思想一致。有两个相邻的大臣A,B,其左右手上所写数字分别为 L A , R B , L A , R B L_A,R_B,L_A,R_B LA,RB,LA,RB,假设其前边所有人左手数字上的乘积为 S,若两人站位为AB,则两个人中所获最大奖赏为 m a x ( ( S ∗ L A ) / R A , ( S ∗ L A ∗ L B ) / R B ) max((S*L_A)/R_A, (S*L_A*L_B)/R_B) max((S∗LA)/RA,(S∗LA∗LB)/RB),若站位为BA,则两个人中最大奖赏为 m a x ( ( S ∗ L B ) / R B , ( S ∗ L A ∗ L B ) / R A ) max((S*L_B)/R_B, (S*L_A*L_B)/R_A) max((S∗LB)/RB,(S∗LA∗LB)/RA),若最后答案中的站位中 A 位于 B 之前,则需要满足 m a x ( ( S ∗ L A ) / R A , ( S ∗ L A ∗ L B ) / R B ) < m a x ( ( S ∗ L B ) / R B , ( S ∗ L A ∗ L B ) / R A ) max((S*L_A)/R_A, (S*L_A*L_B)/R_B) < max((S*L_B)/R_B, (S*L_A*L_B)/R_A) max((S∗LA)/RA,(S∗LA∗LB)/RB)<max((S∗LB)/RB,(S∗LA∗LB)/RA),化简上述不等式得,$ R_A < R_B$。因此只需要按照右手上数字从小到大排列每个大臣即可以使得到最大奖赏的大臣得到的钱数尽可能的少。
课后习题传送门:课后习题