PHP find all combinations to a sum in inner array - php

I'm writing a PHP script for available rooms in a hotel. I want every combination for a group (i.e. 4 person). This is my array.
$room_array = array(
array(
"title" => "1 person room",
"room_for" => 1,
"price" => 79
),
array(
"title" => "2 person room with other",
"room_for" => 1,
"price" => 69
),
array(
"title" => "2 person room alone",
"room_for" => 1,
"price" => 89
),
array(
"title" => "2 person",
"room_for" => 2,
"price" => 69
),
array(
"title" => "3 person",
"room_for" => 3,
"price" => 69
)
);
Possible outcome:
4x 1 person room
4x 2 person room with other
3x 1 person room + 1x 2 person room with other
2x 2 person room
1x 3 person room + 1x 1 person room
etc. etc.
This calls for a recursive function. But every example I looked at doesn't work with counting in the inner array. The closest i found was this question:
Finding potential combinations of numbers for a sum (given a number set to select from)
But i didn't get de solution to work..
UPDATE:
Hi, thanks for all the answers. Really helped me in finding the best practice. In the meantime, the assignment has changed a little so I can't answer my own original question. My problem is solved. Thanks again for the help!

My answer below will get you partway there.
Resources
I borrowed some code logic from this answer.
To quote the answer (in case of future removal), please view below.
You can try
echo "<pre>";
$sum = 12 ; //SUM
$array = array(6,1,3,11,2,5,12);
$list = array();
# Extract All Unique Conbinations
extractList($array, $list);
#Filter By SUM = $sum $list =
array_filter($list,function($var) use ($sum) { return(array_sum($var) == $sum);});
#Return Output
var_dump($list);
Output
array
0 => array
1 => string '1' (length=1)
2 => string '2' (length=1)
3 => string '3' (length=1)
4 => string '6' (length=1)
1 => array
1 => string '1' (length=1)
2 => string '5' (length=1)
3 => string '6' (length=1)
2 => array
1 => string '1' (length=1)
2 => string '11' (length=2)
3 => array
1 => string '12' (length=2)
Functions Used
function extractList($array, &$list, $temp = array()) {
if(count($temp) > 0 && ! in_array($temp, $list))
$list[] = $temp;
for($i = 0; $i < sizeof($array); $i ++) {
$copy = $array;
$elem = array_splice($copy, $i, 1);
if (sizeof($copy) > 0) {
$add = array_merge($temp, array($elem[0]));
sort($add);
extractList($copy, $list, $add);
} else {
$add = array_merge($temp, array($elem[0]));
sort($add);
if (! in_array($temp, $list)) {
$list[] = $add;
}
}
}
}
My answer
The code below uses the code referenced above. I changed the return functionality of the array_filter function to map it to your needs.
The only thing left for you to do is change the function so that it can catch multiple of the same type of room. At the moment, the code below will only output 1 of each type of room (as per the code referenced above). An easy way to get around this would be to multiply the array values you send to the function by the number of guests you are searching for rooms, but up to the amount of rooms available. So: if you are looking to book for 4 guests and you have no single rooms remaining and only 1 double room, your best match result would have to be a 2 person room and a 3 person room. I've added some brief functionality to add this (it's commented out), although I have not tested it. It will likely take a while to process that as well so if you're looking for a quicker method, you're gonna have to use a better algorithm as already mentioned in previous comments/answers or solve P vs NP
The code below also gives you the option to toggle a value of $exact. This value, if set to true, will return only matches exactly equal to the number of guests, and if set to false will return all matches that equal to at least the number of guests.
<?php
class Booking {
private $minGuests = 1;
protected $guests = 1;
protected $rooms = [];
public function getRoomCombinations(bool $exact = true) {
$guests = $this->guests;
$list = [];
$rooms = $this->rooms;
/*for($i = 0; $i < $guests-1; $i++) {
$rooms = array_merge($rooms, $this->rooms);
}
asort($rooms);*/
$this->extractList($rooms, $list);
$result = array_filter($list, function($var) use ($guests, $exact) {
if($exact)
return(array_sum(array_map(function($item) { return $item['room_for'];}, $var)) == $guests);
else
return(array_sum(array_map(function($item) { return $item['room_for'];}, $var)) >= $guests && count($var) <= $guests);
});
array_multisort(array_map('count', $result), SORT_ASC, $result);
return $result;
}
private function extractList(array $array, array &$list, array $temp = []) {
if (count($temp) > 0 && !in_array($temp, $list))
$list[] = $temp;
for($i = 0; $i < sizeof($array); $i++) {
$copy = $array;
$elem = array_splice($copy, $i, 1);
if (sizeof($copy) > 0) {
$add = array_merge($temp, array($elem[0]));
sort($add);
$this->extractList($copy, $list, $add);
} else {
$add = array_merge($temp, array($elem[0]));
sort($add);
if (!in_array($temp, $list)) {
$list[] = $add;
}
}
}
}
public function setGuests(int $guests) {
$this->guests = ($guests >= $this->minGuests ? $guests : $this->minGuests);
return $this;
}
public function setHotelRooms(array $rooms) {
$this->rooms = $rooms;
return $this;
}
}
$booking = (new Booking())
->setGuests(4)
->setHotelRooms([
[
"title" => "1 person room",
"room_for" => 1,
"price" => 79
],
[
"title" => "2 person room with other",
"room_for" => 1,
"price" => 69
],
[
"title" => "2 person room alone",
"room_for" => 1,
"price" => 89
],
[
"title" => "2 person",
"room_for" => 2,
"price" => 69
],
[
"title" => "3 person",
"room_for" => 3,
"price" => 69
]
]);
echo '<pre>' . var_export($booking->getRoomCombinations(true), true) . '</pre>';
?>

