【问题标题】:Partition n players into two teams of size k and equal (or minimally different) strength将 n 名球员分成大小为 k 且实力相同(或差异最小)的两支球队
【发布时间】:2018-12-15 19:11:15
【问题描述】:

我最近遇到了这个问题;我想了很多,但没有找到解决方案:

给定 n 名球员的名单,他们的实力为 [s1 , s2, s3 ... sn],创建两个大小为 k (k ≤ n/2) 的团队(A 和 B),这样:

  • 总强度最大化
  • 强度差异最小化

实力(A) = A队所有队员的实力之和,
力量(B) = B队所有队员的力量之和,
总强度 = 强度(A) + 强度 (B),
强度差异 = abs(强度(A) - 强度(B))。

在总强度相同的情况下,选择强度差异最小的组合。

例子:

n = 5; k = 2  
players:   a  b  c  d  e  
strength:  4  4  2  2  5  

Option  Team A   Team B  Strength  Difference      
  1     [a,b]    [c,e]      15        1
  2     [a,b]    [d,e]      15        1   
  3     [a,c]    [b,e]      15        3
  4     [a,d]    [b,e]      15        3
  5     [a,c]    [b,d]      12        0
  6     [a,d]    [c,b]      12        0
  7     [a,d]    [c,e]      13        1

选项 1 和选项 2 是获胜组合,因为它们的总强度为 15(最大值),而它们的强度差异比选项 3 和 4 更接近最小值。

我的想法:

如果 2k = n ,则已经考虑了强度(因为将涉及所有元素),我们只需要找到两半,使得这两者之和的差异最小。但是如何有效地找到它呢?

如果 2k

【问题讨论】:

  • 您有两个解决方案目标,但没有足够的标准来衡量每个目标的权重。在您的数值示例中,应该选择哪些可能的解决方案? ComboId 5,6 给出最小差异或 1-4 给出最大强度?是否允许每支球队拥有相同数量的球员?
  • 总强度应该是最大的。如果总强度相同,请选择具有最小差异强度的组合 - 如问题中所述
  • 如果是,则按玩家实力排序,选出前2*k名。现在你有问题以最小的差异组成 2 个团队。请参阅以下链接,它可能会有所帮助:en.wikipedia.org/wiki/Partition_problem
  • 两个团队的成员数量必须相同。 DP 解决方案不满足这一点,并且更通用。这似乎是一个特例。一定有一些更简单的算法。
  • 你必须解决一个问题:从 2k 中获取 k 个项目,总和尽可能接近总和的一半。可以用 DP 解决。

标签: arrays algorithm sorting data-structures dynamic-programming


【解决方案1】:

如 cmets 中所述,这是Partitioning Problem 的变体,它本身是Subset Sum Problem 的特例。这些确实具有动态规划和近似解决方案,您可能能够适应这个问题。但是两个同等规模的团队的具体要求意味着非 dp 和非贪婪的解决方案也是可能的。

首先,在考虑球队之间的实力差异之前优化总实力,这意味着当玩家n的数量为奇数时,可以丢弃最弱的玩家,而团队 k 总是 n 的一半。如果 k 作为输入的一部分,则选择 2×k 个最强的玩家并丢弃其他玩家。

(您可能想知道问题是否实际上是首先针对强度差异进行优化,然后针对总强度进行优化。如果您找到两个具有差异 x 的子集,那么找到另外两个具有相似差异 y 的子集意味着您可以将它们组合成两个较大的子集,|xy| 的差异较小。这是动态规划方法的明显基础。)

动态规划解决方案的替代方案

让我们看一下将 n=23(即 n=22)名球员分成两队 11 名球员的例子。如果我们使用蛮力来查看每个选项,我们会保留 A 队中的一名球员(以避免重复的解决方案),并尝试从其他 21 名球员中增加 10 名球员的每一种组合来完成 A 队。这意味着有:

(n-1 选择 k-1) = (21 选择 10) = 352,716 个唯一选项

虽然这是一个可行的检查选项数量,但更多的玩家会很快产生大量的选项;例如将 44 名球员分成 22 人的两队会导致超过 1012 个选项。

我们可以大幅减少需要检查的选项数量,首先将初始分成两支球队,然后检查我们需要交换到哪 1 名球员、2 名球员、...、10 名球员最大程度地减少强度差异。这可以在不必考虑将 A 团队的每个可能子集与 B 团队的每个可能相等大小的子集交换的情况下完成。

我们可以进行初始随机分组,但如果我们按实力对球员进行排序,并交替将一名球员加入 A 队或 B 队,这应该会限制初始实力差异 D ,这反过来应该更有可能快速找到交换次数有限的解决方案(如果有几个完美的解决方案)。

然后我们考虑交换 1 名玩家;我们列出了 A 队中的所有玩家(除了第一个,我们将始终保留在 A 队中以避免重复的解决方案)并将其从最弱到最强排序。我们还列出了 B 队中所有球员的名单,并将其从弱到强排序。然后我们同时遍历两个列表,在每一步移动到列表中的下一个值,使 A 队和 B 队的当前球员之间的实力差异更接近 D 的初始值我>。

