Introduction to Computing B (yhf) Cheetsheet
计算概论 B (实验班) 机考材料
考试地点:机房 6B 座位 71 http://cs101.openjudge.cn
提醒
读题:抄录关键字。写程序前要画出流程图,如标清楚空位从何而来,以防重复或遗漏
debug:(1)把题目一字不落地读 2-3 遍,再看看你的程序;(2)对照测试数据(边界数据)时,走程序流程,笔脑结合,更清晰准确。
时间复杂度:简单题别想复杂。
- 输入数据 $10^4$,$O(n^2)$ 差不多可以 AC。
- 如果$10^5$以上,需要考虑 $O(n\log n)$ 方法。
- $10^2$ ,可以多重循环,但要注意遍历的顺序。要求按字典序排,则必须从前位往后位遍历。后位的
range
参数当然可以与前位依赖!range(b, a)
OJ02810: 完美立方
math:Python 支持大整数运算(这点比 C 好太多了),但是注意数很大的时候运算可能会很慢,所以应当用提前取模的办法尽量避免超大数的运算(OJ02706 麦森数)。
- 有除法的时候如果确保整除,用//代替/(OJ18155 组合乘积)。除法还要注意是否可能出现除以 0 的情况(OJ18223 24 点)
- 还要再注意:取模 $x % n\in[1,n)$、整除 $x//n=a…r$ 是向下取整
- 舍入时注意 round 不是严格意义上的四舍五入,遇到恰好.5 会向偶数舍入。floor 和 ceil 是安全的(当然如果刚好是整数也可能会有精度方面的问题)
- float 的等于判断不能用“==”,return abs(f1 - f2) <= allowed_error 。例如:OJ12065 方程求解,可以 allowed_error = 1e-12
列表:反复 remove 很有可能导致超时,这里的办法一般是==开一个真值表先打标记(参见“懒删除”)==
- 遍历列表的时候通常用 for 循环;但是尽量避免一边循环一边删除/添加元素。如果必须的话建议改用 while 循环。
- 匿名函数 lambda x,y:f(x,y) 有时候比单独定义一个函数方便得多(尤其在作为 key 参数的时候)
数据预处理:预处理是“打表”的一种最简单也最常用的方式,预先记录某些信息以便处理问题时直接调用,避免重复计算。常见方法如记录“前缀和”以快速得到任意连续段的和(这种方法还可推广到高维的前缀和),利用差分数组以方便对连续段的同时更新(一段同时加 x 反映在差分数组上就是头加 x,尾减 x;再用前缀和还原即可),打下标和值的对应表以方便查找值在列表中的位置,打真值表以方便查找值是否出现过(用集合也可以,但效率稍差),打计数桶以方便查找值出现的次数等。
维护适当的信息:这里所说的信息和预处理中的不同,它们在任务执行过程中可能改变。如果维护太多的信息,在更新信息时可能很麻烦;如果维护太少的信息,求解问题时又可能需要复杂的运算。寻找合适的信息来维护,既便于更新,又便于求解(或者说分摊更新与求解的复杂度),常常是这类问题的关键。
通常会考虑维护一些特征量,例如最大值、最小值、端点/边界点、某个特征值出现次数、某个极端位置的量等;这些特征量足够提供求解问题的信息,而又不像全状态那样难以维护。
(这样的题目很多,在很多算法中其实也都有这样的思想;例子暂时想不起来了,大家可以自行补充)
数据的输入与输出
列表常见输入输出。矩阵的输入输出,见“矩阵”模块
1 | # 列表的输入——从空格分隔变成列表(妙招) |
深拷贝列表:使用列表推导式!或者 deepcopy
1 | li = [1,2,3,4,5] |
整数保留最后$n=9$位,小数保留前$n=9$位:
1 | a[i]=(a[i-1]+a[i/2])%1000000000;*//取余是为了只输出最后9位* |
1 | for index,value in enumerate(list) |
基本数据结构的语法技巧
字符串直接进行><=比较:按照字典序
数、字符
虚数:complex(real,imagine)
或complex("1+2j")
(不能带空格)
str()
处理列表等元素序列, 不会删去括号
和""
; 而repr()
方法可以将读取到的格式字符,比如换行符、制表符,转化为其相应的转义字符。
eval()
函数,执行括号内的字符串
tuple(),set(),list()
用法相似, list([str])
可以拆分字母; 还有frozenset()
dict()
的使用方法如下
1 | dict(a='a', b='b', t='t') # 传入关键字 |
ord()
把字符转换成 ASCII 码,在处理字典序问题时常用。
chr()
把 ASCII 码转换成字符。gap = ord('a') - ord('A')
hex()
16 进制数,oct()
8 进制数,bin()
二进制
1 | s = input() |
字符串
str.lstrip() / str.rstrip()
移除字符串左侧/右侧的空白字符。
str.find(sub)
: 返回子字符串sub
在字符串中首次出现的索引,如果未找到,则返回-1。
str.replace(old, new)
: 将字符串中的old
子字符串替换为new
。
str.startswith(prefix) / str.endswith(suffix)
: 检查字符串是否以prefix
开头或以suffix
结尾。
str.isalpha() / str.isdigit() / str.isalnum()
: 检查字符串是否全部由字母/数字/字母和数字组成。
str.title()首字母大写(每个单词),str.lower()/upper()每个字母小/大写,str.zfill()自动在前面补 0 补到所需位数
其他
int(str,n)
for key,value in dict.items()
dict.get(key,default)
math.pow(m,n)
math.log(m,n)
lrucache
工具
正则表达式语法、ASCII 码
![]() | ![]() |
推荐数据结构、常用库
试试使用桶排序(分区间,分治)、堆、优先队列 quene ||||||| 字典、集合、stack
集合打表能过 。set, frozenset (=保证元素唯一性的 tuple) ——set/dict 插入都是 O(1)。例外:用 set/dict 处理保证元素唯一性问题。368B. Sereja and Suffixes
本来使用集合处理“不同元素”的条件,发现集合的 add 方法有最坏时间复杂度$O(n)$;后用字典依然 TLE 于 test11,询问 GPT 得知:当向字典中添加键值对时,如果该键值对的哈希值与字典中的其他键值对发生冲突,将会触发重新哈希和重新分配内存的操作。这种情况下,添加键值对的效率可能会降低,并**导致最坏情况下的时间复杂度$O(n)$**。
① 如何去重效率最高?最后再去重!② 实在过不了——使用 DP 思想解题。
from itertools import permutation
from collections import defaltdict
:更省事,不用判断 key 有无
1 | import heapq |
math
1 | import math |
lru_cache
@lru_cache(maxsize=None)
。使用@lru_cache 装饰器时,应注意以下几点:① 被缓存的函数的参数必须是可哈希的,这意味着参数中不能包含可变数据类型,如列表或字典。② 缓存的大小会影响性能,需要根据实际情况来确定合适的大小或者使用默认值。③ 由于缓存中存储了计算结果,可能导致内存占用过大,需谨慎使用。④ 可以是多参数的。
bisect(二分查找)
1 | import bisect |
年份 calendar 包
1 | 1. `calendar.month(年, 月)`: 返回一个月份的日历字符串。它接受年份和月份作为参数,并以多行字符串的形式返回该月份的日历。 |
Counter 包、Permutations 包
1 | from collections import Counter # O(n) |
default_dict
defaultdict 是 Python 中 collections 模块中的一种数据结构,它是一种特殊的字典,可以为字典的值提供默认值。当你使用一个不存在的键访问字典时,defaultdict 会自动为该键创建一个默认值,而不会引发 KeyError 异常。
defaultdict 的优势在于它能够简化代码逻辑,特别是在处理字典中的值为可迭代对象的情况下。通过设置一个默认的数据类型,它使得我们不需要在访问字典中不存在的键时手动创建默认值,从而减少了代码的复杂性。
使用 defaultdict 时,首先需要导入 collections 模块,然后通过指定一个默认工厂函数来创建一个 defaultdict 对象。一般来说,这个工厂函数可以是 int、list、set 等 Python 的内置数据类型或者自定义函数。
1 | from collections import defaultdict |
其他
1 | # 累积 |
矩阵
1)横纵坐标看清楚了!==“横 x 纵 y”==还是==“行 x 列 y”==,完全不一样!2)矩阵的字典序比较;
1 | # 3)输入。是否split看题意!——————①加保护圈输入 |
4)防止遍历矩阵越界的处理方式
①对于“仅扫描邻居(最多越界一位)”的题目:可以考虑加全 0 或全-1 保护圈(508A. Pasha and Pixels
,12560: 生存游戏
);
②对于“任意扫描半径”的题目:边界检查:for 循环中,range 使用 min/max 避免越界(02659:Bomb Game
)。感觉到矩阵题目的序号问题犯错频率减少了。
5)矩阵旋转题(Perfect Square)技巧总结
① 用 ord('a')
转换为 ASCII
② 旋转 90° 均相等 $\Leftrightarrow$ 中心+轴对称
③ 题解中用 ~j=-j-1
,~
为按位取反,用于此题表示对称的索引值,是妙用。
【OJ18106: 螺旋矩阵】
- 对于无固定步长、有边界限制的操作,可以通过构造边界(如
[-1]
),并在操作中添加边界识别步骤,实现“撞南墙则回头”。 - 有向操作如果有周期,可新建变量
d
、方向列表dirct = []
,并用d%4
(n)表示方位; - 有向操作对前景的感知,需要在换向后进行;
1 | def fill(n, mx): |
【OJ04133: 垃圾炸弹】
- 在某垃圾周围多大范围内,只要投掷炸弹必能炸掉此垃圾?将此范围内的值 +1 ,表示在此处投放炸弹能炸掉 1 个垃圾(即“此垃圾”)
- 相似地,考虑其他垃圾。如果某处值为 2,则表示在此处投放炸弹能炸掉 2 个垃圾
- 遍历整个城市,找到最大值,以及最大值出现了几次。
逆向思考,从垃圾思考,而不是从炸弹思考。
动态规划 dp
递推计数
这类问题严格来说不算 DP,就是用递推方法解决问题。最简单的譬如斐波那契型数列(OJ02786 Pell 数列,OJ23556 小青蛙跳荷叶),分拆数(OJ04119 复杂的整数划分问题)以及某些不定方程的解数之类的。
稍微难一点的例子譬如 OJ09267 核电站(不太容易建立递推),OJ04150 上机(首先要分析出题目需要满足的条件,转化为一个纯数列问题;其次需要按末位分类进行递推),OJ25573 红蓝玫瑰(需要加上一个数列,记录翻转过的序列所需的操作次数)
这类问题关键往往在于建立递推关系。无法直接建立递推关系时可以考虑引入若干辅助问题一并考虑:如果 A 问题能转移到 A 的同构问题和 B 的同构问题之一,而 B 问题也是如此,则同样可以递推解决。
一维 dp
模型 1:【最大连续子列和】每一状态两种想法——① 继承,② 重做。选择 max 即可。返回dp[-1]
模型 2:【最长上升子列】每一状态两种想法——① 放入序列(继承前面最大可接项),② 不放入序列,继承前一项。选择 max 即可。返回max(dp)
还有一个 nlogn 的算法,非常巧妙(我自己肯定想不到)。维护数组 d,d[i-1]表示长为 i 的递增子序列中末位最小值。可以非常巧妙地维护 dp 数组,需要应用二分查找工具。
1 | import bisect |
模型 2 变式 1:【最大上升子序列和】把 dp 代表什么换了就行
模型 2 变式 2:【02711 合唱队列】每一状态两种想法——① 放入序列(继承前面最大**==可接项(如何判断是否可接)==**),② 不放入序列,继承前一项。返回max(dp)
模型 2 变式 3:【1195C Basketball Exercise】对于每一个相同编号的球员——① 选上面的/选下面的,③ 都不选(第一个编号是三种情况)
二维 dp
模型 3:【最长公共子列】每一状态两种想法
① 如果是公共的,这个状态的前一状态是两个子列都还没看到这个公共元素的时候,所以是dp[i-1][j-1]+1
;
② 如果不是公共的,这个状态的前一状态是两个子列 还没看到这个元素的时候
上面的 dp 分别表示什么?
注:最长回文子序列问题可化归为最长公共子序列问题:可以证明,A 的最长回文子序列长度等于 A 与 reversed(A)的最长公共子序列长度(并不显然)。例如 OJ01159 Palindrome 就可如此解决。
模型 4:数塔 dp
① 计算从根节点到第 $i$ 层每一个节点的最大和,并以之替代该节点。
② 已知根节点到第 $i$ 层每一个节点的最大和,又已知第 $i+1$ 层各节点的值,可据此算出第 $i+1$ 层各节点的最大和。
背包 dp
0-1 背包(原子性,课程学习 etc.)
==偷商品的一部分(不具有原子性),使用贪心算法==
有 $n$ 件物品,每件物品的重量为 $w[i],$ 价值为 $c[i]$。现有一个容量为 $V$ 的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。
其中每件物品有且仅有 1 件.
考虑取前 i 个物品用 t 时间所能得到的最大值,枚举第 i 个物品是否取——
A)对于每一种时间情况下,能不能采某一种药,取决于:
- 总时间
t
是否大于 该药采集所耗时md[i][0]
如果时间不允许,则继承上一种药,dp[i-1][t]
B)对于每一种时间情况下,要不要采某一种药,取决于:
- 不采,即该情况下,总价值为
dp[i-1][t]
- 采,即该情况下,总价值为
dp[i-1][t-md[i][0]]+md[i][1]
何者更大。==注意一定要用 (i+1) * (t+1) 的 dp 表!把 0 算进去==
——来完成转移。注意这里加上时间参数 t,因为转移过程中 t 的限定可能会变。“加参数”是 DP 问题中最重要的技巧之一。
1 | dp = [0]*T |
这里采用“滚动数组”的方法将二维数组压缩成一维,是 DP 问题中常用的技巧。这基于选前 i 个物品的状态仅依赖于选前 i-1 个物品的状态。注意内层循环要倒着遍历!
输出最佳方案——如果需要得到最佳方案,使用二维数组,使用回溯。 (注意: 若使用滚动数组,不能直接利用回溯求解方案)
**==初始化技巧==**——初始化的 F 数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为 0 的背包可以在什么也不装且价值为 0 的情况下被“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,应该被赋值为 -∞ 。
如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为 0,所以初始时状态的值也就全部为 0 了。这个小技巧完全可以推广到其它类型的背包问题。
完全背包
将 0-1 背包中内层循环改为正着遍历即可(这里其实就利用了先前已经得到的信息来简化转移:在先前的转移中物品 i 可能已经用过若干次了)
多重背包
有 $n$ 件物品,每件物品的重量为$w[i],$ 价值为$c[i]$。现有一个容量为 $V$ 的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。
其中每件物品有$s[j]$件.(==和 0-1 背包的本质区别==)
最简单的思路是将多个同样的物品看成多个不同的物品,从而化为 0-1 背包。
前情提要:二进制优化——用二进制的各位【1, ~2, ~4, ~8, ~16, ~32, ~64, ~128 …】作**==桶==**,需要时按需凑即可。
——编号最大(如 ~512)这个桶,可能装不满,比如说,装了 100 个。那么,当我需要凑一个大于 100 的数时,优先用这个桶,剩下再用其他桶找零。
二进制优化算法:
1 | dp = [0]*T |
变式
如果需要求出所有可能达到的值,需要用集合更新,状态设计方式不变(OJ01742 coins)
有时候可能需要考虑在给定所选物品的数量的情况下最优化,这时 dp 数组要多带一个数量参数(忘记哪个题了)
有时候背包问题的限制可能更多,需要加更多的参数(OJ04102 宠物小精灵之收服;这个题还有一个要点是更换参数的选取。设计 DP 状态时不一定把要求的作为最终结果,有时把要求的东西作为参数会更方便)
注:背包问题的 DP 解法需要时间 T 不太大,因为要遍历每个可能的 T。如果 T 很大而物品数量很少,采用 DFS 枚举物品的选法有时是更好的选择。
采用合适的顺序遍历
某些问题看起来不方便用 DP 解决,因为状态似乎是无序的;但是只要能够找到状态的某个特征量有序,就可以以此为顺序完成 DP(当然这类问题最省脑子的办法参见“记忆化搜索”)
OJ01088 滑雪:要求最长的递降通路长度,状态转移是容易的,但是顺序并不易确定。注意到每步总是往更低的地方滑,按高度从小到大遍历即可。
OJ01191 棋盘分割:每次分割后棋盘会变小,按棋盘的大小从小到大遍历即可(当然这样的话状态比较复杂;这题其实更适合记忆化搜索)
OJ01661 帮助 Jimmy:每次会往下跳,按高度从下往上遍历即可;为了减少状态数,可以只考虑横坐标为某个平台端点的点(DP 相比记忆化搜索最大的缺陷就在于要设计出能够遍历的状态,而且要尽量减少状态数量)
目前就只能想到这么多了。。。。。。DP 无处不在,肯定还有很多技巧方法没有写到,啥时候想起来再补吧。
dp 技巧
【CF189A: Cut Ribbon】
恰好切分:题目需要 Ribbon 被恰好地切分,因此状态转移方程只能为
1
dp[i] = max(dp[i-a], dp[i-b], dp[i-c]) + 1
而不能为
1
dp[i] = max(1+dp[i-a], 2+dp[i-b], 3+dp[i-c], dp[i-1])
后者意味着:长度 i 的 Ribbon 继承 长度 i-1 的 Ribbon 的切法,即有边角料。
注意 Python 中负数列表索引不报错,被认为是列表末尾。
初始化:以 $-\infty$ 初始化列表非首项,是为了保证:
- 特别地,在 Ribbon 达到可被切分的最小长度前,Ribbon 的切分不会被考虑;
- 对于某一特定长度,如果不存在一种切法,使得 Ribbon 被恰好地切分,那么该长度的 Ribbon 永远不会成为某一 Ribbon 的子 Ribbon,参与切分方法的考虑。
特别地,
-inf
在 Python 中表示 $-\infty$ 。
深搜 DFS
==枚举==所有完整路径,以遍历所有情况。不适合解决【障碍物少、空位多 → 路径可能性极多】的情况。
递归深度调整——sys.setrecursionlimit(1 << 30)
步骤:
从起点开始走;
遇到==分岔路==进行标记,
for i in range(n)
,沿着第i
个分岔路继续前进;(分岔?例如if...else...
,max[A, B]
,min[C, D]
。有分叉都能深搜。)遇到死路【==递归边界==】则
return
上一个分岔路口(==回溯==),i-->i+1
,下一个分叉路继续探寻,标记新建数组或在已有点上。如果达到终点,就
print(ans); return
。
如果搜索超时,可以考虑进行剪枝,以避免搜索不满足约束条件的路径。
这样说来,==递归中的递归式就是岔道口,而递归边界就是死胡同==,
1 | #示例代码:19930寻宝 |

