5

EDIT 1 -since posting I have learnt that the underlying question is about how to find the CARTESIAN PRODUCT (now go google), but not only because I don't want every perm, I want to find the cartesian products that use the same subarray Key never more than once per permuation AND my 'extra' question then is more about how to minimise the workload that a cartesian product would require (accepting a small error rate, I have to say)-

Imagine... I have four cooks and four recipes, each cook has a score for each recipe and today I'd like each cook to make one dish (but no dish should be made twice) and the decision should be based on the best (highest total scores) permutation for all four (so maybe a cook won't make his personal best).

I have put the data into a multi-dimensional array as such

 array(
   array (1,2,3,4),
   array (35,0,0,0),
   array (36,33,1,1),
   array (20,20,5,3)
 )
  • it has the same number of valuepairs in each sub array as the number of sub-arrays (if that helps any)

  • in reality the number of sub-arrays would reach a maximum of 8 (max perms therefore =8!, approx 40,000 not 8^8 because many combinations are not allowed)

  • the choice of having the data in this format is flexible if that helps

I am trying to create a second array that would output the best (ie HIGHEST value) possible combination of the sub-arrays as per KEYs where only ONE of each subarray can be used

--so here each subarray[0][1][2][3] would be used once per permutation and each subarrayKey [0][1][2][3] would be used once per permutaion, in my actual problem I'm using associated arrays, but that is extra to this issue.--

So the example would create an array as such newArray (35,33,5,4) // note that [2][0] was not used

IDEALLY I would prefer to not produce the ALL perms but rather, SOMEHOW, discard many combinations that would clearly not be best fit.

Any ideas for how to start? I would accept pseudo code.

For an example on SO about Cartesian Product, see PHP 2D Array output all combinations

EDIT 2 for more on making cartesian products more efficient, and maybe why it has to be case specific if you want to see if you can cut corners (with risk) Efficient Cartesian Product algorithm

Community
  • 1
  • 1
EnglishAdam
  • 1,380
  • 1
  • 19
  • 42
  • 1
    If max(N sub-arrays) = 8, then max(perm) = 4 ^ 8 = 65536. – HamZa Apr 26 '13 at 09:16
  • From what I understand of this problem, each cook can only cook one recipe, so the number of actual permutations is 4*3*2*1 = 4! = 24. With that in mind, it wouldn't be such a stretch to brute force calculate it if there's only 4 cooks/recipes. – Pudge601 Apr 26 '13 at 15:53
  • Thanks guys! My thinking is indeed that it is 8! which = 40,320. – EnglishAdam Apr 26 '13 at 18:05

4 Answers4

2

Apologies, but this is going to be more of a logic layout than code...

It's not quite clear to me whether the array(1,2,3,4) are the scores for the first dish or for the first cook, but I would probably use an array such that

$array[$cook_id][$dish_number] = $score;

asort() each array so that $array[$cook_id] = array($lowest_scored_dish,...,$highest);

Consider a weighted preference for a particular cook to make a dish to be the difference between the score of the best dish and another.

As a very simple example, cooks a,b,c and dishes 0,1,2

$array['a'] = array(0=>100, 1=>50, 2=>0); // cook a prefers 0 over 1 with weight 50, over 2 with weight 100
$array['b'] = array(0=>100, 1=>100, 2=>50); // cook b prefers 0,1 over 2 with weight 50
$array['c'] = array(0=>50, 1=>50, 2=>100); // cook c prefers 2 with weight 50

After asort(): $array['a'] = array(0=>100, 1=>50, 2=>0); $array['b'] = array(0=>100, 1=>100, 2=>50); $array['c'] = array(2=>100, 0=>50, 1=>50);

Start with cook 'a' who prefers dish 0 over his next best dish by 50 points (weight). Cook 'b' also prefers dih 0, but with a weight of 0 over the next dish. Therefore it's likely (though not yet certain that cook 'a' should make dish 0.

Consider dish 0 to be reserved and move on to cook 'b'. Excluding dish 0, cook 'b' prefers dish 1. No other cook prefers dish 1, so cook 'b' is assigned dish 1.

