极客时间2021算法训练营
作者: 李煜东
贪心算法(GreedyAlgorithm)是一种
(1)在每一步选柽中都采取在当前状态下的最优决策(局部最优)
(2)并希望由此导致的最终结果也是全局最优
的算法
贪心和动规不同: 不对整个状态空间进行 遍历或计算,而是始终按照局部最优选择执行下去,不再回头。
状态空间角度: 贪心算法实际上是在状态空间中按局部最优策略找了一条路径。
使用贪心法要求问题的整体最优性可以由局部最优性导出。贪心算法的正确性需要证明, 常见的证明手段有:
class Solution: def lemonadeChange(self, bills: List[int]) -> bool: dic = defaultdict(int) for bill in bills: dic[bill] += 1 curr = bill - 5 for money in [10, 5]: #从找10元开始考虑 while curr >= money and dic[money] > 0: dic[money] -= 1 curr -= money if curr != 0: return False return True
决策包容性证明:贪心局部最佳>>全局最佳:一块饼干总是想要满足一个孩子的,满足胃口更大的孩子,未来的可能性包含了满足 胃口更小孩子的可能性
class Solution: def findContentChildren(self, g: List[int], s: List[int]) -> int: g.sort() s.sort() count,ans = 0, 0 for child in g: while count < len(s) and s[count] < child: count += 1 if count < len(s): ans += 1 count += 1 return ans
prices[i] - prices[i - 1] > 0
区间的收益class Solution: def maxProfit(self, prices: List[int]) -> int: ans = 0 for i in range(1, len(prices)): ans += max(prices[i] - prices[i-1], 0) return ans
class Solution: def jump(self, nums: List[int]) -> int: ans, now = 0, 0 while now < len(nums) - 1: right = now + nums[now] # right是now位置能达到最大位置 if right >= len(nums) - 1: return ans +1 nextNow = now nextRight = right for i in range(now + 1, right + 1): # now 可以达到范围[now + 1, right] if i + nums[i] > nextRight: #使得 now下下个可达位置最远 nextNow = i nextRight = i + nums[i] now = nextNow ans += 1 return ans
邻项交换
经常用于以某种顺序"排序"为贪心策略的证明
证明在任意局面下,任何局部的逆序改变都会造成整体结果变差
task[1]
)高 具有较优先趋势, 能耗(task[0]
)低同样具有较优先趋势 >>> 考虑task[0] - task[1]
升序排序证明:
设task[0]
为actual
, task[1]
为minimum
;
设做完第i+2
到n
个任务所需的初始能量最少为S
;
对于两个相邻任务:设第i
个和第i+l
个完成的任务分别是p
和q
:
考虑
p
,
q
p, q
p,q中先做者所需要的最低能量
S
p
,
S
q
S_p, S_q
Sp,Sq:
先做p, 所需要能量:
S
p
=
m
a
x
(
m
a
x
(
m
i
n
i
m
u
m
[
q
]
,
S
+
a
c
t
u
a
l
[
q
]
)
+
a
c
t
u
a
l
[
p
]
,
m
i
n
i
m
u
m
[
p
]
)
S_p =max(max(minimum[q], S+actual[q])+actual[p], minimum[p])
Sp=max(max(minimum[q],S+actual[q])+actual[p],minimum[p])
=
m
a
x
(
m
i
n
i
m
u
m
[
q
]
+
a
c
t
u
a
l
[
p
]
,
S
+
a
c
t
u
a
l
[
q
]
+
a
c
t
u
a
l
[
p
]
,
m
i
n
i
m
u
m
[
p
]
)
=max(minimum[q] + actual[p], S+actual[q] + actual[p], minimum[p])
=max(minimum[q]+actual[p],S+actual[q]+actual[p],minimum[p])
先做q, 所需要能量:
S
q
=
m
a
x
(
m
a
x
(
m
i
n
i
m
u
m
[
p
]
,
S
+
a
c
t
u
a
l
[
p
]
)
+
a
c
t
u
a
l
[
q
]
,
m
i
n
i
m
u
m
[
q
]
)
S_q =max(max(minimum[p], S+actual[p])+actual[q], minimum[q])
Sq=max(max(minimum[p],S+actual[p])+actual[q],minimum[q])
=
m
a
x
(
m
i
n
i
m
u
m
[
p
]
+
a
c
t
u
a
l
[
q
]
,
S
+
a
c
t
u
a
l
[
p
]
+
a
c
t
u
a
l
[
q
]
,
m
i
n
i
m
u
m
[
q
]
)
=max(minimum[p] + actual[q], S+actual[p] + actual[q], minimum[q])
=max(minimum[p]+actual[q],S+actual[p]+actual[q],minimum[q])
P优先则 >>> 满足
S
p
<
S
q
S_p<S_q
Sp<Sq 即:
m
a
x
(
m
i
n
i
m
u
m
[
q
]
+
a
c
t
u
a
l
[
p
]
,
m
i
n
i
m
u
m
[
p
]
)
<
m
a
x
(
m
i
n
i
m
u
m
[
p
]
+
a
c
t
u
a
l
[
q
]
,
m
i
n
i
m
u
m
[
q
]
)
max(minimum[q] + actual[p], minimum[p]) < max(minimum[p] + actual[q], minimum[q])
max(minimum[q]+actual[p],minimum[p])<max(minimum[p]+actual[q],minimum[q])
因为必定有
m
i
n
i
m
u
m
[
q
]
+
a
c
t
u
a
l
[
p
]
>
m
i
n
i
m
u
m
[
q
]
minimum[q] + actual[p] > minimum[q]
minimum[q]+actual[p]>minimum[q]
所以上式等价于
m
i
n
i
m
u
m
[
q
]
+
a
c
t
u
a
l
[
p
]
<
m
i
n
i
m
u
m
[
p
]
+
a
c
t
u
a
l
[
q
]
minimum[q] + actual[p] < minimum[p] + actual[q]
minimum[q]+actual[p]<minimum[p]+actual[q]
即
a
c
t
u
a
l
[
p
]
−
m
i
n
i
m
u
m
[
p
]
<
a
c
t
u
a
l
[
q
]
−
m
i
n
i
m
u
m
[
q
]
actual[p] - minimum[p] < actual[q] - minimum[q]
actual[p]−minimum[p]<actual[q]−minimum[q]
于是有: >>>贪心策略:按照actual - minimum
升序排序,以此顺序完成任务
class Solution: def minimumEffort(self, tasks: List[List[int]]) -> int: ans = 0 tasks.sort(key = lambda x: x[0] - x[1]) for task in tasks[::-1]: #倒序考虑完成所需要能量 ans = max(task[1], ans + task[0]) return ans
动态规划(英语:Dynamic programming,简称 DP),是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
amount = 18, coins = [10, 9, 1]
)的状态空间:
opt(n) = min(opt(n - 1), opt(n - 9),opt(n - 10)) + 1
式子本质: 状态+最优化目标+最优化目标之间具有递推关系=最优子结构
动态规划(DP, dynamic programming)是一种对问题的状态空间进行分阶段、有顺序、不重复、 决策性遍历的算法
动态规划的关键与前提:
重叠子问题——与递归、分治等一样,要具有同类子问题,用若干维状态表示
最优子结构 ——状态对应着一个最优化目标,并且最优化目标之间具有推导关系
无后效性——问题的状态空间是一张有向无环图(可按一定的顺序遍历求解)
动态规划一般采用递推的方式实现
也可以写成递归或搜索的形式,因为每个状态只遍历一次,也被称为记忆化搜索
动态规划三要素:阶段、状态、决策
Bottom-up
f[i,j]
表示从(i,j)
到End
的路径数, 如果(i,j)
是空地,f[i,j]
= f[i + l,j] + f[i,j + 1]
否则f[i,j] = 0
反过来类似的:
Top-down
f[i,j]
表示从Start到(i,j)
的路径数, 如果(i,j)
是空地,f[i,j]
= f[i - 1,j] + f[i,j - 1]
否则f[i,j] = 0
class Solution: def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int: m, n = len(obstacleGrid), len(obstacleGrid[0]) f = [[0]*n for _ in range(m)] for i in range(m): for j in range(n): if obstacleGrid[i][j] == 1: f[i][j] = 0 elif i == 0 and j == 0: f[i][j] = 1 elif i == 0: f[i][j] = f[i][j - 1] elif j == 0: f[i][j] = f[i - 1][j] else: f[i][j] = f[i - 1][j] + f[i][j - 1] return f[-1][-1]
f[i][j]
表示text1
的前i
个字符和text2
的前j
个字符能组成的LCS的长度
如果 text1[i] = text2[j]
: f[i][j] = f[i - 1][j-1] + 1
如果 text1[i] != text2[j]
: f[i][j] = max(f[i - 1][j], f[i][j-1] )
class Solution: def longestCommonSubsequence(self, text1: str, text2: str) -> int: m, n = len(text1), len(text2) f = [[0]*(n + 1) for _ in range(m + 1)] text1 = " " + text1 #防止i-1, j-1越界 text2 = " " + text2 for i in range(1, m + 1): for j in range(1, n + 1): if text1[i] == text2[j]: f[i][j] = f[i - 1][j - 1] + 1 else: f[i][j] = max(f[i - 1][j], f[i][j - 1]) return f[-1][-1]
f[i]
表示前i
个数构成的以a[i]
为结尾的最长上升子序列的长度
f [ i ] = max j < i , a [ j ] < a [ i ] { f [ j ] + 1 } f[i]=\max _{j<i, a[j]<a[i]}\{f[j]+1\} f[i]=j<i,a[j]<a[i]max{f[j]+1}
边界:f[i] = 1 (0 <= i < n)
目标:
max
0
≤
i
<
n
{
f
[
i
]
}
\max _{0 \leq i<n}\{f[i]\}
max0≤i<n{f[i]}
class Solution: def lengthOfLIS(self, nums: List[int]) -> int: n, ans = len(nums), 0 f = [1 for _ in range(n)] for i in range(1, n): for j in range(i): if nums[i] > nums[j]: f[i] = max(f[j] + 1, f[i]) for k in range(n): ans = max(ans, f[k]) return ans
class Solution: def lengthOfLIS(self, nums: List[int]) -> int: n, ans,curr,end = len(nums), [], 0, -1 f = [1 for _ in range(n)] pre = [-1 for _ in range(n)] def p(i): if pre[i] != -1: p(pre[i]) ans.append(nums[i]) for i in range(1, n): for j in range(i): if nums[i] > nums[j] and f[i] < f[j] + 1 : f[i] = f[j] + 1 pre[i] = j for k in range(n): if f[k] > curr: end = k p(end) return ans