1. 程式人生 > >python常用演算法(6)——貪心演算法,歐幾里得演算法

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

  

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)

  

 

參考文獻:https://www.cnblogs.com/jason2003/p/9797750.html

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