算法题的时间复杂度是必须考虑的因素,n的数量级和复杂度二者可以决定算法的执行时间。在有进行时间限制的题中,一个过于复杂的题解会因超时而得不到全分。
本文记录了对观光景点组合得分问题的思考、分析和优化。
题目
www.marscode.cn/practice/r3...
方法一:BF
思路
这道题的暴力解法是采用 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2) 的遍历,枚举所有的两两组合,取最大值。
代码
python
def solution(values: list) -> int:
# PLEASE DO NOT MODIFY THE FUNCTION SIGNATURE
# write code here
ans = 0
l = len(values)
for i in range(l):
for j in range(i + 1, l):
ans = max(ans, values[i] + values[j] + i - j)
return ans
if __name__ == '__main__':
print(solution(values=[8, 3, 5, 5, 6]) == 11)
print(solution(values=[10, 4, 8, 7]) == 16)
print(solution(values=[1, 2, 3, 4, 5]) == 8)
复杂度
- 时间复杂度 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2)
- 空间复杂度 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)
思考优化点
这道题是leetcode第1014题,在力扣中明确提到了n最大可以取到 <math xmlns="http://www.w3.org/1998/Math/MathML"> 5 ∗ 1 0 4 5 * 10^4 </math>5∗104。而 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2) 的算法会执行 <math xmlns="http://www.w3.org/1998/Math/MathML"> 25 ∗ 1 0 8 25*10^8 </math>25∗108,即 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2.5 ∗ 1 0 9 2.5*10^9 </math>2.5∗109 次,而现代处理器大约能够每秒执行 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 0 8 10^8 </math>108 到 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 0 9 10^9 </math>109 次基本操作。 因此通常认为如果超过 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 0 9 10^9 </math>109 则超时。
既然超时,那就一定存在不必要的重复计算,暴力求解的方法哪里重复计算了呢?请看图:
我们对 values[i] + values[j] + i - j
重新组合为values[i] + i + values[j] - j
,会发现:对于固定的j
,i < j
,只要 values[i] + i
越大,结果越大。而当 j
移动到 j+1
时,前 j - 1
个 value[i] + i
的最大值已经计算过了。
显然这里可以优化。
方法二:
思路
既然左侧的 values[i] + i
存在重复计算,那显然我们可以用变量将其保存起来。然后在单次循环中进行最大值的维护。
这个思路被叫做双变量问题 的 "枚举右,维护左"。详见文章末尾。
代码
python
def solution(values: list) -> int:
# PLEASE DO NOT MODIFY THE FUNCTION SIGNATURE
# write code here
ans = 0
mx = values[0] + 0
for j in range(1, len(values)):
ans = max(ans, mx + values[j] - j)
# 边遍历边维护
mx = max(mx, values[j] + j)
return ans
if __name__ == '__main__':
print(solution(values=[8, 3, 5, 5, 6]) == 11)
print(solution(values=[10, 4, 8, 7]) == 16)
print(solution(values=[1, 2, 3, 4, 5]) == 8)
复杂度
- 时间复杂度 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n)
- 空间复杂度 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)
总结
由数据范围反推算法复杂度以及算法内容
一般ACM或者笔试题的时间限制是1秒或2秒。 在这种情况下,C++代码的操作次数控制在10⁷~10⁸为最佳。 下面给出在不同数据范围内,代码的时间复杂度和算法该如何选择:
- n ≤ 30,指数级别,dfs+剪枝,状态压缩dp
- n ≤ 100,= O(n³),floyd,dp,高斯消元
- n ≤ 1000,= O(n²),O(n²logn),dp,二分,朴素版Dijkstra、朴素版Prim、Bellman-Ford
- n ≤ 10000,= O(n * √n),块状链表、分块、莫队
- n ≤ 100000,= O(nlogn),各种sort,线段树、树状数组、set/map、heap、拓扑排序、dijkstra+heap、prim+heap、Kruskal、spfa、求凸包、求半平面交、二分、CDQ分治、整体二分、后缀数组、树链剖分、动态树
- n ≤ 1000000 => O(n),以及常数较小的O(nlogn) 算法 => 单调队列、hash、双指针扫描、BFS、并查集、kmp、AC自动机,常数较小的O(nlogn)的做法: sort、树状数组、heap、dijkstra、spfa
- n ≤ 10000000,= O(n),双指针扫描、kmp、AC自动机、线性素数筛
- n ≤ 10⁹ => O(√n),判断质数
- n ≤ 10¹⁸ => O(logn),最大公约数,快速幂,数位DP
- n ≤ 10¹⁰⁰⁰ => O((logn)²),高精度加减乘除
- n ≤ 10¹⁰⁰⁰⁰⁰ => O(logk × loglogk),k表示位数,高精度加减、FFT/NTT
枚举右,维护左
对于 双变量问题 ,例如两数之和 aᵢ + aⱼ = t
,可以枚举右边的 aⱼ
,转换成 单变量问题 ,也就是在 aⱼ
左边查找是否有 aᵢ = t - aⱼ
,这可以用哈希表维护。 这个技巧叫做 枚举右,维护左
。
参考文章: