程序設計與算法(二)算法基礎課--1、枚舉 python實現


程序設計與算法(二)算法基礎課–1、枚舉 python實現
課程源自mooc網,北京大學郭煒老師的課程,這裏只是作爲課程筆記分享,原課程使用c++實現,這裏用python實現。
課件:鏈接:https://pan.baidu.com/s/1Ld2gNhwILc6vDZdiNgFToQ 密碼:1p7b
視頻請見mooc網:程序設計與算法(二)算法基礎


第一節課枚舉一共有4道例題。

(1)完美立方(2)生理週期(3)假幣問題(4)熄燈問題。

前兩題都是非常規矩的枚舉題,思考難度和代碼難度都不大,後兩題都比較複雜。


題1:完美立方

形如a3= b3 + c3 + d3的等式被稱爲完美立方等式。例如 123= 63 + 83 + 103 。編寫一個程序,對任給的正整數N (N≤100),尋找所有的四元組(a, b, c, d),使得a3 = b3 + c3 + d3,其中a,b,c,d 大於 1, 小於等於N,且b<=c<=d。
輸入:一個正整數N (N≤100)。
輸出:每行輸出一個完美立方。輸出格式爲:Cube = a, Triple = (b,c,d) 其中a,b,c,d所在位置分別用實際求出四元組值代入。請按照a的值,從小到大依次輸出。當兩個完美立方 等式中a的值相同,則b值小的優先輸出、仍相同 則c值小的優先輸出、再相同則d值小的先輸出。

樣例輸入:24

樣例輸出:

Cube = 6, Triple = (3,4,5)
Cube = 12, Triple = (6,8,10)
Cube = 18, Triple = (2,12,16)
Cube = 18, Triple = (9,12,15)
Cube = 19, Triple = (3,10,18)
Cube = 20, Triple = (7,14,17)
Cube = 24, Triple = (12,16,20)

解題思路:

題目其實隱藏了遍歷順序,先a,b,c,d
並且:a[2,N],b[2,a1],c[b,a1],d[c,a1]a\in[2,N],b \in [2,a-1],c \in [b,a-1],d \in [c,a-1]
所以:

python代碼:

def perfect_cube(N):
    cube_list = []
    for a in range(2,N+1):
        for b in range(2,a):
            for c in range(b,a):
                for d in range(c,a):
                    if a**3 == (b**3+c**3+d**3):
                        cube_list.append((a,b,c,d))
    for t in cube_list:
        a,b,c,d = t[0],t[1],t[2],t[3]
        print('Cube=%d,Triple=(%d,%d,%d)'%(a,b,c,d))
    return cube_list
cube_list = perfect_cube(24)!

結果:
在這裏插入圖片描述

題2:生理週期

人有體力、情商、智商的高峯日子,它們分別每隔 23天、28天和33天出現一次。對於每個人,我們想 知道何時三個高峯落在同一天。給定三個高峯出現 的日子p,e和i(不一定是第一次高峯出現的日子), 再給定另一個指定的日子d,你的任務是輸出日子d 之後,下一次三個高峯落在同一天的日子(用距離d 的天數表示)。例如:給定日子爲10,下次出現三 個高峯同一天的日子是12,則輸出2。
輸入: 四個整數:p, e, i和d。 p, e, i分別表示體力、情感和智力高峯出現的日子。d是給定的日子,可能小於p, e或 i。所有給 定日子是非負的並且小於或等於365,所求的日子小於或等於 21252。
輸出: 從給定日子起,下一次三個高峯同一天的日子(距離給定日子的天數)。

在這裏插入圖片描述

解題思路:

(1)首先分析數字21252。是23,28和33的最小公倍數。所以說這裏不涉及多個日期,所有的只涉及唯一值。所以不需要選擇公倍數的日期,不涉及週期問題。
(2)下一步就是開始遍歷,即那一天滿足(k – p)%23 == 0 && (k – e)%28 == 0 && (k-i)%33 == 0.
(3)跳着遍歷,先找到第一個符合(k – p)%23 == 0的日子,然後加+23,尋找哪一天(k – e)%28 == 0 ,然後跳23和28的最小公倍數天,然後判斷哪一天還符合(k-i)%33 == 0。
方案二:(3)如果不想找兩個數字的最小公倍數,就直接遍歷也可以。

python代碼:

最小公倍數代碼:

def gcd(a,b): #stein 算法
    if b>a:
        a,b = b,a 
    if b==0:
        return a 
    if b==a:
        return a 
    if a%2 ==0 and b%2 ==0:
        return 2*(gcd(a/2,b/2))
    if a%2 ==0:
        return gcd(a/2,b)
    if b%2 == 0: 
         return gcd(a,b/2)
    return gcd((a+b)/2,(a-b)/2)

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

生理週期代碼:

def life_circle(p,e,i,d):
    cycle_p = 23 
    cycle_e = 28 
    cycle_i = 33
    cycle_pe = lcm(cycle_e,cycle_p)
    d0=d+1
    while d0 <=21252:
        if (d0-p)%cycle_p ==0: 
            if (d0-e)%cycle_e == 0:
                if (d0-i)%cycle_i == 0:
                    return d0-d
                else:
                    d0+= cycle_pe
            else:
                d0+=cycle_p
        else:
            d0+=1
    return -1
print('the next triple peak occurs in %d days!'%(life_circle(5,20,34,325)))

結果:
在這裏插入圖片描述

題3: 假幣問題

有12枚硬幣。其中有11枚真幣和1枚假幣。假幣和真幣重量不同,但不知道假幣比真幣輕還是重。現在, 用一架天平稱了這些幣三次,告訴你稱的結果,請你找出假幣並且確定假幣是輕是重(數據保證一定能找出來)。
輸入:
第一行是測試數據組數。
每組數據有三行,每行表示一次稱量的結果。銀幣標號爲 A-L。每次稱量的結果用三個以空格隔開的字符串表示: 天平左邊放置的硬幣 天平右邊放置的硬幣 平衡狀態。其中平衡狀態用’‘up’’, ‘‘down’’, 或 '‘even’'表示, 分別爲右端高、右端低和平衡。天平左右的硬幣數總是相等的
輸出:
輸出哪一個標號的銀幣是假幣,並說明它比真幣輕還是重。

在這裏插入圖片描述

解題思路

(1)這道題目的不同之處在於,不是讓我們想辦法去稱量出來哪一個是假幣,而是他提供可以找到假幣的稱量方式我們只需要根據結果找出來哪一個爲假幣即可。
(2)輸入左右一定相等,一共12枚硬幣,所以每次稱量左右單邊長度最大爲6,最小爲1。
(3)up爲右端高,即右邊輕,down,右端重,even,一樣重。


老師給的解決方案:

對於每一枚硬幣先假設它是輕的,看這樣是否符合稱 量結果。如果符合,問題即解決。如果不符合,就假 設它是重的,看是否符合稱量結果。把所有硬幣都試一遍,一定能找到特殊硬幣

老師的方案的嘗試的次數爲12*2=24次。


這裏我們可以換一種思路思考,根據題目的特殊性,一共12枚硬幣,而且只有假幣的質量不同,但是不知道假幣的質量是輕還是重。
換句話說:如果稱量結果爲even,現在出現的所有硬幣一定都是真幣,如果稱量結果爲up或者down,出現的硬幣中一定有假幣。
可以歸納爲:
(1)結果只有even,沒有稱量的那一枚硬幣爲假,但是這樣的話無法知道輕重,所以肯定不成立。
(2)結果有even和up/down:我們可以把所有稱量過的硬幣作爲一個集合,排除掉所有出現在even中的硬幣,然後去檢驗剩餘的硬幣是否成立。

python代碼

