5

Okay I have this treemap that contains playerID and a players averageScore. I want to split it up into two other maps so each team has an even amount of players and the overall player score is around about the same(deviation of around +/-2)

private TreeMap<Integer, Double> teamScoreMap = new TreeMap<>();
private TreeMap<Integer, Double> team1 = new TreeMap<>();
private TreeMap<Integer, Double> team2 = new TreeMap<>();

public void createTeam()
   {
       teamScoreMap.put(001, 5.0);
       teamScoreMap.put(002, 8.4);
       teamScoreMap.put(003, 2.1);
       teamScoreMap.put(004, 6.5);
       teamScoreMap.put(005, 4.5);
       teamScoreMap.put(006, 3.2);
       teamScoreMap.put(007, 9.8);
       teamScoreMap.put(008, 7.6);
   } 
DiggidyDale
  • 75
  • 1
  • 7
  • 4
    Just as an aside, don't prefix your integer literals with zero, unless you really mean to specify them in octal. – Andy Turner Jul 03 '16 at 19:06
  • yeah i know i keep doing that and i don't know why, i have corrected that before in my code – DiggidyDale Jul 03 '16 at 19:12
  • So here, the key is the player number and the value is their score? – Jorn Vernee Jul 03 '16 at 19:14
  • 1
    If you've only got 8 players on each team, there are `2^8` ways to assign them to 2 teams (some of which result in no players on one team; some of which are "mirrors" of other assignments). `2^7` if you always assign the first player to `team` (that eliminates "mirrors" too). Nonetheless, just evaluate the "quality" of each of these small number of assignments, pick the best. – Andy Turner Jul 03 '16 at 19:17
  • 3
    You can't necessarily control the deviation. What if you have just two players, one with a score of 1 and the other with a score of 10? – shmosel Jul 03 '16 at 20:33

3 Answers3

1

Try this out

TreeMap<Integer,Double> teamScoreMap = new TreeMap<Integer, Integer>(bigMap);
int size = teamScoreMap.size();
SortedMap<Integer, Double> team1 = teamScoreMap .subMap(0, size/2);
SortedMap<Integer, Double> team2 = teamScoreMap .subMap((size/2)+1,size);

There is no reason to actually cast to a TreeMap because it doesn't provide any additional functionality over what a SortedMap does.

Govinda Sakhare
  • 5,009
  • 6
  • 33
  • 74
1

The logic is there, you just need to finish the code to add each team to each map.

Make all the possible teams, compare their average with the previous best average. If the difference is below the previous, swap them.

At the end, you get the best difference between both.


An example of output

{false=[1=5.0, 4=6.5, 5=4.5, 8=7.6], true=[2=8.4, 3=2.1, 6=3.2, 7=9.8]}
// false = 23.6 in total             true = 23.5 in total
// As you see, the difference between both is the minimum possible (0.1)

public class Test {
    private Map<Integer, Double> tsm = new TreeMap<>();
    private Map<Integer, Double> team1 = new TreeMap<>();
    private Map<Integer, Double> team2 = new TreeMap<>();

    public void splitTeams() {
        double average = tsm.values()
                            .stream()
                            .mapToDouble(x->x)
                            .average()
                            .getAsDouble();
        int[] bestTeam = new int[4];
        double bestAverage = 10;
        double tmp;

        for (int i = 1 ; i <= 8 ; i++) {
            for (int j = 1 ; j <= 8 ; j++) {
                for (int k = 1 ; k <= 8 ; k++) {
                    for (int l = 1 ; l <= 8 ; l++) {
                        if (Stream.of(i, j, k, l).distinct().count() == 4) {
                            tmp = Stream.of(tsm.get(i), tsm.get(j), tsm.get(k), tsm.get(l))
                                        .mapToDouble(x -> x)
                                        .average()
                                        .getAsDouble();
                            if (Math.abs(average - tmp) < bestAverage) {
                                bestTeam[0] = i;
                                bestTeam[1] = j;
                                bestTeam[2] = k;
                                bestTeam[3] = l;
                                bestAverage = Math.abs(average - tmp);
                            }
                        }
                    }
                }
            }
        }

        List<Integer> team1 = Arrays.stream(bestTeam).boxed().collect(Collectors.toList());
        Map<Boolean, List<Entry<Integer, Double>>> both = tsm.entrySet()
                                                             .stream()
                                                             .collect(Collectors.partitioningBy(e -> team1.contains(e.getKey())));
        System.out.println(both);
    }