请注意,我们不会在嵌套循环中将第一个列表中的每个玩家与第二个列表中的每个玩家进行比较。我们只遍历列表一次(这类似于在两个数组中找到差异最小的两个整数;参见例如here)。

如果我们遇到一对玩家在交换时会降低 D,我们会存储这对玩家并设置 D 的新值。

现在我们考虑交换 2 名玩家;我们列出 A 队的每 2 名球员(再次排除球员 1)和 B 队的每对球员的名单,将名单从最弱到最强排序(添加两名球员的实力)。然后我们再次遍历这两个列表,寻找一对在交换时会降低 D 值的对。

我们继续为 3、4、... 10 名玩家做同样的事情。以 23 名玩家为例,这些列表的大小为:

           team A    team B

swap  1        10        11
swap  2        45        55
swap  3       120       165
swap  4       210       330
swap  5       252       462
swap  6       210       462
swap  7       120       330
swap  8        45       165
swap  9        10        55
swap 10         1        11
             ----      ----
             1023      2046

因此,我们会找到最佳交换,以使两支球队在最多 3,069 步后的力量差异最小,而不是暴力算法的 352,716 步。

(我们可以通过按 10、1、9、2、8、3、7、4、6、5 的顺序检查交换大小以找到解决方案来进一步加快存在多个完美解决方案的情况无需生成更大的列表。)

将 44 名玩家分成两队 22 人的示例最多需要 6,291,453 步,而不是超过 1012 步。一般来说,最大步数是:

2k + 2k−1 - 3

时间复杂度为:

O(2k)

这看起来不太好,但比具有 O(C(n-1,k-1)) 复杂度的蛮力算法要好得多。另外,只要找到差值为 0 或 1 的解决方案,就无需再考虑其他选项,因此只需考虑交换 1 个或少数几个玩家即可找到解决方案,并且平均案例复杂度很多优于最坏情况的复杂性(这将在下面进一步讨论。)


代码示例

这是一个 Javascript 代码 sn-p 作为概念证明。玩家的选择由位数组表示(您也可以使用整数作为位模式)。您会看到计算了不同交换后的团队实力变化,但最后实际上只交换了一个选择的球员;所以它不是一个贪心算法,通过执行几次交换来逐渐改善强度差异。