def weight_coins(inputs):
    correct_list = [] #肯定正確的硬幣列表
    suspect_list = [] #有懷疑的硬幣列表
    for i in range(3):
        inp = inputs[i]
        left,right,result = inp.split('\t')       
        if result == 'even':
            #結果爲even的所有硬幣一定爲真
            correct_list.extend(list(left))
            correct_list.extend(list(right))
        else:
            #結果不爲even的硬幣有懷疑
            suspect_list.extend(list(left))
            suspect_list.extend(list(right))
    suspect_list = list(set(suspect_list)-set(correct_list))#將有懷疑的硬幣排除肯定正確的硬幣
    #對有懷疑的硬幣遍歷
    for coin in suspect_list:
        #對輕重進行遍歷
        for state in iter(['light','heavy']):
            flag = True #判斷三個結果是否正確,有一次違背就是False
            for i in range(3):
                inp = inputs[i]
                left,right,result = inp.split('\t')
                if result == 'even':
                    if coin in left or coin in right:
                        flag = False
                        break
                elif result == 'up':
                    if coin not in right:
                        flag = False
                        break
                else:
                    if coin not in left:
                        flag = False
                        break
            if flag:
                artificial_coin = coin 
                state = state
                return artificial_coin,state
            
inputs = ['ABCD\tEFGH\teven','ABCI\tEFJK\tup','ABIJ\tEFGH\teven']
artificial_coin,state = weight_coins(inputs)
print('%s is the counterfeit coin and it is %s'%(artificial_coin,state))

題4: 熄燈問題

有一個由按鈕組成的矩陣, 其中每行有6個按鈕, 共5行,每個按鈕的位置上有一盞燈, 當按下一個按鈕後, 該按鈕以及周圍位置(上邊, 下邊, 左 邊, 右邊)的燈都會改變狀態, 如果燈原來是點亮的, 就會被熄滅 ;如果燈原來是熄滅的, 則會被點亮。
(1)在矩陣角上的按鈕改變3盞燈的狀態(2)在矩陣邊上的按鈕改變4盞燈的狀態 (3) 其他的按鈕改變5盞燈的狀態
在這裏插入圖片描述
與一盞燈毗鄰的多個按鈕被按下時,一個操作會抵消另一次操作的結果,給定矩陣中每盞燈的初始狀態,求一種按按鈕方案,使得所有 的燈都熄滅
輸入:
– 第一行是一個正整數N, 表示需要解決的案例數
– 每個案例由5行組成, 每一行包括6個數字
– 這些數字以空格隔開, 可以是0或1
– 0 表示燈的初始狀態是熄滅的
– 1 表示燈的初始狀態是點亮的
輸出:
– 對每個案例, 首先輸出一行,輸出字符串 “PUZZLE #m”, 其中m是該案例的序 號
– 接着按照該案例的輸入格式輸出5行
• 1 表示需要把對應的按鈕按下
• 0 表示不需要按對應的按鈕
• 每個數字以一個空格隔開
在這裏插入圖片描述

解題思路

(1)題目隱藏信息:第2次按下同一個按鈕時, 將抵消第1次按下時所產生的結果。所以只有按或者不按兩個選擇,不用記錄次數。
(2)各個按鈕被按下的順序對最終的結果沒有影響。
(3)遍歷:

  • 可以對每個燈的狀態進行開關遍歷,看是否成立,需要遍歷230次。次數太多,會超時。
    老師的改進思路:

如果存在某個局部, 一旦這個局部的狀態被確定,那麼剩餘其他部分的狀態只能是確定的一種, 或者不多的種, 那麼就只需枚舉這個局部的狀態即可.

  • 經過觀察, 發現第1行就是這樣的一個 “局部”,因爲第1行的各開關狀態確定的情況下, 這些開關作用過後, 將 導致第1行某些燈是亮的, 某些燈是滅的。要熄滅第1行某個亮着的燈(假設位於第i列), 那麼唯一的辦法就 是按下第2行第i列的開關。爲了熄滅第2行的燈, 第3行的合理開關狀態就也是唯一的。
    推算出最後一行的開關狀態, 然後看看最後一行的開關起作用後, 最後一行的所有燈是否都熄滅:
    • 如果是,那麼A就是一個解的狀態
    • 如果不是,那麼A不是解的狀態,第1行換個狀態重新試試

只需枚舉第1行的狀態, 狀態數是26 = 64

Q: 有沒有狀態數更少的做法?
AS: 枚舉第一列, 狀態數是25 = 32