If you need all the combinations then you can use an backtracking iterative algorithm (depth path).
In summary:
Type of tree: binary tree because all the levels can contain a solution when the number of persons contabilized = objetive
Binary tree
Algorithm functions
You need to increment the cont every time that a level is generated with the number of persons of the level and decrement when you change your track (exploring brothers or back)
solution: array[0..levels-1] values {0 (node not selected) ,1 (node selected)}
solution[0] = 1 -> You choose that "1 person room" belongs to the solution
solutions: list/array of objects and every object contains array of titles of rooms
function Backtracking ()
level:= 1
solution:= s_initial
end:= false
repeat
generate(level, solution)
IF solution(level, solution) then
save_solution
else if test(level, solution) then
level:= level+ 1
else
while NOT MoreBrothers(level, solution)
go_back(level, s)
until level==0
2.1. Generate: generate next node
2.2. Solution: test if it's a solution
2.3. Critery: if we must continue by this track or bound
2.4. MoreBrothers: if there are nodes without check at this level
2.5. Backtrack: all the nodes at this level were explored
2.6. Save solution: add to the solutions array your object that contains strings

$room_array = array(
array(
"title" => "1 person room",
"room_for" => 1,
"price" => 79
),
array(
"title" => "2 person room with other",
"room_for" => 1,
"price" => 69
),
array(
"title" => "2 person room alone",
"room_for" => 1,
"price" => 89
),
array(
"title" => "2 person",
"room_for" => 2,
"price" => 69
),
array(
"title" => "3 person",
"room_for" => 3,
"price" => 69
)
);
// Gets rooms based on a given number of guests
function get_possible_rooms($num_guests) {
global $room_array;
$possible_rooms = [];
foreach ($room_array as $room) {
if ($num_guests <= $room['room_for']) {
$possible_rooms[] = $room['title'];
}
}
return $possible_rooms;
}
// Gets the available room capacities
function get_room_capacities() {
global $room_array;
$capacities = [];
foreach ($room_array as $room) {
$capacities[] = $room['room_for'];
}
return array_unique($capacities);
}
// Gets the different combinations of groups of guests based on the room capacities
function get_guest_assignments($remaining_guests, $parent_id = '', $num_guests, &$result) {
$room_capacities = get_room_capacities();
for ($i = 1; $i <= $remaining_guests; ++$i) {
if (in_array($i, $room_capacities)) {
$parent_guests = (isset($result[$parent_id])) ? $result[$parent_id] : 0;
$result[$parent_id . $i] = $parent_guests + $i;
for ($j = 1; $j <= $remaining_guests - $i; ++$j) {
// Recursively get the results for children
get_guest_assignments($j, $parent_id . $i, $num_guests, $result);
}
}
}
if ($remaining_guests === 1 && $parent_id !== '') {
// If it reaches the end and it does not fulfill the required number of guests,
// mark it for removal later
if ($result[$parent_id] < $num_guests) {
$result[$parent_id] = null;
}
}
// This is the last recursion
if ($result[$parent_id . '1'] === $num_guests) {
// Remove duplicates.
// To do this, we need to re-sort the keys (e.g. 21 becomes 12) and call array_unique()
// I admit this is a bit sloppy implementation.
$combinations = [];
foreach ($result as $key => $value) {
if ($value !== null) {
$nums = str_split($key);
sort($nums);
$combinations[] = implode('', $nums);
}
}
$result = array_unique($combinations);
}
}
// Gets the rooms for each group of guest
function get_room_assignments($guest_str) {
$rooms = [];
for ($i = 0; $i < strlen($guest_str); ++$i) {
$num_guests = intval(substr($guest_str, $i, 1));
$rooms[] = get_possible_rooms($num_guests);
}
return $rooms;
}
//----------
// RUN
//----------
$guests = 4;
$result = [];
get_guest_assignments($guests, null, $guests, $result);
foreach ($result as $guest_combi) {
$assignments = get_room_assignments($guest_combi);
// Printing output
echo 'Guest Combination ' . $guest_combi . "\n";
echo json_encode($assignments, JSON_PRETTY_PRINT);
echo "\n\n";
}
The output will look something like this:
...
Guest Combination 13
[
[
"1 person room",
"2 person room with other",
"2 person room alone",
"2 person",
"3 person"
],
[
"3 person"
]
]
...
"Guest combination 13" means the 4 guests will be split into groups of 1 and 3 persons.
Output is an array of possible rooms for each group. So in the example, the group of 1 can book 1 person room, 2 person room with other, ... 3 person room. And the group of 3 can book 3 person room.
—
Other notes:
I know we hate global but doing this just for brevity. Feel free to modify.
There's a shorter way to code this, but this implementation makes it easier to debug since guest combinations are used as keys.