广搜 BFS【图转树】
① 定义队列 q,并将起点 s 入队。
② 写一个 while 循环,循环条件是队列 q 非空。
③ 在 while 循环中,先取出队首元素 top,然后访问它(访问可以是任何事情,例如将其输出)。访问完后将其出队。
④ 将 top 的下一层结点中所有==未曾入队==的结点入队,并标记它们的层号为 now 的层号加 1,同时设置这些入队的结点已入过队。
⑤ 返回 ② 继续循环。
强调:在 BFS 中设置的 inq
数组【inq
数组一般开到“遍历”的所有可能】的含义是==判断结点是否已入过队==,而不是结点是否已被访问。区别在于:如果设置成是否已被访问,有可能在某个结点正在队列中、但还未访问时由于其他结点可以到达它而将这个结点再次入队,导致很多结点反复入队,计算量大大增加。因此 BFS 中让每个结点只入队一次,故需要设置 inq 数组的含义为结点是否已入过队而非结点是否已被访问。
入队了一定会被访问,因此要从入队开始卡人,才及时。
1 | # 示例代码:02802小游戏伪AC代码,只计算了步数,未换算成线段数 |
二分查找/二分法/双指针
二分查找使用条件:**==① 有序序列;==**② 数据量适中,太小提升少,太大 MLE;
1 | import bisect |
时间复杂度 $O(\log n)$。二分查找代码实现
1 | arr = [1,2,3,3,3,4,5] |
双指针
本质还是遍历。但通过双指针,证明好了,可以
左右指针:需要两个指针,一个指向开头,一个指向末尾,然后向中间遍历,直到满足条件或者两个指针相遇。
快慢指针:需要两个指针,开始都指向开头,根据条件不同,快指针走得快,慢指针走的慢,直到满足条件或者快指针走到结尾。
后序指针:常规指针操作是从前向后便利,对于合并和替换类型题,防止之前的数据被覆盖,双指针需从后向前便利。
==记忆口诀:左右指针中间夹,快慢指针走到头,后序指针往回走==
做形如 n 数之和——双指针!
思路:
注意点
while 的条件不会继承。大 while 内的小 while 还得限制 L<R,否则会一下子就不满足大 while 条件,可能导致报错(IndexError)。数据
0 0 0
做好输出:就算没法走
arr[i] > 0
的输出(考虑全 0),也得输出当前累计的counter
值。数据0 0 0 0
1 | # 三数之和 |
贪心
【OJ02431 expedition】这个题就需要一些思维上的转换了。直观上看应该每次加尽量多的油,但是在哪个范围里选最多呢?这里有一种看法:每次路过一个加油站就把油全拿上,但是不加;每次没油的时候再挑一桶加,这个时候挑的一定是目前手上最多的,于是问题迎刃而解。
这种“延迟操作”的思考方式在某些时候是很有用的。
【26971 分发糖果】:两次遍历方法,使得序列既满足“左规则”,又满足“右规则”
- 从左往右遍历,使得对于 cds[i],其与右边的元素 cds[i+1] 的关系总符合题意“谁大就谁分的多”。但要注意,此时对于 cds[i],其与左边的元素不一定满足题意。
- 从右往左遍历,使得对于 cds[i],其与左边的元素 cds[i-1] 的关系总符合题意“谁大就谁分的多”
【CF1000B: Light It Up(前缀和思想)】:有一台灯,这个灯在时间为$0$时打开,$m$时关闭,在$0$到$m$这段时间内有$n$个时间点灯的状态会改变(即开变关,关变开),在哪里插个点,使得这个灯亮着的时间最大。1)如果不插点,亮着的时间就是 所有 $i, i+1$ 的区间长度和。
困难在于:如何解决“插点改变状态”后,开、关操作对应索引值的奇偶性逆转的问题。
在插点后,“开”区间平移变为 $i+1,i+2$ ,并增加 $插点, i$ 。如果对于每次插点操作,都要重新计算“开”区间总长度,时间复杂度是 $O(n^2)$,对于 $n=10^5$ 的数量级无法承受。
解决方法:前缀和思想。思路是,新建“前(后)缀和”数组 pix = []
:
pix
中索引为 $i = 2k(k\in\Z^+)$ 的值,代表从前往后累计的,所有 $i, i+1$ 的区间长度和;pix
中索引为 $i = 2k-1(k\in\Z^+)$ 的值,代表从后往前累计的,所有 $i, i+1$ 的区间长度和。
这样,对于每次插点操作(插在 索引值为奇数 $j$ 的项 前面),“开”区间总长度则可用 $O(1)$ 的操作计算,公式为:
$$
\text{len} &=& \text{pix}[j-1] + \text{pix}[j+2] + [插点,i] \
&=& \text{pix}[j-1] + \text{pix}[j+2] +(\text{tds}[j+1]-\text{tds}[j]-1)
$$
- 为什么是 j+2 不是 j ?因为要挖掉 插点 所在的那个区间,同时增加 $插点, i$ 。
【18164 剪绳子(哈夫曼编码 Huffman 思想)】:剪绳子就是拼绳子,在一定的操作次数下,每次选择最短(heappop
)的两段绳子拼起来,拼后压回绳组(heappush
)。
==以谁为参照系,遍历谁???==
区间覆盖问题
区间合并
给出一堆区间,要求合并所有有交集的区间 (端点处相交也算有交集)。最后问合并之后的区间个数。
【步骤一】:按照区间左端点从小到大排序:
【步骤二】:维护前面区间中最右边的端点为ed
。从前往后枚举每一个区间,判断是否应该将当前区间视为新区间。
假设当前遍历到的区间为第i
个区间 $[l_i, r_i]$,有以下两种情况:
- $l_i\le ed$:说明当前区间与前面区间有交集。因此不需要增加区间个数,但需要设置 $ed=\max(ed, r_i)$。
- $l_i\ge ed$:说明当前区间与前面没有交集。因此需要增加区间个数,并设置 $ed=\max(ed, r_i)$。
选择不相交区间
给出一堆区间,要求选择尽量多的区间,使得这些区间互不相交,求可选取的区间的最大数量。这里端点相同也算有重复。
【步骤一】:按照区间右端点从小到大排序。
【步骤二】:从前往后依次枚举每个区间。
假设当前遍历到的区间为第i
个区间 $[l_i,r_i]$ ,有以下两种情况:
- $l_i\le ed$:说明当前区间与前面区间有交集。因此直接跳过。
- $l_i>ed$:说明当前区间与前面没有交集。因此选中当前区间,并设置 。
区间选点
给出一堆区间,取尽量少的点,使得每个区间内至少有一个点(不同区间内含的点可以是同一个,位于区间端点上的点也算作区间内)。
这个题可以转化为上一题的求最大不相交区间的数量。
对于这些最大的不相交区间,肯定是每个区间都需要选出一个点。而其他的区间都是和这些选出的区间有重复的,我们只需要把点的位置选在重合的部分即可。
也可以换一种思路:
我们将区间按照右端点从小到大排序,这时我们应该尽量选择当前区间最右边的点。
因为最右边的点可能和下面的其他区间重复,所以至少不比选择区间靠前位置的点差。
所以,最后的解法与选择不相交区间问题解法完全一样。
区间覆盖
给出一堆区间和一个目标区间,问最少选择多少区间可以覆盖掉题中给出的这段目标区间。
【步骤一】:按照区间左端点从小到大排序。
【步骤二】:从前往后依次枚举每个区间,在所有能覆盖当前目标区间起始位置start
的区间之中,选择右端点最大的区间。
假设右端点最大的区间是第$i$个区间,右端点为 $r_i$,
最后将目标区间的 start 更新成 $r_i$
区间分组
区间分组问题大概题意就是:给出一堆区间,问最少可以将这些区间分成多少组,使得每个组内的区间互不相交。
【步骤一】:按照区间左端点从小到大排序。
【步骤二】:从前往后依次枚举每个区间,判断当前区间能否被放到某个现有组里面。
(即判断是否存在某个组的右端点在当前区间之中。如果可以,则不能放到这一组)
假设现在已经分了 $m$ 组了,第 $k$ 组最右边的一个点是 $r_k$,当前区间 $i$ 的范围是 $[L_i, R_i]$。则:
如果 $L_i\le r_k$ ,则表示第 $i$ 个区间无法放到第 $k$ 组里面。
反之,如果 $L_i>r_k$, 则表示可以放到第 $k$ 组。
- 如果所有 $m$ 个组里面没有组可以接收当前区间,则当前区间新开一个组,并把自己放进去。
- 如果存在可以接收当前区间的组 $k$ ,则将当前区间放进去,并更新当前组的 。
注意:
为了能快速的找到能够接收当前区间的组,我们可以使用优先队列 (小顶堆)。
优先队列里面记录每个组的右端点值,每次可以在 的时间拿到右端点中的的最小值。
【27104 世界杯只因】:如何以尽可能少的摄像头,覆盖尽可能多的区域。
初始化 结果数组 各处均为 0。
- 结果数组每一位的值,表示 范围要覆盖到这里所需要的最少摄像头数目;
- 初始化的 0 表示 此处无法被覆盖到。
每一次:
对于第一个没被覆盖到的点,寻找一个摄像头,使得这个摄像头:
- 能覆盖到这个点 (==不一定在这个点上!==)
- 最省心——能覆盖尽可能多的区域(即右边界最大),这样下一次就可以节省摄像头
找到这个摄像头后,把这个摄像头覆盖的所有区域的值 都更新为所需的最少的摄像头数目。
下一次寻找没被覆盖到的点时,从这个摄像头的右边界开始寻找即可。
这个算法对于任给一族区间要找最小覆盖的问题都对。
【Radar Installation】:我们可以逆向思维来贪心。首先我们已经知道一个贪心模板——最小区间覆盖,且这题也很像它,那么我们就把这题转成这个模板。
我们发现可以覆盖某个小岛的雷达在海岸线上的坐标呈一条线分布。
那么我们就把这一段线看成一段区间,用雷达覆盖所有区间。
由勾股定理得第 $i$ 个小岛的区间为:
$(x−sqrt(d∗d−y_i∗y_i),x+sqrt(d∗d−y_i∗y_i))$
那么我们再按最小区间覆盖的做法,将右端点从小到大排序,然后取点,结束。
数学
素数筛法——欧拉筛
1 | def pre(n): |
OJ01218:THE DRUNK JAILER
:能够拨动奇数次的开关,必然是因子个数为奇数的开关。而因子个数为奇数,一定是完全平方数。因为因子是成对出现的,只有完全平方数才会有一个独自的因子。如 $36\to(1,36),(2,18),(3,12),(4,9),6=7$ 个因子。
OJ01008: Maya Calendar
:注意取模 $\in[0,除数-1]$,而题中日历 $\in[1,除数]$
defaultdict 使用方法
assignment9 好多没做,矩阵卷积运算
01922: Ride to Schoolhttp://cs101.openjudge.cn/practice/01922/
思路:
虽然是追及问题,但注意时间变化的连续性。不可用 t += 1 模拟时间变化。
向上取整 math.ceil()
==Charley 一定和某人一起到。==
脑筋急转弯
买学区房接收数据
pairs = [i[1:-1] for i in input().split()]
distances = [ sum(map(int,i.split(‘,’))) for i in pairs]
http://cs101.openjudge.cn/practice/19963
递归方法的技巧——想要一直往前冲,不回头,就把 n 弄成全局变量,而非函数的参数。【波兰表达式】
题目数据没说保序,就必须排序!
其他
OJ02707: 求一元二次方程的根
OJ4146:数字方格
:math
OJ02746: 约瑟夫问题
CF96A