    public void createTeam() {
        tsm.put(1, 5.0);
        tsm.put(2, 8.4);
        tsm.put(3, 2.1);
        tsm.put(4, 6.5);
        tsm.put(5, 4.5);
        tsm.put(6, 3.2);
        tsm.put(7, 9.8);
        tsm.put(8, 7.6);
    }

    public static void main(String[] args) {
        Test t = new Test();
        t.createTeam();
        t.splitTeams();
        System.out.println();
    }
}
Yassin Hajaj
  • 21,337
  • 9
  • 51
  • 89
  • While this may give the correct and optimal answer in this case, it does not scale very well. From an implementation point of view, because you can't easily extend it to more than 4 players per team. And from an algorithmical point of view, because it will have exponential complexity. – Marco13 Jul 04 '16 at 09:27
  • @Marco13 I agree but this answers OP's specific question for the moment which I think is OK for now. I don't have enough mathematical skills to perform a better/scalable/generalistic code. – Yassin Hajaj Jul 04 '16 at 10:00
  • Sure, no offense - I just wanted to mention it, so that the OP does not try to apply this to a map with 100 entries and runs out of loop variables ;-) – Marco13 Jul 04 '16 at 10:11
  • @Marco13 No problem thank you for the comment in either way :). I find your solution better in terms of general problem solving of course. – Yassin Hajaj Jul 04 '16 at 10:12
0

The answer by Yassin Hajaj shows one way of computing the optimal result for this particular case. More generally, what you are trying to solve is a special instance of the subset sum problem, and there are related questions - e.g. divide an array into two sets with minimal difference.

There basically is no efficient way of finding the optimal solution.

The brute-force way to compute the optimal solution is to compute all possible choices of half of the elements, compute their sum, and find the sum that has the least deviation from the desired sum.

However, in many cases, one is not interested in the optimal solution, but only in a "good" solution. This can be achieved with a greedy approach: First, sort the scores in decreasing order. Then, go through the list of team members, and assign each of them to the team that currently has the lower score.

Both approaches are implemented here, as an example:

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeMap;


public class MapSplit
{
    public static void main(String[] args)
    {
        TreeMap<Integer, Double> teamScoreMap = new TreeMap<Integer, Double>();
        teamScoreMap.put(1, 5.0);
        teamScoreMap.put(2, 8.4);
        teamScoreMap.put(3, 2.1);
        teamScoreMap.put(4, 6.5);
        teamScoreMap.put(5, 4.5);
        teamScoreMap.put(6, 3.2);
        teamScoreMap.put(7, 9.8);
        teamScoreMap.put(8, 7.6);

        solveOptimal(teamScoreMap);
        solveGreedy(teamScoreMap);
    }

    private static void solveOptimal(Map<Integer, Double> teamScoreMap)
    {
        List<Entry<Integer, Double>> entries = 
            new ArrayList<Entry<Integer, Double>>(
                teamScoreMap.entrySet());

        double totalSum = computeSum(entries);
        ChoiceIterable<Entry<Integer, Double>> ci = 
            new ChoiceIterable<Entry<Integer, Double>>(
                teamScoreMap.size() / 2, entries);

        List<Entry<Integer, Double>> bestTeam = null;
        double minError = Double.POSITIVE_INFINITY;
        for (List<Entry<Integer, Double>> team : ci)
        {
            double teamSum = computeSum(team);
            double error = Math.abs(teamSum - totalSum * 0.5);
            if (error < minError)
            {
                bestTeam = team;
                minError = error;
            }
        }

        Set<Entry<Integer, Double>> teamA = 
            new LinkedHashSet<Entry<Integer, Double>>(bestTeam);
        Set<Entry<Integer, Double>> teamB = 
            new LinkedHashSet<Entry<Integer, Double>>(
                teamScoreMap.entrySet());
        teamB.removeAll(teamA);

        System.out.println("Optimal result:");
        printResult(teamA, teamB);
    }