Related

Get data from multiple array in Controller

I have country list in array with multiple array, like:
public static function listCountries()
{
$this->country = array(
array(1, 'SAD', 'sad.png'),
array(2, 'Argentina', 'argentina.png'),
array(3, 'Australija', 'australija.png'),
array(4, 'Novi Zenland', 'noviz.png'),
array(5, 'Belgija', 'belg.png'),
array(6, 'Nizozemska', 'nizozemska.png')
);
}
But when i do foreach for array, i'm getting this:
//From DB
$item->country = "1,4";
$item->country = explode(",", $item->country);
for($i=0; $i < count($item->country); $i++) {
$index = $item->country[$i];
if( !empty($this->country[$index]) ) {
$item->country[$i] = $this->country[$index];
}
}
$item->country = implode(",", $item->country);
echo $item->country;
But i'm getting something like this:
array:2 [▼
0 => array:3 [▼
0 => 5
1 => "Belgija"
2 => "belg.png"
]
1 => array:3 [▼
0 => 2
1 => "Argentina"
2 => "argentina.png"
]
]
1 = SAD, 4 = Novi Zenland, not Belgija and Argentina
There is no good country, also no data what i want. How to fix this?
You can use this foreach loop to go through the other array and swap the string if the number matches:
$item->country = "1,4";
$item->country = explode(",", $item->country);
for($i=0; $i < count($item->country); $i++) {
$index = $item->country[$i];
foreach($this->country as $c) {
if($c[0] == $index) {
$item->country[$i] = $c[1]; // or $item->country[$i] = $c; if you want all three items
break;
}
}
}
$item->country = implode(",", $item->country);
echo $item->country;
// Should output: SAD,Novi Zenland
The indexes in arrays are 0-based, which means that:
$index = $item->country[$i];
Has to become
$index = $item->country[$i - 1];
To correlate with the country ids. Otherwise, it is always one off. This is assuming that the ids are always ordered from least to greatest, and all ids are a continuous range.

Average columns in PHP

