リストの要素の公平な分割


12

プレイヤーの評価のリストを考えると、プレイヤー(つまり、評価)をできるだけ2つのグループに分割する必要があります。目標は、チームの累積評価の差を最小限に抑えることです。プレーヤーをチームに分割する方法に制約はありません(1つのチームには2人のプレーヤーがいて、もう1つのチームには10人のプレーヤーがいます)。

例:[5, 6, 2, 10, 2, 3, 4]返すべき([6, 5, 3, 2], [10, 4, 2])

この問題を解決するアルゴリズムを知りたいのですが。私はオンラインプログラミングの入門コースを受講しているので、簡単なアルゴリズムをいただければ幸いです。

次のコードを使用していますが、なんらかの理由で、オンラインコードチェッカーで間違っていると表示されています。

def partition(ratings):
    set1 = []
    set2 =[]
    sum_1 = 0
    sum_2 = 0
    for n in sorted(ratings, reverse=True):
        if sum_1 < sum_2:
            set1.append(n)
            sum_1 = sum_1 + n
        else:
            set2.append(n)
            sum_2 = sum_2 + n
    return(set1, set2)

更新:インストラクターに連絡し、すべての異なる組み合わせをチェックするために関数内に別の「ヘルパー」関数を定義する必要があると言われたので、最小の違いをチェックする必要があります。


2
Googleの「サブセット合計問題」
John Coleman、

@JohnColemanご提案ありがとうございます。サブセットの合計を使用して問題を解決する方法について、正しい方向に案内していただけませんか?
EddieEC、

6
さらに具体的に言うと、「パーティション問題」と呼ばれるサブセット合計問題の特別なケースがあります。それに関するウィキペディアの記事はアルゴリズムについて論じています。
John Coleman、

4
これはあなたの質問に答えますか? リストを2つの等しい部分に分割するアルゴリズム
kaya3

1
あなたがた両方に感謝します!助けてくれて本当にありがとう!
EddieEC、

回答:


4

注:すべての数値の合計が奇数である場合の処理​​を改善するために編集されました。

バックトラッキングは、この問題の可能性です。

大量のメモリを必要とせずに、すべての可能性を再帰的に調べることができます。

最適な解が見つかるとすぐに停止しますsum = 0sumは、セットAの要素の合計とセットBの要素の合計の差です。編集:すぐに停止しますsum < 2すべての数値の合計が発生した場合を処理、ます。は奇数、つまり1の最小差に対応します。このグローバル合計が偶数の場合、最小差を1にすることはできません。

それは時期尚早の放棄の簡単な手順を実装することを可能にします
与えられた時間に、もしそれsumがより高いならば、残りのすべての要素の合計(すなわちAまたはBに配置されない)と得られた現在の最小値の絶対値、それから私たちは調査をあきらめることができます残りの要素を調べずに現在のパス。この手順は、次のように最適化されています。

  • 入力データを降順にソートする
  • 各ステップでは、最初に最も可能性の高い選択肢を調べます。これにより、最適に近いソリューションにすばやく移行できます

ここに擬似コードがあります

初期化:

  • 要素の並べ替え a[]
  • 残りの要素の合計を計算します。 sum_back[i] = sum_back[i+1] + a[i];
  • 最小「差」を最大値に設定します。 min_diff = sum_back[0];
  • a[0]Aを挿入->インデックスi検査要素の1に設定されています
  • セットup_down = true;:このブール値は、現在進む(true)か、戻る(false)かを示します。

