1.贪心算法适用的问题
贪心策略适用的前提是:局部最优策略能导致产生全局最优解。
实际上,贪心算法适用的情况很少。一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可做出判断。
2.贪心算法的实现框架
从问题的某一初始解出发;
while (能朝给定总目标前进一步)
{
利用可行的决策,求出可行解的一个解元素;
}
由所有解元素组合成问题的一个可行解;
题目1:基础问题
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
解法:遍历数组并在每个步骤中更新:
当前元素
当前元素位置的最大和
迄今为止的最大和
class Solution:
def maxSubArray(self, nums):
n = len(nums)
curr_sum = max_sum = nums[0]
for i in range(1, n):
curr_sum = max(nums[i], curr_sum + nums[i])#局部最优解
max_sum = max(max_sum, curr_sum)
#全局最优解
return max_sum
题目2:分割数组
给你一个按升序排序的整数数组 num(可能包含重复数字),请你将它们分割成一个或多个子序列,其中每个子序列都由连续整数组成且长度至少为 3 。
如果可以完成上述分割,则返回 true ;否则,返回 false
输入: [1,2,3,3,4,4,5,5]
输出: True
解释:
你可以分割出这样两个连续子序列 :
1, 2, 3, 4, 5
3, 4, 5
题解:
对于数组中的元素 x,如果存在一个子序列以 x-1 结尾,则可以将 x加入该子序列中。将 x加入已有的子序列总是比新建一个只包含 x 的子序列更优,因为前者可以将一个已有的子序列的长度增加 1,而后者新建一个长度为 1 的子序列,而题目要求分割成的子序列的长度都不小于 3,因此应该尽量避免新建短的子序列。基于此,可以通过贪心的方法判断是否可以完成分割。
使用两个字典,一个记录数字出现的次数。一个记录以该数字为结尾的列表出现次数。
class Solution(object):
def isPossible(self, nums):
"""
:type nums: List[int]
:rtype: bool
"""
if len(nums)<3: return False
count=collections.Counter(nums)
tail={}
for k in count.keys():
tail[k]=0
for x in nums:
#如果有以x-1作为末尾的子序列
if count[x]>0:
count[x]-=1
if x-1 in tail.keys() and tail[x-1]>0:
tail[x-1]-=1
tail[x]+=1
else:
if count[x+1]==0 or count[x+2]==0:
return False
else:
count[x+1]-=1
count[x+2]-=1
tail[x+2]+=1
return True
题目3:重构字符串(贪心+最大堆)
给定一个字符串S,检查是否能重新排布其中的字母,使得两相邻的字符不同。若可行,输出任意可行的结果。若不可行,返回空字符串。
输入: S = “aab”
输出: “aba”
题解:
维护最大堆存储字母,堆顶元素为出现次数最多的字母。首先统计每个字母的出现次数,将出现次数大于0的字母加入最大堆。当最大堆的元素个数大于 1 时,每次从最大堆取出两个字母,拼接到重构的字符串,然后将两个字母的出现次数分别减 1,并将剩余出现次数大于 0 的字母重新加入最大堆。由于最大堆中的元素都是不同的,因此取出的两个字母一定也是不同的,将两个不同的字母拼接到重构的字符串,可以确保相邻的字母都不相同。如果最大堆变成空,则已经完成字符串的重构。如果最大堆剩下 1 个元素,则取出最后一个字母,拼接到重构的字符串。
class Solution(object):
def reorganizeString(self, S):
s_list = list(S)
if len(s_list) < 2:
return s_list
count = collections.Counter(s_list)
sort_key=sorted(count,key=count.__getitem__,reverse=True)
maxCount = max(count.items(), key=lambda x: x[1])[1]
if maxCount > (len(s_list) + 1) // 2:
return ""
new_S = []
while True:
max_pile=[]
for k in sort_key:
if count[k]>=1:
max_pile.append(k)
if len(max_pile)>1:
if new_S != []:
i=0
for p in max_pile:
if p !=new_S[-1]:
new_S.append(p)
count[p] -= 1
i+=1
if i==2:
break
else:
new_S.append(max_pile[0])
new_S.append(max_pile[1])
count[max_pile[0]] -= 1
count[max_pile[1]] -= 1
elif len(max_pile)==1:
if max_pile[0]!=new_S[-1]:
new_S.append(max_pile[0])
break
elif max_pile[0]!=new_S[0]:
new_S.insert(0,max_pile[0])
break
else:
for i in range(len(new_S)-1):
if new_S[i]!=max_pile[0] and new_S[i+1]!=max_pile[0]:
new_S.insert(i, max_pile[0])
break
else:
break
return "".join(new_S)
题目4:任务调度
给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。
然而,两个 相同种类 的任务之间必须有长度为整数 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。
你需要计算完成所有任务所需要的 最短时间 。
输入:tasks = [“A”,“A”,“A”,“B”,“B”,“B”], n = 2
输出:8
解释:A -> B -> (待命) -> A -> B -> (待命) -> A -> B
在本示例中,两个相同类型任务之间必须间隔长度为 n = 2 的冷却时间,而执行一个任务只需要一个单位时间,所以中间出现了(待命)状态。
与上一个题目类似,我们应当选择剩余执行次数最多的那个任务,将每种任务的剩余执行次数尽可能平均,使得 CPU 处于待命状态的时间尽可能少。我们用step记录当前的时间。根据我们的策略,我们需要选择不在冷却中并且剩余执行次数最多的那个任务。
class Solution(object):
def leastInterval(self, tasks, n):
count = collections.Counter(tasks)
last_time = {}
for key in count.keys():
last_time[key] = -1
cur_time = 0
while True:
if max(count.values())==0:
break
if cur_time<min(last_time.values()):
cur_time += 1
continue
for key in sorted(count,key=count.__getitem__,reverse=True):
if count[key] > 0:
if cur_time > last_time[key]:
last_time[key] = cur_time + n
count[key]-=1
break
else:
del count[key]
cur_time += 1
return (cur_time)
题目5:矩阵翻转
有一个二维矩阵 A 其中每个元素的值为 0 或 1 。移动是指选择任一行或列,并转换该行或列中的每一个值:将所有 0 都更改为 1,将所有 1 都更改为 0。在做出任意次数的移动后,将该矩阵的每一行都按照二进制数来解释,矩阵的得分就是这些数字的总和。返回尽可能高的分数。
输入:[[0,0,1,1],[1,0,1,0],[1,1,0,0]]
输出:39
解释:
转换为 [[1,1,1,1],[1,0,0,1],[1,1,1,1]]
0b1111 + 0b1001 + 0b1111 = 15 + 9 + 15 = 39
可以先考虑所有的行翻转,再考虑所有的列翻转。
聪明法:为了得到最高的分数,矩阵的每一行的最左边的数都必须为 1。为了做到这一点,我们可以翻转那些最左边的数不为 1 的那些行,而其他的行则保持不动。
当将每一行的最左边的数都变为 1 之后,就只能进行列翻转了。为了使得总得分最大,我们要让每个列中 1 的数目尽可能多。因此,我们扫描除了最左边的列以外的每一列,如果该列 0 的数目多于 1 的数目,就翻转该列,其他的列则保持不变。
笨笨法:普普通通的贪心算法,每一步看谁的得分最高
class Solution(object):
def matrixScore(self, A):
"""
:type A: List[List[int]]
:rtype: int
"""
def cow_turn(ga, i):
mat=[]
for cow in ga:
mat.append(cow[:])
for j in range(len(mat[i])):
if mat[i][j]==1: mat[i][j]=0
else: mat[i][j]=1
return mat
def row_turn(ga, i):
mat = []
for cow in ga:
mat.append(cow[:])
for j in range(len(mat)):
if mat[j][i]==1: mat[j][i]=0
else: mat[j][i]=1
return mat
def get_score(mat):
score=0
for col in mat:
cur_score,j=0,1
for i in range(len(col)-1,-1,-1):
cur_score+=col[i]*j
j*=2
score+=cur_score
return score
for i in range(len(A)):
new_A=cow_turn(A,i)
if get_score(new_A)>get_score(A):
A=new_A
for j in range(len(A[0])):
new_A=row_turn(A,j)
if get_score(new_A)>get_score(A):
A=new_A
return get_score(A)
题目6:找零问题
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。
顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
示例 1:
输入:[5,5,5,10,20]
输出:true
题解:输入20的时候要尽可能的把10块用掉。
class Solution(object):
def lemonadeChange(self, bills):
"""
:type bills: List[int]
:rtype: bool
"""
#如果收到20, 前面要有一张10与一张5,或三张5
#如果收到10,前面要有一张5
#如果收到5没有事吧
count={5:0,10:0,20:0}
for i in range(len(bills)):
if bills[i]==5:
count[5]+=1
elif bills[i]==10:
if count[5]==0: return False
count[5]-=1
count[10]+=1
elif bills[i]==20:
if count[5]==0: return False
if count[5]<3 and count[10]==0: return False
if count[10]!=0:
count[5]-=1
count[10]-=1
else:
count[5]-=3
count[20]+=1
return True
题目7:参议院投票
Dota2 的世界里有两个阵营:Radiant(天辉)和 Dire(夜魇)
Dota2 参议院由来自两派的参议员组成。现在参议院希望对一个 Dota2 游戏里的改变作出决定。他们以一个基于轮为过程的投票进行。在每一轮中,每一位参议员都可以行使两项权利中的一项:
1.禁止一名参议员的权利:
参议员可以让另一位参议员在这一轮和随后的几轮中丧失所有的权利。
2.宣布胜利:
如果参议员发现有权利投票的参议员都是同一个阵营的,他可以宣布胜利并决定在游戏中的有关变化。
给定一个字符串代表每个参议员的阵营。字母 “R” 和 “D” 分别代表了 Radiant(天辉)和 Dire(夜魇)。然后,如果有 n 个参议员,给定字符串的大小将是 n。以轮为基础的过程从给定顺序的第一个参议员开始到最后一个参议员结束。这一过程将持续到投票结束。所有失去权利的参议员将在过程中被跳过。假设每一位参议员都足够聪明,会为自己的政党做出最好的策略,你需要预测哪一方最终会宣布胜利并在 Dota2 游戏中决定改变。输出应该是 Radiant 或 Dire。
输入:‘RD’
输出:Dire
输入:‘RRDDDD’
输出:Radiant
题解:将天辉和梦魇分别入堆(最小堆),堆的 值 为 索引,以便找到下一个投票的。
贪心:每一个议员应该把后面的敌对议员给禁止。因为后面的敌对议员有权利禁止我方议员。
然后就是模拟,每次从两个堆中取索引最小的。
如果是 R,则移除 D 一个,并将 R 重新入堆,索引调整为 i + len(s)(他下一轮还可以投票),其中 i 为 R 的索引。
如果是 D,则移除 R 一个,并将 D 重新入堆,索引调整为 i + len(s)(他下一轮还可以投票),其中 i 为 D 的索引。
class Solution(object):
def predictPartyVictory(self, senate):
"""
:type senate: str
:rtype: str
"""
s=list(senate)
r_time=[]
d_time=[]
for i in range(len(s)):
if s[i]=='R': r_time.append(i)
else: d_time.append(i)
while r_time and d_time:
if r_time[0] < d_time[0]:
r_time.append(r_time[0] + len(s))
else:
d_time.append(d_time[0] + len(s))
r_time.pop(0)
d_time.pop(0)
return "Radiant" if r_time else "Dire"
题目8:摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
输入: [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。
题解:
序列中的某个元素被称为「峰」,当且仅当该元素两侧的相邻元素均小于它。如序列 [1,3,2,4]中,3 就是一个「峰」。
序列中的某个元素被称为「谷」,当且仅当该元素两侧的相邻元素均大于它。如序列 [1,3,2,4] 中,2 就是一个「谷」。
贪心:加入一个数要尽可能的形成峰或谷。
在实际代码中,我们记录当前序列的上升下降趋势。每次加入一个新元素时,用新的上升下降趋势与之前对比,如果出现了「峰」或「谷」,答案加一,并更新当前序列的上升下降趋势。
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
n = len(nums)
if n < 2:
return n
prevdiff = nums[1] - nums[0]
ret = (2 if prevdiff != 0 else 1)
for i in range(2, n):
diff = nums[i] - nums[i - 1]
if (diff > 0 and prevdiff <= 0) or (diff < 0 and prevdiff >= 0):
ret += 1
prevdiff = diff
return ret
题目9:分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
题解:为了尽可能满足最多数量的孩子,从贪心的角度考虑,应该按照孩子的胃口从小到大的顺序依次满足每个孩子,且对于每个孩子,应该选择可以满足这个孩子的胃口且尺寸最小的饼干。
class Solution(object):
def findContentChildren(self, g, s):
"""
:type g: List[int]
:type s: List[int]
:rtype: int
"""
if not s: return 0
g,s=sorted(g),sorted(s)
result=0
i=j=0
while i<len(g):
if j>=len(s) or g[i]>max(s): return result
if g[i] <= s[j]: #该同学可以得到满足
result+=1
j+=1
i+=1
else:
j+=1
return result