Справедливий розподіл елементів списку


12

З огляду на перелік рейтингів гравців, я зобов’язаний максимально справедливо розподілити гравців (тобто рейтинги) на дві групи. Мета - мінімізувати різницю між сукупним рейтингом команд. Немає обмежень щодо того, як я можу розділити гравців на команди (одна команда може мати двох гравців, а друга команда може мати 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 "проблема суми підмножини"
Джон Коулман

@JohnColeman дякую за пропозицію. Чи можете ви, будь ласка, направити мене в правильному напрямку щодо того, як використовувати суми підмножини для вирішення моєї проблеми?
EddieEC

6
Навіть конкретніше, у вас є особливий випадок проблеми підмножини, яка називається проблемою розділу . У статті Вікіпедії в ній обговорюються алгоритми.
Джон Коулман

4
Чи відповідає це на ваше запитання? Розділіть список на дві алгоритми рівних частин
kaya3

1
Дякую обом! Я щиро вдячний за допомогу!
EddieEC

Відповіді:


4

Примітка. Відредаговано для кращого опрацювання випадку, коли сума всіх чисел непарна.

Зворотний трек - це можливість для цієї проблеми.

Це дозволяє рецензувати всі можливості рекурсивно, не потребуючи великого обсягу пам'яті.

Він зупиняється, як тільки знайдеться оптимальне рішення: sum = 0де sumрізниця між сумою елементів множини A і сумою елементів множини B. EDIT: вона зупиняється якнайшвидше sum < 2, щоб обробити випадок, коли сума всіх чисел непарна, тобто відповідає мінімальній різниці 1. Якщо ця глобальна сума є парною, мінімальна різниця не може бути дорівнює 1.

Це дозволяє реалізувати просту процедуру передчасного відмови :
у даний момент часу, якщо sumбільша сума всіх решти елементів (тобто не розміщених у А або В) плюс абсолютне значення отриманого мінімуму струму, то ми можемо відмовитися від вивчення поточний шлях, не вивчаючи інших елементів. Ця процедура оптимізована за допомогою:

  • сортувати вхідні дані у порядку зменшення
  • Кожен крок спочатку вивчіть найбільш ймовірний вибір: це дозволяє швидко перейти до оптимального рішення

Ось псевдокод

Ініціалізація:

  • сортувати елементи a[]
  • Обчисліть суму решти елементів: sum_back[i] = sum_back[i+1] + a[i];
  • Встановіть мінімальну "різницю" на максимальне значення: min_diff = sum_back[0];
  • Помістіть a[0]A -> індекс iдосліджуваного елемента встановлено на 1
  • Встановити up_down = true;: цей булевий сигнал вказує, чи ми зараз йдемо вперед (істинно) або назад (помилково)

Під час циклу:

  • Якщо (вгору_ вниз): вперед

    • Тест передчасного відмови, за допомогою sum_back
    • Виберіть найбільш вірогідне значення, відрегулюйте sumвідповідно до цього вибору
    • if (i == n-1): LEAF -> перевірити, чи покращено оптимальне значення, і повернути, якщо нове значення дорівнює 0 (EDIT:) if (... < 2); йти назад
    • Якщо не в листі: продовжуйте рухатися вперед
  • Якщо (! Вгору): назад

    • Якщо ми приїдемо i == 0: повернення
    • Якщо це другий прохід у цьому вузлі: виберіть друге значення, перейдіть вгору
    • інше: спуститися
    • В обох випадках: перерахуйте нове 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
Демієн

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):

Двійкове число: 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

Один за ним - 7 нулів: 1 << 7 == 0b10000000

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

Всі 3-бітові комбінації: 0b000==0, 0b001==1, 0b010==2, 0b011==3, 0b100==4, 0b101==5, 0b110==6, 0b111==7(зауважимо , що 0b111 + 1 == 0b1000 == 1 << 3)


Дуже дякую! Чи можете ви поясніть, що ви зробили? Також в чому полягає використання <<? Такі речі, наприклад, я ніколи не навчився це робити. Але я знав, що мені потрібно створити всі можливості і повернути ту, що має меншу різницю!
EddieEC

Я додав мікролісон на двійкові числа та бітові операції
Уолтер Трос

Ви, мабуть, не повинні визначати функцію всередині іншої.
AMC

1
@ AlexanderCécile це залежить . У цьому випадку я думаю, що це прийнятно і покращує чистоту, і все одно, це те, що ОП запропонували його інструктори (дивіться оновлення у своєму запитанні).
Вальтер Трос

1
@MiniMax перестановки N елементів є N !, але їх підмножини - 2 ^ N: перший елемент може бути в підмножині чи ні: 2 можливості; другий елемент може бути у підмножині чи ні: × 2; третій пункт ... і так далі, N разів.
Вальтер Тросс

1

Наступний алгоритм робить це:

  • сортує предмети
  • ставить навіть членів у список a, непарних у списку bдля початку
  • випадковим чином переміщає та міняє елементи між собою, aі bякщо зміни будуть на краще

Я додав заяви про друк, щоб показати прогрес у вашому списку прикладів:

# -*- 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
оскільки ви запитали мене: ваше рішення добре, якщо ви навчаєтесь. У нього є лише одна проблема, яка, на щастя, не виникає раніше, ніж інша проблема, яка є спільною з іншими рішеннями: вона використовує експоненціальний простір (O (n2ⁿ)). Але експоненціальний час виникає як проблема задовго до цього. Тим не менш, уникнути використання експоненціального простору було б просто.
Вальтер Трос

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]

Редагувати:

Іншим підходом буде генерування всіх можливих підмножин списку. Скажімо, у вас l1, яка є однією з підмножин списку, ви можете легко отримати список l2 таким, що l2 = список (оригінал) - l1. Кількість усіх можливих підмножин списку розміром 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, в цьому випадку можна легко отримати, взявши xor з 2 ^ n-1.

Рішення:

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.