whileループ:

  • if(up_down):フォワード

    • の助けを借りて、早期放棄をテストする sum_back
    • 最も可能性の高い値を選択し、sumこの選択に従って調整します
    • if (i == n-1):LEAF->最適値が改善されているかどうかをテストし、新しい値が0に等しい場合に返します(編集: if (... < 2) ; 後退する
    • 葉の中にない場合:続行します
  • (!updown)の場合:後方

    • 着いたら i == 0:戻る
    • このノードの2番目のウォークの場合:2番目の値を選択して、上に移動します。
    • それ以外の場合:下に行く
    • どちらの場合も、新しいsum値を再計算します

ここにC ++のコードがあります(申し訳ありませんが、Pythonを知りません)

#include    <iostream>
#include    <vector>
#include    <algorithm>
#include    <tuple>

std::tuple<int, std::vector<int>> partition(std::vector<int> &a) {
    int n = a.size();
    std::vector<int> parti (n, -1);     // current partition studies
    std::vector<int> parti_opt (n, 0);  // optimal partition
    std::vector<int> sum_back (n, 0);   // sum of remaining elements
    std::vector<int> n_poss (n, 0);     // number of possibilities already examined at position i

    sum_back[n-1] = a[n-1];
    for (int i = n-2; i >= 0; --i) {
        sum_back[i] = sum_back[i+1] + a[i];
    }

    std::sort(a.begin(), a.end(), std::greater<int>());
    parti[0] = 0;       // a[0] in A always !
    int sum = a[0];     // current sum

    int i = 1;          // index of the element being examined (we force a[0] to be in A !)
    int min_diff = sum_back[0];
    bool up_down = true;

    while (true) {          // UP
        if (up_down) {
            if (std::abs(sum) > sum_back[i] + min_diff) {  //premature abandon
                i--;
                up_down = false;
                continue;
            }
            n_poss[i] = 1;
            if (sum > 0) {
                sum -= a[i];
                parti[i] = 1;
            } else {
                sum += a[i];
                parti[i] = 0;
            }

            if (i == (n-1)) {           // leaf
                if (std::abs(sum) < min_diff) {
                    min_diff = std::abs(sum);
                    parti_opt = parti;
                    if (min_diff < 2) return std::make_tuple (min_diff, parti_opt);   // EDIT: if (... < 2) instead of (... == 0)
                }
                up_down = false;
                i--;
            } else {
                i++;        
            }

        } else {            // DOWN
            if (i == 0) break;
            if (n_poss[i] == 2) {
                if (parti[i]) sum += a[i];
                else sum -= a[i];
                //parti[i] = 0;
                i--;
            } else {
                n_poss[i] = 2;
                parti[i] = 1 - parti[i];
                if (parti[i]) sum -= 2*a[i];
                else sum += 2*a[i];
                i++;
                up_down = true;
            }
        }
    }
    return std::make_tuple (min_diff, parti_opt);
}

int main () {
    std::vector<int> a = {5, 6, 2, 10, 2, 3, 4, 13, 17, 38, 42};
    int diff;
    std::vector<int> parti;
    std::tie (diff, parti) = partition (a);

    std::cout << "Difference = " << diff << "\n";

    std::cout << "set A: ";
    for (int i = 0; i < a.size(); ++i) {
        if (parti[i] == 0) std::cout << a[i] << " ";
    }
    std::cout << "\n";

    std::cout << "set B: ";
    for (int i = 0; i < a.size(); ++i) {
        if (parti[i] == 1) std::cout << a[i] << " ";
    }
    std::cout << "\n";
}

ここでの唯一の問題は、常に最適な合計が0になるとは限らないことです。C++をうまく読み取ることができないので、それを十分に説明していただきありがとうございます。
EddieEC

最適な合計が0でない場合、コードはすべての可能性を調べ、最適なソリューションを記憶します。検討されていないパスは、最適ではないと確信しているパスです。これは返品に対応しif I == 0ます。私はあなたの例で10を11に置き換えることによってそれをテストしました
Damien

3

次の練習は一人でやるべきだと思います。これについては、あなたのインストラクターによるアドバイスを実装しようとするソリューションがあります:

def partition(ratings):

    def split(lst, bits):
        ret = ([], [])
        for i, item in enumerate(lst):
            ret[(bits >> i) & 1].append(item)
        return ret

    target = sum(ratings) // 2
    best_distance = target
    best_split = ([], [])
    for bits in range(0, 1 << len(ratings)):
        parts = split(ratings, bits)
        distance = abs(sum(parts[0]) - target)
        if best_distance > distance:
            best_distance = distance
            best_split = parts
    return best_split

ratings = [5, 6, 2, 10, 2, 3, 4]
print(ratings)
print(partition(ratings))

出力:

[5, 6, 2, 10, 2, 3, 4]
([5, 2, 2, 3, 4], [6, 10])

この出力は目的のものとは異なりますが、どちらも正しいことに注意してください。

このアルゴリズムは、N個の要素を持つ特定のセットのすべての可能なサブセットを選択するために、Nビットのすべての整数を生成し、I番目のビットの値に応じてI番目のアイテムを選択できるという事実に基づいています。best_distanceゼロになるとすぐに停止するために、数行を追加します(もちろん、それ以上改善できないため)。

ビットオンビット(これ0bはPythonの2進数の接頭辞であることに注意してください):

2進数: 0b0111001 == 0·2⁶+1·2⁵+1·2⁴+1·2³+0·2²+0·2¹+1·2⁰ == 57

右シフト1: 0b0111001 >> 1 == 0b011100 == 28

左シフト1: 0b0111001 << 1 == 0b01110010 == 114

右シフト4: 0b0111001 >> 4 == 0b011 == 3

ビット単位&(および):0b00110 & 0b10101 == 0b00100

5番目のビット(インデックス4)が1かどうかを確認するには: (0b0111001 >> 4) & 1 == 0b011 & 1 == 1

1つの後にゼロが7つ続く: 1 << 7 == 0b10000000

7つ: (1 << 7) - 1 == 0b10000000 - 1 == 0b1111111

すべての3ビットの組み合わせ:0b000==00b001==10b010==20b011==30b100==40b101==50b110==60b111==7(注0b111 + 1 == 0b1000 == 1 << 3


どうもありがとうございます!何をしたのか説明してもらえますか?また、<<の使用は何ですか?たとえば、これらのことは、私が方法を学んだことがありません。しかし、私はすべての可能性を生成し、違いがほとんどないものを返す必要があることを知っていました。
EddieEC

2進数とビット操作に関するマイクロレッスンを追加しました
Walter

おそらく、別の関数内で関数を定義するべきではありません。
AMC

1
@AlexanderCécile それは依存します。この場合、それは許容可能であり、清潔さを向上させると思います。とにかく、それはOPが彼のインストラクターによって提案されたものです(彼の質問の更新を参照)。
Walter

1
@MiniMax N個の項目の順列はN!ですが、それらのサブセットは2 ^ Nです。最初の項目はサブセットに含まれる場合と含まれない場合があります。2つの可能性。2番目の項目はサブセットに含まれる場合と含まれない場合があります。×2。3番目の項目...など、N回。
Walter

1

次のアルゴリズムがこれを行います。

  • アイテムを並べ替えます
  • 偶数のメンバーをリストに入れa、奇数をリストに入れbて開始します
  • 間でランダムに移動してスワップアイテムab変更が良い方向にありますか

例のリストの進行状況を示すために、printステートメントを追加しました。

# -*- coding: utf-8 -*-
"""
Created on Fri Dec  6 18:10:07 2019

@author: Paddy3118
"""

from random import shuffle, random, randint

#%%
items = [5, 6, 2, 10, 2, 3, 4]

def eq(a, b):
    "Equal enough"
    return int(abs(a - b)) == 0

def fair_partition(items, jiggles=100):
    target = sum(items) / 2
    print(f"  Target sum: {target}")
    srt = sorted(items)
    a = srt[::2]    # every even
    b = srt[1::2]   # every odd
    asum = sum(a)
    bsum = sum(b)
    n = 0
    while n < jiggles and not eq(asum, target):
        n += 1
        if random() <0.5:
            # move from a to b?
            if random() <0.5:
                a, b, asum, bsum = b, a, bsum, asum     # Switch
            shuffle(a)
            trial = a[0]
            if abs(target - (bsum + trial)) < abs(target - bsum):  # closer
                b.append(a.pop(0))
                asum -= trial
                bsum += trial
                print(f"  Jiggle {n:2}: Delta after Move: {abs(target - asum)}")
        else:
            # swap between a and b?
            apos = randint(0, len(a) - 1)
            bpos = randint(0, len(b) - 1)
            trya, tryb = a[apos], b[bpos]
            if abs(target - (bsum + trya - tryb)) < abs(target - bsum):  # closer
                b.append(trya)  # adds to end
                b.pop(bpos)     # remove what is swapped
                a.append(tryb)
                a.pop(apos)
                asum += tryb - trya
                bsum += trya - tryb
                print(f"  Jiggle {n:2}: Delta after Swap: {abs(target - asum)}")
    return sorted(a), sorted(b)

if __name__ == '__main__':
    for _ in range(5):           
        print('\nFinal:', fair_partition(items), '\n')  

出力:

  Target sum: 16.0
  Jiggle  1: Delta after Swap: 2.0
  Jiggle  7: Delta after Swap: 0.0

Final: ([2, 3, 5, 6], [2, 4, 10]) 

  Target sum: 16.0
  Jiggle  4: Delta after Swap: 0.0

Final: ([2, 4, 10], [2, 3, 5, 6]) 

  Target sum: 16.0
  Jiggle  9: Delta after Swap: 3.0
  Jiggle 13: Delta after Move: 2.0
  Jiggle 14: Delta after Swap: 1.0
  Jiggle 21: Delta after Swap: 0.0

Final: ([2, 3, 5, 6], [2, 4, 10]) 

  Target sum: 16.0
  Jiggle  7: Delta after Swap: 3.0
  Jiggle  8: Delta after Move: 1.0
  Jiggle 13: Delta after Swap: 0.0

Final: ([2, 3, 5, 6], [2, 4, 10]) 

  Target sum: 16.0
  Jiggle  5: Delta after Swap: 0.0

Final: ([2, 4, 10], [2, 3, 5, 6]) 

誠にありがとうございましたが、何もインポートせずに行うことになっています。
EddieEC

1

私はすべての可能なリストを生成する必要があることを知っているので、すべての可能性を生成するのに役立つ「ヘルパー」関数を作成する必要があります。それを行った後、私は真の違いをチェックすることに真実であり、その最小の違いを持つリストの組み合わせが望ましい解決策です。

ヘルパー関数は再帰的であり、リストの組み合わせのすべての可能性をチェックします。

def partition(ratings):

    def helper(ratings, left, right, aux_list, current_index):
        if current_index >= len(ratings):
            aux_list.append((left, right))
            return

        first = ratings[current_index]
        helper(ratings, left + [first], right, aux_list, current_index + 1)
        helper(ratings, left, right + [first], aux_list, current_index + 1)

    #l contains all possible sublists
    l = []
    helper(ratings, [], [], l, 0)
    set1 = []
    set2 = []
    #set mindiff to a large number
    mindiff = 1000
    for sets in l:
        diff = abs(sum(sets[0]) - sum(sets[1]))
        if diff < mindiff:
            mindiff = diff
            set1 = sets[0]
            set2 = sets[1]
    return (set1, set2)

例: r = [1, 2, 2, 3, 5, 4, 2, 4, 5, 5, 2]、最適なパーティションは次のとおり([1, 2, 2, 3, 5, 4], [2, 4, 5, 5, 2])です1

r = [73, 7, 44, 21, 43, 42, 92, 88, 82, 70]、最適なパーティションは次のとおり([73, 7, 21, 92, 88], [44, 43, 42, 82, 70])です0


1
あなたが私に尋ねたので:あなたが学んでいるならあなたの解決策は素晴らしいです。これには1つの問題しかありません。幸運なことに、他のソリューションに共通する他の問題の前に問題が発生することはありません。指数空間(O(n2ⁿ))を使用します。しかし、指数時間はずっと前から問題として発生しています。それにもかかわらず、指数空間の使用を避けるのは簡単です。
Walter

1

これは、パフォーマンスよりも教育を目的とした、かなり複雑な例です。リスト内包表記やジェネレータなどのいくつかの興味深いPythonの概念と、適切なケースをチェックする必要がある再帰の良い例を紹介します。エクステンション、例えば同数のプレーヤーを持つチームのみが有効であるなど、適切な個々の関数に実装するのは簡単です。

def listFairestWeakTeams(ratings):
    current_best_weak_team_rating = -1
    fairest_weak_teams = []
    for weak_team in recursiveWeakTeamGenerator(ratings):
        weak_team_rating = teamRating(weak_team, ratings)
        if weak_team_rating > current_best_weak_team_rating:
            fairest_weak_teams = []
            current_best_weak_team_rating = weak_team_rating
        if weak_team_rating == current_best_weak_team_rating:
            fairest_weak_teams.append(weak_team)
    return fairest_weak_teams


def recursiveWeakTeamGenerator(
    ratings,
    weak_team=[],
    current_applicant_index=0
):
    if not isValidWeakTeam(weak_team, ratings):
        return
    if current_applicant_index == len(ratings):
        yield weak_team
        return
    for new_team in recursiveWeakTeamGenerator(
        ratings,
        weak_team + [current_applicant_index],
        current_applicant_index + 1
    ):
        yield new_team
    for new_team in recursiveWeakTeamGenerator(
        ratings,
        weak_team,
        current_applicant_index + 1
    ):
        yield new_team


def isValidWeakTeam(weak_team, ratings):
    total_rating = sum(ratings)
    weak_team_rating = teamRating(weak_team, ratings)
    optimal_weak_team_rating = total_rating // 2
    if weak_team_rating > optimal_weak_team_rating:
        return False
    elif weak_team_rating * 2 == total_rating:
        # In case of equal strengths, player 0 is assumed
        # to be in the "weak" team
        return 0 in weak_team
    else:
        return True


def teamRating(team_members, ratings):
    return sum(memberRatings(team_members, ratings))    


def memberRatings(team_members, ratings):
    return [ratings[i] for i in team_members]


def getOpposingTeam(team, ratings):
    return [i for i in range(len(ratings)) if i not in team]


ratings = [5, 6, 2, 10, 2, 3, 4]
print("Player ratings:     ", ratings)
print("*" * 40)
for option, weak_team in enumerate(listFairestWeakTeams(ratings)):
    strong_team = getOpposingTeam(weak_team, ratings)
    print("Possible partition", option + 1)
    print("Weak team members:  ", weak_team)
    print("Weak team ratings:  ", memberRatings(weak_team, ratings))
    print("Strong team members:", strong_team)
    print("Strong team ratings:", memberRatings(strong_team, ratings))
    print("*" * 40)

出力:

Player ratings:      [5, 6, 2, 10, 2, 3, 4]
****************************************
Possible partition 1
Weak team members:   [0, 1, 2, 5]
Weak team ratings:   [5, 6, 2, 3]
Strong team members: [3, 4, 6]
Strong team ratings: [10, 2, 4]
****************************************
Possible partition 2
Weak team members:   [0, 1, 4, 5]
Weak team ratings:   [5, 6, 2, 3]
Strong team members: [2, 3, 6]
Strong team ratings: [2, 10, 4]
****************************************
Possible partition 3
Weak team members:   [0, 2, 4, 5, 6]
Weak team ratings:   [5, 2, 2, 3, 4]
Strong team members: [1, 3]
Strong team ratings: [6, 10]
****************************************

1

あなたがチームでさえ欲しいなら、あなたは各チームのレーティングの目標スコアを知っています。これは、評価の合計を2で割ったものです。

したがって、次のコードはあなたが望むことをするはずです。

from itertools import combinations

ratings = [5, 6, 2, 10, 2, 3, 4]

target = sum(ratings)/2 

difference_dictionary = {}
for i in range(1, len(ratings)): 
    for combination in combinations(ratings, i): 
        diff = sum(combination) - target
        if diff >= 0: 
            difference_dictionary[diff] = difference_dictionary.get(diff, []) + [combination]

# get min difference to target score 
min_difference_to_target = min(difference_dictionary.keys())
strong_ratings = difference_dictionary[min_difference_to_target]
first_strong_ratings = [x for x in strong_ratings[0]]

weak_ratings = ratings.copy()
for strong_rating in first_strong_ratings: 
    weak_ratings.remove(strong_rating)

出力

first_strong_ratings 
[6, 10]

weak_rating 
[5, 2, 2, 3, 4]

これと同じスプリットが他にfairnessもあります。これらはすべてstrong_ratingsタプル内で見つけることができます。最初のスプリットを確認することを選択します。これは、渡されるすべての評価リストに常に存在するためです(提供されますlen(ratings) > 1)。


この質問の課題は、質問で述べたように、何もインポートしないことでした。ご入力ありがとうございます!
EddieEC、

0

貪欲なソリューションは、次善のソリューションをもたらす可能性があります。これはかなり単純な貪欲なソリューションです。アイデアは、バケットへのレーティングの追加の影響を減らすために、リストを降順にソートすることです。レーティングの合計が少ないバケットにレーティングが追加されます

lis = [5, 6, 2, 10, 2, 3, 4]
lis.sort()
lis.reverse()

bucket_1 = []
bucket_2 = []

for item in lis:
    if sum(bucket_1) <= sum(bucket_2):
        bucket_1.append(item)
    else:
        bucket_2.append(item)

print("Bucket 1 : {}".format(bucket_1))
print("Bucket 2 : {}".format(bucket_2))

出力:

Bucket 1 : [10, 4, 2]
Bucket 2 : [6, 5, 3, 2]

編集:

別のアプローチは、リストのすべての可能なサブセットを生成することです。リストのサブセットの1つであるl1があるとすると、l2 = list(original)-l1のようなリストl2を簡単に取得できます。サイズnのリストのすべての可能なサブセットの数は2 ^ nです。それらを0から2 ^ n -1までの整数のシーケンスとして表すことができます。例として、リスト= [1、3、5]とすると、可能な組み合わせは2 ^ 3、つまり8になります。すべての組み合わせを次のように記述できます。

  1. 000-[]-0
  2. 001-[1]-1
  3. 010-[3]-2
  4. 011-[1,3]-3
  5. 100-[5]-4
  6. 101-[1,5]-5
  7. 110-[3,5]-6
  8. 111-[1,3,5]-7とl2は、この場合、2 ^ n-1のxorをとることで簡単に取得できます。

解決:

def sum_list(lis, n, X):
    """
    This function will return sum of all elemenst whose bit is set to 1 in X
    """
    sum_ = 0
    # print(X)
    for i in range(n):
        if (X & 1<<i ) !=0:
            # print( lis[i], end=" ")
            sum_ += lis[i]
    # print()
    return sum_

def return_list(lis, n, X):
    """
    This function will return list of all element whose bit is set to 1 in X
    """
    new_lis = []
    for i in range(n):
        if (X & 1<<i) != 0:
            new_lis.append(lis[i])
    return new_lis

lis = [5, 6, 2, 10, 2, 3, 4]
n = len(lis)
total = 2**n -1 

result_1 = 0
result_2 = total
result_1_sum = 0
result_2_sum = sum_list(lis,n, result_2)
ans = total
for i in range(total):
    x = (total ^ i)
    sum_x = sum_list(lis, n, x)
    sum_y = sum_list(lis, n, i)

    if abs(sum_x-sum_y) < ans:
        result_1 =  x
        result_2 = i
        result_1_sum = sum_x
        result_2_sum = sum_y
        ans = abs(result_1_sum-result_2_sum)

"""
Produce resultant list
"""

bucket_1 = return_list(lis,n,result_1)
bucket_2 = return_list(lis, n, result_2)

print("Bucket 1 : {}".format(bucket_1))
print("Bucket 2 : {}".format(bucket_2))

出力:

Bucket 1 : [5, 2, 2, 3, 4]
Bucket 2 : [6, 10]

こんにちは、私の元の質問を読むと、私がすでに貪欲メソッドを使用していることがわかり、拒否されました。でもご意見ありがとうございます!
EddieEC、

@EddieEC n(配列の長さ)に対する制約は何ですか?可能なすべての組み合わせを生成したい場合、それは基本的にサブセット合計問題であり、これはNP完全問題です。
vkSinha
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.