1

I'm trying to generate pairs and then would like to choose the best pair based on set parameters. Generating pairs isn't that hard, but what's tricky is to select the best out of them.

I think it'd be best if i'd continue with example, let's take we currently have 4 elements and name them element1,element2,element3,element4. Each element has properties which are important when generating pairs:

while ($element = mysqli_fetch_assoc($queryToGetElements)){
    $elementId = $element['eid'];
    $elementOpponents = getElementOpponents($elementId); //Returns opponents Id's as an array. These are the possible elements which can be paired.
    $elementPairs = generatePairsWithElement($elementId,$elementOpponents); //Pair generating function, this uses the possible pairs which is defined below. Pretty much checks who hasn't paired before and puts them together in the pair.
    $bestPair = chooseBest($elementPairs,$allOtherPairs) // This should select the best pair out of the elementPairs considering uniqueness with other pairs.

}

//So let's define our four elements:
$element1Place = 1;
$element2Place = 2;
$element3Place = 3;
$element4Place = 4;
//execute while loop with $element1:
$element1Opponents = [$element2,$element3,$element4];
$elementPairs = [[$element1,$element3],[$element1,$element3],[$element1,$element4]];
$element2Opponents = [$element3]
$elementPairs = [[$element2,$element3],[$element2,$element1]];
$elemenet3Opponents = [$element2]
$elementPairs = [[$element2,$element3],[$element1,$element3]];
$element4Opponents = [$element1]
$elementPairs = [[$element1,$element4];
//Pairs returned should be: [$element1,$element4] & [$element2,$element3].
  • Possible pairs - This is an array of other elements which can be paired with current element. With current example I do have 4 elements, but some of them can not be paired together as they may have been paired together previously (and it's not allowed to pair someone together twice). This constraint is applied in function generatePairsWithElements.
  • Place - This is an unique integer value which cannot be same for two elements. When choosing paired element, the element with lower Place will be selected. This constraint is applied in function chooseBest.
  • Combinations has to be unique - For example if element1 can pair with either element2 and element3 and element4 can pair with only element2 then element1 can only pair with element3 even if element1 is iterated earlier and element2 has lower place. So the combinations would be [element1,element3] and [element2,element4]. This constraint is applied in function chooseBest.

This task wouldn't be so difficult if there wouldn't be the third aspect, generating unique combinations. It would be a pretty easy to iterate over the possible opponents and just choose the best one however generating unique combinations is vital for my project.

Banana
  • 814
  • 1
  • 8
  • 28
  • Put all the pairs in an array, then sort the array by the parameters. Then the first element in the array will be the best pair. – Barmar Sep 01 '17 at 19:30
  • Do you have any suggestions on how to sort guaranteeing uniqueness? – Banana Sep 01 '17 at 19:31
  • That's part of creating all the pairs. You said that part isn't hard. – Barmar Sep 01 '17 at 19:33
  • I think you misunderstood or I worded myself badly. Possible pair does not filter out the uniqueness constraint in the beginning, it just maps who aren't yet paired and the constraints are set in `chooseBest` function. – Banana Sep 01 '17 at 19:35
  • As you generate pairs, put them in an array. Before adding a new pair to the array, check if it's already in there. – Barmar Sep 01 '17 at 19:37
  • That wouldn't work when the last element cannot be matched with the element left (since there are always even number of elements). This can also be seen in my example in uniqueness. – Banana Sep 01 '17 at 19:40
  • I thought it was pairs that have to be unique, not individual items. – Barmar Sep 01 '17 at 20:10
  • I think you need to post some sample data and what you're trying to achieve with it. – Barmar Sep 01 '17 at 20:10
  • @Barmar I think what OP means is that by doing that there's a chance that the last two items won't match – William Perron Sep 01 '17 at 20:12
  • I can write a longer example with more difficult scenarios as well if it's necessary, right now it kind of leaves an assumption that it's possible to sort out by single opponent however with wider range of opponents it's not possible. – Banana Sep 01 '17 at 20:51

2 Answers2

1

So I want to preface this answer by a disclaimer: I am not a mathematician; my understanding of linear algebra, matrix algebra and statistic is adequate but by no means extensive. There may be ways to achieve the same results with fewer lines of code or more efficiently. However, I believe that the verbosity level of this answer will allow more people to understand it, and follow the logic step by step. Now that that's out of the way, let's jump into it.

Calculate the Weight of Each Pair

The problem with the current approach is that the logic for finding the best possible match is happening as you loop through the results of the query. What that means is that you may be left with elements that can't be matched together when you get to the last iteration. To fix this, we're going to need to split the process a bit. The first step is to get all of the possible pairs for the given array elements.

$elements = [
    1,2,3,4,5,6    
];

$possiblePairs = [];
for($i = 1; $i <= $elementCount; $i++) {
    for($j = $i + 1; $j <= $elementCount; $j++) {
        $possiblePairs[] = [
            'values' => [$elements[$i - 1], $elements[$j - 1]],
            'score' => rand(1, 100)
        ];
    }
}

As you can see, for each possible pair, I am also attaching a score element, a random integer in this case. This score represents how strongly this pair matches. The value of this score doesn't actually matter: it could be values in a specific range (ex: 0-100) or every value defined by some functions of your own. The important thing here is that pairs that have highest match potential should have a higher score, and pairs with little to no potential should have a low score. If some pairs absolutely cannot go together, simply set the score to zero.

The next step is to then use than score value to sort the array so the the pairs that are more strongly matched are on top, and the weaker (or impossible) pairs at the bottom. I used PHP 7.0's spaceship operator to get the job done, but you can find other ways to achieve this sort here.

usort($possiblePairs, function($a, $b) {
    return $b['score'] <=> $a['score'];
});

Now we're finally equipped to build our final output. This step is actually fairly easy: loop through the possible pairs, check if the values haven't already been used, then push them to the output array.

$used = []; // I used a temporary array to store the processed items for simplicity
foreach($possiblePairs as $key => $pair) {
    if($pair['score'] !== 0 && // additional safety if two elements cannot go together
        !in_array($pair['values'][0], $used) && 
        !in_array($pair['values'][1], $used)) 
    {
        $output[] = $pair['values']; // push the values to the return of the function
        array_push($used, $pair['values'][0], $pair['values'][1]); // add the element to $used so they get ignored on the next iteration
    }
}

var_dump($output);

// output
array(3) {
    [0]=> array(2) {
        [0]=> int(2)
        [1]=> int(4)
    }
    [1]=>  array(2) {
        [0]=> int(1)
        [1]=> int(3)
    }
    [2]=> array(2) {
        [0]=> int(5)
        [1]=> int(6)
    }
}

And there it is! The strongest pair is chosen first, and then it goes down in priority. Play around with weighting algorithms, see what works for your specific needs. As a final word: if I were writing this in the context of a business application, I would probably add some error reporting if there happens to be un-matcheable pairs (after all, it is statistically possible) to make it easier to spot the cause of the problem and decide if the weighting needs tweaking.


You can try the example here


Edit to add:

In order to prevent the scenario where an element gets stored in a pair when another element can only be paired with that first element (which results in the pair never being created) you can try this little patch. I started by recreating your requirements in a getScore() function.

function getScore($elem1, $elem2) {
    $score;

    if($elem1 > $elem2) {
        $tmp = $elem1;
        $elem1 = $elem2;
        $elem2 = $tmp;
    }

    if($elem1 === 1 && $elem2 === 2) {
        $score = 100;
    }
    elseif(($elem1 === 3 && $elem2 !== 2) || ($elem1 !== 2 && $elem2 === 3)) {
        $score = 0;
    }
    else {
        $score = rand(0, 100);
    }

    return $score;
}

Then, I modified the $possiblePairs array creation to do two additional things.

  1. if the score is 0, don't append to the array and
  2. keep track of how many matches were found for each element. By doing this, any element that only has one possible match will have an associated value in $nbMatches of 1.

    $nbMatches = [
        1 => 0,
        2 => 0,
        3 => 0,
        4 => 0
    ]
    
    $possiblePairs = [];
    for($i = 1; $i <= $elementCount; $i++) {
        for($j = $i + 1; $j <= $elementCount; $j++) {
            $score = getScore($elements[$i - 1], $elements[$j - 1]);
            if($score > 0) {
                $possiblePairs[] = [
                    'values' => [$elements[$i - 1], $elements[$j - 1]],
                    'score' => $score
                ];
                $nbMatches[$elements[$i - 1]]++;
                $nbMatches[$elements[$j - 1]]++;
            }
        }
    }
    

Then I added another loop that will bump up those elements so that they end up on top of the list to be processed before the rest.

foreach($nbMatches as $elem => $intMatches) {
    if($intMatches === 1) {
        foreach($possiblePairs as $key => $pair) {
            if(in_array($elem, $pair['values'])) {
                $possiblePairs[$key]['score'] = 101;
            }
        }
    }
}

usort($possiblePairs, function($a, $b) {
    return $b['score'] <=> $a['score'];
});

The output is then:

array(2) {
    [0]=> array(2) {
        [0]=> int(2)
        [1]=> int(3)
    }
    [1]=> array(2) {
        [0]=> int(1)
        [1]=> int(4)
    }
}

I just want to stress: this is only a temporary patch. It will not protect you in cases where, for instance, element 1 can only match element 2 and element 3 can also only match element 2. However, we are dealing with a very small sample size, and the more elements you have, the less likely these edge case will be likely to occur. In my opinion this fix is not necessary unless you are only working with 4 elements. working with 6 elements yields 15 possible combinations, 10 elements yields 45 possible combinations. Already those cases will be unlikely to happen.

Also, if you find that those edge cases still happen, it may be a good idea to go back and tweak the matching algorithm to be more flexible, or take into account more parameters.


You can try the updated version here

Sᴀᴍ Onᴇᴌᴀ
  • 8,218
  • 8
  • 36
  • 58
William Perron
  • 1,288
  • 13
  • 18
  • Hey William, i haven't fully understood your code yet, but actually it's not possible that all the pairs won't match (atleast in my application). Im having trouble understanding the reason behind random score value as it can turn the best pair into the worst pair. – Banana Sep 02 '17 at 07:08
  • @Banana, I used a random value here just to get *some* number since I don't know exactly what *your* requirements are. At that point in the code, you can replace that by calling a function that will return a number that is representative of how strong that pair matches, and if no match is possible, return `0` – William Perron Sep 02 '17 at 11:31
  • Hey William, i worked through your code and it seems pretty legit, though i'd indeed like to add the failure scenario, any ideas how to add it when possiblePairs are iterated through and no pairs found? Just add else statement and check whether i'm in the last element of possiblePairs? – Banana Sep 02 '17 at 18:30
  • @Banana There's so many ways to do this and so many variables to take into account that I can't possibly enumerate them all here. You could throw an exception and continue, you could throw an exception and stop execution, you could simply write to a log file or you could do nothing and deal with it farther in the workflow. It's really up to you at this point. [This question](https://stackoverflow.com/questions/11894115/best-way-to-handle-errors-on-a-php-page) has some interesting information, I would start there – William Perron Sep 02 '17 at 18:56
  • Thank you William, one more question though - selecting best for first may cause an issue mentioned in my original question. An example with numbers 1,2,3,4 would be that number 1 can make pairs with 2 and 4 (thus creating pairs (1,2),(1,4)).Now an issue happens when number 3 can make a pair only with number 2, because (1,2) is a better pair than (1,4) and thus first off (1,2) is chosen (and (1,2) would be reasonable choice if (3,4) would be possible). – Banana Sep 03 '17 at 07:25
  • @Banana this one is a bit long to put in a comment ;) I updated the answer to include a potential fix to this problem – William Perron Sep 03 '17 at 16:57
0

So i think i got it figured out, thanks to William and thanks to ishegg from another thread.

So as a prequisite i have an array $unmatchedPlayers which is an associative array where element name is a key and element position (Place) is a value.

First off using that info, i'm generating unique permutation pairs

function generatePermutations($array) {
    $permutations = [];
    $pairs = [];
    $i = 0;
    foreach ($array as $key => $value) {
        foreach ($array as $key2 => $value2) {
            if ($key === $key2) continue;
            $permutations[] = [$key, $key2];
        }
        array_shift($array);
    }
    foreach ($permutations as $key => $value) {
        foreach ($permutations as $key2=>$value2) {
            if (!in_array($value2[0], $value) && !in_array($value2[1], $value)) {
                $pairs[] = [$value, $value2];
            }
        }
        array_shift($permutations);
    }
    return $pairs;
}

This will return me an array called $pairs which has arrays of different possible pairs inside in a manner of: $pairs= [[['One','Two'],['Three','Four']],[['One','Three'],['Two','Four']],[['One','Four'],['Two','Three']]];

Now i will iterate over the array $pairs and choose the best, giving each permutation combination a 'score':

function chooseBest($permutations,$unmatchedPlayers){
    $currentBest = 0;
    $best = [];
    foreach ($permutations as &$oneCombo){ //Iterate over all permutations [[1,2],[3,4]],[..]
        $score = 0;
        foreach ($oneCombo as &$pair){
            $firstElement = $pair[0];
            $secondElement = $pair[1];
            //Check if these two has played against each other? If has then stop!
            if(hasPlayedTP($firstElement,$secondElement)){
                $score = 0;
                break;
            }
            $score += $unmatchedPlayers[$firstElement];
            $score += $unmatchedPlayers[$secondElement];
        }
        if($score > $currentBest){
            $currentBest = $score;
            $best = $oneCombo;
        }
    }
    return $best;
}

The best score is calculated and permutation pair is returned.

Banana
  • 814
  • 1
  • 8
  • 28