公平划分列表中的元素


12

给定玩家的评分列表,我需要将玩家(即评分)尽可能公平地分为两组。目标是最大程度地减少团队的累积评分之间的差异。我如何将玩家分成团队没有任何限制(一个团队可以有2个玩家,而另一个团队可以有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 '19

6
更具体地说,您有子集和问题的特殊情况,称为分区问题。维基百科上的文章讨论了算法。
约翰·科尔曼

4
这回答了你的问题了吗?将列表分为两个等份算法
kaya3

1
谢谢你俩!我衷心感谢您的帮助!
EddieEC '19

Answers:


4

注意:编辑以更好地处理所有数字的总和为奇数的情况。

回溯是解决此问题的可能。

它允许递归检查所有可能性,而不需要大量内存。

找到最佳解后,它立即停止:sum = 0,其中sumA集的元素之和与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
  • Set up_down = true;:此布尔值指示我们当前是前进(true)还是后退(false)

While循环:

  • 如果(up_down):向前

    • 在以下人员的帮助下测试过早的放弃 sum_back
    • 选择最可能的值,并sum根据此选择进行调整
    • if (i == n-1):LEAF->测试最佳值是否得到改善,如果新值等于0,则返回(编辑:)if (... < 2);向后走
    • 如果不是一片叶子:继续前进
  • 如果(!updown):向后

    • 如果我们到达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 '19

如果最优总和不等于0,则代码会考虑所有可能性,并记住最佳解决方案。我们确定这些路径不是最佳路径。这对应于return 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个项。我请您添加几行,以便在s 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

检查第五位(索引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 '19

我在二进制数字和位运算上添加了一个
微型课程

您可能不应该在另一个函数中定义一个函数。
AMC

1
@AlexanderCécile 取决于。在这种情况下,我认为这是可以接受的并且可以提高清洁度,无论如何,这就是他的讲师所建议的操作说明(请参阅问题中的更新内容)。
Walter Tross

1
@MiniMax N个项的排列为N !,但它们的子集为2 ^ N:第一个项是否可以在子集中:2种可能性;第二项可以在子集中,也可以不在:×2; 第三项...依此类推,N次。
沃尔特·特罗斯

1

以下算法可以做到这一点:

  • 对项目进行排序
  • 甚至将成员列入列表 a,奇列表b启动
  • 随机移动和交换项目之间 ab如果更改更好,

我添加了打印语句以在示例列表中显示进度:

# -*- 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 '19

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 '19

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 = list(original)-l1。大小为n的列表的所有可能子集的数量为2 ^ n。我们可以将它们表示为从0到2 ^ n -1的整数的seq。举个例子,假设您有list = [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进行异或来轻松获得。

解:

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 '19

@EddieEC对n(数组长度)的约束是什么。如果要生成所有可能的组合,则基本上是一个子集和问题,这是一个NP完全问题。
vkSinha '19
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.