Cook 'c' gets dish 2 by default.

This is a VERY convenient example where each cook gets to cook something that's a personal max, but I hope it's illustrative of some logic that would work out.

Let's make it less convenient:

$array['a'] = array(0=>75, 1=>50, 2=>0);
$array['b'] = array(0=>100, 1=>50, 2=>50);
$array['c'] = array(0=>100, 1=>25, 2=>25);

Start again with cook 'a' and see that 0 is preferred, but this time with weight 25. Cook 'b' prefers with a weight of 50 and cook 'c' prefers with a weight of 75. Cook 'c' wins dish 0.

Going back to the list of available cooks, 'a' prefers 1 with a weight of 50, but 'b' prefers it with weight 0. 'a' gets dish 1 and 'b' gets dish 2.

This still doesn't take care of all complexities, but it's a step in the right direction. Sometimes the assumption made for the first cook/dish combination will be wrong.

WAY less convenient:

$array['a'] = array(0=>200, 1=>148, 2=>148, 3=>0);
$array['b'] = array(0=>200, 1=>149, 2=>0, 3=>0);
$array['c'] = array(0=>200, 1=>150, 2=>147, 3=>147);
$array['d'] = array(0=>69, 1=>18, 2=>16, 3=>15);

'a' gets 0 since that's the max and no one else who prefers 0 has a higher weight 'b' wins 1 with a weight of 149 'd' wins 2 since 'c' doesn't have a preference from the available options 'c' gets 3

score: 200+149+147+16 = 512

While that's a good guess that's gathered without checking every permutation, it may be wrong. From here, ask, "If one cook traded with any one other cook, would the total increase?"

The answer is YES, a(0)+d(2) = 200+16 = 216, but a(2)+d(0) = 148+69 = 217.

I'll leave it to you to write the code for the "best guess" using the weighted approach, but after that, here's a good start for you:

// a totally uneducated guess...
$picks = array(0=>'a', 1=>'b', 2=>'c', 3=>'d');

do {
    $best_change = false;
    $best_change_weight = 0;
    foreach ($picks as $dish1 => $cook1) {
        foreach ($picks as $dish2 => $cook2) {
            if (($array[$cook1][$dish1] + $array[$cook2][$dish2]) <
                ($array[$cook1][$dish2] + $array[$cook2][$dish1]))
            {
                $old_score = $array[$cook1][$dish1] + $array[$cook2][$dish2];
                $new_score = $array[$cook1][$dish2] + $array[$cook2][$dish1];
                if (($new_score - $old_score) > $best_change_weight) {
                    $best_change_weight = $new_score - $old_score;
                    $best_change = $dish2;
                }
            }
        }
        if ($best_change !== false) {
            $cook2 = $picks[$best_change];
            $picks[$dish1] = $cook2;
            $picks[$dish2] = $cook1;
            break;
        }
    }
} while ($best_change !== false);

I can't find a counter example to show that this doesn't work, but I'm suspicious of the case where ($array[$cook1][$dish1] + $array[$cook2][$dish2]) == ($array[$cook1][$dish2] + $array[$cook2][$dish1])

Maybe someone else will follow up with an answer to this "What if?"

Given this matrix, where the items in brackets are the "picks"

[a1]   a2   a3
 b1   [b2]  b3
 c1    c2  [c3]

If a1 + b2 == a2 + b1, then 'a' and 'b' will not switch dishes. The case I'm not 100% sure about is if there exists a matrix such that this is a better choice:

 a1   [a2]   a3
 b1    b2   [b3]
[c1]   c2    c3

Getting from the first state to the second requires two switches, the first of which seems arbitrary since it doesn't change the total. But, only by going through this arbitrary change can the last switch be made.

I tried to find an example 3x3 such that based on the "weighted preference" model I wrote about above, the first would be selected, but also such that the real optimum selection is given by the second. I wasn't able to find an example, but that doesn't mean that it doesn't exist. I don't feel like doing more matrix algebra right now, but maybe someone will pick up where I left off. Heck, maybe the case doesn't exist, but I thought I should point out the concern.

