LeetCode數學問題(Python)

一隻乾巴巴的海綿發表於2020-10-08

公倍數與公因數

輾轉相除法(歐幾里得演算法)

利用輾轉相除法,可以很方便地求得兩個數的最大公因數(greatest common divisor, gcd)

證明: g c d ( a , b ) = g c d ( b , a % b ) gcd(a,b)=gcd(b,a\% b) gcd(a,b)=gcd(b,a%b)
g c d ( a , b ) = d , g c d ( b , a % b ) = e gcd(a,b)=d,gcd(b,a\% b)=e gcd(a,b)=d,gcd(b,a%b)=e
d ∣ a , d ∣ b , a % b = a − ⌊ a / b ⌋ b ⇒ d ∣ a % b d|a,\quad d|b,\quad a\% b = a-\lfloor a/b\rfloor b\Rightarrow d|a\%b da,db,a%b=aa/bbda%b

d d d b b b a % b a\%b a%b 的公因子, d ≤ e d\leq e de
e ∣ b , e ∣ a % b , a = ⌊ a / b ⌋ b + ( a − ⌊ a / b ⌋ b ) ⇒ e ∣ a e|b,\quad e|a\%b,\quad a= \lfloor a/b\rfloor b+(a-\lfloor a/b\rfloor b)\Rightarrow e|a eb,ea%b,a=a/bb+(aa/bb)ea

e e e a a a b b b 的公因子, e ≤ d e\leq d ed e = d e=d e=d

具體做法:用較大數除以較小數,再用出現的餘數(第一餘數)去除除數,再用出現的餘數(第二餘數)去除第一餘數,如此反覆,直到最後餘數是0為止,最後的除數就是這兩個數的最大公約數。

將兩個數相乘再除以最大公因數即可得到最小公倍數(least common multiple, lcm)

def gcd(a,b):
    return a if b==0 else gcd(b,a%b)

def lcm(a,b):
    return a*b/gcd(a,b)

擴充套件的歐幾里得演算法

任意兩個整數 a a a b b b,必存在整數 x x x y y y 使得 a x + b y = g c d ( a , b ) ax + by = gcd(a,b) ax+by=gcd(a,b)。且 g c d ( a , b ) gcd(a,b) gcd(a,b) a a a b b b 最小的正線性組合。

證明:

g c d ( a , b ) gcd(a,b) gcd(a,b) d d d a a a b b b 的最小正線性組合為 s s s,下證 d = s d=s d=s
d = g c d ( a , b ) ⇒ d ∣ a , d ∣ b ⇒ d ∣ s d=gcd(a,b)\Rightarrow d|a ,\quad d|b \Rightarrow d|s d=gcd(a,b)da,dbds

a % s = a − ⌊ a / s ⌋ s = a − ⌊ a / s ⌋ ( a x + b y ) = a ( 1 − ⌊ a / s ⌋ x ) − b ⌊ a / s ⌋ y a\% s = a-\lfloor a/s\rfloor s = a-\lfloor a/s\rfloor (ax+by) = a(1-\lfloor a/s\rfloor x)-b\lfloor a/s\rfloor y a%s=aa/ss=aa/s(ax+by)=a(1a/sx)ba/sy

表明 a % s a\% s a%s也是 a a a b b b 的線性組合,但 a % s < s a\% s<s a%s<s a % s a\% s a%s不能是 a a a b b b 的最小的正線性組合,故 $ a % s = 0 a\% s=0 a%s=0,即 s ∣ a s|a sa。同理有 s ∣ b s|b sb 。所以 s s s a a a b b b 的公因子,所以 s ≤ d s\leq d sd。又 d ∣ s d|s ds ,所以 d = s d=s d=s

如何求出 x x x y y y

b = 0 b=0 b=0 時, g c d ( a , b ) = a gcd(a,b)=a gcd(a,b)=a,此時 x = 1 , y = 0 x=1,y=0 x=1,y=0
b ≠ 0 b\neq0 b=0時,設已經求出 g c d ( b , a % b ) gcd(b,a\%b) gcd(b,a%b)的線性組合: g c d ( b , a % b ) = b x ′ + a % b y ′ gcd(b,a\%b)=bx'+a\% b y' gcd(b,a%b)=bx+a%by,則
g c d ( a , b ) = g c d ( b , a % b ) = b x ′ + a % b y ′ = b x ′ + ( a − ⌊ a / b ⌋ b ) y ′ = a y ′ + b ( x ′ − ⌊ a / b ⌋ y ′ ) gcd(a,b)=gcd(b,a\%b)=bx'+a\% b y'=bx'+(a-\lfloor a/b\rfloor b)y'=ay'+b(x'-\lfloor a/b\rfloor y') gcd(a,b)=gcd(b,a%b)=bx+a%by=bx+(aa/bb)y=ay+b(xa/by)

x = y ′ , y = x ′ − ⌊ a / b ⌋ y ′ x=y',y=x'-\lfloor a/b\rfloor y' x=y,y=xa/by 即可遞迴的求得。

def xGCD(a,b):
    if b==0:
        x,y=1,0
        return x,y,a
    x1,y1,gcd=xGCD(b,a%b)
    x,y=y1,x1-a//b*y1
    return x,y,gcd

質數

204.計數質數【簡單】

LeetCode傳送門
統計所有小於非負整數 n 的質數的數量。

題解:
埃拉託斯特尼篩法(Sieve of Eratosthenes,簡稱埃氏篩法)是非常常用的判斷一個整數是否是質數的方法,並且,它可以在判斷一個整數 n n n 是不是質數的同時,判斷所有小於 n n n 的整數。原理為:從 1 到 n n n 遍歷,假設當前遍歷到 m m m ,則把所有小於 n n n 的、且是 m m m 的倍數的整數標為和數;遍歷完成後,沒有被標為和數的數字即為質數。

class Solution:
    def countPrimes(self, n: int) -> int:
        if n<=2:return 0
        prime=[True]*n
        count=n-2 # 去掉1
        for i in range(2,n):
            if prime[i]:
                j=2*i
                while j<n:
                    if prime[j]:
                        prime[j]=False
                        count-=1
                    j+=i
        return count
  • 如果一個數不是素數是合數,那麼一定可以由兩個自然數相乘得到,其中一個大於或等於它的平方根,一個小於或等於它的平方根。
class Solution:
    def countPrimes(self, n: int) -> int:
        if n<=2:return 0
        prime=[True]*n
        i,sqrtn,count=3,n**0.5,n//2 # 偶數一定不是質數
        while i<=sqrtn:
            j=i*i
            while j<n:
                if prime[j]:
                    count-=1
                    prime[j]=False
                j+=2*i
            i+=2 # 排除偶數
            while i<=sqrtn and not prime[i]:
                i+=2
        return count

數字處理

504.七進位制數【簡單】

LeetCode傳送門
給定一個整數,將其轉化為7進位制,並以字串形式輸出。
注意: 輸入範圍是 [-1e7, 1e7] 。

題解:
LeetCode數學問題(Python)LeetCode數學問題(Python)

進位制轉換型別的題,通常是利用除法和取模(mod)來進行計算,同時也要注意一些細節,如負數和零。如果輸出是數字型別而非字串,則也需要考慮是否會超出整數上下界。

class Solution:
    def convertToBase7(self, num: int) -> str:
        if num==0:return '0'
        is_negative=num<0
        if is_negative:
            num=-num
        ans=''
        while num:
            a,b=num//7,num%7
            ans=str(b)+ans
            num=a
        if is_negative:
            return '-'+ans
        else:
            return ans

172.階乘後的零【簡單】

LeetCode傳送門
給定一個整數 n,返回 n! 結果尾數中零的數量。