(4)高效的存儲和遍歷方案:

  • 一個char的變量類型一個位有8個bit,這裏只需要用6個,所以只需要5個char的數組,就可以存儲燈的狀態。
  • 開關起作用,就是從0->1 1>0,即使用位運算
  • 枚舉的時候的快捷方法:如果需要對k個比特進行枚舉,可以使用一個整數從0-2^k-1,然後每個數都可以對應k個數的一種組合,所以我們這裏可以從0變換到63.

python代碼

python中的數據類型選擇:
(1)存儲格式選擇列表原因:

Python不允許程序員選擇採用傳值還是傳 引用。Python參數傳遞採用的肯定是“傳對象引用”的方式。
Python中的對象有可變對象(number,string,tuple等)和不可變對象之分(list,dict等)。
不可變對象作爲函數參數,相當於C語言的值傳遞(值傳遞的特點是被調函數對形式參數的任何操作都是作爲局部變量進行,不會影響主調函數的實參變量的值。)。
可變對象作爲函數參數,相當於C語言的引用傳遞。(被調函數對形參做的任何操作都影響了主調函數中的實參變量)

所以這裏存儲選擇字符列表。
(2)python中字符串不能直接進行位運算,所以需要用到兩個命令:ord和chr。
ord用來變成數字形式,chr將數字返回字符串形式。

  • ps:這裏有個疑問,我還沒確認這裏爲什麼還可以輸出,python中字符可能不是8bit。待確認。
    在這裏插入圖片描述
    此外問題: chr出現溢出問題,所以最後還是採用數組形式存儲,然後採用位運算。
def turn_off_lights():
    N = int(input())
    for m in range(N):
        #第m個
        #讀取原始檯燈狀態。
        raw_lights =np.zeros((5,6))
        for j in range(5):
            line = input().split(' ')[:6]
            raw_lights[j] = [int(light) for light in line]
        #遍歷一遍改變狀態
        for first_swich in range(64):
            #構建一個switches記錄每一行開關狀態。
            switches = np.zeros((5,6))
            bin_s = list(bin(first_swich).lstrip('0b'))
            bin_s = [0]*(6-len(bin_s)) +[int(i) for i in bin_s] #將二進制數轉換成定長爲6
            #複製一遍
            tmp_lights = deepcopy(raw_lights)
            #一行一行改變狀態,從上往下改變,就不要該上面了。
            switch = bin_s
            for j in range(5):
                switches[j] = deepcopy(switch)
                #一行有6個
                for s_index in range(6):
                    if switch[s_index]:#如果按鈕是1
                        #改變自己的狀態
                        tmp_lights[j][s_index] = int(tmp_lights[j][s_index])^1
                        #按鈕左右都要改變狀態
                        if s_index>0:
                            tmp_lights[j][s_index-1] = int(tmp_lights[j][s_index-1])^1
                        if s_index<5:
                            tmp_lights[j][s_index+1] =int(tmp_lights[j][s_index+1])^1
                        #按鈕下面一行改變狀態
                        if j <4 :
                            tmp_lights[j+1][s_index] =int(tmp_lights[j+1][s_index])^ 1
                #下一行的按鈕狀態就是這一行等的狀態,這一行爲0即燈關閉,下一行就要爲0,這一行爲1,下一行就要按滅
                switch = tmp_lights[j]
                #判斷最後一行的狀態是不是全部關閉,全部關閉則輸出
                if j==4 and not tmp_lights[4].any():
                    #輸出switches狀態
                    print('PUZZLE #%d'%(int(m)))
                    for jj in range(5):
                        print(' '.join([str(int(s)) for s in switches[jj]]))
                    break
turn_off_lights()

結果:
在這裏插入圖片描述

總結:其實枚舉是一個比較常用的解法,雖然很樸素,但是很解決很多問題,而且可以根據一些先驗知識減少枚舉的次數,就可以取到比較好的效果。
此外位運算也是一個在使用中經常被忽視的東西,但是其實位運算很好用,但是在python中使用位運算好像不是很方便,或者是我寫法的問題,位運算還是遇到了一些麻煩的,最後還是採用了數據的解法。
明天攻克第二章的內容,然後在做博客分享吧~


一些其它的心情分享:
最近肺炎疫情真的很嚴重,希望一切都快好起來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章