I have a series of columns (Jan, Feb, Mar, etc) and I want to average the values of each column for each row however many there may be.
I have:
protected function generateAverage($staff, $type)
{
$months = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
$staff = array_map(function ($row) use ($type) {
if ($row['row_type'] == $type) {
return $row;
}
}, $staff);
foreach ($staff as $key => $employee) {
$months[0] += $employee['amounts'][0];
$months[1] += $employee['amounts'][1];
$months[2] += $employee['amounts'][2];
$months[3] += $employee['amounts'][3];
$months[4] += $employee['amounts'][4];
$months[5] += $employee['amounts'][5];
$months[6] += $employee['amounts'][6];
$months[7] += $employee['amounts'][7];
$months[8] += $employee['amounts'][8];
$months[9] += $employee['amounts'][9];
$months[10] += $employee['amounts'][10];
$months[11] += $employee['amounts'][11];
}
$months = array_map(function ($value) use ($staff) {
return $value / (count($staff) / 2);
}, $months);
$months[] = array_sum($months);
return $months;
}
Here is a sample of the data that goes into the above function:
array:6 [
0 => array:4 [
"amounts" => array:13 [
0 => "30000.00"
1 => "30000.00"
2 => "30000.00"
3 => "30000.00"
4 => "30000.00"
5 => "30000.00"
6 => "30000.00"
7 => "30000.00"
8 => "30000.00"
9 => "30000.00"
10 => "30000.00"
11 => "30000.00"
12 => 360000.0
]
"image" => "test.jpg"
"row_name" => "Target"
"row_type" => "target"
]
...
Usage:
$data['aggregates']['Target average'] = $this->generateAverage(array_values($data['staff']), 'target');
I feel the way the average is calculated is messy, is there a better way to do this?
A couple of small footprint reductions
protected function generateAverage($staff, $type)
{
// create 12 months with 0 value
$months = array_fill(0, 12, 0);
// use array_filter instead of map
$staff = array_filter(function ($row) use ($type) {
return $row['row_type'] === $type;
}, $staff);
// do count outside loop
$staffCount = count($staff);
// loop employees and add up each month, dividing early
foreach ($staff as $employee) {
for ($i = 0; $i < 12; $i++) {
$months[$i] += $employee['amounts'][$i] / $staffCount;
}
}
return $months;
}
I dont know why you are dividing the staff count by 2 or why you are summing in the end, my function just gives an average per month.
Since you're using Laravel, most of the data you work with is collections. So, before converting a collection into an array, you could use avg() helper:
$collection->avg($key);
I think you can consider using array_colum,
1) array_column makes an array of all the values in a particular index position in a row
$column1 = array_column($employee, 0 );
$column2 = array_column($employee, 1 );
$column3 = array_column($employee, 2 );
.
.
.
2)Get the column count by count($columnX), where X is the index of the column
3) Using (1) and (2) calculate the average as needed

PHP algorithm to find between values in varying distance

I'm trying to create an algorithm that returns a price depending on number of hours. But the distance between the number of hours are varying. For example I have an array:
$set = [
1 => 0.5,
2 => 1,
3 => 1.5,
4 => 2,
5 => 2.5,
12 => 4
];
$value = 3;
end($set);
$limit = (int)key($set);
foreach($set as $v => $k) {
// WRONG, doesn't account for varying distance
if($value >= $v && $value <= $v) {
if($value <= $limit) {
return $k;
} else {
return false;
}
}
}
The trouble is, the distance between 5 and 12 register as null. I might as well use $value == $v instead as the line I've marked as incorrect does anyway.
So I was wondering if there was a better way to round up to the next index in that array and return the value for it?
Cheers in advance!
Try this:
$set = array(1 => 0.5, 2 => 1, 3 => 1.5, 4 => 2, 5 => 2.5, 12 => 4);
function whatever(idx, ary){
if(in_array(idx, array_keys(ary))){
return ary[idx];
}
else{
foreach(ary as $i => $v){
if($i > idx){
return $v;
}
}
}
return false;
}
echo whatever(7, $set);
The problem is that $v is a single value, so $value >= $v && $value <= $v is equivalent to $value == $v.
Instead, consider that if the loop hasn't ended, then the cutoff hasn't been reached yet - and a current "best price" is recorded. This requires that the keys are iterated in a well-ordered manner that can be stepped, but the logic can be updated for a descending order as well.
$price_chart = [
1 => 0.5,
2 => 1,
3 => 1.5,
4 => 2,
5 => 2.5,
12 => 4
];
function get_price ($hours) {
global $price_chart;
$best_price = 0;
foreach($price_chart as $min_hours => $price) {
if($hours >= $min_hours) {
// continue to next higher bracket, but remember the best price
// which is issued for this time bracket
$best_price = $price;
continue;
} else {
// "before" the next time cut-off, $hours < $min_hours
return $best_price;
}
}
// $hours > all $min_hours
return $best_price;
}
See the ideone demo. This code could also be updated to "fill in" the $price_chart, such that a price could be found simply by $price_chart[$hours] - but such is left as an exercise.

Count specific values in multidimensional array

