第1章 枚举(尺取法、前缀和、差分等)、贪心
文章目录
一、基础知识
时间复杂度
- O(1)
- O(logn) 一般默认底数是2 是比较优的时间复杂度
- O(n**1/2)
- O(n) 线性
- O(n**2)
- O(n**3)
- O(C**n) 指数级 基本上跑不动了
- O(n!) 大于等于2**n小于等于n**n
一般10**8是可以在一秒左右能完成的,可以接受的,但是前面的常数不能太大
二、枚举(暴力)
一一列举
要点:不重复,不遗漏
优化枚举的基本思路: 减少枚举次数
1.选择合适的枚举对象
2.选择合适的枚举方向 – 方便排除非法和不爽最优的情况
3.选择合适的数据维护方法 – 转化问题
做暴力的时候一定要根据给的数据范围算一下时间复杂度有没有超过10**8,看要不要优化,下面是优化的技巧。
2.1 数列求和问题(前缀和)
问题:
给你一个数列{an}(1≤n≤100000),有q(1≤q≤100000)次询问,每次询问数列的第li个元素到第ri个元素的和。
解决思路
• 我们可以用sum[i]存储前i个数的和,那么sum[i] = sum[i-1] + a[i],当我们要查询第li个元素到第ri个元素的和时,**用sum[ri] - sum[li-1]**即可。
• 这样单次查询的复杂度是O(1)的,总复杂度是O(n+q)的。
• 这里的sum[i]是前缀和,前缀和也是算法竞赛当中非常常用的小技巧。
int sums[N];
int a[N];
sums[0]=0;
for(int i=1;i<n;i++)
{
sum[i]=sum[i-1]+a[i-1]
}
//或者直接用一个数组
a[0]=0
for(int i=1;i<n;i++)
{
a[i]+=a[i-1]
}
2.2 数列修改问题(差分)
问题:
给你一个数列{an}(1≤n≤100000),有q(1≤q≤100000)次修改,每次把数列中的第li到第ri的每个元素都加上一个值ki,求所有的修改之后每个数的值。
解决思路:
对每个区间的端点进行操作就行。第li和ri+1这俩个点,注意ri+1和ri间相差的是-ki,li和li-1相差的是ki
• 当我们对第li个到第ri个数加上ki时,第li个数与第li-1个数的差值增加了ki,第ri+1个数与第ri个数的差值减少了ki,而区间内部的相邻两个数的差值是不变的!
• 所以我们可以用数组delta[i]来维护第i个数和其前一个数的差值,(可以默认第一个数前面有一个0),然后当需要将[li,ri]区间的每一个数+ki时,只需要修改delta[li]和delta[ri+1]即可。
• 在所有的修改操作进行完之后,我们再对delta[i]求一次前缀和,就可以得到数列的每个元素的值了。
• 用数组delta[i]来维护第i个数和其前一个数的差值的办法叫做差分。
差分和前缀和是一对对称的操作(即对差分数组求前缀和就是原数组,对前缀和求差分也会得到原数组)
2.3 扩展问题:校门外的树
问题:
某校大门外长度为L的马路上有一排树,每两棵相邻的树之间的间隔都是1米。我们可以把马路看成一个数轴,马路的一端在数轴0的位置,另一端在L的位置;数轴上的每个整数点,即0,1,2,…,L都种有一棵树。 由于马路上有一些区域要用来建地铁。这些区域用它们在数轴上的起始点和终止点表示。已知任一区域的起始点和终止点的坐标都是整数,区域之间可能有重合的部分。现在要把这些区域中的树(包括区域端点处的两棵树)移走。你的任务是计算将这些树都移走后,马路上还有多少棵树。
解决思路:
转换成差分和前缀和的思想
初始所有点为0,对修地铁的区间-1即可(转为了数列修改的问题),最后算哪些点还是0。
进阶:当数据范围扩大到1≤L≤10^9和1≤M≤100000
(纯暴力的时候为O(L*M))
512MB时int的数组最多只能开10**8个,上面的进阶问题会限制空间大小。
解决方案:
进行离散化操作,不需要在数组存放每个点,把修改的区间的端点存下就行,排好序,算没有被覆盖的区域
2.4 尺取法/追逐法/Two-Pointer
问题:
给定长度为n的整数数列以及整数S,求出总和不小于S的连续子串的长度的最小值,如果解不存在,输出0
解决思路:
如果完全不优化的情况下,O(n**3)
求和的过程可以用前缀和来代替,此时为O(n**2)
用L和R两个pointer寻找,当L和R的区间总和第一次大于等于S时候,L往右移动,直到再次小于S,此时R往右移动,如此遍历,这样最终为O(n)
2.5 状态压缩(状压)
问题:
有一个N*M的灯泡,(N<=10,M<=100),每次按某一个点可以使得其本身以及其上下左右共五个的灯的开关反向。给定初始状态(每个灯泡的亮或者灭),问:能否把所有灯都灭掉?
解决思路:
只需要考虑第一列,因为第一列的状态确定下来后面几列的也确定了。每一列确保暗后,主要看最后一列的状态,因为它右边没有列不需要考虑。
第一列的状态用0,1的二进制串来表示,用int整型来存,
如7 =》 0111 就是状态压缩
时间复杂度O(2**N*N*M)
位运算
左移:等价于*2
右移:等价于除2
去掉最后一位
x>>1
在最后加一个0
x<<1
在最后加个1
(x<<1+1)
把最后一位变成1
x|1
把最后一位变成0
(x|1)-1
最后一位取反
x^1
把右数第k位变1
x|(1<<(k-1))
把右数第k位变成0
x&(~(1<<(k-1)))
右数第k位取反
x^(1<<(k-1))
三、贪心
定义:
总是做出当前看来是最好的选择,某种意义上的局部最优解
能够使用贪心算法的问题都是能严格证明贪心出的局部最优解就是所求的全局最优解的
3.1 排队接水问题
问题:
有n个人在一个水龙头前排队接水,假如每个人接水的时间为Ti,请编程找出这n个人排队的一种顺序,使得n个人的平均等待时间最小
解决思路:
总=T1+(T1+T2)+(T1+T2+T3)+…+(T1+T2+…+Tn-1)
=T1(n-1)+T2(n-2)+…+T(n-1)*1
即排序从小到大
3.2 连数问题
问题:
设有n个正整数,将它们连接成一排,组成一个最大的多位整数。
例如:n=3时,3个整数13,312,343,连成的最大整数为34331213又如:n=4时,4个整数7,13,4,246,连成的最大整数为7424613 (n<=1000,每个数都在int范围内)
解决思路:
原本思路,按照字典序排序,但是有特例,如13,3,31,明显33113大于31313。如果用特判来的话会很麻烦。更好的解决思路是如两个数AB,_AB_,前后都有数字,只要知道A和B谁大就行,若A大于B则组为_AB_
3.3 区间覆盖问题(工作安排)(经典)
问题:
在0到L的数轴上有n个区间[li,ri],现在需要你选出其中尽量多个区间,使得其两两不相交。(n<=100000)
解决思路:
贪心策略是选结束时间最早的,按照结束时间最早的来排序
3.4 活动安排(套路)
问题:
给n个活动,每个活动需要一段时间Ci来完成,并且有一个截止时间Di,当完成时间ti大于截止时间完成时,会扣除ti-Di分,让你找出如何使自己所扣分的最大值最小。(n<=100000)
解决思路:
还是_AB_的问题,AB交换顺序不影响前后,只要看A和B哪个在前,通过公式推导发现,若A要在B前则A的截止时间要小于B的截止时间才行
3.5 国王游戏
问题:
恰逢 H 国国庆,国王邀请 n 位大臣来玩一个有奖游戏。首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。然后,让这 n 位大臣排成一排,国王站在队伍的最前面。排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。 国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序,使得获得奖赏最多的大臣,所获奖赏尽可能的少。注意,国王的位置始终在队伍的最前面。 求获得奖赏最多的大臣获得的奖赏。1≤n≤1,000, 0<a,b<10000。
解决思路:
还是_AB_的问题,AB交换顺序不影响前后,只要看A和B哪个在前,通过公式推导发现,若A要在B前则A左手乘右手的乘积要小于B的,这样只要对左手乘右手的乘积进行排序即可。