Python實現在給定整數序列中找到和為100的所有數字組合


 

    摘要:  使用Python在給定整數序列中找到和為100的所有數字組合。可以學習貪婪算法及遞歸技巧。

    難度:  初級

 

     問題

  給定一個整數序列,要求將這些整數的和盡可能拼成 100。

  比如 [17, 17, 4, 20, 1, 20, 17, 6, 18, 17, 12, 11, 10, 7, 19, 6, 16, 5, 6, 21, 22, 21, 10, 1, 10, 12, 5, 10, 6, 18] , 其中一個解是 [ [17, 17, 4, 20, 1, 17, 6, 18] [20, 18, 17, 12, 11, 10, 6, 6] [7, 19, 16, 5, 6, 21, 10, 1, 5, 10] ] , 沒有用到的數字是 [12, 10, 21, 22] 

 

       累加性的序列問題,通常可以使用動態規划法。不過這個略有不同,因為和為 sum 與 和為 sum-1 的解基本沒有聯系; 此題將使用貪心算法,每次總是根據當下找到一個數字組合來拼成 100 , 而不顧及使用的數字對后面拼100 造成的影響;因為不會窮盡所有可行解集合,所以不確保一定是最優解(未使用的數字最少)。

 

  算法設計

        STEP1:  從序列 origin 中取出一個數 NUM ;

        STEP2:  在剩下的數中尋找和為 100 - NUM 的數字組合 matched = m1,m2,..., mN ;

     A.  如果找到, 那么在 origin 中移除 NUM 和 matched ,  將 [matched, NUM] 加入匹配解集合 finalResults,  然后跳轉到 STEP3 ;

                B.  如果沒有找到, 將 NUM 加入未匹配序列 unMatched, 跳轉到 STEP3 ;

        STEP3:   檢測 origin 是否還有元素; 如果還有元素, 跳轉到 STEP1 ; 否則跳轉到 STEP4

        STEP4:   退出算法。

  

  注意到這里需要一個函數 matchSum,實現以下功能: 給定一個數字 sum 及一個序列 seq, 如果能夠在 seq 中找到和為 sum 的數字組合 matched,則返回 (True, matched) ; 否則返回 (False, []) .  這個函數可以遞歸實現。遞歸的三要素是: (解結構,終止條件, 遞歸方式)。如果解結構是一個無序序列,那么可以直接將解結構傳入遞歸函數,在遞歸函數中逐步添加解結構的組成元素; 如果解結構是有序序列,則要注意遞歸添加解元素確保其順序。

      A0.  設置解結構 matched , 傳入遞歸函數 matchSum(sum, seq, matched) , 在遞歸調用 matchSum 的過程中在 matched 中添加解元素;

   A1.  規定終止條件。通常就是最簡單的情況,為空或只有一個元素。

     a1.  如果 sum > 0 , seq 為空,那么必然不存在匹配,返回 False;

                a2.  如果 sum > 0, seq 僅含有一個數字, 且 seq[0] != sum, 那么仍然不存在匹配,返回 False ;

                a3.  如果 sum > 0, seq 僅含有一個數字, 且 seq[0] == sum, 那么存在匹配,將 seq[0] 加入 matched, 返回 True; 

     A2.  設置遞歸方式。 seq 非空, 且含有兩個及以上元素,該如何遞歸呢? 可以將 sum 與 seq[0] 比較,分情況討論:

     a1.  如果 sum < seq[0] , 那么很顯然 sum 只能在除去 seq[0] 之外的序列 seq[1:] 中尋找匹配了: matchSum(sum, seq, prematched) =  matchSum(sum, seq[1:], prematched) ;  

     a2.  如果 sum == seq[0] , 那么很顯然找到了一個匹配, 可以直接返回: matchSum(sum, seq, prematched) =  seq[0] + prematched ;  注意到, seq[0] 並不一定是指初始序列的第一個值,而是指在遞歸過程中獲取到的當前序列的第一個值,而 prematched 在之前的遞歸過程中可能已經填充部分數字了;

     a3.  如果 sum > seq[0],就分兩種情況,要么匹配結果包含 seq[0], 為 seq[0] + matchSum(sum-seq[0], seq[1:], prematched) ; 要么匹配結果不包含 seq[0], 為 matchSum(sum, seq[1:], prematched) . 

 

      解結構是一個列表,每個列表元素是一個小的列表,其元素之和為 100 ; 比如 [[54,36,10], [32, 63, 5], [24, 20, 34, 6, 12, 4]] , 未匹配數字為 [16, 20, 32] ; 

 

       更優的解法

       在算法結束時,很可能存在未匹配數字組合。尤其是當所有整數的和不能除盡100時。

       將拼成100的數字組合中的數字替換成未匹配數字的組合,可以減少未匹配數字的數量。比如解結構里的一個元素是 [54, 36, 10] , 未匹配數字為 [16, 20, 32] , 那么可以將 36 與 16, 20 替換。

    更多的解法: 將解結構的子列表中的大數字與小數字組合交換,即可得到新的解法,比如將 54 與 (20,34) , 10 與 (6,4) , 32 與 (20,12) 交換。

    這些都會用到 matchSum(sum, seq, matched) , 可以說這個函數是核心而基本的組件。通過這個組件可以衍生出各種的方法和解法。這就像業務開發中的基礎接口,通過基礎接口的疊加可以衍生出多樣靈活的業務。

  

      應對大數據量: 當序列中數字比較多,且遠小於要拼成的和時,就會出現遞歸深度很深的問題。Python 中有遞歸深度的限制。一種方法是提高遞歸深度設置;一種是將遞歸改寫成非遞歸;一種是分治法,將大數字拆解為較小數字之和,在序列中先尋找較小數字的解結構,然后再拼接出最終的解結構。問題是,較小數字的選取很難確定,因為序列中可能根本不存在和為某一個較小數字的數字組合。

      使用非遞歸方式, 可以考慮預排序。如果預排序可以使得后續處理更快速,那么算法復雜度就可能是 O(nlogn); 可以仍然按照之前的算法流程, 編寫非遞歸的實現接口。注意到下面的實現是非優化的。取序列元素,只要和小於指定數,就一直添加; 當和為指定數時,就可以輸出解;而當和大於指定數時,要考慮回溯。下面的實現僅考慮了最簡單的情形的處理, 比如,序列為 [12,13,14,14,16,18,18,20,20,20,21,...] ,要構造和為 76 ; 假設已經添加的元素為 [12,13,14,14,16] ,  sum = 69 ,還差 7 , 那么就用后面的序列中的數替換前面的數 [12,13,14,14,16] + 7 = [19, 20, 21, 21, 23] , 只要后面的序列存在這幾個數之一,替換后就可以輸出解,比如 [12,20,14,14,16] . 但后面的序列不存在這幾個數,就不會輸出解。要么添加邏輯嚴密的回溯算法,要么添加更完備的處理方法。

 

     代碼實現