I'm trying to count the number of times a certain value appears in my multidimensional array based on a condition. Here's an example array;
$fruit = array (
"oranges" => array(
"name" => "Orange",
"color" => "orange",
"taste" => "sweet",
"healthy" => "yes"
),
"apples" => array(
"name" => "Apple",
"color" => "green",
"taste" => "sweet",
"healthy" => "yes"
),
"bananas" => array(
"name" => "Banana",
"color" => "yellow",
"taste" => "sweet",
"healthy" => "yes"
),
"grapes" => array(
"name" => "Grape",
"color" => "green",
"taste" => "sweet",
"healthy" => "yes"
)
);
If I want to DISPLAY all green coloured fruit, I can do the following (let me know if this is the best way of doing it);
for ($row = 0; $row < 3; $row++) {
if($fruit[$row]["color"]=="green") {
echo $fruit[$row]["name"] . '<br />';
}
}
This will output;
Apple
Grape
That's great and I can see their are 2 values there, but how can I actually get PHP to count the number of fruit where the colour is green and put it in a variable for me to use further down the script to work stuff out? E.g. I want to do something like;
if($number_of_green_fruit > 1) { echo "You have more than 1 piece of green fruit"; }
I've taken a look at count(); but I don't see any way to add a 'WHERE/conditional' clause (a la SQL).
Any help would be really appreciated.
PHP has no support for a SQL where sort of thing, especially not with an array of arrays. But you can do the counting your own while you iterate over the data:
$count = array();
foreach($fruit as $one)
{
#$count[$one['color']]++;
}
printf("You have %d green fruit(s).\n", $count['green']);
The alternative is to write yourself some little helper function:
/**
* array_column
*
* #param array $array rows - multidimensional
* #param int|string $key column
* #return array;
*/
function array_column($array, $key) {
$column = array();
foreach($array as $origKey => $value) {
if (isset($value[$key])) {
$column[$origKey] = $value[$key];
}
}
return $column;
}
You then can get all colors:
$colors = array_column($fruit, 'color');
And then count values:
$count = array_count_values($colors);
printf("You have %d green fruit(s).\n", $count['green']);
That kind of helper function often is useful for multidimensional arrays. It is also suggested as a new PHP function for PHP 5.5.
$number_of_green_fruit = 0;
for ($row = 0; $row < 3; $row++) {
if($fruit[$row]["color"]=="green") {
$number_of_green_fruit++;
echo $fruit[$row]["name"] . '<br />';
}
}
All you need is an extra counter:
for ($row = $number_of_green_fruit = 0; $row < 3; $row++) {
if($fruit[$row]["color"]=="green") {
echo $fruit[$row]["name"] . '<br />';
$number_of_green_fruit++;
}
}
if($number_of_green_fruit > 1) {
echo "You have more than 1 piece of green fruit";
}
With PHP 5.4+ you can have this short snippet to count specific values (don't even need to declare the $count variable previously)
array_walk_recursive($fruit, function ($value) use (&$count) {
$count += (int) ($value === 'green');
});
var_dump($count); // Outputs: int(2)

Split Array by Value

I'm working on a leader board that pulls the top scorers into first, second, and third place based on points. Right now I'm working with a sorted array that looks like this (but could be of infinite length with infinite point values):
$scores = Array
(
["bob"] => 20
["Jane"] => 20
["Jill"] => 15
["John"] => 10
["Jacob"] => 5
)
I imagine I could use a simple slice or chunk, but I'd like to allow for ties, and ignore any points that don't fit into the top three places, like so:
$first = Array
(
["bob"] => 20
["Jane"] => 20
)
$second = Array
(
["Jill"] => 15
)
$third = Array
(
["John"] => 10
)
Any ideas?
$arr = array(
"Jacob" => 5,
"bob" => 20,
"Jane" => 20,
"Jill" => 15,
"John" => 10,
);
arsort($arr);
$output = array();
foreach($arr as $name=>$score)
{
$output[$score][$name] = $score;
if (count($output)>3)
{
array_pop($output);
break;
}
}
$output = array_values($output);
var_dump($output);
$first will be in $output[0], $second in $output[1] and so on.. Code is limited to 3 first places.
ps: updated to deal with tie on the third place
I would do something like:
function chunk_top_n($scores, $limit)
{
arsort($scores);
$current_score = null;
$rank = array();
$n = 0;
foreach ($scores as $person => $score)
{
if ($current_score != $score)
{
if ($n++ == $limit) break;
$current_score = $score;
$rank[] = array();
$p = &$rank[$n - 1];
}
$p[$person] = $score;
}
return $rank;
}
It sorts the array, then creates numbered groups. It breaks as soon as the limit has been reached.
You can do it with less code if you use the score as the key of the array, but the benefit of the above approach is it creates the array exactly how you want it the first time through.
You could also pass $scores by reference if you don't mind the original getting sorted.
Here's my go at it:
<?php
function array_split_value($array)
{
$result = array();
$indexes = array();
foreach ($array as $key => $value)
{
if (!in_array($value, $indexes))
{
$indexes[] = $value;
$result[] = array($key => $value);
}
else
{
$index_search = array_search($value, $indexes);
$result[$index_search] = array_merge($result[$index_search], array($key => $value));
}
}
return $result;
}
$scores = Array(
'bob' => 20,
'Jane' => 20,
'Jill' => 15,
'John' => 10,
'Jacob' => 5
);
echo '<pre>';
print_r(array_split_value($scores));
echo '</pre>';
?>

Categories