題解:

  • 在一個階乘中,把所有 1 1 1 n n n 之間的數相乘,這和把所有 1 1 1 n n n 之間所有數字的因子相乘是一樣的。
  • 每個尾部的 0 0 0 2 × 5 = 10 2\times5=10 2×5=10 而來,因此我們可以把階乘的每一個元素拆成質數相乘,統計有多少個2 和5。
  • 質因子2 的數量遠多於質因子5 的數量,例如 n = 16 n=16 n=16,包含因子5的數字有5、10、15,包含因子2的數字有2、4、6、8、10、12、14、16。因此我們可以只統計階乘結果裡有多少個質因子5。
class Solution:
    def trailingZeroes(self, n: int) -> int:
        count=0
        for i in range(5,n+1,5):
            cur=i
            while cur%5==0:
                count+=1
                cur//=5
        return count

415.字串相加【簡單】

LeetCode傳送門
給定兩個字串形式的非負整數 num1 和num2 ,計算它們的和。
提示:

  • num1 和num2 的長度都小於 5100
  • num1 和num2 都只包含數字 0-9
  • num1 和num2 都不包含任何前導零
  • 你不能使用任何內建 BigInteger 庫, 也不能直接將輸入的字串轉換為整數形式

題解:
設定 i,j 兩指標分別指向 num1,num2 尾部,模擬人工加法;

  • 計算進位: 計算 carry = tmp // 10,代表當前位相加是否產生進位;
  • 新增當前位: 計算 tmp = n1 + n2 + carry,並將當前位 tmp % 10 新增至 res 頭部;
  • 索引溢位處理: 當指標 i或j 走過數字首部後,給 n1,n2 賦值為 00,相當於給 num1,num2 中長度較短的數字前面填 00,以便後續計算。
  • 當遍歷完 num1,num2 後跳出迴圈,並根據 carry 值決定是否在頭部新增進位 11,最終返回 res 即可。
class Solution:
    def addStrings(self, num1: str, num2: str) -> str:
        ans=''
        i,j,carry=len(num1)-1,len(num2)-1,0
        while i>=0 or j>=0:
            n1=int(num1[i]) if i>=0 else 0
            n2=int(num2[j]) if j>=0 else 0
            tmp=n1+n2+carry
            carry=tmp//10
            ans=str(tmp%10)+ans
            i,j=i-1,j-1
        return str(carry)+ans if carry else ans

326.3的冪【簡單】

LeetCode傳送門
給定一個整數,寫一個函式來判斷它是否是 3 的冪次方。

題解:
方法一:利用對數。設 log ⁡ 3 n = m \log_3 n=m log3n=m,如果 n n n 是 3 的冪次方,則 m m m 一定是整數。

class Solution:
    def isPowerOfThree(self, n: int) -> bool:
        if n<=0:return False
        import math
        log3 = math.log(n,3)
        if log3%1==0:
            return True
        return False

n = 243 n=243 n=243 時,答案錯誤,這是由於Python的精度問題。

class Solution:
    def isPowerOfThree(self, n: int) -> bool:
        if n<=0:return False
        import math
        log3 = math.log(n,3)
        if 3**round(log3)==n:
            return True
        return False

方法二:因為在int 範圍內3 的最大次方是 3 19 = 1162261467 3^{19} = 1162261467 319=1162261467,如果n 是3 的整數次方,那麼1162261467 除以n 的餘數一定是零;反之亦然。

class Solution:
    def isPowerOfThree(self, n: int) -> bool:
        return n>0 and 1162261467%n==0

補充:Python精度問題

浮點數在計算機中的表示

LeetCode數學問題(Python)

  在計算機中無論是整數、浮點數、還是字串最終都是用二進位制來表示的。

  根據國際標準IEEE 754,一個二進位制浮點數分為3部分:s(符號位,0表示正數,1表示負數)、E(指數位)、M(有效數字位)。

  • 對於32位的浮點數,最高1位是符號位,接著8位是指數位,即整數部分,剩下的23位為小數位。
    在這裡插入圖片描述

  • 對於64位的浮點數,最高1位是符號位,接著11位是指數位,剩下的52位為小數位。
    在這裡插入圖片描述

