将n个玩家分成大小为k且强度相等(或略有不同)的两支球队

时间:2018-07-07 11:44:22

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

我最近遇到了这个问题;我考虑了很多,但找不到解决方案:

  

给出实力为[s 1 ,s 2 ,s 3 ... s n < / sub>],创建大小为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

1 个答案:

答案 0 :(得分:1)

如评论中所述,这是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人组成的团队将产生10个以上的 12 选项。

我们可以大幅度减少所需检查的选项,方法是从最初分为两队开始,然后检查我们需要将其中的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步,而不是超过10 12 步。通常,最大步骤数为:

  

2 k + 2 k-1 − 3

时间复杂度是:

  

O(2 k

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


代码示例

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

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个不同的整数表示每个玩家实力的价值观。