树状数组、线段树(期中阶段の补充)
确实,dijksta 可以套 bfs 的堆写法模板
==dfs——把传入数据写到 def 上?TA-Hu Yang==
==dp——模版?==
优先队列????——顺序会改变!
from colletions import deque,双端队列,两边出数都快
背包问题 dp 模版!!!
CF230B、DrunkJailor
宽搜——heapq!!!//quene!!!(先进先出)
宽搜用字典记录也可,字典套 tuple,vis=dict()
all()
作业 | 没做出来的题目 |
---|---|
3 | 装箱问题 |
4 | CF1364A: XXXXX、OJ04146: 数字方格 |
5 | 12559: 最大最小整数 v0.3、230B. T-primes |
6 | 生存游戏 |
7 | 1793C. Dora and Search、368B. Sereja and Suffixes |
8 (Nov 月考) | 23566: 决战双十一 |
9 | 几乎没做 |
A | 认真看, https://codeforces.com/contest/455/problem/A |
B√ | 摆动数列 |
C√ | 乌鸦坐飞机——装箱问题 |
D√ | over,最多差一个 2050 成绩计算 |
打魔兽
set dict 双指针 能过
回溯
全排列、八皇后,生成方案,遍历方案
装箱——题解
Numpy×
经典问题
OJ25353: 排队
Greedy, http://cs101.openjudge.cn/practice/25353/
有 $N$ 名同学从左到右排成一排,第 $i$ 名同学的身高为 $h_i$。现在张老师想改变排队的顺序,他能进行任意多次(包括 0 次)如下操作:
- 如果两名同学相邻,并且他们的身高之差不超过 $D$,那么老师就能交换他俩的顺序。
请你帮张老师算一算,通过以上操作,字典序最小的所有同学(从左到右)身高序列是什么?
输入
第一行包含两个正整数 $N, D (1≤N≤10^5, 1≤D≤10^9)$。
接下去 $N$ 行,每行一个正整数 $h_i (1\le h_i \le 10^9)$ 表示从左到右每名同学的身高。
输出
输出 $N$ 行,第 $i$ 行表示答案中第 $i$ 名同学的身高。
样例输入
1 | 5 3 |
样例输出
1 | 6 |
提示
【样例解释】
一种交换位置的过程如下:7 7 3 6 2-> 7 7 6 3 2-> 7 7 6 2 3-> 7 6 7 2 3-> 6 7 7 2 3
OJ01017: 装箱问题
greedy, http://cs101.openjudge.cn/practice/01017
一个工厂制造的产品形状都是长方体,它们的高度都是 h,长和宽都相等,一共有六个型号,他们的长宽分别为 1*1, 2*2, 3*3, 4*4, 5*5, 6*6。这些产品通常使用一个 6*6*h 的长方体包裹包装然后邮寄给客户。因为邮费很贵,所以工厂要想方设法的减小每个订单运送时的包裹数量。他们很需要有一个好的程序帮他们解决这个问题从而节省费用。现在这个程序由你来设计。
输入:输入文件包括几行,每一行代表一个订单。每个订单里的一行包括六个整数,中间用空格隔开,分别为 11 至 66 这六种产品的数量。输入文件将以 6 个 0 组成的一行结尾。
输出:除了输入的最后一行 6 个 0 以外,输入文件里每一行对应着输出文件的一行,每一行输出一个整数代表对应的订单所需的最小包裹数。
解题思路:4*4, 5*5, 6*6 这三种的处理方式较简单,就是每一个箱子至多只能有其中 1 个,根据他们的数量添加箱子,再用 2*2 和 1*1 填补。1*1, 2*2, 3*3 这些就需要额外分情况讨论,若有剩余的 3*3,每 4 个 3*3 可以填满一个箱子,剩下的 3*3 用 2*2 和 1*1 填补装箱。剩余的 2*2,每 9 个可以填满一个箱子,剩下的与 1*1 一起装箱。最后每 36 个 1*1 可以填满一个箱子,剩下的为一箱子。
样例输入
1 | 0 0 4 0 0 1 |
样例输出
1 | 2 |
来源:Central Europe 1996
【张概论,中国语言文学系,2023 年秋】 ==(请改为同学的姓名、院系)==
思路:==(请改为同学的思路和代码)==
用时至少 3 个半天(早上/中午/晚上)
代码
1 | ## OJ1017: 装箱问题 |
代码运行截图 ==(AC 代码截图,至少包含有”Accepted”)==

Introduction to Computing B (yhf) Cheetsheet