只有1%的人搞懂過Python 浮點數陷阱

Python中浮點數精度處理

  因浮點數在計算機中實際是以二進位制儲存的,有些數不精確。比如說:0.1是十進位制,轉化為二進位制後它是個無限迴圈的數:0.00011001100110011001100110011001100110011001100110011001100。Python是以雙精度(64)位來儲存浮點數,多餘的位會被截掉,所以看到的是0.1,但在電腦上實際儲存的已不是精確的0.1,參與運算後,也就有可能點誤差。

如何在Python中獲取特定位數精度值:

方法一:
round(x[,n]):返回浮點數 x 的四捨五入值,n 預設值為0。

方法二:decimal 模組
在這裡插入圖片描述

Python中浮點數精度處理

隨機與取樣

384.打亂陣列【中等】

LeetCode傳送門
打亂一個沒有重複元素的陣列。

示例:

// 以數字集合 1, 2 和 3 初始化陣列。
int[] nums = {1,2,3};
Solution solution = new Solution(nums);

// 打亂陣列 [1,2,3] 並返回結果。任何 [1,2,3]的排列返回的概率應該相同。
solution.shuffle();

// 重設陣列到它的初始狀態[1,2,3]。
solution.reset();

// 隨機返回陣列[1,2,3]打亂後的結果。
solution.shuffle();

題解:
方法一:暴力

把每個數放在一個 ”帽子“ 裡面,每次從 ”帽子“ 裡面隨機摸一個數出來,直到 “帽子” 為空。下面是具體操作,首先我們把陣列 array 複製一份給陣列 aux,之後每次隨機從 aux 中取一個數,為了防止數被重複取出,每次取完就把這個數從 aux 中移除。重置 的實現方式很簡單,只需把 array 恢復稱最開始的狀態就可以了。

class Solution:
    def __init__(self, nums: List[int]):
        self.array=nums
        self.original=list(nums)

    def reset(self) -> List[int]:
        self.array=self.original
        self.original=list(self.original)
        return self.array       

    def shuffle(self) -> List[int]:
        aux=list(self.array)
        for idx in range(len(self.array)):
            remove_idx=random.randrange(len(aux))
            self.array[idx]=aux.pop(remove_idx)
        return self.array

方法二: Fisher-Yates 洗牌演算法

  對於洗牌問題,Fisher-Yates 洗牌演算法即是通俗解法,同時也是漸進最優的解法。
  Fisher-Yates 洗牌演算法跟暴力演算法很像。在每次迭代中,生成一個範圍在當前下標到陣列末尾元素下標之間的隨機整數。接下來,將當前元素和隨機選出的下標所指的元素互相交換,這一步模擬了每次從 “帽子” 裡面摸一個元素的過程,其中選取下標範圍的依據在於每個被摸出的元素都不可能再被摸出來了。此外還有一個需要注意的細節,當前元素是可以和它本身互相交換的,否則生成最後的排列組合的概率就不對了。

class Solution:
    def __init__(self, nums: List[int]):
        self.array=nums
        self.original=list(nums)

    def reset(self) -> List[int]:
        self.array=self.original
        self.original=list(self.original)
        return self.array       

    def shuffle(self) -> List[int]:
        for idx in range(len(self.array)):
            swap_idx=random.randrange(idx,len(self.array))
            self.array[idx],self.array[swap_idx]=self.array[swap_idx],self.array[idx]
        return self.array

528.按權重隨機選擇【中等】

LeetCode傳送門
給定一個正整數陣列 w ,其中 w[i] 代表下標 i 的權重(下標從 0 開始),請寫一個函式 pickIndex ,它可以隨機地獲取下標 i,選取下標 i 的概率與 w[i] 成正比。
例如,對於 w = [1, 3],挑選下標 0 的概率為 1 / (1 + 3) = 0.25 (即25%),而選取下標 1 的概率為 3 / (1 + 3) = 0.75(即75%)。也就是說,選取下標 i 的概率為 w[i] / sum(w) 。