    private static void solveGreedy(Map<Integer, Double> teamScoreMap)
    {
        List<Entry<Integer, Double>> entries = 
            new ArrayList<Entry<Integer, Double>>(
                teamScoreMap.entrySet());
        Collections.sort(entries, (e0, e1) -> 
            Double.compare(e1.getValue(), e0.getValue()));

        Set<Entry<Integer, Double>> teamA = 
            new LinkedHashSet<Entry<Integer, Double>>();
        double sumA = 0;
        Set<Entry<Integer, Double>> teamB = 
            new LinkedHashSet<Entry<Integer, Double>>();
        double sumB = 0;

        for (Entry<Integer, Double> entry : entries)
        {
            if (sumA < sumB)
            {
                teamA.add(entry);
                sumA += entry.getValue();
            }
            else
            {
                teamB.add(entry);
                sumB += entry.getValue();
            }
        }

        System.out.println("Greedy result:");
        printResult(teamA, teamB);
    }

    private static void printResult(
        Collection<Entry<Integer, Double>> teamA, 
        Collection<Entry<Integer, Double>> teamB)
    {
        System.out.println("Team A:");
        for (Entry<Integer, Double> entry : teamA)
        {
            System.out.println("  " + entry);
        }
        System.out.println("Sum: " + computeSum(teamA));

        System.out.println("Team B:");
        for (Entry<Integer, Double> entry : teamB)
        {
            System.out.println("  " + entry);
        }
        System.out.println("Sum: " + computeSum(teamB));
    }

    private static double computeSum(
        Collection<Entry<Integer, Double>> entries)
    {
        return entries.stream().map(
            e -> e.getValue()).reduce(0.0, (a,b) -> a+b);
    }


}

// From https://github.com/javagl/Combinatorics/blob/master/src/main/
// java/de/javagl/utils/math/combinatorics/CombinationIterable.java
class ChoiceIterable<T> implements Iterable<List<T>>
{
    private final List<T> input;
    private final int sampleSize;
    private final long numElements;
    public ChoiceIterable(int sampleSize, List<T> input)
    {
        this.sampleSize = sampleSize;
        this.input = input;
        BigInteger nf = factorial(input.size());
        BigInteger kf = factorial(sampleSize);
        BigInteger nmkf = factorial(input.size() - sampleSize);
        BigInteger divisor = kf.multiply(nmkf);
        BigInteger result = nf.divide(divisor);
        numElements = result.longValue();    
    }

    public static BigInteger factorial(int n)
    {
        BigInteger f = BigInteger.ONE;
        for (int i = 2; i <= n; i++)
        {
            f = f.multiply(BigInteger.valueOf(i));
        }
        return f;
}     
    @Override
    public Iterator<List<T>> iterator()
    {
        return new Iterator<List<T>>()
        {
            private int current = 0;
            private final int chosen[] = new int[sampleSize];
            // Initialization of first choice
            {
                for (int i = 0; i < sampleSize; i++)
                {
                    chosen[i] = i;
                }
            }

            @Override
            public boolean hasNext()
            {
                return current < numElements;
            }

            @Override
            public List<T> next()
            {
                if (!hasNext())
                {
                    throw new NoSuchElementException("No more elements");
                }

                List<T> result = new ArrayList<T>(sampleSize);
                for (int i = 0; i < sampleSize; i++)
                {
                    result.add(input.get(chosen[i]));
                }
                current++;
                if (current < numElements)
                {
                    increase(sampleSize - 1, input.size() - 1);
                }
                return result;
            }

            private void increase(int n, int max)
            {
                if (chosen[n] < max)
                {
                    chosen[n]++;
                    for (int i = n + 1; i < sampleSize; i++)
                    {
                        chosen[i] = chosen[i - 1] + 1;
                    }
                }
                else
                {
                    increase(n - 1, max - 1);
                }
            }

            @Override
            public void remove()
            {
                throw new UnsupportedOperationException(
                    "May not remove elements from a choice");
            }
        };
    }
}

(the class for computing the choices is taken from here)

The output is as follows:

Optimal result:
Team A:
  1=5.0
  4=6.5
  5=4.5
  8=7.6
Sum: 23.6
Team B:
  2=8.4
  3=2.1
  6=3.2
  7=9.8
Sum: 23.5
Greedy result:
Team A:
  2=8.4
  8=7.6
  1=5.0
  3=2.1
Sum: 23.1
Team B:
  7=9.8
  4=6.5
  5=4.5
  6=3.2
Sum: 24.0

showing that the greedy result is not the optimal one, but still rather good in this case.

Community
  • 1
  • 1
Marco13
  • 53,703
  • 9
  • 80
  • 159