簡體   English   中英

在不同數組中找到最接近和的算法

[英]Algorithm for finding the closest sum across different arrays

我最近開始了一份游戲編程工作,並試圖用 C# 編寫一個有趣的算法。 我沒有學習 IT,所以我的數學和代碼知識非常基礎。 問題應該與代碼無關,但如果您想要示例,我可以嘗試復制一些。

這是我的問題的簡化版本:

我有多個元素數組; 有些可能相同,有些可能不同。 可以有 1 到 8 個數組。 每個元素都有一個權重。 不同元素的權重可以相同,但最終結果中不應有兩次相同的元素。

目標是在每個數組中選擇一個元素,以便它們所有權重的總和盡可能接近目標值,但永遠不會低於目標值。

(我還有其他復雜的事情要處理,但我認為核心問題總結在這里。)

舉個例子(為簡單起見,假設元素 = 它的權重):

數據:

  • 目標重量總和 = 590
  • 數組 A = [100, 300]
  • 數組 B = [50, 200, 300, 600]
  • 數組 C = [400, 450, 500, 600]

這里的預期結果是:

  • 數組結果 = [100, 50, 450]
  • 總重量總和 = 600

我希望你能看到我遇到的邊緣情況的數量。 我正在處理多達 8 個數組中的數十萬個條目的數據集,因此無法使用蠻力。

我已經迭代了幾個版本的算法,除了在某些邊緣情況下,這些算法大部分都有效,或者結果最終不是絕對最低的總和。

老實說,我們可以忍受結果不是最好的,只要它高於總和要求,但我對此感到好奇和完美主義......我覺得我錯過了一個能讓它高效而簡單。

到目前為止,我的解決方案已包含:

  • 從下到上遍歷所有排序的數據,直到達到所需的總和
  • 同樣從上到下
  • 轉了個彎,決定首先實現二分搜索以找到近似結果,然后通過嘗試在每個數組中獲得更低或更高的權重而不破壞求和要求來進行優化
  • 我已經開始研究動態編程和背包問題,但我認為它不適用於我的問題,或者我不知道如何。

我想聽聽其他意見,我沒有想到的解決方案,或者知道是否真的很難為此獲得完美的算法,我應該繼續下去。

提前致謝!

仍然是背包問題,但容量上有一點小技巧呢?

令容量C為所有非重復權重之和與需求之間的差。 實現通常的 0-1 背包以找到C的子集。 子集的補充將是您所需要的。

中止:

你可能已經明白了。

合並數組以獲得具有n權重的數組S 排序S使得S[0] <= S[1] ... <= S[n] 遍歷S直到權重之和大於或等於要求。 如果S[i]等於S[i - 1] ,則不要求和並繼續。 應該很容易證明總和在終止時最接近要求。 盡管進行了排序,但算法只有 O(n),足以滿足您的需要。

“最終結果中不應該有兩次相同的元素”使這變得更加棘手。 所以我會在不立即考慮這種情況的情況下開始,然后在最后處理它。

我不是 C# 程序員,所以我會用 Python 來代替。 你必須自己翻譯。


首先,讓我們手動瀏覽您的示例。 我們將使用動態規划來回答這個問題,“對於特定的最終總和,我可能的下一個選擇是什么?” 也就是說,我們希望字典D0, D1, D2允許我們在您的示例中執行以下操作:

D0 -> choose from A -> D1 -> choose from B -> D2 -> choose from C -> answer

我們實際上將反向構建它們。 C生成D2很簡單

    D2 = {
        400: [400],
        450: [450],
        500: [500],
        600: [600]
    }

我們從BD2生成D1

    D1 = {
        450: [50],
        500: [50],
        550: [50],
        600: [200],
        650: [50, 200]
        700: [200, 300],
        750: [300],
        800: [200, 300],
        900: [300],
        1000: [600],
        1050: [600],
        1100: [600],
        1200: [600],
    }

例如,您可以通過(50, 600)(200, 450)達到650 因此, B中的下一個選擇的選項是[50, 200]

最后是D0來自A的第一選擇:

    D0 = {
        550: [100],
        600: [100],
        650: [100],
        700: [100],
        750: [100, 300]
        800: [100, 300],
        850: [100, 300],
        900: [100, 300],
        950: [300],
        1000: [100, 300],
        1050: [300],
        1100: [100, 300],
        1150: [100],
        1200: [100, 300],
        1300: [100, 300],
        1350: [300],
        1400: [300],
        1500: [300]
    }

現在我們有了它,我們可以簡單地進行遞歸搜索。

    From D0 we don't want the key 550 because it is not above the target
    From D0 we try 600, choose 100
        From D1 for 500 we choose 50.  (Not 100.)
            From D2 for 450 we choose 450.  (Not 100 or 50.)
                FOUND THE ANSWER.

在這個遞歸搜索中,我們最終提出了不能兩次選擇相同權重的條件。


好的,我們如何在代碼中做到這一點?

    def minimal_for_target (target, *arrays):
        # Construct D3 equivalent
        transition = {}
        for value in arrays[-1]:
            transition[value] = [value]
    
        # We construct this as [D3, D2, D1]
        transitions = [transition]
        for i in reversed(range(len(arrays) - 1)):
            last_transition = transitions[-1]
            transition = {}
            for value in arrays[i]:
                for sum_remaining in last_transition.keys():
                    this_sum = value + sum_remaining
                    if this_sum in transition:
                        transition[this_sum].append(value)
                    else:
                        transition[this_sum] = [value]
            transitions.append(transition)
    
        transitions = list(reversed(transitions)) # Reorder to D1, D2, D3
    
        # A helper function for the recursion.  Returns true/false
        # based on chosen.
        def find (new_target, chosen):
            i = len(chosen)
            if i == len(arrays):
                return True # Found a full set of choices
            else:
               for value in transitions[i][new_target]:
                   if value not in chosen:
                       chosen.append(value)
                       if find(new_target-value, chosen):
                           return True # Recursively found it
                       chosen.pop() # Remove value from chosen
               return False # Failed to find it.
    
        # Find the possible sums.
        for total_sum in sorted(transitions[0].keys()):
            if target <= total_sum:
                for value in transitions[0][total_sum]:
                    # Try to find it.
                    answer = [value]
                    if find(total_sum - value, answer):
                        return answer # Filled it in recursively
        return None # No answer could be found.
    
    print(minimal_for_target(590, [100, 300], [50, 200, 300, 600], [400, 450, 500, 600]))

這段代碼在大多數情況下運行良好。 您會很快找到可能的值,並且希望沒有重復的遞歸測試運行得很快。 但它仍然有一些指數級的災難。 下面是一個很好的例子。

    minimal_for_target(
        1000,
        [1000],
        list(range(100)),
        list(range(100)),
        list(range(100)),
        list(range(100)),
        list(range(100)),
        [1000, 2000]
    )

它必須經歷很多嘗試使用1000兩次的可能性,然后才能開始使用有機會的值。

希望這已經足夠好了。 有些技術甚至可以處理那些邊緣情況,但平均而言它們會更慢並且更浪費內存。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM