How to limit duplicate values in an array - php

First of all, I apologize for my lack of English. I hope you do understand what I'm trying to explain here.
So basically I need to build a function that would limit the number of duplicate values inside an array.
The reason I need to do this is that I'm building a system that would divide numbers into groups and every group has to have the same amount of numbers.
EDIT: Random number represents the group number.
I've written a function do this but for some reason, it is not working properly.
function jagaTiimid($max, $liiget, $tArvLength, $tArv){
$tiimid = []; //Starting array
for($z=0;$z<$liiget;$z++){
$numbers = [];
$rn = randomNumber($tArvLength, $tArv, $numbers); //Generate a random number for a group, etc group 1, group 2, group 3
$mitu = countInArray($tiimid, $rn); //Check how many times that number has occured in array
if($mitu == $max){ //If it equals to maximum number of times then...
$rnUus = randomNumber($tArvLength, $tArv, $numbers); //generate a new random number
while($rnUus == $rn){
$numbers = [];
$rnUus = randomNumber($tArvLength, $tArv, $numbers);
} //loop until the new generated number doesn't equal to old rn.
$tiimid[] = $rnUus; //if it doesn't equal to $rn then push into array
}else{
$tiimid[] = $rn;
}
}
return $tiimid;
}
For some reason the number still occures more than it is suppose to.
Basically how it shouldn't end up is.
As you can see, one group(group 2) occurs more times than other group but it should be equal for both groups.
EDIT: CountInArray();
function countInArray($array, $what) {
$count = 0;
for ($i = 0; $i < count($array); $i++) {
if ($array[$i] === $what) {
$count++;
}
}
return $count;
}

When the first random pick hits a number that is already used $liiget times, the inner loop kicks in, but it does not check whether the newly generated random number already occurs $liiget times.
For efficiency I would keep track of the number of times a number has been used. Also, you could benefit from a safety net, in case there really is no number any more that would not exceed the maximum recurrence.
It is not necessary to have a nested loop. The code would look like this:
function jagaTiimid($max, $liiget, $tArvLength, $tArv){
$tiimid = []; //Starting array
$counts = []; // Helper for quick count
$tries = 0; // Counter to avoid infinite looping
while (count($tiimid) < $liiget && $tries++ < 100) {
$numbers = [];
$rn = randomNumber($tArvLength, $tArv, $numbers); //Generate a random number for a group, etc group 1, group 2, group 3
if (!isset($counts[$rn])) $counts[$rn] = 0; // initialise on first occurence
if ($counts[$rn] < $max) {
$tiimid[] = $rn; // add it to the result
$counts[$rn]++; // ... and adjust the count
$tries = 0; // reset the safety
}
}
return $tiimid;
}

replace while($rnUus == $rn) with while(countInArray($tiimid, $rnUus) >= $max)
– Ilya Bursov

Related

Randomly add 'items' or 'ads' into PHP loop

I have an events loop set up; and I also have an ads loop set up.
I want to inject each 'ad' into the events loop at random points. These loops/arrays have different set ups so can't push into the loop/array.
I had the below set up, which tended to work, but within ad.inc it was getting a random ad... whereas it should be getting the total count of ads and injecting them randomly into the events until that count is reached.
$count = 1;
$total = count($events);
$random = rand(3, $total);
foreach ($events as $event) {
include('./inc/events-item.inc');
if ($count == $random) {
include("./inc/ad.inc");
$random = rand($count, $total);
}
$count++;
}
For example, if my total events count is 30, and my total ads count is 4 then I should see the four ads randomly injected into the 30 events.
Any help?
Create array of all positions for ads. If you have 30 ads - there're 30 positions, from 0 to 29:
$positions = range(0, 29);
// now get 4 random elements from this array:
$rand_positions = array_rand($positions, 4);
// though `array_rand` returns array of keys
// in this case keys are the same as values
// iterate over your events and if counter equals
// to any value in $rand_positions - show ad
$i = 0;
foreach ($events as $event) {
include('./inc/events-item.inc');
if (in_array($i, $rand_positions, true)) {
include("./inc/ad.inc");
}
$i++;
}
You need to randomly select 4 (or however many ads you have) points between 30 (or however many entries you have) other points. Here is a possible solution.
// Set up counts
$entry_count = count($events);
$ad_count = 4;
// Create an array of entry indices, and an array of ad indices
$entry_indices = range(0, $entry_count - 1);
$ad_indices = array();
// Fill the ad indices with random elements from the entry indices
for ($i = 0; $i < $ad_count; $i++) {
$entry = rand(0, count($entry_indices));
array_push($ad_indices, $entry_indices[$entry]);
array_splice($entry_indices, $entry, 1);
}
// Sort it so we only need to look at the first element
sort($ad_indices);
// Iterate through the events
$count = 0;
foreach ($events as $event) {
include('./inc/events-item.inc');
// If we have any ad indices left, see if this entry is one of them
if (count($ad_indices) > 0 && $count == $ad_indices[0]) {
include("./inc/ad.inc");
array_shift($ad_indices);
}
$count++;
}

How to generate a random number with fixed length only with 0's and 1's and a fixed amount of 1's?

I did some research, but didn't found any solution for my question.
What I want to archive:
Generate a random number out of 0's ($min) and 1's ($max), but with a fixed amount ($many) of 1's in the random number. The random number should have a length of 6 as in my while loop (while($xLoop <= 6)).
Here is my current code:
$min = 0;
$max = 1;
$many = 3;
$xLoop = 1;
while($xLoop <= 6) {
$nRand = mt_rand($min,$max);
if($nRand == 1){ //if random number comes out number 1
$many--; // Prevent number 1 more then $many...
//Do something...
}else{ //if random number comes out not number 1
//Do something and still looping until get 6 times
}
echo $nRand.' - '.$many.'</br>'; //For debugin... i want to see how many number 1 comes out.
$xLoop++;
}
It will loop 6 times, so we have a random number of the length 6, but I want a fixed amount of 1's in my random number, which is $many (here 3). And the rest filled with 0's until we reach the length 6.
How can I fix this code? Or is there a simpler way?
This should work for you:
No need for a loop. Just first fill an array with 1's $many times. Then array_merge() the array with the 0's which you fill up until $length elements.
At the end just shuffle() the array and implode() it to print it
<?php
$min = 0;
$max = 1;
$many = 3;
$length = 6;
$arr = array_fill(0, $many, $min);
$arr = array_merge($arr, array_fill($many, $length-$many, $max));
shuffle($arr);
echo implode("", $arr);
?>
possible output:
011010

How to tell if a comma delimited list of numbers obeys the natural order of numbers

I have a comma delimited list of numbers which i am converting into an array and what i want to know about the list of numbers is if the numbers listed obey a natural ordering of numbers,you know,have a difference of exactly 1 between the next and the previous.
If its true the list obeys the natural ordering,i want to pick the first number of the list and if not the list obeys not the natural order,i pick the second.
This is my code.
<?php
error_reporting(0);
/**
Analyze numbers
Condition 1
if from number to the next has a difference of 1,then pick the first number in the list
Condition 2
if from one number the next,a difference of greater than 1 was found,then pick next from first
Condition 3
if list contains only one number,pick the number
*/
$number_picked = null;
$a = '5,7,8,9,10';
$b = '2,3,4,5,6,7,8,9,10';
$c = '10';
$data = explode(',', $b);
$count = count($data);
foreach($data as $index => $number)
{
/**
If array has exactly one value
*/
if($count == 1){
echo 'number is:'.$number;
exit();
}
$previous = $data[($count+$index-1) % $count];
$current = $number;
$next = $data[($index+1) % $count];
$diff = ($next - $previous);
if($diff == 1){
$number_picked = array_values($data)[0];
echo $number_picked.'correct';
}
elseif($diff > 1){
$number_picked = array_values($data)[1];
echo $number_picked.'wrong';
}
}
?>
The problem i am having is to figure out how to test the difference for all array elements.
No loops are needed, a little bit of maths will help you here. Once you have your numbers in an array:
$a = explode(',', '5,7,8,9,10');
pass them to this function:-
function isSequential(array $sequence, $diff = 1)
{
return $sequence[count($sequence) - 1] === $sequence[0] + ($diff * (count($sequence) - 1));
}
The function will return true if the numbers in the array follow a natural sequence. You should even be able to adjust it for different spacings between numbers, eg 2, 4, 6, 8, etc using the $diff parameter, although I haven't tested that thoroughly.
See it working.
Keep in mind that this will only work if your list of numbers is ordered from smallest to largest.
Try using a function to solve this... Like so:
<?php
error_reporting(0);
/**
Analyze numbers
Condition 1
if from number to the next has a difference of 1,then pick the first number in the list
Condition 2
if from one number the next,a difference of greater than 1 was found,then pick next from first
Condition 3
if list contains only one number,pick the number
*/
$number_picked = null;
$a = '5,7,8,9,10';
$b = '2,3,4,5,6,7,8,9,10';
$c = '10';
function test($string) {
$data = explode(',', $string);
if(count($data) === 1){
return 'number is:'.$number;
}
foreach($data as $index => $number)
{
$previous = $data[($count+$index-1) % $count];
$current = $number;
$next = $data[($index+1) % $count];
$diff = ($next - $previous);
if($diff == 1){
$number_picked = array_values($data)[0];
return $number_picked.'correct';
}
elseif($diff > 1){
$number_picked = array_values($data)[1];
return $number_picked.'wrong';
}
}
}
echo test($a);
echo test($b);
echo test($c);
?>
You already know how to explode the list, so I'll skip that.
You already handle a single item, so I'll skip that as well.
What is left, is checking the rest of the array. Basically; there's two possible outcome values: either the first element or the second. So we'll save those two first:
$outcome1 = $list[0];
$outcome2 = $list[1];
Next, we'll loop over the items. We'll remember the last found item, and make sure that the difference between the new and the old is 1. If it is, we continue. If it isn't, we abort and immediately return $outcome2.
If we reach the end of the list without aborting, it's naturally ordered, so we return $outcome1.
$lastNumber = null;
foreach( $items as $number ) {
if($lastNumber === null || $number - $lastNumber == 1 ) {
// continue scanning
$lastNumber = $number;
}
else {
// not ordened
return $outcome2;
}
}
return $outcome1; // scanned everything; was ordened.
(Note: code not tested)
To avoid the headache of accessing the previous or next element, and deciding whether it still is inside the array or not, use the fact that on a natural ordering the item i and the first item have a difference of i.
Also the corner case you call condition 3 is easier to handle outside the loop than inside of it. But easier still, the way we characterize a natural ordered list holds for a 1-item list :
$natural = true;
for($i=1; $i<$count && $natural; $i++)
$natural &= ($data[$i] == $data[0] + $i)
$number = $natural ? $data[0] : $data[1];
For $count == 1 the loop is never entered and thus $natural stays true : you select the first element.

PHP Generate x amount of random odd numbers within a range

I need to generate x amount of random odd numbers, within a given range.
I know this can be achieved with simple looping, but I'm unsure which approach would be the best, and is there a better mathematical way of solving this.
EDIT: Also I cannot have the same number more than once.
Generate x integer values over half the range, and for each value double it and add 1.
ANSWERING REVISED QUESTION: 1) Generate a list of candidates in range, shuffle them, and then take the first x. Or 2) generate values as per my original recommendation, and reject and retry if the generated value is in the list of already generated values.
The first will work better if x is a substantial fraction of the range, the latter if x is small relative to the range.
ADDENDUM: Should have thought of this approach earlier, it's based on conditional probability. I don't know php (I came at this from the "random" tag), so I'll express it as pseudo-code:
generate(x, upper_limit)
loop with index i from upper_limit downto 1 by 2
p_value = x / floor((i + 1) / 2)
if rand <= p_value
include i in selected set
decrement x
return/exit if x <= 0
end if
end loop
end generate
x is the desired number of values to generate, upper_limit is the largest odd number in the range, and rand generates a uniformly distributed random number between zero and one. Basically, it steps through the candidate set of odd numbers and accepts or rejects each one based how many values you still need and how many candidates still remain.
I've tested this and it really works. It requires less intermediate storage than shuffling and fewer iterations than the original acceptance/rejection.
Generate a list of elements in the range, remove the element you want in your random series. Repeat x times.
Or you can generate an array with the odd numbers in the range, then do a shuffle
Generation is easy:
$range_array = array();
for( $i = 0; $i < $max_value; $i++){
$range_array[] .= $i*2 + 1;
}
Shuffle
shuffle( $range_array );
splice out the x first elements.
$result = array_slice( $range_array, 0, $x );
This is a complete solution.
function mt_rands($min_rand, $max_rand, $num_rand){
if(!is_integer($min_rand) or !is_integer($max_rand)){
return false;
}
if($min_rand >= $max_rand){
return false;
}
if(!is_integer($num_rand) or ($num_rand < 1)){
return false;
}
if($num_rand <= ($max_rand - $min_rand)){
return false;
}
$rands = array();
while(count($rands) < $num_rand){
$loops = 0;
do{
++$loops; // loop limiter, use it if you want to
$rand = mt_rand($min_rand, $max_rand);
}while(in_array($rand, $rands, true));
$rands[] = $rand;
}
return $rands;
}
// let's see how it went
var_export($rands = mt_rands(0, 50, 5));
Code is not tested. Just wrote it. Can be improved a bit but it's up to you.
This code generates 5 odd unique numbers in the interval [1, 20]. Change $min, $max and $n = 5 according to your needs.
<?php
function odd_filter($x)
{
if (($x % 2) == 1)
{
return true;
}
return false;
}
// seed with microseconds
function make_seed()
{
list($usec, $sec) = explode(' ', microtime());
return (float) $sec + ((float) $usec * 100000);
}
srand(make_seed());
$min = 1;
$max = 20;
//number of random numbers
$n = 5;
if (($max - $min + 1)/2 < $n)
{
print "iterval [$min, $max] is too short to generate $n odd numbers!\n";
exit(1);
}
$result = array();
for ($i = 0; $i < $n; ++$i)
{
$x = rand($min, $max);
//not exists in the hash and is odd
if(!isset($result{$x}) && odd_filter($x))
{
$result[$x] = 1;
}
else//new iteration needed
{
--$i;
}
}
$result = array_keys($result);
var_dump($result);

Awkward criteria when generating random sequence

What I need to do to generate a sequence of non-repeating integers within a given range that meets the specific criteria that I have?
Here are the criteria:
Use only the numbers between 1 and MAX (let's say 9).
Numbers cannot repeat within the sequence except:
2a. Two of the first 5 numbers from the sequence must be repeated.
2b. These two numbers must be repeated at random points within the last 5 places in the final sequence (the last 5 includes the repeats).
For example:
SET: 1,2,3,4,5,6,7,8,9
Random Sequence (with repeats):
2,4,6,9,3,1,5,2,8,7,3
r, , , ,r, , ,x, , ,x
Here I have indicated the numbers that were randomly selected to be repeated (out of the first 5 in the random sequence) with an r and the insertion points where they were randomly placed (into the last 5 of the final sequence) with an x.
Any help in figuring this out is much appreciated. Actual use will be a bit more complicated than this, but I know what I will need to do once I can get this far.
Edit
To clarify a little more, I have 1-20, and I need a 22 digit random sequence. Every number must be used, two will be used twice as discussed in my original post. I chose 10 above to simplify a little. I should be able to adapt the logic you've all given.
I assume when you say "non-repeating" you mean "distinct" (unique) as opposed to "eventually becomes periodic" (as in "the digits of pi do not repeat")
Generate n distinct integers in your range.
Pick two from the first 5. Call these a and b.
Remove the last 3 from the list.
Insert a at position 0, 1, 2, or 3 in the sublist.
Insert b at position 0, 1, 2, 3, or 4 in the sublist.
Add the sublist back to the end of the list.
Removal of the sublist is not necessary but makes it easier to conceptualize.
Not obvious what to do if n+2 is less than 10. In particular, this algorithm may crash for n < 5 and return the wrong result for n=7.
If I understand you correctly, you have 1 to N random numbers that must be used in a 10-set permutation with some specific criteria about repeats. In php, I suggest this (not counting php-internals) O(n) solution:
//Generate a full list of keys
$source = range(1, MAX);
//NOTE: if MAX < 10, you must pad the array
//Get a random group of 10 of the keys
$input = array_rand(array_flip($source), 10);
//Shuffle (can be done later as well; this is the randomization).
//array_rand() does not change order.
shuffle($input);
//Select the first of 5 that must be repeated in the last 5
$one = rand(0, 4);
$onev = $input[$one];
//Remove this array key to prevent collisions with the second of 5
$input = array_diff($input, array($onev));
//Select a random index in the last 5 to be replaced with $one
$rep = rand(5, 9);
$repv = $input[$rep];
//Remove this array key to prevent collisions with the other to-be-replaced
$input = array_diff($input, array($repv));
//Acquire the new keys list of input now that two elements have been removed
$keys = array_slice(array_keys($input), 0, 3);
//Select the second-of-5 to replace in the last 5. No worry of collision now.
$two = array_rand($keys, 1);
$two = $keys[$two];
//Select the second from the last-of-5 to be replaced by $two
//No worry of collision because the other index is removed.
$keys = array_slice(array_keys($input), 4, 8);
$rept = array_rand($keys, 1);
$rept = $keys[$rept];
//Replace one of the last-of-five with one of the first-of-five
$input[$rept] = $input[$two];
//Restore removed keys as well as perform replacement of other last-of-five
$input[$one] = $onev;
$input[$rep] = $onev;
//re-randomize based on shuffle
ksort($input);
No loops, no conditionals.
A word of warning on this solution. I wouldn't use it for a large set of numbers. If I were doing this same solution for a much larger set, I would use array_splice to drop chosen members from the array. As you get a much larger space, finding an unused number in your range becomes quite expensive, and demands a better solution than the brute force method below.
This will build half of your target set. You will call it twice, once for each half.
function build_half($min, $max, $num_elements, $arr = array() ){
while( count($arr) <= $num_elements)
{
$candidate = rand($min, $max);
if( !in_array($candidate, $arr))
{
array_push($arr, $candidate);
}
}
return $arr;
}
This will grab $this_many elements from the array.
function random_grab($arr, $this_many){ // don't try this on the subway
$nums_to_repeat = array();
// catch some edge cases...
if( $this_many > count($arr) )
{
return FALSE;
}
else if( $this_many == count($arr) )
{
return shuffle($arr);
}
while( count($nums_to_repeat) <= $this_many)
{
$rand_key = rand(0, count($arr) - 1);
if( ! in_array($arr[$rand_key], $nums_to_repeat))
{
array_push($nums_to_repeat, $arr[$rand_key]);
}
}
return $nums_to_repeat;
}
This is a fairly specialized case, but could be made more general by allowing the offset floor and ceiling to be passed in as parameters. For your problem they would be 5 and 9, so we just derive them directly.
function random_insert_2nd_half($target, $source){
$offsets_consumed = array();
$num_elements = count($target);
while( count($source) > 0 )
{
$offset = rand( ($num_elements/2), $num_elements - 1);
if( ! in_array( $offset, $offsets_consumed)
{
$arr[$offset] = array_pop($nums_to_repeat);
}
}
}
Ok so after having done all that, let's put it to work.
// Generate the first half of the array
$my_array = $repeated_nums = array();
$my_array = build_half(1, 10, 5);
// then grab the 2 random numbers from that first half.
$repeated_nums = random_grab($my_array, 2);
// So now we have our random numbers and can build the 2nd half of the array.
// we'll just repeat the call to the first function.
$my_array = build_half(1, 10, 5, $my_array);
// Then swap out two of the values in the second half.
$my_array = random_insert_2nd_half($my_array, $repeated_nums);
// at this point $my_array should match what you are looking for.
Hope this gets you on your way:
$max = 20; // max value
$repeats = 2; // numbers to be repeated
$nums = range(1, $max);
shuffle($nums);
$halfPoint = ceil($max / 2);
$firstHalf = array_slice($nums, 0, $halfPoint);
$repeaters = array_intersect_key($firstHalf, array_flip(array_rand($firstHalf, $repeats)));
$secondHalf = array_merge(array_slice($nums, $halfPoint), $repeaters);
shuffle($secondHalf);
$result = array_merge($firstHalf, $secondHalf);
var_dump(join(',', $result));
To generate distinct numbers within a range you can use something like this:
$arr_num = array();
while(count($arr_num)<=7)
{
$num = rand(1, 9);
if (!in_array($num, $arr_num))
{
$arr_num[] = $num;
}
}
$arr_num now has 8 distinct elements. Pick five elements of the array:
for ($i=0; $i<=4; $i+=1)
{
$new_arr[$i] = $arr_num[$i];
}
Now pick two numbers from $new_arr numbers:
$r1 = array_rand($new_arr);
$r2 = array_rand($new_arr);
Now you can insert these numbers into the original array at two of the last random positions. Hope it helped!
$max = 15;
$array = array(1, $max);
for($x = 1; $x <= $max; $x++)
{ $array[$x] = rand(1, $max); }
$firstDup = $array[rand(1,5)];
$secondDup = $firstDup;
do { $firstDup = $array[rand(1,5)];
} while($firstDup == $secondDup);
do { $array[rand($max-5,$max)] = $firstDup;
} while(!in_array($firstDup,array_slice($array,$max-5,5)));
do { $array[rand($max-5,$max)] = $secondDup;
} while(!in_array($secondDup,array_slice($array,$max-5,5)));

Categories