zoukankan      html  css  js  c++  java
  • python常用算法(6)——贪心算法,欧几里得算法

    1,贪心算法

      贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的的时在某种意义上的局部最优解。

      贪心算法并不保证会得到最优解,但是在某些问题上贪心算法的解就是最优解。要会判断一个问题能否用贪心算法来计算。贪心算法和其他算法比较有明显的区别,动态规划每次都是综合所有问题的子问题的解得到当前的最优解(全局最优解),而不是贪心地选择;回溯法是尝试选择一条路,如果选择错了的话可以“反悔”,也就是回过头来重新选择其他的试试。

    1.1  找零问题

      假设商店老板需要找零 n 元钱,钱币的面额有100元,50元,20元,5元,1元,如何找零使得所需钱币的数量最少?(注意:没有10元的面额)

      那要是找376元零钱呢? 100*3+50*1+20*1+5*1+1*1=375

      代码如下:

    # t表示商店有的零钱的面额
    t = [100, 50, 20, 5, 1]
    
    # n 表示n元钱
    def change(t, n):
        m = [0 for _ in range(len(t))]
        for i, money in enumerate(t):
            m[i] = n // money  # 除法向下取整
            n = n % money  # 除法取余
        return m, n
    
    print(change(t, 376)) # ([3, 1, 1, 1, 1], 0)

    1.2  背包问题

      常见的背包问题有整数背包和部分背包问题。那问题的描述大致是这样的。

      一个小偷在某个商店发现有 n 个商品,第 i 个商品价值 Vi元,重 Wi 千克。他希望拿走的价值尽量高,但他的背包最多只能容纳W千克的东西。他应该拿走那些商品?

      0-1背包:对于一个商品,小偷要么把他完整拿走,要么留下。不能只拿走一部分,或把一个商品拿走多次(商品为金条)

      分数背包:对于一个商品,小偷可以拿走其中任意一部分。(商品为金砂)

    举例:

      对于 0-1 背包 和 分数背包,贪心算法是否都能得到最优解?为什么?

       显然,贪心算法对于分数背包肯定能得到最优解,我们计算每个物品的单位重量的价值,然后将他们降序排序,接着开始拿物品,只要装得下全部的该类物品那么就可以全装进去,如果不能全部装下就装部分进去直到背包装满为止。

      而对于此问题来说,显然0-1背包肯定装不满。即使偶然可以,但是也不能满足所有0-1背包问题。0-1背包(又叫整数背包问题)还可以分为两种:一种是每类物品数量都是有限的(bounded)。一种是数量无限(unbounded),也就是你想要的多少有多少,这两种都不能使用贪心策略。0-1背包是典型的第一种整数背包问题。

      分数背包代码实现:

    # 每个商品元组表示(价格,重量)
    goods = [(60, 10), (100, 20), (120, 30)]
    # 我们需要对商品首先进行排序,当然这里是排好序的
    goods.sort(key=lambda x: x[0]/x[1], reverse=True)
    
    # w 表示背包的容量
    def fractional_backpack(goods, w):
        # m 表示每个商品拿走多少个
        total_v = 0
        m = [0 for _ in range(len(goods))]
        for i, (prize, weight) in enumerate(goods):
            if w >= weight:
                m[i] = 1
                total_v += prize
                w -= weight
            # m[i] = 1 if w>= weight else weight / w
            else:
                m[i] = w / weight
                total_v += m[i]*prize
                w = 0
                break
        return m, total_v
    
    res1, res2 = fractional_backpack(goods, 50)
    print(res1, res2)  # [1, 1, 0.6666666666666666]
    

      

    1.3  拼接最大数字问题

      有 n 个非负数,将其按照字符串拼接的方式拼接为一个整数。如何拼接可以使得得到的整数最大?

      例如:32, 94, 128, 1286, 6, 71 可以拼接成的最大整数为 94716321286128.

         注意1:字符串比较数字大小和整数比较数字大小不一样!!! 字符串比较大小就是首先看第一位,大的就大,可是一个字符串长,一个字符串短如何比较呢?比如128和1286比较

      思路如下:

    # 简单的:当两个等位数相比较
    a = '96'
    b = '97'
    
    a + b if a > b else b + a
    
    # 当出现了下面的不等位数相比较,如何使用贪心算法呢?
    # 我们转化思路,拼接字符串,比较结果
    
    a = '128'
    b = '1286'
    
    # 字符串相加
    a + b = '1281286'
    b + a = '1286128'
    
    a + b if a + b > b + a else b + a
    

      数字拼接代码如下:

    from functools import cmp_to_key
    
    li = [32, 94, 128, 1286, 6, 71]
    
    def xy_cmp(x, y):
        # 其中1表示x>y,-1,0同理
        if x+y < y+x:
            return 1
        elif x+y > y+x:
            return -1
        else:
            return 0
    
    def number_join(li):
        li = list(map(str, li))
        li.sort(key=cmp_to_key(xy_cmp))
        return "".join(li)
    
    print(number_join(li)) # 94716321286128
    

    补充:python  cmp_to_key函数

      下面学习一下Python中一个比较好用的模块,就是functools 中的 cmp_to_key函数,这里的 cmp_to_key就是在Python3中使用的,在Python2中就是 cmp函数。

      它的具体作用就是比较函数。当然上面函数也可以写成下面形式:

    def largestNumber(self, nums):
            from functools import cmp_to_key
            temp = list(map(str, nums))
            temp.sort(key=cmp_to_key(lambda x, y: int(x + y) - int(y + x)), reverse=True)
            return ''.join(temp if temp[0] != '0' else '0')
    

      上面函数有两个传入的参数 x, y,当 x>y 时返回1 等于时候返回0,否则返回-1。其实我最上面的函数比较明显。它在list的工作机制就是将列表中的元素去两两比较,当 cmp返回的时正数时交换两元素。

      

    1.4  活动选择问题

      假设有 n 个活动,这些活动要占用同一片场地,而场地在某时刻只能供一个活动使用。

      每一个活动都有一个开始时间 Si 和结束时间 Fi (题目中时间以整数表示)表示活动在  [Si,  fi) 区间占用场地。(注意:左开右闭)

      问:安排哪些活动能够使该场地举办的活动的个数最多?

       贪心结论:最先结束的活动一定是最优解的一部分。

      证明:假设 a 是所有活动中最先结束的活动,b是最优解中最先结束的活动。

      如果 a=b,结论成立

      如果 a!=b,则 b 的结束时间一定晚于 a 的结束时间,则此时用 a 替换掉最优解中的 b ,a 一定不与最优解中的其他活动时间重叠,因此替换后的解也是最优解。

       代码如下:

    # 一个元组表示一个活动,(开始时间,结束时间)
    activities = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11),
                  (8, 12), (2, 14), (12, 16)]
    
    # 保证活动是按照结束时间排好序,我们可以自己先排序
    activities.sort(key=lambda x:x[1])
    
    def activity_selection(a):
        # 首先a[0] 肯定是最早结束的
        res = [a[0]]
        for i in range(1, len(a)):
            if a[i][0] >= res[-1][1]:  # 当前活动的开始时间小于等于最后一个入选活动的结束时间
                # 不冲突
                res.append(a[i])
        return res
    
    res = activity_selection(activities)
    print(res)
    

     

    1.5  最大子序和

      求最大子数组之和的问题就是给定一个整数数组(数组元素有负有正),求其连续子数组之和的最大值。下面使用贪心算法逐个遍历。

     代码如下:

    def maxSubarray(li):
        s_max, s_sum = 0, 0
        for i in range(len(li)):
            s_sum += li[i]
            s_max = max(s_max, s_sum)
            if s_sum < 0:
                s_sum = 0
    
        return s_max

    2,欧几里得算法——最大公约数

    2.1,最大公约数的定义

      约数:如果整数 a 能被整数 b 整除,那么 a 叫做 b 的倍数,b 叫做 a 的约数。

      最大公约数(Greatest  Common  Divisor):给定两个整数 a, b,两个数的所有公共约数中的最大值即为最大公约数。

      例如:12和16的最大公约数是 4 。

    2.2,欧几里得算法如下:

      欧几里得算法又称为辗转相除法,用于计算两个正整数a,b的最大公约数。

    • E:设两个正整数a, b,且已知a>b
    • E1:令r = a%b('%'代表取余)
    • E2:若r=0(即n整除m),结束运算,n即为结果
    • E3:否则令a=b,b=r,并返回步骤E1

      欧几里得算法运用了这样一个等价式(设 gcd(a, b)代表 a 和 b 的最大公约数,mod()代表取余运算或模运算)则:

     gcd(a,  b) =  gcd(b, a mod b ) = gcd(b, a%b)

      也就是说 m , n 的最大公约数等于他们相除余数(r)和 n 的最大公约数。  

      例如:gcd(60, 21) = gcd(21, 18) = gcd(18, 3) = gcd(3, 0) = 3

      意思就是 60对21取余18,同理21对18余3,18对3取余0,所以3为两个数的最大公约数。

    2.3,证明欧几里得公式

      我们的证明分为两步。第一步,证明gcd(a, b)是b, a%b 的一个公约数。第二步,证明这个公约数是最大的。

    1,证明gcd(a, b)是b, a%b 的一个公约数

      1,因为任意两个正整数都有最大公因数,设为 d。

      2,将 a , b 分别用最大公因数 d 来表示为 a = k1*d  b = k2*d (k1,k2是两个常数)

      3,设 a = k*b + c (也就是a 除以 b 商 k 余 c),然后把a = k1*d  b = k2*d  两个式子中的 a,b代入式子,得到:

    c = a - k*b = k1*d - k * k2 * d,然后再提取公因数 d,得到 c = (k1 - k2 * k)*d,这就说明,c也就是 a%b有 d 这个约数,因为开始我们设 任意两个数都有最大公约数d,所以 gcd(a, b) 是 b, a%b 的一个公约数。

      4,由此可以得到 c 是最大公因数 d 的倍数,得证:gcd(a, b) = gcd(b, a mod b)。所以以此类推,可以将 m n中较大的数用较小的数的余数 r 替换,实现了降维,所以有了E3的步骤。 

    2,证明我们求出来的公约数是最大的

      1,数学是一门严谨的学科,我们需要严谨的正面,我们知道 c(a%b) =  k1*d - k * k2 * d    b = k2*d,所以我们只需要证明k1-k*k2, k2互质即可。

      2,这里可以用到反证法,我们假设 k1 - k*k2 = q*t  k2=p*t,再讲这个k1 代入最开始的 a=k1*d ,得到 a=(q*t + k*k2)*d,再利用乘法分配律得到: a = q*t*d + k*k2*d,这时候我们发现,k2*d就是b,将其代入,得到 a=q*t*d + b*d

      3,我们在将k2 = p*t代入开始的b = k2*d,得到b = p*t*d,再把这个式子代到a = q*t*d+b*d.得到了:a = q*t*d+p*t*d.提取公因数:a=(q+p)*t*d

      4,再和b=p*t*d比较,发现他们的最大公因数变成了t*d和开始矛盾,所以假设不成立,反证成功!

    2.4,如何计算最大公约数?

      1,欧几里得:辗转相除法(欧几里得算法)

      2,《九章算术》:更相减损术

       代码如下:

    # 递归法:保证a>b
    def gcd(a, b):
        if b == 0:
            return a
        else:
            return gcd(b, a % b)
    
    # 递推法
    def gcd1(a, b):
        if a < b:
            a, b = b, a
        while b > 0:
            r = a % b
            a = b
            b = r
        return a

      因为这是一个伪递归,所以时间复杂度不高。

    2.5,应用:实现分数计算

       利用欧几里得算法实现一个分数类,支持分数的四则运算。

       代码如下:

    # _*_coding:utf-8_*_
    
    class Fraction:
        def __init__(self, a, b):
            self.a = a
            self.b = b
            x = self.gcd(a, b)
            self.a /= x
            self.b /= x
    
        # 最大公约数
        def gcd(self, a, b):
            while b > 0:
                r = a % b
                a = b
                b = r
            return a
    
        # 最小公倍数
        def zgs(self, a, b):
            # 12 16 -> 4
            # 3 * 4 * 4=48
            x = self.gcd(a, b)
            return (a * b / x)
    
        # 加法的内置方法
        def __add__(self, other):
            # 1/12 + 1/20
            a = self.a
            b = self.b
            c = other.a
            d = other.b
            fenmu = self.zgs(b, d)
            femzi = a * (fenmu / b) + c * (fenmu / d)
            return Fraction(femzi, fenmu)
    
        def __str__(self):
            return "%d/%d" % (self.a, self.b)
    
    
    f = Fraction(30, 16)
    print(f)
    

      

     2.7 欧几里得算法的缺点

       欧几里得算法是计算两个数最大公约数的传统算法,无论从理论还是实际效率上都是很好地。但是却有一个致命的缺陷,这个缺陷在素数比较小的时候一般是感受不到的,只有在大素数时才会显现出来。

      一般实际应用中的整数很少会超过64位(当然现在已经允许128位),对于这样的整数,计算两个数之间的模很简单。对于字长为32位的平台,计算两个不超过32位的整数的模,只需要一个指令周期,而计算64位以下的整数模,也不过几个周期而已。但是对于更大的素数,这样的计算过程就不得不由用户来设计,为了计算两个超过64位的整数的模,用户也许不得不采用类似于多位数除法手算过程中的试商法,这个过程不但复杂,而且消耗了很多CPU时间。对于现代密码算法,要求计算128位以上的素数的情况比比皆是,设计这样的程序迫切希望能够抛弃除法和取模。

      由J. Stein 1961年提出的Stein算法很好的解决了欧几里德算法中的这个缺陷,Stein算法只有整数的移位和加减法,为了说明Stein算法的正确性,首先必须注意到以下结论:

      gcd(a,a)=a,也就是一个数和其自身的公约数仍是其自身。
      gcd(ka,kb)=k gcd(a,b),也就是最大公约数运算和倍乘运算可以交换。特殊地,当k=2时,说明两个偶数的最大公约数必然能被2整除。
      当k与b互为质数,gcd(ka,b)=gcd(a,b),也就是约掉两个数中只有其中一个含有的因子不影响最大公约数。特殊地,当k=2时,说明计算一个偶数和一个奇数的最大公约数时,可以先将偶数除以2。

       代码如下:

    def gcd_Stein(a, b):  
        if a < b:
            a, b = b, a
        if (0 == b):
            return a
        if a % 2 == 0 and b % 2 == 0:
            return 2 * gcd_Stein(a/2, b/2)
        if a % 2 == 0:
            return gcd_Stein(a / 2, b)
        if b % 2 == 0:
            return gcd_Stein(a, b / 2)
        
        return gcd_Stein((a + b) / 2, (a - b) / 2)
    

      

    传送门:代码的GitHub地址:https://github.com/LeBron-Jian/BasicAlgorithmPractice 

    参考文献:https://www.cnblogs.com/jason2003/p/9797750.html

    https://www.cnblogs.com/Dragon5/p/6401596.html

  • 相关阅读:
    监控服务器配置(一)-----Prometheus安装配置
    mongo可视化工具adminMongo安装
    Grafana 下载与安装(v5.4.1)
    Grafana+Prometheus系统监控之Redis
    聊聊redis的监控工具
    Linux 服务器buff/cache清理
    redis为什么内存不宜过大
    Python 操作 mongodb 亿级数据量使用 Bloomfilter 高效率判断唯一性 例子
    Redis-3.2.0集群配置(redis cluster)
    在reshard过程中,将会询问reshard多少slots:
  • 原文地址:https://www.cnblogs.com/wj-1314/p/11661314.html
Copyright © 2011-2022 走看看