import random
import sys   
sys.setrecursionlimit(10000)

UPLIMIT = 100000
NUMBERS = 500
NUMBER_RANGE = (1,UPLIMIT/4)


def randNums(n):
    return [randGen(NUMBER_RANGE[0], NUMBER_RANGE[1]) for i in range(n)]

def randGen(start, end):
    return random.randint(start, end)

def solve(numlist):
    if UPLIMIT >= 10000 and NUMBERS >= 500:
        numlist.sort()
        (finalResults, unMatchedNums) = baseSolve(numlist, matchSumUnrec)
    else:
        (finalResults, unMatchedNums) = baseSolve(numlist, matchSum)
    improve(finalResults, unMatchedNums)
    return (finalResults, unMatchedNums)

def baseSolve(numlist, matchSum):    
    if not numlist or len(numlist) == 0:
       return ([],[]);
    finalResults = []
    unMatchNums = []
    while len(numlist) > 0: 
        num = numlist.pop()
        matched = []
        if matchSum(UPLIMIT-num, numlist, matched):
            removelist(numlist, matched)
            matched.append(num)        
            finalResults.append(matched)
        else:
            unMatchNums.append(num)

    return (finalResults, unMatchNums)       

def removelist(origin, toberemoved):
    for e in toberemoved:
        if e in origin:
           origin.remove(e)