提示:

  • 1 <= w.length <= 10000
  • 1 <= w[i] <= 10^5
  • pickIndex 將被呼叫不超過 10000 次

題解: 字首和+二分查詢
  先使用partial_sum 求字首和(即到每個位置為止之前所有數字的和),這個結果對於正整數陣列是單調遞增的。每當需要取樣時,我們可以先隨機產生一個數字,然後使用二分法查詢其在字首和中的位置,以模擬加權取樣的過程。這裡的二分法可以用 bisect_left 實現,該函式用於處理將會插入重複數值的情況,返回將會插入的位置。以樣例為例,權重陣列[1,3] 的字首和為[1,4]。如果我們隨機生成的數字為1,那麼 bisect_left 返回的位置為0;如果我們隨機生成的數字是2、3、4,那麼 bisect_left 返回的位置為1。

class Solution:
    import random
    from bisect import bisect
    def __init__(self, w: List[int]):
        self.partial_sum=[]
        tmp=0
        for x in w:
            tmp+=x
            self.partial_sum.append(tmp)

    def pickIndex(self) -> int:
        randnum=random.randint(1,self.partial_sum[-1])
        return bisect.bisect_left(self.partial_sum,randnum)

382.連結串列隨機節點【中等】

LeetCode傳送門
給定一個單連結串列,隨機選擇連結串列的一個節點,並返回相應的節點值。保證每個節點被選的概率一樣。
進階:如果連結串列十分大且長度未知,如何解決這個問題?你能否使用常數級空間複雜度實現?

題解: 蓄水池抽樣演算法

演算法背景:大資料流中的隨機抽樣問題。當記憶體無法載入全部資料時,如何從包含未知大小的資料流中隨機選取k個資料,並且要保證每個資料被抽取到的概率相等。

演算法原理:

一、假設每次只能讀取一個資料,資料流中含有 N 個數,如果要保證所有的數被抽到的概率相等,那麼每個數被抽到的概率應該為 1 N \frac{1}{N} N1。蓄水庫演算法的方案是:每次只保留一個數,當遇到第 i 個數時,以 1 i \frac{1}{i} i1 的概率保留它, i − 1 i \frac{i-1}{i} ii1 的概率保留原來的數。 舉例說明: 1 - 10

  • 遇到1,概率為1,保留第一個數。
  • 遇到2,概率為1/2,這個時候,1和2各1/2的概率被保留
  • 遇到3,3被保留的概率為1/3,(之前剩下的數假設1被保留),2/3的概率 1 被保留,(此時1被保留的總概率為 2/3 * 1/2 = 1/3)
  • 遇到4,4被保留的概率為1/4,(之前剩下的數假設1被保留),3/4的概率 1 被保留,(此時1被保留的總概率為 3/4 * 2/3 * 1/2 = 1/4)

以此類推,每個數被保留的概率都是1/N。

證明:第 i 個數最後被取樣的充要條件是,它被選擇,且之後都被保留。這種情況發生的概率為
1 i × i i + 1 × i + 1 i + 2 × ⋅ × N − 1 N = 1 N \frac{1}{i}\times\frac{i}{i+1}\times\frac{i+1}{i+2}\times\cdot\times\frac{N-1}{N}=\frac{1}{N} i1×i+1i×i+2i+1××NN1=N1

二、假設每次只能讀取 k(k>1) 個資料
在這裡插入圖片描述

class Solution:
    import random
    def __init__(self, head: ListNode):
        """
        @param head The linked list's head.
        Note that the head is guaranteed to be not null, so it contains at least one node.
        """
        self.head=head

    def getRandom(self) -> int:
        """
        Returns a random node's value.
        """
        count,reserve=0,0
        cur=self.head
        while cur:
            count+=1
            rand=random.randint(1,count)
            if rand==count:
                reserve=cur.val
            cur=cur.next
        return reserve

LeetCode 101: A LeetCode Grinding Guide (C++ Version) 作者:高暢

相關文章