Random But Unique Pairings, with Conditions - php

I need some help/direction in setting up a PHP script to randomly pair up items in an array.
The items should be randomly paired up each time.
The items should not match themselves ( item1-1 should not pair up with item1-1 )
Most of the items have a mate (ie. item1-1 and item1-2). The items should not be paired with their mate.
I've been playing around with the second script in this post but, I haven't been able to make any progress. Any help is appreciated.

Very simple approach, but hopefully helpful to you:
(mates, if grouped in an array (e.g. array('a1', 'a2')), will not be paired.)
function matchUp($array) {
$result = array();
while($el = array_pop($array)) {
shuffle($array);
if (sizeof($array) > 0) {
$candidate = array_pop($array);
$result[] = array(
array_pop($el),
array_pop($candidate)
);
if (sizeof($el) > 0) {
$array[] = $el;
}
if (sizeof($candidate) > 0) {
$array[] = $candidate;
}
}
else {
$result[] = array(array_pop($el));
}
}
return $result;
}
$array = array(
array('a1', 'a2'),
array('b1', 'b2'),
array('c1'),
array('d1'),
array('e1', 'e2'),
array('f1'),
array('g1', 'g2'),
);
Update:
foreach(matchUp($array) as $pair) {
list($a, $b) = $pair + array(null, null);
echo '<div style="border: solid 1px #000000;">' . $a . ' + ' . $b . '</div>';
}

With the randomness, there is no guarantee that a full correct solution will be reached.
Certain problem sets are more likely to be solved than others. Some will be impossible.
You can configure how many times it will try to achieve a good solution. After the specified number of tries it will return the best solution it could find.
function pairUp (array $subjectArray) {
// Config options
$tries = 50;
// Variables
$bestPaired = array();
$bestUnpaired = array();
for($try = 1; $try <= 50; $try++) {
$paired = array();
$unpaired = array();
$toBePaired = $subjectArray;
foreach($subjectArray as $subjectIndex => $subjectValue) {
// Create array without $thisValue anywhere, from the unpaired items
$cleanArray = array();
foreach($toBePaired as $index => $value) {
if($value != $subjectValue) {
array_push($cleanArray, array(
'index' => $index,
'value' => $value
));
}
}
sort($cleanArray); // reset indexes in array
// See if we have any different values left to match
if(count($cleanArray) == 0) {
array_push($unpaired, $subjectValue);
continue;
}
// Get a random item from the clean array
$randomIndex = rand(0,count($cleanArray)-1);
// Store this pair
$paired[$subjectIndex] = $subjectValue . '-' . $cleanArray[$randomIndex]['value'];
// This item has been paired, remove it from unpairedItems
unset($toBePaired[$cleanArray[$randomIndex]['index']]);
sort($toBePaired);
}
// Decide if this is our best try
if(count($paired) > count($bestPaired)) {
$bestPaired = $paired;
$bestUnpaired = $unpaired;
}
// If we had no failures, this was a perfect try - finish
if(count($unpaired) == 0) { $break; }
}
// We're done, send our array of pairs back.
return array(
'paired' => $bestPaired,
'unpaired' => $bestUnpaired
);
}
var_dump(pairUp(array('a','b','c','d','e','a','b','c','d','e')));
/*
Example output:
array(2) {
["paired"]=>
array(10) {
[0]=>
string(3) "a-b"
[1]=>
string(3) "b-c"
[2]=>
string(3) "c-d"
[3]=>
string(3) "d-e"
[4]=>
string(3) "e-a"
[5]=>
string(3) "a-b"
[6]=>
string(3) "b-e"
[7]=>
string(3) "c-d"
[8]=>
string(3) "d-c"
[9]=>
string(3) "e-a"
}
["unpaired"]=>
array(0) {
}
}
*/

Case 1: if all elements had a mate
If all elements had a mate, the following solution would work, although I don't know if it would be perfectly random (as in, all possible outputs having the same probability):
Shuffle the list of elements, keeping mates together
original list = (a1,a2),(b1,b2),(c1,c2),(d1,d2)
shuffled = (c1,c2),(d1,d2),(a1,a2),(b1,b2)
Shift the second mate to the right. The matches have been formed.
shifted = (c1,b2),(d1,c2),(a1,d2),(b1,a2)
(Edit1: if applied exactly as described, there is no way a1 ends up matched with b1. So, before shifting, you may want to throw a coin for each pair of mates to decide whether they should change their order or not.)
Case 2: if only some elements have a mate
Since in your question only some elements will have a mate, I guess one could come up with the following:
Arbitrarily pair up those elements who don't have a mate. There should be an even number of such elements. Otherwise, the total number of elements would be odd, so no matching could be done in the first place.
original list = (a1,a2),(b1,b2),c1,d1,e1,f1 // c1,d1,e1 and f1 don't have mates
list2 = (a1,a2),(b1,b2),(c1,d1),(e1,f1) // pair them up
Shuffle and shift as in case 1 to form the matches.
shuffled = (e1,f1),(a1,a2),(c1,d1),(b1,b2)
shifted = (e1,b2),(a1,f1),(c1,a2),(b1,d1)
Again, I don't know if this is perfectly random, but I think it should work.
(Edit2: simplified the solution)
(Edit3: if the total number of elements is odd, someone will be left without a match, so pick an element randomly at the beginning to leave it out and then apply the algorithm above).

Related

Normalize array values in natural order PHP

I have faced with the problem, I need to normalize/sort in natural order values in array after some item has been removed.
Consider following example. Initial array
{ [313]=> int(2) [303]=> int(1) [295]=> int(3) [290]=> int(4) }
Sorted array
{ [303]=> int(1) [313]=> int(2) [295]=> int(3) [290]=> int(4) }
Consider case when we are removing first item, array should look like this now
{ [313]=> int(1) [295]=> int(2) [290]=> int(3) }
In case of item inside the array range for example 295 (3) it should be
{ [303]=> int(1) [313]=> int(2) [290]=> int(3) }
I hope you get an idea.
But my function doesn't do this correctly.
I've implemented part of this sorting, here is the code, but maybe there are other ways to do this easier ?
const MIN_VALUE = 1;
public function sort_items(&$items_map)
{
if (!empty($items_map)) {
asort($items_map);
var_dump($items_map);
$first_item = reset($items_map);
if ($first_item > self::MIN_VALUE) {
$normalize_delta = $first_item - self::MIN_VALUE;
$prev_item_id = null;
foreach ($items_map as $id => $part) {
$items_map[$id] = $part - $normalize_delta;
if (!empty($prev_item_id)) {
$difference = $items_map[$id] - $items_map[$prev_item_id];
if ($difference > 1) {
$items_map[$id] = $items_map[$id] - ($difference - 1);
}
}
$prev_item_id = $id;
}
}
}
return $items_map;
}
I would be grateful for any help.
Thanks
UPDATE
To clarify.
I want items not to be just sorted in the correct order, but to be in natural order, for example
Sequence 1,3,5,6,7,9 should be transformed into 1,2,3,4,5,6 but keeping keys the same.
2,3,7,9 => 1,2,3,4
Please see my example above with real word case.
If you need to use a custom sort algorithm, use usort to do so. From PhP the documentation :
The comparison function must return an integer less than, equal to, or greater than zero if the first argument is considered to be respectively less than, equal to, or greater than the second.
So you just need to provide those integers if you are in case an item is "greater" or "lower", and usort will do the job for you.
In your case, it could lead to this function :
<?php
function sort_items_map($a, $b)
{
$value = 0;
if( $a < $b )
{
$value = -1;
}
else if( $a > $b )
{
$value = 1;
}
else if( $a == $b )
{
$value = 0;
}
return $value;
}
$items_map = [1, 3, 1, 7]; // or fill it with your own values
usort($items_map, "sort_items_map");
?>

Finding the 3 most occurring substrings in a string in PHP

I wanted to go over the thought process of these since I am not sure how to improve this. I have a string that are separated by commas and they have reoccurring substrings and I want to find the 3 most occurring substrings.
I was going to explode the string by commas into an array.
Perform a substr_count in the original string for each element in the array and store it in a separate array to store the counts? (Not sure how to improve this since that would create duplicate counts for the same substring)
Perform a max on the array to find the first, second, and third most occurring substrings.
Return an array with the first, second, and third most occurring substrings.
I am guessing after I perform an explode, I can do a quick sort and go from there?
This is what I have tried so far:
$result = findThreeMostOccuringStrings("apple, apple, berry, cherry, cherry, cherry, dog, dog, dog");
var_dump($result);
function findThreeMostOccuringStrings($str){
$first = PHP_INT_MIN;
$second = PHP_INT_MIN;
$third = PHP_INT_MIN;
$arr = explode(",", $str);
for ($i = 0; $i < count($str); $i++){
$arrIdx[] = substr_count($arr[$i]);
}
$first = max($arrIdx);
$arrIdx[$first] = -1;
$second = max($arrIdx);
$arrIdx[$first] = -1;
$third = max($arrIdx);
$arrIdx[$first] = -1;
$threeMostOccuringStrings = array($first, $second, $third);
return $threeMostOccuringStrings;
}
If by substring you mean only the strings separated by commas and not substrings of these, use array_count_values after explode
If you're looking for an efficient way to solve substring search, the answer is a Trie, or prefix tree. This basically reduces the look up time to search for a substring as it creates a deterministic path for all prefixes (i.e. a prefix tree).
Consider the string "A cat does not categorize its food while among other cats." Here the substrings categorize and cats all share the same prefix of cat. So finding the most frequented substring in a prefix tree is easy if you count the number of EOW nodes stemming from each branch in the root.
Building the tree is also quite trivial.
function insertString($str, $trie) {
$str = strtolower($str); // normalize case
$node = $trie;
foreach(str_split($str) as $letter) {
if (!isset($node->$letter)) {
$node->$letter = new stdClass;
}
$node = $node->$letter;
}
$node->EOW = true; // place EOL node
}
function countNodes($branch) {
$n = 0;
foreach($branch as $e => $node) {
if ($node instanceof stdClass) {
$n += countNodes($node);
} elseif ($e === 'EOW') {
$n++;
}
}
return $n;
}
$trie = new stdClass;
$str = "A cat does not categorize its food while among other cats";
foreach(explode(' ', $str) as $word) {
insertString($word, $trie);
}
$s = [];
foreach($trie as $n => $rootNodes) {
$s[$n] = countNodes($rootNodes);
}
var_dump($s);
Which should give you...
array(8) {
["a"]=>
int(2)
["c"]=>
int(3)
["d"]=>
int(1)
["n"]=>
int(1)
["i"]=>
int(1)
["f"]=>
int(1)
["w"]=>
int(1)
["o"]=>
int(1)
}
From there you can see the root branch c has the highest number of substrings (which if walked match cat, cats, and categorize.
Based on your post and the comments, you actually want to tally terms in a comma delimited list of terms, and then rank them, so: let's just do that, using an associative array for tallying and arsort to sort that associative array by value, in reverse order (so that the highest counts are at the start of the array):
function rank($input) {
$terms = explode(',', $input);
$ranked = array();
foreach($terms as $word) {
$word = trim($word);
if (!isset($ranked[$word])) {
$ranked[$word] = 0;
}
$ranked[$word]++;
}
arsort($ranked);
return $ranked;
}
So if we run that through print_r(rank("apple, apple, berry, cherry, cherry, cherry, dog, dog, dog")) we get:
Array
(
[dog] => 3
[cherry] => 3
[apple] => 2
[berry] => 1
)
Splendid.

Changing values of multidimensional array php

Its my first time working with multidimensional arrays in php. I need to change the second number in each sub array.
What I want is to check if the Id in the array matches the Id from the database. When the two match I want to change the 2nd entry in the sub array by adding a number to it. If the Id from the query does not match anything in the list I want a new sub array to be pushed to the end of the array with the values of Id and points_description.
Also, if its helpful, my program right now does find the matches. The only thing is, it does not update the 2D array.
$array = array(array());
while ($row_description = mysqli_fetch_array($query_description)) {
$check = 1;
$is_match = 0;
foreach ($array as $i) {
foreach ($i as $value) {
if ($check == 1) {
if ($row_description['Id'] == $value) {
//$array[$i] += $points_description;
$is_match = 1;
}
}
$check++;
$check %= 2; //toggle between check and points
}
}
if ($is_match == 0) {
array_push($array, array($row_description['Id'], $points_description));
}
}
I feel like Im doing this so wrong. I just want to go through my 2D array and change every second value. The expected output should be a print out of all the Ids and their corresponding point value
I hope this is helpful enough.
Example: $row_description['Id'] = 2 and $array = array(array(2,1), array(5,1) , array(6,1))
output should be $array = array(array(2,4), array(5,1) , array(6,1))
if $row_description['Id'] = 3 and $array = array(array(2,1), array(5,1) , array(6,1))
output should be $array = array(array(2,4), array(5,1) , array(6,1),array(3,3))
By default PHP will copy an array when you use it in a foreach.
To prevent PHP from creating this copy you need to use to reference the value with &
Simple example :
<?php
$arrFoo = [1, 2, 3, 4, 5,];
$arrBar = [3, 6, 9,];
Default PHP behavior : Make a copy
foreach($arrFoo as $value_foo) {
foreach($arrBar as $value_bar) {
$value_foo *= $value_bar;
}
}
var_dump($arrFoo);
/* Output :
array(5) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
[3]=>
int(4)
[4]=>
int(5)
}
*/
ByReference : Don't create the copy :
foreach($arrFoo as &$value_foo) {
foreach($arrBar as $value_bar) {
$value_foo *= $value_bar;
}
}
var_dump($arrFoo);
/* Output :
array(5) {
[0]=>
int(162)
[1]=>
int(324)
[2]=>
int(486)
[3]=>
int(648)
[4]=>
&int(810)
}
*/

Continue to next for loop if current loop is 0

It seems this is a general question of programming logic, as this issue seems to arise in my code regardless of what language it's coded in.
Basically I have 2 nested for loops, inside a for loop. The purpose of these for loops is to enumerate all possible values between two sets of values.
The problem with the code is that is the second set of values contains a 0 the values won't enumerate.
Say for example we want to enumerate all values between 0,0,0 and 1,1,1, this works perfectly fine, as each nested loop is processed. However is we try to calculate between 0,0,0 and 0,1,0 the loop will not continue on to the next loop, instead it will exit the loop and continue on to the rest of the code.
for ($i1=$coords1[0]; $i1<=$coords2[0]; $i1++) { //if $coords2[0] = 0 loop will fail
for ($i2=$coords1[1]; $i2<=$coords2[1]; $i2++) { //if $coords2[1] = 0 loop will fail
for ($i3=$coords1[2]; $i3<=$coords2[2]; $i3++) {//if $coords2[2] = 0 loop will fail
$blocks.= $i1.",".$i2.",".$i3."|";
}
}
}
return $blocks;
Full code: PHPFIDDLE
Okay, to copy this over from our chat, I believe this is the solution:
<?php
$set1 = explode(",", '1,0,1');
$set2 = explode(",", '1,1,0');
$allbetween = _allbetween($set1, $set2);
echo 'All Between: '.$allbetween.'<br/>';
$allcount = count(explode("|", $allbetween))-1;
echo 'Number Of Blocks: '.$allcount;
function _allbetween($coords1, $coords2) {
$blocks = "";
for ($i=0; $i<=2; $i++) {
if ($coords1[$i] > $coords2[$i]) {
$tmp = $coords1[$i];
$coords1[$i] = $coords2[$i];
$coords2[$i] = $tmp;
}
}
for ($i1=$coords1[0]; $i1<=$coords2[0]; $i1++)
{
for ($i2=$coords1[1]; $i2<=$coords2[1]; $i2++)
{
for ($i3=$coords1[2]; $i3<=$coords2[2]; $i3++)
{
$blocks.= $i1.",".$i2.",".$i3."|";
}
}
}
return $blocks;
}
?>
DEMO HERE
The reason this works is that there is a swap loop at the beginning of your function, which swaps any of the three sets of values if the first is greater than the second. This ensures that all values "between" them can be calculated.
Edit: Fixing the demo link to the correct URL
the break; will do. see more information in the link:
http://www.php.net/manual/en/control-structures.break.php
Your loops will never loop as you are setting each initial value to the end range value. Instead use:
for ($i1=0; $i1<=$coords2[0]; $i1++)
...
etc
In terms of fall-through, try the following:
for ($i1=0; $i1<=$coords2[0] + 1; $i1++)
...
etc
Here's the problem: Dumping out your two $coords arrays:
$coords1:
array(3) {
[0]=>
string(1) "0"
[1]=>
string(1) "0"
[2]=>
string(1) "1"
}
$coords2:
array(3) {
[0]=>
string(1) "1"
[1]=>
string(1) "0"
[2]=>
string(1) "1"
}
On your first iteration:
$coords1[0] => 1
$coords2[0] => 0
1 <= 0 -> FALSE
so your outermost loop NEVER executes at all.
I think you are searching for
GOTO function
And for the
Continue function

Shuffle 2 php arrays in the same way

I have this for example:
$array['one'][0] = 0;
$array['one'][1] = 1;
$array['one'][2] = 2;
$array['one'][3] = 3;
$array['two'][0] = 00;
$array['two'][1] = 11;
$array['two'][2] = 22;
$array['two'][3] = 33;
How can I shuffle them both to get something like:
$array['one'][0] = 2;
$array['one'][1] = 1;
$array['one'][2] = 3;
$array['one'][3] = 0;
$array['two'][0] = 22;
$array['two'][1] = 11;
$array['two'][2] = 33;
$array['two'][3] = 00;
Or any other random order, but having the same "random factor" in both?
For example, I want that $array['one'][0] and $array['two'][0] get shuffled to get $array['one'][x] and $array['two'][x] (x being a random key, but the SAME on both arrays).
$count = count($array['one']);
$order = range(1, $count);
shuffle($order);
array_multisort($order, $array['one'], $array['two']);
Works with arrays with elements of any type (objects and arrays too).
This way may by used with any number of arrays (not only two).
Works with duplicated values.
Clean code.
Something like this could work. It's similar to Mimikry's answer except this one will work even if you happen to have duplicate values in array one (this one doesn't use values of array one as keys of the temporary array).
Assuming both arrays are of the same size.
$c = count($array['one']);
$tmp = array();
for ($i=0; $i<$c; $i++) {
$tmp[$i] = array($array['one'][$i], $array['two'][$i]);
}
shuffle($tmp);
for ($i=0; $i<$c; $i++) {
$array['one'][$i] = $tmp[$i][0];
$array['two'][$i] = $tmp[$i][1];
}
hopefully i get you right.
For your example above, this code should work. But it's pretty hacky...
$array['one'][0] = 'A';
$array['one'][1] = 'B';
$array['one'][2] = 'C';
$array['one'][3] = 'D';
$array['two'][0] = 'AA';
$array['two'][1] = 'BB';
$array['two'][2] = 'CC';
$array['two'][3] = 'DD';
// save the dependencies in tmp array
foreach ($array['two'] as $key => $val) {
$tmp[$array['one'][$key]] = $val;
}
shuffle($array['one']);
// restore dependencies in tmp2 array
foreach ($array['one'] as $key => $val) {
$tmp2[$key] = $tmp[$val];
}
// overwrite with restore array
$array['two'] = $tmp2;
var_dump($array):
array(2) {
["one"]=>
array(4) {
[0]=>
string(1) "B"
[1]=>
string(1) "A"
[2]=>
string(1) "C"
[3]=>
string(1) "D"
}
["two"]=>
array(4) {
[0]=>
string(2) "BB"
[1]=>
string(2) "AA"
[2]=>
string(2) "CC"
[3]=>
string(2) "DD"
}
}
The best practice, imho, is as follows:
$tmp = array_combine($array['one'],$array['two']);
shuffle($tmp);
$array['one'] = array_keys($tmp);
$array['two'] = array_values($tmp);
This is clear code and should be fast. No need to reinvent the wheel like in some other answers.
This isn't an example with multidimensional arrays but it works great for sorting multiple normal arrays the same way and with shuffle. Maybe it could be adapted for multidimensional arrays too. It does assume all arrays are the same length.
$array_count = count($array_1);
for($i=0;$i<=($array_count-1);$i++){
$temp_array[$i] = $i;
}
shuffle($temp_array);
for($i=0;$i<=($array_count-1);$i++){
$o = $temp_array[$i];
$array_1_sorted[$i]=$array_1[$o];
$array_2_sorted[$i]=$array_2[$o];
$array_3_sorted[$i]=$array_3[$o];
}
This will give you new arrays which are all sorted the same random way. You could then set the old arrays equal to the new ones or keep them in-tact.
Not sure exactly what you are trying to do, but your best bet is probably to put array 1 and array 2 into a multi-dimensional array and then random the primary array. That will give you a random arrangement of values, but within each key will be the values of each array. We could give you more specific answers if you can provide a bit more detail of exactly what you are doing.

Categories