使用贪心算法的5道算法题

贪心算法解决问题(python实现)

当数据规模较大,暴力求解会导致超时,我们考虑想到一些更优的算法求解,即找规律

1. 种花问题

思路一

计算空地的可种花数
  1. 如果是左右边界,连续两个0即可种一朵花;
  2. 如果在中间,0的个数为奇数,种花数为(num-1)//2;
  3. 如果在中间,0的个数为偶数,种花数为num//2-1;
  4. 将各个位置的种花数量累加,即得到总的可种花数;
  5. 总可种花数 >= 给定种花数,则为True;反之,则为False。
代码实现
class Solution:
    def canPlaceFlowers(self, flowerbed: List[int], n: int) -> bool:
        count = 0
        sign = True
        sum = 0
        if len(flowerbed) == 1:
            if flowerbed[0] == 0:
                sum = 1
        elif 1 not in flowerbed:
            sum = (len(flowerbed)+1)//2
        else:
            if flowerbed[0] == 0 and flowerbed[1] == 0: # 两个0在开头需要再加1
                sum += 1
            if flowerbed[-1] == 0 and flowerbed[-2] == 0 and len(flowerbed) > 2: # 两个0在结尾需要再加1
                sum += 1
            for i in range(len(flowerbed)):
                if flowerbed[i] == 0:
                    count += 1 # 1与1之间有多时少个0
                elif count > 0:
                    sum += self.flower(count) # 计算0在中间时的种花数,并累加
                    count = 0
            if count > 0:
                sum += self.flower(count)
        if sum < n:
            sign = False
        return sign
    def flower(self, count):
        if count%2==1:
            return (count-1)//2
        else:
            return count//2-1

思路二

将给定的花束种到花坛
  1. 如果是左右边界,出现两个0,n-1
  2. 如果是在中间,出现三个0,n-1
  3. 最后,n < 0,则返回False;反之,则为True
代码实现
class Solution:
    def canPlaceFlowers(self, flowerbed: List[int], n: int) -> bool:
        for i in range(len(flowerbed)):
            if i == 0:
                left = 0 # # 对左边界的特殊处理                    
            else:
                flowerbed[i-1]
            if i == len(flowerbed)-1:
                right = 0
            else:
                flowerbed[i+1]   # 对右边界的特殊处理
            if flowerbed[i] == 0 and left == 0 and right == 0:       # 当前节点为空,且左右为空的情况下
                flowerbed[i] = 1                                     # 在当前节点种花
                n -= 1                                               # 剩余花朵减一
        return n <= 0

2. 最长回文串

思路

其实就是一个对称的问题,判断字符串能否写成左右对称的形式。

  1. 如果每个字符的数量都是偶数,那么回文串的长度即为原字符串长度;
  2. 如果有的字符数量是奇数,由于对称的中间元素不要求对称,因此回文串的长度为各个字符数量整除2再乘2,最后加1
代码实现
class Solution:
    def longestPalindrome(self, s: str) -> int:
        num = set(s) # 用集合,判断字符串有几种元素
        dic = {} # 用键值对记录不同元素的数量
        for i in num:
            dic[i] = 0 
        for i in range(len(s)): # 遍历一次传入的字符串,得到每个元素的数量
            dic[s[i]] += 1
        sum = 0 # 计算回文串长度
        for i in num:
            sum += dic[i]//2 # 对不同的字典值的1/2求和(够不够对称)
        if sum*2 < len(s):
            return sum*2+1 # 中间元素可以不必对称,如果有剩余元素,字符串长度可以加1
        else:
            return sum*2 # 所有元素数量刚好都是偶数的情况
代码优化
  1. 遍历字符串直接用for i in s, 得到的i即为字符 代码第4行
  2. 判断键或值存在于字典中,i in static.keys(), count in static.values() 第 5、7行
  3. divmod函数可以直接得到除数和余数 代码第8行
class Solution:
    def longestPalindrome(self, s: str) -> int:
        static = {}
        for i in s:
            if i in static.keys():
                static[i] = static[i] + 1
            else:
                static[i] = 1
        remain, total = 0, 0
        for count in static.values():
            total_, remain_ = divmod(count, 2)
            remain += remain_
            total += total_
        if remain == 0:
            return total * 2 + 0
        else:
            return total * 2 + 1

3. 买卖股票的最佳时机

思想

  1. 遍历字符串,min_index存储所过之处股票最低时的数组下标;
  2. 计算min_index时买入股票,当前位置卖股票时的利润(后面的-前面的),如果大于当前最大利润,则更新最大利润的值

代码实现

class Solution:
   def maxProfit(self, prices: List[int]) -> int:
       profit, min_index = 0, 0
       for i in range(len(prices)):
           if prices[i] < prices[min_index]: # 当前股票的最低价格
               min_index = i
           elif prices[i] - prices[min_index] > profit: # 当前卖可以获得更大利润
               profit = prices[i] - prices[min_index]
       return profit

4. 盛最多水的容器

思路1

