枚举、贪心


去重、排序

明明的随机数 NC16669
明明想在学校中请一些同学一起做一项问卷调查,为了实验的客观性,他先用计算机生成了N个1到1000之间的随机整数,对于其中重复的数字,只保留一个,把其余相同的数去掉。然后再把这些数从小到大排序,按照排好的顺序去找同学做调查。请你协助明明完成“去重”与“排序”的工作。

143141521
思路一:可以先去重再排序。
去重:可以用一个数组标记,赋初值为1,如果有元素出现第二次,则标记为0,得到去重后的数组。
比如序列143141521,得到标记数组
b[]的值为111000110。进而得到去重后的序列为14352

冒泡排序:把所有数字都想象为一个气泡,数字小的气泡小,数字大的气泡大;每次比较相邻两个数的大小,比如序列14352,先比较1和4,大的气泡上浮,4放在1后面;再比较4和3,大的气泡上浮,4放在3后面,序列变成13452;再比较4和5,大的气泡上浮,5放在4后面;再比较5和2,大的气泡上浮,5放在2后面,序列变成13425。进行这样一遍气泡上浮的操作,使得最大的气泡一定上浮到最后。接下来再进行第二遍比较,但是这次比较局限在前4个数字里面(因为最大的气泡已经上浮到最后了,就不管它了)……使得第二大的气泡上浮到倒数第二个位置……经过5遍这样的排序后,排序完成。

思路二:也可以先排序再去重。
排序:143141521排序后得到结果111123445
去重:相邻元素之间进行比较,进行去重。

思路三:统计的思想
类似点名签到的思维,用一个数组b[]进行统计,其中b[i]表示这个数出现过没有。
数组b[]初值为0,数字1出现,给b[1]赋值为1,表示1出现过;数字2出现,给b[2]赋值为1,表示2出现过……最后从小到大循环一遍,如果b[i]==1说明元素 i 出现过,进行输出。实现了去重+排序,(叫做桶排序)用到了统计的思想。

总结一下:
1.对数列进行去重
没有标记的元素和他后面的元素两两比较,相同的则把后一个标记为不要,对去重之后的数组再排序(以冒泡为例)。
2.对数列进行排序
对数列进行排序(以冒泡为例),然后从小到大遍历,若当前a[i]和a[i-1]相等就不输出,否则就可以输出。
3.统计的思想
用一个数组b[i]表示i有没有出现过,每读入一个x ,就将b[x]赋值为1,表示x出现过了,最后从0到1000遍历b数组,如果b[i] =1 就输出i。


空间复杂度

int 32位二进制

int

long long 64位二进制
unsigned 用于修饰 int 或者long long ,将符号位变为数值位
double 64位二进制 科学计数法存储 eg:2.33*10^2333
double
char 8位二进制
bool 只有0和1 但还是8位二进制

计算变量所占的空间
8位二进制 = 1个字节
1024字节 = 1KB
1024kB = 1MB
1024MB = 1GB
比赛时题目的空间限制位512M,在没有递归等其他消耗下,且只需要开一个int类型的数组,那么这个数组最大可以开到什么数量级? 108

一个长度为10^6的double类型数组占多少空间? 8M左右


枚举

例1:最大正方形
在一个N*N(N<=100)矩阵中求一个最大的正方形,使得该正方形的四个顶点都由字符“#”构成。

#*#***
******
#*#*#* 
******
#*****
***#**

思路一:写一个四重for循环,枚举4个#,看是否能够组成一个正方形。(肯定TLE)

思考:几个点能确定一个正方形?
假设知道A、C两点,能否表达出B、D两点的坐标?
连线AC、BD,交点坐标为((xA + xC) / 2, (yA + yC) / 2)
因此得到:
B点的纵坐标:(yA + yC) / 2 - (xC - xA) / 2
B点的横坐标:(xC + xA) / 2 - (yC - yA) / 2
D点的横纵坐标同理可求得。(就是加减号变了一下)
得到B、D点坐标后,只需枚举A、C两个点的坐标,看B、D两点是不是#,就可以确定是否为正方形。

思考:几个点能确定一个长方形?

如果这个长方形的四条边与横纵坐标平行,几个点能确定一个长方形?


前缀和

例2:数列求和问题
给你一个数列{an}(1≤n≤100000),有q(1≤q≤100000)次询问,每次询问数列的第li个元素到第ri个元素的和。

暴力:每次询问扫一遍数组求和,时间复杂度O(q*n)。

思考:主要的时间复杂度瓶颈在哪里?
对区间的查询需要将整个区间扫一遍

考虑如何进行转化
——将对区间的查询变为对区间端点的查询

我们可以用sum[i]存储前i个数的和,那么sum[i] = sum[i-1] + a[i],当我们要查询第li个
元素到第ri个元素的和时,用sum[ri] - sum[li-1]即可。
这样单次查询的复杂度是O(1)的,总复杂度是O(n+q)的。
这里的sum[i]是前缀和


差分

例3:数列修改问题
给你一个数列{an}(1≤n≤100000),有q(1≤q≤100000)次修改,每次把数列中的第li到第ri的每个元素都加上一个值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个数和其前一个数的差值的办法叫做差分

差分和前缀和是一对对称的操作(即对差分数组求前缀和就是原数组,对前缀和求差分也会得到原数组)


尺取

扩展:校门外的树 NC16649
某校大门外长度为L的马路上有一排树,每两棵相邻的树之间的间隔都是1米。我们可以把马路看成一个数轴,马路的一端在数轴0的位置,另一端在L的位置;数轴上的每个整数点,即0,1,2,…,L都种有一棵树。