If it does work and you start with the correct pick, the above code will only loop through 64 times (8x8) for 8 cooks/dishes. If the pick is not correct and the first cook has a change, then it will go up to 72. If the 8th cook has a change, it's up to 128. It's possible that the do-while will loop several times, but I doubt it will get up near the CPU cycles required to sum all of the 40k combinations.

thatthatisis
  • 783
  • 7
  • 18
  • I really like the thinking (+1) and I totally appreciate your time and effort, a big thank you. I'll be looking at this problem (again) all day tomorrow and will defintely have a go with this thinking, the problem is as you've thought a question also about marginal values and differences in trying to maximise assignment. (ps your thinking about initial arrays was as I wanted, sorry for the confusion) – EnglishAdam Apr 26 '13 at 18:34
  • After learning that I'm trying to short-cut doing a full Cartesian Product search, I'm accepting that I'll have to accept that any short cuts via guess-timating will generate errors. Another issue for me is that this pairing up needs to be done 20 times a second inside a much larger game loop. I think it could save a lot compared to 40,000 loops (and boy I've realised that if anybody is interested in following up this weighted idea there are even other Qs on SO looking to shortcut this problem). However I'm going to see if I can conjure up some REALLY ROUGH AND READY ideas. – EnglishAdam Apr 27 '13 at 14:56
1

I may have a starting point for you with this algorithm that tries to choose cooks based on their ratio of max score to sum of scores (thus trying to choose chefs who are really good at one recipe but bad at the rest of the recipes to do that recipe)

$cooks = array(
    array(1,2,3,4),
    array(35,0,0,0),
    array(36,33,1,1),
    array(20,20,5,3)
);
$results = array();

while (count($cooks)) {
    $curResult = array(
        'cookId' => -1,
        'recipe' => -1,
        'score'  => -1,
        'ratio'  => -1
    );
    foreach ($cooks as $cookId => $scores) {
        $max = max($scores);
        $ratio = $max / array_sum($scores);
        if ($ratio > $curResult['ratio']) {
            $curResult['cookId'] = $cookId;
            $curResult['ratio']  = $ratio;
            foreach ($scores as $recipe => $score) {
                if ($score == $max) {
                    $curResult['recipe'] = $recipe;
                    $curResult['score']  = $score;
                }
            }
        }
    }

    $results[$curResult['recipe']] = $curResult['score'];
    unset($cooks[$curResult['cookId']]);
    foreach ($cooks as &$cook) {
        unset($cook[$curResult['recipe']]);
    }
}

For the dataset provided, it does find what seems to be the optimum answer (35,33,5,4). However, it is still not perfect, for example, with the array:

$cooks = array(
    array(1,2,3,4),
    array(35,0,33,0),
    array(36,33,1,1),
    array(20,20,5,3)
);

The ideal answer would be (20,33,33,4), however this algorithm would return (35,33,5,4).

But since the question was asking for ideas of where to start, I guess this at least might suffice as something to start from :P

Pudge601
  • 2,048
  • 1
  • 12
  • 11
  • Thanks, nice one, I really appreciate your effort and clarity, +1, even though the logic on its own isn't enough (as you've shown) it could definitely help in trying to cut down on having to do 40K combos... thanks again, I'll be immersed in this all day tomorrow so we'll see what happens! – EnglishAdam Apr 26 '13 at 18:41
  • Ciao! still on this.. just looking at your code I can't see how you check to see if either the cook or the recipe is a duplicate. Any ideas? – EnglishAdam Apr 28 '13 at 15:06
  • When I choose the cook/recipe, I then unset all that cook, and unset that recipe on the remaining cooks – Pudge601 Apr 29 '13 at 08:17
0

Try this

$mainArr = array(
   array (1,2,3,4) ,
   array (35,0,0,0) ,
   array (36,33,1,1) ,
   array (20,20,5,3) 
 );
$i = 0;
foreach( $mainArr as $subArray )
{
    foreach( $subArray as $key => $value)
    {
        $newArr[$key][$i]=$value;
        $i++;   
    }
}
$finalArr = array();
foreach( $newArr as $newSubArray )
{
    $finalArr[] = max($newSubArray);    
}
print_r( $finalArr );
  • Thanks but... ouputs Array ( [0] => 36 [1] => 33 [2] => 5 [3] => 4 ), which is simply the highest for each, having cook[2] making 2 recipes [0] & [1] – EnglishAdam Apr 26 '13 at 09:59
0

OK here is a solution that allows you to find the best permutation of one cook to one recipe and no cook works twice and no recipe is made twice.

Thanks for the code to calculate perm of arrays goes to o'reilly... http://docstore.mik.ua/orelly/webprog/pcook/ch04_26.htm

CONSIDERATIONS:

  • The number of cooks and the number of recipes are the same.

  • Going above a 5 by 5 matrix as here will get very big very fast. (see part 2 to be posted shortly)

The logic: A permutation of an array assigns a place as well as just being included (ie what a combination does), so why not then assign each key of such an array to a recipe, the permutation guarantees no cook is repeated and the keys guarantee no recipe is repeated.

Please let me know if there are improvements or errors in my thinking or my code but here it is!

<?php

function pc_next_permutation($p, $size) {
//this is from http://docstore.mik.ua/orelly/webprog/pcook/ch04_26.htm
    // slide down the array looking for where we're smaller than the next guy
    for ($i = $size - 1; $p[$i] >= $p[$i+1]; --$i) { }

    // if this doesn't occur, we've finished our permutations
    // the array is reversed: (1, 2, 3, 4) => (4, 3, 2, 1)
    if ($i == -1) { return false; }

    // slide down the array looking for a bigger number than what we found before
    for ($j = $size; $p[$j] <= $p[$i]; --$j) { }

    // swap them
    $tmp = $p[$i]; $p[$i] = $p[$j]; $p[$j] = $tmp;

    // now reverse the elements in between by swapping the ends
    for (++$i, $j = $size; $i < $j; ++$i, --$j) {
         $tmp = $p[$i]; $p[$i] = $p[$j]; $p[$j] = $tmp;
    }

    return $p;
}
$cooks[441] = array(340=>5,342=>43,343=>50,344=>9,345=>0);
$cooks[442] = array(340=>5,342=>-33,343=>-30,344=>29,345=>0);
$cooks[443] = array(340=>5,342=>3,343=>0,344=>9,345=>10,);                     
$cooks[444] = array(340=>25,342=>23,343=>20,344=>19,345=>20,); 
$cooks[445] = array(340=>27,342=>27,343=>26,344=>39,345=>50,); 

//a consideration: this solution requires that the number of cooks equal the number of recipes
foreach ($cooks as $cooksCode => $cooksProfile){
        $arrayOfCooks[]=$cooksCode;
        $arrayOfRecipes = (array_keys($cooksProfile));
}
echo "<br/> here is the array of the different cooks<br/>";
print_r($arrayOfCooks);
echo "<br/> here is the array of the different recipes<br/>";
print_r($arrayOfRecipes);

$set = $arrayOfCooks;
$size = count($set) - 1;
$perm = range(0, $size);
$j = 0;

do { 
     foreach ($perm as $i) { $perms[$j][] = $set[$i]; }
} while ($perm = pc_next_permutation($perm, $size) and ++$j);
echo "<br/> here are all the permutations of the cooks<br/>";
print_r($perms);

$bestCombo = 0;
foreach($perms as $perm){
    $thisScore =0;
        foreach($perm as $key =>$cook){
        $recipe= $arrayOfRecipes[$key];
        $cookScore =$cooks[$cook][$recipe];
        $thisScore = $thisScore+$cookScore;
        }
    if ($thisScore>$bestCombo){
        $bestCombo=$thisScore;
        $bestArray= $perm;
    }
}



echo "<br/> here is the very best array<br/>";
print_r ($bestArray);
echo "<br/> best recipe assignment value is:".$bestCombo."<br/><br/>";  




?>
EnglishAdam
  • 1,380
  • 1
  • 19
  • 42