让height数组中的每个元素依次为高度,然后计算宽度,最后得到面积
  1. 计算面积时分为左面和右面两部分,如果是边界,则左面或右面面积为0;
  2. 中间时候,如果左面或者右面有大于或者当前高度的,则这一侧存在面积;否则,这一侧面积为0。
代码实现
class Solution:
    def maxArea(self, height: List[int]) -> int:
        max_v = 0
        for i in range(len(height)):
            j = 0
            while j < i and height[j] < height[i]: # 找到左面第一个大于或者等于当前高度的
                j += 1
            left_v = (i-j)*height[i]
            k = len(height)-1
            while k > i and height[k] < height[i]: # 找到右面第一个大于或者等于当前高度的
                k -= 1
            right_v = (k-i)*height[i]

            if left_v + right_v > max_v:
                max_v = left_v + right_v
        return max_v

思路2

从两侧向中间移动,我们只需要移动较矮的一侧就可以

两侧即宽度最大,那么要想面积增大,宽度缩小时,只能增加高度。简单来说,如果高度增加了,宽度即使缩小,是不是面积也可能增大
怎么让高度增加:短的那边移动。

代码实现
class Solution:
    def maxArea(self, height: List[int]) -> int:
        i, j, v = 0, len(height) - 1, 0
        while i < j:
            if height[i] < height[j]:
                v = max(v, height[i] * (j - i))
                i += 1 # 左面矮,移动左面
            else:
                v = max(v, height[j] * (j - i))
                j -= 1 # 右面矮,移动右面
        return v

5. 划分字母区间

思路1

遍历两次字符串,第一次统计字符数量,第二次遍历得到字母区间
  1. 第一次遍历字符串,用字典记录每个字符的数量
  2. 复制字典,第二次遍历字符串,其中每访问字符一次,对应的字典值减1
  3. 当某个字符数量为0时,判断其它字符是否在前面出现过,如果没出现过,当前位置即可划分;反之,寻找下一次字符数量为0
代码实现
class Solution:
    def partitionLabels(self, s: str) -> List[int]:
        unit = sorted(set(s), key = s.index) # 字符串所有包含的元素
        dic = {} # 用字典存元素和对应的个数。键是元素,值是元素个数
        for i in unit:
            dic[i] = 0
        for i in range(len(s)):
            dic[s[i]] += 1 # 遍历字符串,并计算每个元素的个数
        cop_dic = {}
        for i in unit:
            cop_dic[i] = dic[i] # 复制一份字典
        num = [] # 最后的返回值
        k = -1 # 记录当前遍历的字符串位置
        for i in range(len(s)):
            dic[s[i]] -= 1 # 每访问一次,值-1
            if dic[s[i]] == 0:
                unit.remove(s[i]) # 字符串后面没有某个元素了,从集合中删除
            sign = True
            for j in unit:
                if dic[j] != cop_dic[j]: # 判断其余元素的个数,是否前面没出现过
                    sign = False
            if sign:
                num.append(i-k) # 集合中剩余的元素都没出现过,可以从这里划分字符串
                k = i # 当前访问的字符串位置
        return num

思路2

通过找到相同字符串包含的区间,然后合并成不相交的区间
  1. 字符串只访问一次,为了创建一个字典{key1:[start,end], key2:[start,end],…};
  2. 读写一个列表为[[start, end], [start, end],…] 每个元素为一个字母区间的start, end。
  3. 怎么创建这个列表?我们已经得到了每个字符的起止位置,如果上一个元素的结束位置之前有其他元素开始了,(代码倒数第5行)那么我们需要合并这些元素的终止位置。
代码实现
class Solution:
    def partitionLabels(self, s: str) -> List[int]:
        index = {}
        for i in range(len(s)): # 字符串只访问一次,为了创建一个字典{key1:[start,end], key2:[start,end],....}
            if s[i] in index.keys():
                index[s[i]][1] = i
            else:
                index[s[i]] = [i, i] # key存元素,value存[起始位置, 终止位置]
        answer = [] # [[start, end], [start, end],...] 每个元素为一个字母区间的start, end
        key_list = list(index.keys()) # 元素组成的列表,为了迭代enumerate产生 i 和 key
        for i, key in enumerate(key_list): # 遍历index字典,更新answer
            if len(answer) == 0:
                answer.append(index[key]) # answer = [[i, j]] 字符串中第一个元素的起止位置
            # 上一个元素的 结束 位置 > 当前元素的 开始 位置 ≈ 当前字母区间不只有一个元素
            elif answer[-1][1] > index[key][0]: 
                # 上一个元素的结束位置为,现在和之前所有元素的结束位置,即max
                answer[-1][1] = max(index[key][1], answer[-1][1]) 
            else:
                # 混合元素的字符串已经全部结束,开启了新一轮的字母区间,将新一轮的第一个元素位置加入列表
                answer.append(index[key])
        return [i[1] - i[0] + 1 for i in answer] # answer里面的每个元素 end-start+1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值