由于马路上有一些区域要用来建地铁。这些区域用它们在数轴上的起始点和终止点表示。已知任一区域的起始点和终止点的坐标都是整数,区域之间可能有重合的部分。现在要把这些区域中的树(包括区域端点处的两棵树)移走。你的任务是计算将这些树都移走后,马路上还有多少棵树。

原题数据范围:1≤L≤10000和1≤M≤100
暴力

扩大数据范围:1≤L≤100000和1≤M≤100000
将数组元素初始化为1,挖树就相当于对区间内元素-1,最后统计区间内等于1的元素的数量。

继续增大数据范围:1≤L≤10^9和1≤M≤100000
数组开不下了。离散化。(还不是太懂)


例4:给定长度为n的正整数数列以及整数S,求出总和不小于S的连续子串的长度的最小值,如果解不存在,输出0。
暴力:枚举子串的左界L和右界R,时间复杂度是O(n3),(枚举左界,有界,遍历整个区间)也就是枚举所有情况。

前缀和:前缀和求区间L到R的和,从L到R的循环优化掉了,只需要枚举左右界,时间复杂度是O(n2)。

尺取(双指针):假设L固定不动,R往右移,第一次 ≥ s时:
在这里插入图片描述
R不需要往后移,看L能否往后移:(R没必要往左移)
在这里插入图片描述
如果右移之后 ≥ s:
在这里插入图片描述
L继续向右移,否则R向右移。

再模拟一遍:
最开始的状态:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
就好像是拿着一把尺子,不断右移,左界和右界只右移,不回溯,因此时间复杂度是O(n)的。

这种方法叫尺取法,也叫双指针法。

在这里插入图片描述


状态压缩

例5:有一个N*M的灯泡,(N<=10,M<=100),每次按某一个点可以使得其本身以及其上下左右共五个的灯的开关反向。给定初始状态(每个灯泡的亮或者灭),问:能否把所有灯都灭掉?

观察题目,可以发现:
①每个灯只能按一次。
②按了第一列的灯,后面列的按法也就随之确定了。

先把第一列的灯按灭,后面的灯的按法唯一确定,看最后一列的灯是否都灭。如果都灭,说明方法可行;如果没有都灭,说明方法不可行,从第一列重新枚举。

怎样枚举第一列?
把亮记为1,灭记为0;把按记为1,不按记为0;把按或不按的策略变为一个二进制字符串(可用int类型存),进行枚举。(状态压缩)


位运算

<< 左移
1011 << 1 = 10110
在数值上等价于 乘2
>> 右移
>1011 >> 1 = 101
在数值上等价于 除以2
~ 取反
int a = 7;
~a = -8;
^ 异或
^1 取反
^0 没变

位运算一定注意位运算优先级问题!!!


贪心

贪心算法(又称贪婪算法)是指:在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。

能够使用贪心算法的问题都是能严格证明贪心出的局部最优解就是所求的全局最优解的。

每次都选看起来最好的!

例1:排队接水
有n个人在一个水龙头前排队接水,假如每个人接水的时间为Ti,请编程找出这n个人排队的
一种顺序,使得n个人的平均等待时间最小。
n<=1000,ti<=1e6

平均等待时间最小,也就是让总等待时间最小。
排序,接水快的人先接水。


例2:连数问题
设有n个正整数,将它们连接成一排,组成一个最大的多位整数。
例如:n=3时,3个整数13,312,343,连成的最大整数为34331213
又如:n=4时,4个整数7,13,4,246,连成的最大整数为7424613
(n<=1000,每个数都在int范围内)

根据数的最高位进行排序,最高位一样比较第二位,第二位一样比较第三位……

C++的string比较大小,就是按照字典序进行的。但是这道题不能用字典序,举个反例:3,31,39 按照字典序是33139,然而它们能组成的最大数字是39331。

思路:可以先把数连起来,再比较大小。
因为393大于339,所以39应该排在3的前面。


例3:区间覆盖(工作安排)
在0到L的数轴上有n个区间[li,ri],现在需要你选出其中尽量多个区间,使得其两两不相交。(n<=100000)
在这里插入图片描述
策略:选取结束时间尽量早的。

循环不变式证明贪心。


例4:活动安排
给n个活动,每个活动需要一段时间Ci来完成,并且有一个截止时间Di,当完成时间ti大于截止时间完成时,会扣除ti-Di分,让你找出如何使自己所扣分的最大值最小。(n<=100000)

题意:使扣分扣得最多的那个活动,扣的分最少。

相邻的两个活动A和B,交换AB不影响其他活动,那么就尝试交换A和B,最后得出结论:
截止时间小的在前面


例5:国王游戏
恰逢 H 国国庆,国王邀请 n 位大臣来玩一个有奖游戏。首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。然后,让这 n 位大臣排成一排,国王站在队伍的最前面。排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。
国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序,使得获得奖赏最多的大臣,所获奖赏尽可能的少。注意,国王的位置始终在队伍的最前面。求获得奖赏最多的大臣获得的奖赏。1≤n≤1,000, 0<a,b<10000。

与上一题相似,假设A和B是相邻的两位大臣,交换A和B的顺序,既对AB两人前面的人不产生影响,也不对AB两人后面的人产生影响。

如果A在B前:
A:π / RA ,记为①;B:π * LA / RB ,记为②
A:π * LB / RA ,记为③;B:π / RB,记为④

假设A在B前更好:
max(①,②) ≤ max(③,④)
由于① ≤ ③,② ≥ ④,则 π * LB / RA ≥ π * LA / RB
所以得到LARA ≤ LBRB
也就是说,我只需要按照每个人左手乘右手的乘积,从小到大排序,就是最终的队伍顺序,进而得到获得奖赏最多的大臣获得的奖赏。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值