function compareStrength(a, b) { // for sorting players and selections
    return a.strength - b.strength;
}
function teamStrength(players) {
    return players.reduce(function(total, player) {return total + player.strength;}, 0);
}
function selectionStrength(players, selection) {
    return players.reduce(function(total, player, index) {return total + player.strength * selection[index];}, 0);
}
function nextPermutation(selection) { // reverse-lexicographical next permutation of a bit array
    var max = true, pos = selection.length, set = 1;
    while (pos-- && (max || !selection[pos])) if (selection[pos]) ++set; else max = false;
    if (pos < 0) return false;
    selection[pos] = 0;
    while (++pos < selection.length) selection[pos] = set-- > 0 ? 1 : 0;
    return true;
}
function swapPlayers(wTeam, sTeam, wSelect, sSelect) {
    for (var i = 0, j = 0; i < wSelect.length; i++) {
        if (wSelect[i]) {
            while (!sSelect[j]) ++j;
            var temp = wTeam[i];
            wTeam[i] = sTeam[j];
            sTeam[j++] = temp;
        }
    }
}
function equalTeams(players) {
    // SORT PLAYERS FROM WEAKEST TO STRONGEST
    players.sort(compareStrength);
    // INITIAL DISTRIBUTION OF PLAYERS INTO WEAKER AND STRONGER TEAM (ALTERNATING)
    var wTeam = [], sTeam = [];
    for (var i = players.length % 2; i < players.length; i += 2) {
        wTeam.push(players[i]);
        sTeam.push(players[i + 1]);
    }
    var teamSize = wTeam.length;
    // CALCULATE INITIAL STRENGTH DIFFERENCE
    var initDiff = teamStrength(sTeam) - teamStrength(wTeam);
    var bestDiff = initDiff;
    var wBestSel = [], sBestSel = [];
    // CHECK SELECTIONS OF EVERY SIZE
    for (var selSize = 1; selSize < teamSize && bestDiff > 1; selSize++) {
        var wSelections = [], sSelections = [], selection = [];
        // CREATE INITIAL SELECTION BIT-ARRAY FOR WEAKER TEAM (SKIP PLAYER 1)
        for (var i = 0; i < teamSize; i++)
            selection[i] = (i > 0 && i <= selSize) ? 1 : 0;
        // STORE ALL SELECTIONS FROM WEAKER TEAM AND THEIR STRENGTH
        do wSelections.push({selection: selection.slice(), strength: selectionStrength(wTeam, selection)});
        while (nextPermutation(selection));
        // SORT SELECTIONS FROM WEAKEST TO STRONGEST
        wSelections.sort(compareStrength);
        // CREATE INITIAL SELECTION BIT-ARRAY FOR STRONGER TEAM
        for (var i = 0; i < teamSize; i++)
            selection[i] = (i < selSize) ? 1 : 0;
        // STORE ALL SELECTIONS FROM STRONGER TEAM AND THEIR STRENGTH
        do sSelections.push({selection: selection.slice(), strength: selectionStrength(sTeam, selection)});
        while (nextPermutation(selection));
        // SORT SELECTIONS FROM WEAKEST TO STRONGEST
        sSelections.sort(compareStrength);
        // ITERATE OVER SELECTIONS FROM BOTH TEAMS
        var wPos = 0, sPos = 0;
        while (wPos < wSelections.length && sPos < sSelections.length) {
            // CALCULATE STRENGTH DIFFERENCE IF THESE SELECTIONS WERE SWAPPED
            var wStrength = wSelections[wPos].strength, sStrength = sSelections[sPos].strength;
            var diff = Math.abs(initDiff - 2 * (sStrength - wStrength));
            // SET NEW BEST STRENGTH DIFFERENCE IF SMALLER THAN CURRENT BEST
            if (diff < bestDiff) {
                bestDiff = diff;
                wBestSel = wSelections[wPos].selection.slice();
                sBestSel = sSelections[sPos].selection.slice();
                // STOP SEARCHING IF PERFECT SOLUTION FOUND (DIFFERENCE 0 OR 1)
                if (bestDiff < 2) break;
            }
            // ADVANCE TO NEXT SELECTION FROM WEAKER OR STRONGER TEAM
            if (2 * (sStrength - wStrength) > initDiff) ++wPos; else ++sPos;
        }
    }
    // PERFORM SWAP OF BEST PAIR OF SELECTIONS FROM EACH TEAM
    swapPlayers(wTeam, sTeam, wBestSel, sBestSel);
    return {teams: [wTeam, sTeam], strengths: [teamStrength(wTeam), teamStrength(sTeam)]};
}
var players = [{id:"Courtois", strength:65}, {id:"Mignolet", strength:21}, {id:"Casteels", strength:0},
               {id:"Alderweireld", strength:83}, {id:"Vermaelen", strength:69}, {id:"Kompany", strength:82},
               {id:"Vertonghen", strength:108}, {id:"Meunier", strength:30}, {id:"Boyata", strength:10},
               {id:"Dendoncker", strength:6}, {id:"Witsel", strength:96}, {id:"De Bruyne", strength:68},
               {id:"Fellaini", strength:87}, {id:"Carrasco", strength:30}, {id:"Tielemans", strength:13},
               {id:"Januzaj", strength:9}, {id:"Dembele", strength:80}, {id:"Chadli", strength:51},
               {id:"Lukaku", strength:75}, {id:"E. Hazard", strength:92}, {id:"Mertens", strength:75},
               {id:"T. Hazard", strength:13}, {id:"Batshuayi", strength:19}];
var result = equalTeams(players);
for (var t in result.teams) {
    for (var i in result.teams[t]) {
        document.write(result.teams[t][i].id + " (" + result.teams[t][i].strength + ") ");
    }
    document.write("<br>&rarr; team strength = " + result.strengths[t] + "<br><br>");
}

找到完美解决方案的概率

当算法找到一个完美的解决方案(强度差为 0 或 1)时,这无法进一步改进,因此算法可以停止查看其他选项并返回解决方案。这当然意味着,对于某些输入,几乎可以立即找到解决方案,并且该算法可以用于大量玩家。

如果没有完美的解决方案,算法必须运行它的全部过程以确保它找到了最佳解决方案。对于大量玩家,这可能需要很长时间并占用大量内存空间(我能够在我的计算机上运行最多 64 个玩家的 C++ 版本)。

虽然制作没有完美解决方案的输入很简单(例如一名球员的力量为 3,而其他球员的力量均为 1),但使用随机数据进行的测试表明,几乎所有随机输入的球员数量都有完美的解决方案出奇的低(类似于Birthday Paradox)。

在 n=24(两队 12 人)或更多的情况下,一千万个随机输入实例不会提供一个团队之间的强度差异大于 1 的情况,无论是使用 10、100、1000 还是 10000 不同的整数值来表达每个球员的实力。

【讨论】:

  • 这看起来像一个“贪婪的方法”。是否保证最佳解决方案?
  • 不,不是贪心;它应该找到最佳解决方案。稍后我会尝试发布一些示例代码。
  • 我不认为分区问题是子集和问题的特例。他们的目标完全不同
  • @discoverAnkit 是什么让您认为贪婪的解决方案不是最优的?您将贪婪方法与启发式方法混淆
  • @mangusta 这就是维基百科的描述:en.wikipedia.org/wiki/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-31
  • 1970-01-01
相关资源
最近更新 更多