def copylist(numlist):
    return [num for num in numlist]

def matchSum(rest, restlist, prematched):
        
    if rest > 0 and len(restlist) == 0:
        return False

    if rest > 0 and len(restlist) == 1 and restlist[0] != rest:
        return False

    if rest > 0 and len(restlist) == 1 and restlist[0] == rest:
        prematched.append(restlist[0])
        return True

    getone = restlist[0]
    if rest > getone:
        prematched.append(getone)
        if matchSum(rest-getone, restlist[1:], prematched):
            return True
        else:
            prematched.remove(getone)
            return matchSum(rest, restlist[1:], prematched)
    elif rest == getone:
        prematched.append(getone)
        return True;
    else:
        return matchSum(rest, restlist[1:], prematched)


def matchSumUnrec(asum, sortedlist, matched):
    
    if asum > 0 and len(sortedlist) == 0:
        return False

    if asum > 0 and len(sortedlist) == 1 and sortedlist[0] != asum:
        return False

    if asum > 0 and len(sortedlist) == 1 and sortedlist[0] == asum:
        matched.append(sortedlist[0])
        return True

    tmpsum = 0
    ind = 0
    size = len(sortedlist)
    while ind < size:
        num = sortedlist[ind]
        tmpsum += num
        if tmpsum < asum:
            matched.append(num)
            ind += 1
            continue
        elif tmpsum == asum:
            matched.append(num)
            return True
        else:
            tmpsum -= num
            break
   
    need = asum - tmpsum
    tosearch = map(lambda x:x+need, matched)
    restlist = sortedlist[ind:]
    
    for e in tosearch:
        if e in restlist:
            matched.append(e)
            matched.remove(e-need)
            return True

    return False
    

def improve(finalResults, unMatchedNums):
    for comb in finalResults:
        for num in comb:
            matched = []
            if matchSum(num, unMatchedNums, matched) and len(matched) > 1:
                print 'Improved: ' , num, ' ', matched
                comb.remove(num)
                comb.extend(matched)
                unMatchedNums.append(num)
                for e in matched:
                    unMatchedNums.remove(e)
                if len(unMatchedNums) == 0:
                    return

def printResult(finalResults, unMatchedNums, numlist):
    
    f_res = open('/tmp/res.txt', 'w')
    f_res.write('origin: ' + str(numlist) + '\n')
    f_res.write('averag: ' + str((float(sum(numlist))/len(numlist))) + '\n')
    f_res.write('solution: ')
    
    usedNums = 0
    finalNumList = []
    for comb in finalResults:
        f_res.write(str(comb) + ' ')
        assert sum(comb) == UPLIMIT
        usedNums += len(comb)
        finalNumList.extend(comb)
    finalNumList.extend(unMatchedNums)
    f_res.write('\nUnMatched Numbers: ' + str(unMatchedNums) + '\n')
    f_res.write('Used numbers: %s, UnMatched numbers: %d.\n' % (usedNums, len(unMatchedNums)))

    f_res.write('origin: %d , final: %d\n' % (len(numlist), len(finalNumList)))
    for e in finalNumList:
        numlist.remove(e)
    if len(numlist) > 0:
        f_res.write('Not Occurred numbers: ' + str(numlist))
    
    f_res.close() 


def to100(numlist):
  
    newnumlist = copylist(numlist)
    (finalResults, unMatchedNums) = solve(newnumlist)

    newnumlist = copylist(numlist)
    printResult(finalResults, unMatchedNums, newnumlist)

if __name__ == '__main__':
    to100(randNums(NUMBERS))

 

  需要優化的地方:

  將遞歸的 matchSum 改成非遞歸版本的 matchSumUnrec .

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM