I've encountered a problem similar to the old two sum problem but instead of solving for a value, it needs to be within a range, and I'm not sure how to efficiently approach this. Here is a simplified version of my problem:
Given an array of integers in order of preference, find the first two integers whose sum lies between the range of X and Y s.t. X <= sum <= Y (where X < Y and are known, i.e. arbitrarily X=20 and Y=40).
I've done a brute force approach using a for loop, but I'm unsure if this is the most performant solution. I've considered using a hash table, but I don't know how to apply it.
note: by order of preference I mean, the return the first two integers that fulfill this criteria
You could use the binary search method of resolving the 2 sum problem, and tweak your binary search function to search within a range. Something like this:
$arr = [1,2,4,6,8,14,15,17];
print_r(first_sum_in_range($arr, 25, 40));
function first_sum_in_range($array, $min, $max){
foreach ($array as $k=>$a) {
$b = binary_search_range($array, $a, $min, $max);
if ($b !== false) {
return [$a,$b];
}
}
}
function binary_search_range($array, $a, $min, $max) {
$top = sizeof($array) -1;
$bot = 0;
while($top >= $bot)
{
$p = floor(($top + $bot) / 2);
if ($a+$array[$p] < $min) $bot = $p + 1;
elseif ($a+$array[$p] > $max) $top = $p - 1;
else return $array[$p];
}
return false;
}
OUTPUT:
Array
(
[0] => 8
[1] => 17
)
Add each element to a tree map with key as element and value as list of indices where that element occurs.
While adding element to tree map check if there is a submap whose keys range from X - current_element to Y - current_element both inclusive. If you have a submap your answer is [curr_element, A[first_index_of_submap's value_of_first_key] ]
Maybe this is the brute force method you've already tried, but I think this is the simplest way.
Starting with a subset of the first two elements, iterate subsets of increasing size comparing the sum of the value of each element in the subset and the value of the last element. When you find a sum within the range, you're done.
This will find the first pair of numbers within the range based on the definition of "first" being "the pair with the lowest maximum index".
function findFirstSumInRange(int $min, int $max, array $values = []): array
{
for ($b = 1, $n = count($values); $b < $n; $b++) {
for ($a = 0; $a < $b; $a++) {
if ($min <= ($sum = $values[$a] + $values[$b]) && $sum <= $max) {
return [$values[$a], $values[$b]];
// or return [$a => $values[$a], $b => $values[$b]]; if you need the keys as well
}
}
}
return [];
}
You can make it faster by skipping any values that are already greater than the upper limit of the range.
function findFirstSumInRangeB(int $min, int $max, array $values = []): array
{
for ($b = 1, $n = count($values); $b < $n; $b++) {
if ($values[$b] < $max) { // else these sums will all be > the range because one addend is
for ($a = 0; $a < $b; $a++) {
if ($values[$a] < $max && $min <= ($sum = $values[$a] + $values[$b]) && $sum <= $max) {
return [$a => $values[$a], $b => $values[$b]];
}
}
}
}
return [];
}
Regarding "the most performant solution", I'd prefer to go for simplicity rather than optimizing for performance unless the performance is causing problems. Just my opinion.
Related
Say I have an array [10000,5000,1000,1000] and I would like to find the closest sum of numbers to a given number. Sorry for the bad explanation but here's an example:
Say I have an array [10000,5000,1000,1000] I want to find the closest numbers to, say 6000.
Then the method should return 5000 and 1000
another example : we want the closest to 14000 , so then he should return 10000 and 5000
I've tried with code below, here is working one but if the $desiredSum and $numbers array is big. it's running so slow until php execution timeout
$numbers = array(
10000,5000,1000,1000
);
$desiredSum = 6000;
$minDist = null;
$minDist_I = null;
// Iterate on every possible combination
$maxI = pow(2,sizeof($numbers));
for($i=0;$i<$maxI;$i++) {
if(!(($i+1) % 1000)) echo ".";
// Figure out which numbers to select in this
$sum = 0;
for($j=0;$j<sizeof($numbers);$j++) {
if($i & (1 << $j)) {
$sum += $numbers[$j];
}
}
$diff = abs($sum - $desiredSum);
if($minDist_I === null || $diff < $minDist) {
$minDist_I = $i;
$minDist = $diff;
}
if($diff == 0) break;
}
$chosen = array();
for($j=0;$j<sizeof($numbers);$j++) {
if($minDist_I & (1 << $j)) $chosen[] = $numbers[$j];
}
echo "\nThese numbers sum to " . array_sum($chosen) . " (closest to $desiredSum): ";
echo implode(", ", $chosen);
echo "\n";
Anyone can help me out ?
<?php
function coinChange($numbers,$desiredSum){
sort($numbers);
$set = [];
$set[0] = [];
for($i = $numbers[0];$i <= $desiredSum;++$i){
foreach($numbers as $index => $current_number){
if($i >= $current_number && isset($set[$i - $current_number])){
if(isset($set[$i - $current_number][$index])) continue;
$set[$i] = $set[$i - $current_number];
$set[$i][$index] = true;
break;
}
}
}
if(count($set) === 0){
return [0,[]];
}
if(isset($set[$desiredSum])){
return [
$desiredSum,
formatResult($numbers,array_keys($set[$desiredSum]))
];
}else{
$keys = array_keys($set);
$nearestSum = end($keys);
$sum = 0;
$rev_numbers = array_reverse($numbers);
$result = [];
foreach($rev_numbers as $number){
$sum += $number;
$result[] = $number;
if($sum > $nearestSum && abs($nearestSum - $desiredSum) > abs($sum - $desiredSum)){
$nearestSum = $sum;
break;
}else if($sum > $nearestSum && abs($nearestSum - $desiredSum) < abs($sum - $desiredSum)){
$result = formatResult($numbers,array_keys($set[$nearestSum]));
break;
}
}
return [
$nearestSum,
$result
];
}
}
function formatResult($numbers,$keys){
$result = [];
foreach($keys as $key) $result[] = $numbers[$key];
return $result;
}
print_r(coinChange([10000,5000,1000,1000],14000));
print_r(coinChange([10000,5000,1000,1000],13000));
print_r(coinChange([100000,100000,100000,100000,100000,100000,50000,50000,50000,50000,10000,10000,500,500,500,1000,1000],250000));
print_r(coinChange([100000,100000,100000,100000,100000,100000,50000,50000,50000,50000,10000,10000,500,500,500,1000,1000],179999));
Demo: https://3v4l.org/hBGeW
Algorithm:
This is similar to coin change problem.
We first sort the numbers.
Now, we iterate from minimum number in the array to the desired sum.
Inside it, we iterate through all elements in the array.
Now, we can make $i(which is a sum) only if we have made sum $i - $current_number. If we have the previous one, then we add $current_number to our collection for sum $i.
Two Scenarios:
If we can make the exact sum, then we return the result as is.
If we can't, then are 2 possibilities:
We would already have nearest sum possible in our $set which would be the last entry. We keep them in a variable.
Now, the nearest sum could also be higher than the desired sum. So, we get the larger sum and check if it's nearer than nearest smallest sum and then compare both and return the result.
Result format:
Let's take the below sample output:
Array
(
[0] => 15000
[1] => Array
(
[0] => 10000
[1] => 5000
)
)
It simply means that the first index is the nearest sum possible and array at 2nd index is all elements it took from $numbers to make that sum.
I have three integers: A, B, C
I want to print all integers from 1 to range which are divisible by A or B but not by C.
My code
for($n=0; $n < $range; $n++){
if(($n < $a && $n < $b) || ($n % $c == 0)){
return [];
}
if(($n % $a == 0 || $n % $b == 0) && ($n % $c > 0)){
$outputArr[] = $n;
}
}
Is there any more efficient way to do this?
You can speed this up but it is more complicated, especially if you must print these out in order. Here is a method that doesn't print them out in order.
First write a greatest common divisor (gcd) function in PHP, and then write a least common multiple (lcm) function that uses the gcd function. Compute m = lcm(a, b). Iterate over multiples of a and print them out if they are not divisible by c. Next, iterate over multiples of b and print them out if they are not divisible by m or c.
Other optimizations along these lines are possible. For example, you can precompute the multiples of a or b that are not multiples of m and store them in an array. This works if m is not too large, division is more expensive than array access in PHP, and range is significantly larger than m.
PHP version 7 or higher is so fast when only integer operations are used that micro-optimizations are no longer needed.
$res = [];
$a = 9;
$b = 13;
$c = 26;
$range = 10000;
for($n=$a; $n <= $range; $n += $a){
if($n%$c != 0) $res[] = $n;
}
for($n=$b; $n <= $range; $n += $b){
if($n%$c != 0) $res[] = $n;
}
$res = array_unique($res);
sort($res);
This example takes about 1 millisecond to calculate the 1411 values on my 8-year-old system. This time for the presentation of the result is several times greater.
I would use range() and array_filter().
$range = 20;
$A = 2;
$B = 3;
$C = 9;
$nums = array_filter(range(1, $range), function ($x) use ($A, $B, $C) {
return (!($x % $A) || !($x % $B)) && $x % $C;
});
var_dump($nums);
Here is a more optimized solution, that also works efficient when a and b are large. You can simply run through the multiples of a and b:
for($na=$a, $nb=$b; $na <= $range || $nb <= $range; ){
if ($na <= $nb) {
if ($na % $c != 0)
$outputArr[] = $na;
if ($na == $nb)
$nb += $b;
$na += $a;
} else {
if ($nb % $c != 0)
$outputArr[] = $nb;
$nb += $b;
}
}
Each output number is only generated once, and already in the desired order.
If you are afraid the modulo test is slow, you could also have a next multiple of c running along, but that looks like too much overhead.
I've searched through a number of similar questions, but unfortunately I haven't been able to find an answer to this problem. I hope someone can point me in the right direction.
I need to come up with a PHP function which will produce a random number within a set range and mean. The range, in my case, will always be 1 to 100. The mean could be anything within the range.
For example...
r = f(x)
where...
r = the resulting random number
x = the mean
...running this function in a loop should produce random values where the average of the resulting values should be very close to x. (The more times we loop the closer we get to x)
Running the function in a loop, assuming x = 10, should produce a curve similar to this:
+
+ +
+ +
+ +
+ +
Where the curve starts at 1, peeks at 10, and ends at 100.
Unfortunately, I'm not well versed in statistics. Perhaps someone can help me word this problem correctly to find a solution?
interesting question. I'll sum it up:
We need a funcion f(x)
f returns an integer
if we run f a million times the average of the integer is x(or very close at least)
I am sure there are several approaches, but this uses the binomial distribution: http://en.wikipedia.org/wiki/Binomial_distribution
Here is the code:
function f($x){
$min = 0;
$max = 100;
$curve = 1.1;
$mean = $x;
$precision = 5; //higher is more precise but slower
$dist = array();
$lastval = $precision;
$belowsize = $mean-$min;
$abovesize = $max-$mean;
$belowfactor = pow(pow($curve,50),1/$belowsize);
$left = 0;
for($i = $min; $i< $mean; $i++){
$dist[$i] = round($lastval*$belowfactor);
$lastval = $lastval*$belowfactor;
$left += $dist[$i];
}
$dist[$mean] = round($lastval*$belowfactor);
$abovefactor = pow($left,1/$abovesize);
for($i = $mean+1; $i <= $max; $i++){
$dist[$i] = round($left-$left/$abovefactor);
$left = $left/$abovefactor;
}
$map = array();
foreach ($dist as $int => $quantity) {
for ($x = 0; $x < $quantity; $x++) {
$map[] = $int;
}
}
shuffle($map);
return current($map);
}
You can test it out like this(worked for me):
$results = array();
for($i = 0;$i<100;$i++){
$results[] = f(20);
}
$average = array_sum($results) / count($results);
echo $average;
It gives a distribution curve that looks like this:
I'm not sure if I got what you mean, even if I didn't this is still a pretty neat snippet:
<?php
function array_avg($array) { // Returns the average (mean) of the numbers in an array
return array_sum($array)/count($array);
}
function randomFromMean($x, $min = 1, $max = 100, $leniency = 3) {
/*
$x The number that you want to get close to
$min The minimum number in the range
$max Self-explanatory
$leniency How far off of $x can the result be
*/
$res = [mt_rand($min,$max)];
while (true) {
$res_avg = array_avg($res);
if ($res_avg >= ($x - $leniency) && $res_avg <= ($x + $leniency)) {
return $res;
break;
}
else if ($res_avg > $x && $res_avg < $max) {
array_push($res,mt_rand($min, $x));
}
else if ($res_avg > $min && $res_avg < $x) {
array_push($res, mt_rand($x,$max));
}
}
}
$res = randomFromMean(22); // This function returns an array of random numbers that have a mean close to the first param.
?>
If you then var_dump($res), You get something like this:
array (size=4)
0 => int 18
1 => int 54
2 => int 22
3 => int 4
EDIT: Using a low value for $leniency (like 1 or 2) will result in huge arrays, since testing, I recommend a leniency of around 3.
I want to generate in PHP an array of random numbers, but each number should not be the same as any of the X (for example 2 ) numbers bofore it and not even close to any of them by a define range (for example 5).
So for example:
I need numbers between 1 and 100
i've set my "range" to 5
the first two generated number are 20 and 50.
the third number will be a random number between 1 and 100, excluding all the numbers between 15 and 25, and between 45 and 55.
I can't figure out a function to achieve it. Ideally I want to call something like this:
getRandomNumbers( $min, $max, $previous, $range);
where $previous is the number of previous elements to take in consideration when generating the next one and $range is the "proximity" to those number where I don't want the next number to be.
I hope I explained in a decent way my request. :) Please, add a comment if you have any question about it.
I just came up with this:
function getRandomNumbers($min, $max, $previous, $range) {
static $generated = array();
$chunk = array_slice($generated, -$previous);
// Added this infinite loop check to save you some headache.
if (((($max - $min + 1) / (($range * 2) + 1)) + 1) <= $previous) {
die("Values set have the potential of running into an infinite loop. Min: $min, Max: $max, Previous: $previous, Range: $range");
}
while(true) {
$number = rand($min, $max);
$found = true;
foreach ($chunk as $value) {
if (in_array($number, range($value-$range, $value+$range))) {
$found = false;
}
}
if ($found) {
$generated[] = $number;
return $number;
}
}
}
Test it using this:
for ($i = 1; $i < 25; $i++) {
echo getRandomNumbers(1, 100, 5, 5) . "<br />";
}
PHPFiddle Link: http://phpfiddle.org/main/code/51ke-4qzs
Edit: Added a check to prevent a possible infinite loop. For example: if you set the following values:
$min = 1;
$max = 100;
$previous = 5;
$range = 12;
echo getRandomNumbers($min, $max, $previous, $range);
Then let's say, in a really unfortunate situation it would generate 13, 38, 63 and 88. So the 5th number cannot be anything between 1 and 25, 26 and 50, 51 and 75, 76 and 100. So it would result in an infinite loop. I've updated the PHPFiddle link as well.
getRandomNumbers( $previous, $range ) {
//I'm assuming that previous will be an array of your previous X that you don't want to be close to
$num = getRandomNumber() //However you are doing this now
foreach( $previous as $key => $value ) {
if ( ( $value - $range ) > $num && ( $value + $range ) < $num ) {
return getRandomNumbers($previous, $range);
}
}
//You need to also replace a value in previous
return num;
}
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);