I need to design an algorithm that displays ads on a website based on imprints. i record each imprint in DB. Cannot make use of mysql rand() limit 1 in my query as it will be uncontrolled query. i have to make sure that if an ad is selected its not only just an ad with lowest imprints but there is some randomness to it. Also New Ads get a biased selection some way.
In order to achieve this i have managed to write a function but somewhere I am not satisfied with it. Please go through it and let me know if it could done any better.
function getRandomAd($result){
while($row = mysql_fetch_assoc($result)){
$rows[] = $row;
}
$filteredRow = array();
foreach($rows as $row){
if($row['imprints'] == 0 ){
$row['piority'] = 10;
$filteredRow[] = $row;
}
else if($row['imprints'] >= 0 and $row['imprints'] <= 5){
$row['piority'] = 8;
$filteredRow[] = $row;
}
else if($row['imprints'] >= 5 and $row['imprints'] <= 15){
$row['piority'] = 6;
$filteredRow[] = $row;
}
else if($row['imprints'] >= 15 and $row['imprints'] <= 30){
$row['piority'] = 4;
$filteredRow[] = $row;
}
else if($row['imprints'] >= 30 and $row['imprints'] <= 60) {
$row['piority'] = 2;
$filteredRow[] = $row;
}
else{
$row['piority'] = 0;
$filteredRow[] = $row;
}
foreach($filteredRow as $row){
if($row['piority'] == 10 or $row['piority'] == 8) $high[] = $row;
if($row['piority'] == 6 or $row['piority'] == 4) $medium[] = $row;
if($row['piority'] == 2 or $row['piority'] == 0) $low[] = $row;
}
$countHigh = count($high);
$countMed = count($medium);
$countLow = count($low);
if($countHigh > $countLow and $countHigh > $countMed)$rowReturned = $high;
if($countMed > $countLow and $countMed > $countHigh) $rowReturned = $medium;
if($countLow > $countMed and $countLow > $countHigh) $rowReturned = $low;
}
return $rowReturned[array_rand($rowReturned,1)];
}
Returning all the rows and using PHP to sort and determine which row to spit out is terribly inefficient. This would be better handled on the database end than in PHP.
Also, your current set of if statements overlap with the >= operators.
Finally, I know you didn't do this but just a general note: you should almost never use ORDER BY RAND() with MySQL because the performance is terrible. This is because when you use a function return value to order results MySQL can't use an index for the order operation.
Here's a better solution ...
Generate a weighted random number in PHP that we can use as a condition for our query
Write a query that will return only one result based on the weighted random number
So, lets generate a random integer weighted towards the lower end of the imprint spectrum:
$max = 60;
$random = floor($max - mt_rand(mt_rand(1, $max), $max));
And write a query that uses the weighted random number to pick a single result from the table:
$query = '
SELECT id, advert_txt
FROM table
WHERE imprints >= '.$random.'
ORDER BY imprints ASC
LIMIT 1
';
Note that you would normally want to escape a PHP variable in a query like this (or preferably use prepared statements with PDO), but since we know the variable is an integer in this case it's okay.
This solution will appropriately weight returned results based on their respective imprint counts while maintaining MySQL's ability to use indexes to select the record, which is of paramount importance if you have a large number of rows in the table.
UPDATE
I would add that you could tweak the random number generation to your own needs. With this particular solution you'll never get back a row with >= 60 imprints. The solution remains sound though, because it's trivial to tweak how you determine the condition for the query. For example, maybe you would first run a query to get the MAX(imprints) from the database and use that value for your $max when determining the random number.
And second, just to demonstrate the behavior of the random number generation specified above, if you run it 10,000 times using a $max = 10 you'll get results similar to this:
0: 2966
1: 1965
2: 1420
3: 1101
4: 820
5: 621
6: 457
7: 333
8: 210
9: 107
UPDATE 2
In response to your comment for further clarification ...
In my original update I'm trying to say that if you just hard-code in the max value as 60, the original code will never return a row where imprints >= 60. This means that if you just use that code on it's own, it will work fine for a while but eventually (assuming you increment the imprints column each time you display an advertisement) all your records will have been imprinted 60+ times and you won't get any results back.
So to avoid such an issue the entire process would be something like this:
Query the table once to get the highest imprints column value to use for your maximum random value range like so: SELECT MAX(imprints) AS max FROM my_table;
Use the max value you retrieved in step one in the $max variable when generating your random weighted number.
Update the table by incrementing the imprints column up for the row you retrieved in step 2.
function getRandomAd($result) {
$imprint_weights = array(
array(
'min' => 0,
'max' => 0,
'weight' => 10
),
array(
'min' => 1,
'max' => 5,
'weight' => 8
),
array(
'min' => 6,
'max' => 15,
'weight' => 6
),
array(
'min' => 16,
'max' => 30,
'weight' => 4
),
array(
'min' => 31,
'max' => 60,
'weight' => 2
)
);
$weighted = array();
foreach ($result as $row) {
$imprints = $row['imprints'];
$imprint_weight = 1;
foreach ($imprint_weights as $match) {
if ($imprints >= $match['min'] && $imprints <= $match['max']) {
$imprint_weight = $match['weight'];
}
}
$weighted = array_merge($weighted, array_fill(0, $imprint_weight, $row));
}
return $weighted[array_rand($weighted)];
}
$data = array();
for ($x = 1; $x <= 100; $x++) {
$data[] = array(
'id' => $x,
'imprints' => rand(0, 65)
);
}
print_r(getRandomAd($data));
This provides an alternative way of doing what you want. The benifit here being that you can more easily change how an amount of imprints affect the weight of an ad, and also easily add more parameters (like age) to influence the selection of an ad.
Related
i am working on an algorithm for sorting teams based on highest number of score. Teams are to be generated from a list of players. The conditions for creating a team is
It should have 6 players.
The collective salary for 6 players must be less than or equal to 50K.
Teams are to be generated based on highest collective projection.
What i did to get this result is generate all possibilities of team then run checks on them to exclude those teams that have more than 50K salary and then sort the remainder based on projection. But generating all the possibilities takes a lot of time and sometimes it consume all the memory. For a list of 160 players it takes around 90 seconds. Here is the code
$base_array = array();
$query1 = mysqli_query($conn, "SELECT * FROM temp_players ORDER BY projection DESC");
while($row1 = mysqli_fetch_array($query1))
{
$player = array();
$mma_id = $row1['mma_player_id'];
$salary = $row1['salary'];
$projection = $row1['projection'];
$wclass = $row1['wclass'];
array_push($player, $mma_id);
array_push($player, $salary);
array_push($player, $projection);
array_push($player, $wclass);
array_push($base_array, $player);
}
$result_base_array = array();
$totalsalary = 0;
for($i=0; $i<count($base_array)-5; $i++)
{
for($j=$i+1; $j<count($base_array)-4; $j++)
{
for($k=$j+1; $k<count($base_array)-3; $k++)
{
for($l=$k+1; $l<count($base_array)-2; $l++)
{
for($m=$l+1; $m<count($base_array)-1; $m++)
{
for($n=$m+1; $n<count($base_array)-0; $n++)
{
$totalsalary = $base_array[$i][1]+$base_array[$j][1]+$base_array[$k][1]+$base_array[$l][1]+$base_array[$m][1]+$base_array[$n][1];
$totalprojection = $base_array[$i][2]+$base_array[$j][2]+$base_array[$k][2]+$base_array[$l][2]+$base_array[$m][2]+$base_array[$n][2];
if($totalsalary <= 50000)
{
array_push($result_base_array,
array($base_array[$i], $base_array[$j], $base_array[$k], $base_array[$l], $base_array[$m], $base_array[$n],
$totalprojection, $totalsalary)
);
}
}
}
}
}
}
}
usort($result_base_array, "cmp");
And the cmp function
function cmp($a, $b) {
if ($a[6] == $b[6]) {
return 0;
}
return ($a[6] < $b[6]) ? 1 : -1;
}
Is there anyway to reduce the time it takes to do this task, or any other workaround for getting the desired number of teams
Regards
Because number of elements in array can be very big (for example 100 players can generate 1.2*10^9 teams), you can't hold it in memory. Try to save resulting array to file by parts (truncate array after each save). Then use external file sorting.
It will be slow, but at least it will not fall because of memory.
If you need top n teams (like 10 teams with highest projection) then you should convert code that generates result_base_array to Generator, so it will yield next team instead of pushing it into array. Then iterate over this generator. On each iteration add new item to sorted resulted array and cut redundant elements.
Depending on whether the salaries are often the cause of exclusion, you could perform tests on this in the other loops as well. If after 4 player selections their summed salaries are already above 50K, there is no use to select the remaining 2 players. This could save you some iterations.
This can be further improved by remembering the lowest 6 salaries in the pack, and then check if after selecting 4 members you would still stay under 50K if you would add the 2 lowest existing salaries. If this is not possible, then again it is of no use to try to add the two remaining players. Of course, this can be done at each stage of the selection (after selecting 1 player, 2 players, ...)
Another related improvement comes into play when you sort your data by ascending salary. If after selecting the 4th player, the above logic brings you to conclude you cannot stay under 50K by adding 2 more players, then there is no use to replace the 4th player with the next one in the data series either: that player would have a greater salary, so it would also yield to a total above 50K. So that means you can backtrack immediately and work on the 3rd player selection.
As others pointed out, the number of potential solutions is enormous. For 160 teams and a team size of 6 members, the number of combinations is:
160 . 159 . 158 . 157 . 156 . 155
--------------------------------- = 21 193 254 160
6 . 5 . 4 . 3 . 2
21 billion entries is a stretch for memory, and probably not useful to you either: will you really be interested in the team at the 4 432 456 911th place?
You'll probably be interested in something like the top-10 of those teams (in terms of projection). This you can achieve by keeping a list of 10 best teams, and then, when you get a new team with an acceptable salary, you add it to that list, keeping it sorted (via a binary search), and ejecting the entry with the lowest projection from that top-10.
Here is the code you could use:
$base_array = array();
// Order by salary, ascending, and only select what you need
$query1 = mysqli_query($conn, "
SELECT mma_player_id, salary, projection, wclass
FROM temp_players
ORDER BY salary ASC");
// Specify with option argument that you only need the associative keys:
while($row1 = mysqli_fetch_array($query1, MYSQLI_ASSOC)) {
// Keep the named keys, it makes interpreting the data easier:
$base_array[] = $row1;
}
function combinations($base_array, $salary_limit, $team_size) {
// Get lowest salaries, so we know the least value that still needs to
// be added when composing a team. This will allow an early exit when
// the cumulative salary is already too great to stay under the limit.
$remaining_salary = [];
foreach ($base_array as $i => $row) {
if ($i == $team_size) break;
array_unshift($remaining_salary, $salary_limit);
$salary_limit -= $row['salary'];
}
$result = [];
$stack = [0];
$sum_salary = [0];
$sum_projection = [0];
$index = 0;
while (true) {
$player = $base_array[$stack[$index]];
if ($sum_salary[$index] + $player['salary'] <= $remaining_salary[$index]) {
$result[$index] = $player;
if ($index == $team_size - 1) {
// Use yield so we don't need to build an enormous result array:
yield [
"total_salary" => $sum_salary[$index] + $player['salary'],
"total_projection" => $sum_projection[$index] + $player['projection'],
"members" => $result
];
} else {
$index++;
$sum_salary[$index] = $sum_salary[$index-1] + $player['salary'];
$sum_projection[$index] = $sum_projection[$index-1] + $player['projection'];
$stack[$index] = $stack[$index-1];
}
} else {
$index--;
}
while (true) {
if ($index < 0) {
return; // all done
}
$stack[$index]++;
if ($stack[$index] <= count($base_array) - $team_size + $index) break;
$index--;
}
}
}
// Helper function to quickly find where to insert a value in an ordered list
function binary_search($needle, $haystack) {
$high = count($haystack)-1;
$low = 0;
while ($high >= $low) {
$mid = (int)floor(($high + $low) / 2);
$val = $haystack[$mid];
if ($needle < $val) {
$high = $mid - 1;
} elseif ($needle > $val) {
$low = $mid + 1;
} else {
return $mid;
}
}
return $low;
}
$top_team_count = 10; // set this to the desired size of the output
$top_teams = []; // this will be the output
$top_projections = [];
foreach(combinations($base_array, 50000, 6) as $team) {
$j = binary_search($team['total_projection'], $top_projections);
array_splice($top_teams, $j, 0, [$team]);
array_splice($top_projections, $j, 0, [$team['total_projection']]);
if (count($top_teams) > $top_team_count) {
// forget about lowest projection, to keep memory usage low
array_shift($top_teams);
array_shift($top_projections);
}
}
$top_teams = array_reverse($top_teams); // Put highest projection first
print_r($top_teams);
Have a look at the demo on eval.in, which just generates 12 players with random salary and projection data.
Final remarks
Even with the above mentioned optimisations, doing this for 160 teams might still require a lot of iterations. The more often the salaries amount to more than 50K, the better the performance will be. If this never happens, the algorithm cannot escape from having to look at each of the 21 billion combinations. If you would know beforehand that the 50K limit would not play any role, you would of course order the data by descending projection, like you originally did.
Another optimisation could be if you would feed back into the combination function the 10th highest team projection you have so far. The function could then eliminate combinations that would lead to a lower total projection. You could first take the 6 highest player projection values and use this to determine how high a partial team projection can still grow by adding the missing players. It might turn out that this becomes impossible after having selected a few players, and then you can skip some iterations, much like done on the basis of salaries.
I'm struggling with (probably simple) array shuffling/generating algorithm.
I'm creating simple backend (PHP/MySQL) for a roulette wheel game (JS/HTML). The roulette doesn't have numeric values as usual instead there are 4 prizes user can win, distributed within 12 segments of a roulette wheel.
I need an array like this:
// Note that PRIZE4 is listed only once as it is valuable prize.
// PRIZE3 is less valuable so it is listed twice, etc.
// Prizes are skipping each other so you should never see two identic prizes next each other.
var items = [PRIZE1, PRIZE2, PRIZE1, PRIZE2, PRIZE1, PRIZE2, PRIZE1, PRIZE2, PRIZE1, PRIZE3, PRIZE4, PRIZE3];
And I have prizes in a SQL table like this:
+----+------------+--------------+
| id | name | giveaway_cap |
+----+------------+--------------+
| 1 | PRIZE1 | 255 |
| 2 | PRIZE2 | 300 |
| 3 | PRIZE3 | 30 |
| 4 | PRIZE4 | 15 |
+----+------------+--------------+
4 rows in set (0.00 sec)
Column giveaway_cap determines how many of each prize can be won (I'm storing these counts in different table) but could be used as weight of each prize.
I need some algorithm (preferably PHP) which will generate an array as described above based on this table.
Thanks.
I found this really nice algorithm searching on SO. It generates random numbers based on weight. Using it, one approach to your problem could be as follows:
// This is the function that generates random numbers based on weigth.
function getRandomWeightedElement(array $weightedValues) {
$rand = mt_rand(1, (int) array_sum($weightedValues));
foreach ($weightedValues as $key => $value) {
$rand -= $value;
if ($rand <= 0) {
return $key;
}
}
}
$items = [ "PRIZE1" => 255, "PRIZE2" => 300, "PRIZE3" => 30, "PRIZE4" => 15];// Array of available prizes. It can be retrieved from the DB.
$total = (int) array_sum($items);// Total. I use it to work out the weight.
$items_w = [ "PRIZE1" => (255 / $total) * 1000, "PRIZE2" => (300 / $total) * 1000, "PRIZE3" => (30 / $total) * 1000, "PRIZE4" => (15 / $total) * 1000];// find out the weight somehow. I just divide number of available items by the total.
$res = [];
$previous = NULL;
while ( count(array_diff(array_keys($items), $res))) {// Loop until the result has all available prizes.
$res = [];
for ($i = 0; $i < 12; $i++) {
while ($previous == ($new = getRandomWeightedElement($items_w))) {}// Two consecutive prizes of the same type aren't allowed.
$res[] = $new;
$previous = $new;
}
}
echo implode(',', $res);
It's just a solution, I'm sure there are multiple ways of solving the problem.
Note: I'm using php 5.4 short array syntax, if your PHP version is lower than PHP 5.4, substitute [] by array(). Also have in mind that there can be problems in certain situations such as there's only one type of prize left or if it's impossible to create an array of prizes without two consecutive prizes being the same. You'd have to control those situations anyhow.
Hope it helps.
<?php
$connect=mysqli_connect("localhost","my_user","my_password","my_db");
$sql="SELECT id,name,giveaway_cap FROM table";
$result=mysqli_query($connect,$sql);
$prizes = array();
while($row=mysqli_fetch_array($result)) //iterate 4 times on caps != to 0
{
$count = $row['giveaway_cap'];
if($count != '0')
{
$prizename = $row['name'];
$prizes[$prizename]=$count;
}
}
$prizewheel = array();
foreach ($prizes as $prize=>$value) // iterate 600 times if caps !=0
{
$prizewheel = array_merge($prizewheel, array_fill(0, $value, $prize));
}
$finalprize=array();
$f = 0;
while($f < 12) //iterate 12 times is # of caps >= 12,final array
{
$prizename = $prizewheel[array_rand($prizewheel)];
$finalprize[] = $prizename;
$f++;
}
?>
You could generate a random array like this:
$rows = array(
array(
'id' => 1,
'name' => 'PRIZE1',
'giveaway_cap' => 255,
),
array(
'id' => 2,
'name' => 'PRIZE2',
'giveaway_cap' => 300,
),
array(
'id' => 3,
'name' => 'PRIZE3',
'giveaway_cap' => 30,
),
array(
'id' => 4,
'name' => 'PRIZE4',
'giveaway_cap' => 15,
),
);
$output = array();
foreach ($rows as $row) {
for ($i = 0; $i < $row['giveaway_cap']; $i++) {
$output[] = $row['name'];
}
}
shuffle($output);
//to get the next prize
$nextPrizeKey = array_rand($output);
$nextPrize = $output[$nextPrizeKey];
//remove the won prize
unset($output[$nextPrizeKey]);
What this does is creates a separate element in the array for each of the prizes giveaway_caps. Then as prizes are won you reduce the number of the giveaway_cap and this will reduce the chance that a user will hit that prize again.
If you wanted the array to be static you could save it after the initial generation, either in a database or caching (APC, Memcahche). Then you would just remove each entry that the users land on until there are none left. You could get the next prize with array_rand and then remove the key from the array.
I hope this helps.
Mic
I'm having trouble figuring out an algorithm...
I've got a long series of GPS data that records Time, Speed, Distance, at 1 second intervals. Assume the distance is meters, and speed in m/s. There may be upwards of 2 hours of data, or 7200 points. The "time" field in here is mainly just for reference.
So, the first 5 seconds would be values something like this, with [1-5] being seconds.
$data = array(
1 : array('distance'=>0, 'time'=>'2014-01-09 17:50:00', 'speed'=>0.0),
2 : array('distance'=>2, 'time'=>'2014-01-09 17:50:01', 'speed'=>2.0),
3 : array('distance'=>6, 'time'=>'2014-01-09 17:50:02', 'speed'=>4.0),
4 : array('distance'=>10, 'time'=>'2014-01-09 17:50:03', 'speed'=>4.0),
5 : array('distance'=>12, 'time'=>'2014-01-09 17:50:04', 'speed'=>2.0)
);
I'd like to convert this to data that is listed at 1 meter intervals instead, like this with [1-6] being meters.
$data = array(
1 : array('seconds'=>1.5, 'time'=>'2014-01-09 17:50:01.500', 'speed'=>.666),
2 : array('seconds'=>2, 'time'=>'2014-01-09 17:50:02', 'speed'=>2.0),
3 : array('seconds'=>2.25, 'time'=>'2014-01-09 17:50:02.250', 'speed'=>4.0),
4 : array('seconds'=>2.5, 'time'=>'2014-01-09 17:50:02.500', 'speed'=>4.0),
5 : array('seconds'=>2.75, 'time'=>'2014-01-09 17:50:02.750', 'speed'=>4.0),
6 : array('seconds'=>3, 'time'=>'2014-01-09 17:50:03', 'speed'=>4.0)
);
This can be done w/o the time field of course. I'm having trouble with the calculation, since it definitely isn't 1-to-1. If we start with 7200 seconds of data, we could end up with more or less depending on the distance covered (more or less than 7200 meters).
EDIT (01/10/2014)
Below are the actual implementations of the two methods. I'm actually having trouble deciding which I like better, the iterative or recursive method. I may go with the iterative
METHOD 1, iterative (#Ezequiel Muns, with very minor modifications by me):
function timeToDistance($data) {
if(sizeof($data) == 0){ return; }
$startTime = $data[0]['time'];
$prev = null;
$result = array();
foreach ($data as $secs => $row) {
$row['seconds'] = $secs; // to simplify passing in secs
if ($prev == null) {
// make sure we have a pair
$prev = array( 'distance'=>0 );
}
foreach (distanceRowsBetween($startTime,$prev, $row) as $dist => $distRow) {
$result[$dist] = $distRow;
}
$prev = $row;
}
return $result;
}
function distanceRowsBetween($startTime,$prevRow, $nextRow) {
// Return the by-distance rows that are between $prevRow (exclusive)
// and $nextRow (inclusive)
$rows = array();
$currDist = $prevRow['distance'];
while (true) {
// try to move to the next whole unit of distance
$dDist = ceil($currDist) - $currDist;
$dDist = $dDist == 0.0? 1.0 : $dDist; // dDist is 1 unit if currDist is whole
$currDist += $dDist;
if ($currDist > $nextRow['distance'])
break;
$currSpeed = $nextRow['speed'];
$currSecs = strtotime($nextRow['time']) - strtotime($startTime);
$currTime = $nextRow['time'];
$rows[$currDist] = array(
'speed' => $currSpeed,
'seconds' => $currSecs,
'time' => $currTime,
);
}
return $rows;
}
METHOD 2, recursive (#Nathaniel Ford pseudocode, me actual code):
function data2dist($time_data = array()){
$dist_data = array();
if(sizeof($time_data) == 0){ return $dist_data; }
$start_point = array_shift($time_data);
$start_time = $start_point['time'];
data2dist_sub($start_time, $time_data,$dist_data,$start_point);
return $dist_data;
}
function data2dist_sub($start_time,&$time_data, &$dist_data, $start_point = array()){
if(sizeof($time_data) == 0 && !isset($start_point)){
return;
}
if(sizeof($dist_data) == 0){
$prev_dist = 0;
} else {
$prev_dist = $dist_data[sizeof($dist_data)-1]['distance'];
}
// since distances are accumulating, get curr distance by subtracting last one
$point_dist = $start_point['distance'] - $prev_dist;
if($point_dist == 1){
// exactly 1: perfect, add and continue
$dist_data[] = $start_point;
$start_point = array_shift($time_data);
} else if($point_dist > 1){
// larger than 1: effectively remove 1 from current point and send it forward
$partial_point = $start_point;
$partial_point['distance'] = 1 + $prev_dist;
$dist_data[] = $partial_point;
} else if($point_dist < 1){
// less than 1, carry forward to the next item and continue (minor: this partial speed is absorbed into next item)
$start_point = array_shift($time_data);
if(!isset($start_point)){ return; }
$start_point['distance'] += $point_dist;
}
data2dist_sub($start_time,$time_data,$dist_data,$start_point);
}
You can simplify this by noting that for every contiguous pair of by-time rows you need to calculate 0 or more by-distance rows, and these depend solely on those two by-time rows.
So start with a function to do this simpler calculation, this is a skeleton leaving the calculation of the transformed 'seconds', 'speed' and 'time' values out for simplicity.
function distanceRowsBetween($prevRow, $nextRow) {
// Return the by-distance rows that are between $prevRow (exclusive)
// and $nextRow (inclusive)
$rows = array();
$currDist = $prevRow['distance'];
while (true) {
// try to move to the next whole unit of distance
$dDist = ceil($currDist) - $currDist;
$dDist = $dDist == 0.0? 1.0 : $dDist; // dDist is 1 unit if currDist is whole
$currDist += $dDist;
if ($currDist > $nextRow['distance'])
break;
// calculate $currSecs at distance $currDist
// calculate $currSpeed
// calculate $currTime
$rows[$currDist] = array(
'speed' => $currSpeed,
'seconds' => $currSecs,
'time' => $currTime,
);
}
return $rows;
}
Now that you have this all that remains is iterating over each contiguous pair in the input and accumulate resulting by-distance rows:
function timeToDistance($data) {
$prev = null;
$result = array();
foreach ($data as $secs => $row) {
$row['seconds'] = $secs; // to simplify passing in secs
if ($prev == null) {
$prev = $row; // make sure we have a pair
continue;
}
foreach (distanceRowsBetween($prev, $row) as $dist => $distRow) {
$result[$dist] = $distRow;
}
$prev = $row;
}
return $result;
}
Note in this function I am populating and passing in the current 'seconds' value in the row, to reduce the number of parameters passed into the previous function.
This is a bit of a mind-bender, and there are a couple of edge cases that make it difficult. However, your basic algorithm should boil down to:
Take in an array of by-Time data points
Create a new array of by-Distance data points
Create a first by-Distance data point with 'zero' speed/distance
Pass this to your subfunction
Subfunction (Takes by-Time array, by-Distance array and 'start point')
Take the first by-Time data point and 'add' it to the by-Distance data point, call this 'temp'
Convert to seconds/speed
If distance covered by temp is exactly 1, add this new array to the by-Distance array
If it is more than one, subtract the portion that would equal one
back-calculate distance/speed/time, add to by-Distance array
Recurse into the subfunction, using the remainder as your new start point
If it is less than one
Recurse into the subfunction, using the modified start point as new start point
Note that the sub-function will need to use mutable copies of the arrays: the by-Time array should slowly shrink and the by-Distance array grow. Also, you will want to trampoline the function (rather than use straight recursion) because with 7200 datapoints you will probably have more than that in stack frames, and you run into a potential memory problem.
I'm making a leveling system based on experience you have on the site. I already have all the experience stuff figured out, and how I want to do the leveling, but I need a more efficient way to do it. I know this would probably would be achieved using an array, but I don't really know how to go about doing that. Enough blabbering, though, this is what I'm trying to do...
Level 1 will be anything under 150 experience
Then I'm going to multiply that by 1.5, so
Level 2 will be anything under 225
Level 3 will be anything under 337.5 and so on. This is the inefficient way that I was going to do.
if($xp < 150){
$level = "1";
}elseif($xp < 225){
$level = "2";
}elseif($xp < 337.5){
$level = "3";
}
I could use a variable for the number and multiple by 1.5 ($number*1.5), but like I said before I don't really know how that'd work.
*Some more info..
I have a session file included on every page and I have queries that would check every time there is new experience earned and this code would be used to update the level on the database automatically.
Try
$level = min(max((int)(log($xp / 100, 1.5) + 1), 1), $maxLevel);
That takes the logarithm base 1.5 of xp/100 (i.e. the number of times you'd have to multiply 100 by to get $xp), then adds one (since log($x, 1.5) is less than one if $x is less than 1.5). The min(max(..., minLevel), maxLevel) construct lets you clamp the level to lie between 1 and $maxLevel, also avoiding any issues with negative levels (if $xp is sufficiently less than 150).
Here's how I did my level system. It's a little more advanced featuring skill points..
<?php
$level_up = ($level + 1);
if ($exp >= $max_exp)
{
$sql = "UPDATE users SET level=(level + 1) , max_exp=(exp * 1.05) , skill_points=(skill_points + 3) WHERE id='".$id."' LIMIT 1";
$res = mysql_query($sql);
if ($exp >= $max_exp)
echo '<div class="Leveled">' . 'You sucessfully leveled up to ' . $level_up . '!' . ' As a reward you were given 3 skill points!' . '</div>';
}
else
{
}
?>
I'd agree. either objects or arrays would be best.
Something like:
$array = array(
array(
'xp_required' => 150,
'level' => 1
),
array(
'xp_required' => 225,
'level' => 2
),
array(
'xp_required' => 337.5,
'level' => 3
)
);
$current_xp = 155;
foreach( $array as $reqs ){
if( $current_xp > $reqs['xp_required'] ){
$level = $reqs['level'];
}
}
echo $level';
well at the moment once you grab the users exp you can have that code block as a function to constantly check for variations in exp levels, but use a ranged statement
if($xp < 150){
$level = "1";
}elseif($xp > 150 && $xp < 225 ){
$level = "2";
}elseif($xp > 225 && $xp < 337.5){
$level = "3";
}
I have a table that looks like this:
<22 23-27
8-10 1.3 1.8
11-13 2.2 2.8
14-16 3.2 3.8
and it goes on. So I'd like to lookup a value like this:
lookup(11,25)
and get the response, in this case 2.8. What is the best data structure to use for this? I have the data in CSV format.
I'm looking to program this in PHP.
Thank you.
I'm certainly not claiming this is the best or most efficient data structure, but this is how I'd map your data into a two-dimensional PHP array that very closely resembles your raw data:
$fp = fopen('data.csv', 'r');
$cols = fgetcsv($fp);
array_shift($cols); // remove empty first item
$data = array();
while ($row = fgetcsv($fp)) {
list($min, $max) = explode('-', $row[0]);
// TODO: Handle non-range values here (e.g. column header "<22")
$data["$min-$max"] = array();
for ($x = 0; $x < count($cols); $x++) {
$data["$min-$max"][$cols[$x]] = $row[$x + 1];
}
}
You'd then need to add some parsing logic in your lookup function:
function lookup($row, $col) {
$return = null;
// Loop through all rows
foreach ($data as $row_name => $cols) {
list($min, $max) = explode('-', $row_name);
if ($min <= $row && $max >= $row) {
// If row matches, loop through columns
foreach ($cols as $col_name => $value) {
// TODO: Add support for "<22"
list($min, $max) = explode('-', $col_name);
if ($min <= $col && $max >= $col) {
$return = $value;
break;
}
}
break;
}
}
return $return;
}
How about some kind of two dimensional data structure.
X "coordinates" being <22, 23-27
Y "coordinates" being ...
A two dimensional Array would probably work for this purpose.
You will then need some function to map the specific X and Y values to the ranges, but that should not be too hard.
Database structure:
values
------
value
x_range_start
x_range_end
y_range_start
y_range_end
Code:
function lookup(x, y) {
sql = "
SELECT * FROM values
WHERE
x >= x_range_start
AND
x <= x_range_end
AND
y >= y_range_start
AND
y <= y_range_end
"
/---/
}
Your data would map to the database like so:
<22 23-27
8-10 1.3 1.8
11-13 2.2 2.8
14-16 3.2 3.8
(value, x start, x end, y start, y end)
1.3, 0, 22, 8, 10
1.8, 23, 27, 8, 10
2.2, 0, 22, 11, 13
...
Basically store the x and y axis start and end numbers for each value in the table.
I'm partial to the 2 Dimensional array with a "hash" function that maps the ranges into specific addresses in the table.
So your underlying data structure would be a 2 dimensional array:
0 1
0 1.3 1.8
1 2.2 2.8
2 3.2 3.8
Then you would write two functions:
int xhash(int);
int yhash(int);
That take the original arguments and convert them into indexes into your array. So xhash performs the conversion:
8-10 0
11-13 1
14-16 2
Finally, your lookup operation becomes.
function lookup($x, $y)
{
$xIndex = xhash($x);
$yIndex = yhash($y);
// Handle invalid indices!
return $data[$xIndex][$yIndex];
}
Well, the other answers all use 2D arrays, which means using a 2D loop to retrieve it. Which, if your ranges are age ranges or something similar, may be finite (there are only so many age ranges!), and not an issue (what's a few hundred iterations?). If your ranges are expected to scale to enormous numbers, a play on a hash map may be your best bet. So, you create a hashing function that turns any number into the relevant range, then you do direct lookups, instead of a loop. It would be O(1) access instead of O(n^2).
So your hash function could be like: function hash(n) { if (n < 22) return 1; if (n < 25) return 2; return -1; }, and then you can specify your ranges in terms of those hash values (1, 2, etc.), and then just go $data[hash(11)][hash(25)]
the simplest option: create array of arrays, where each array consists of 5 elements: minX, maxX, minY, maxY, value, in your case it would be
$data = array(
array(8, 10, 0, 22, 1.3),
array(8, 10, 23, 27, 1.8),
array(11, 13, 0, 22, 2.2), etc
write a loop that goes through every element and compares min & max values with your arguments:
function find($x, $y) {
foreach($data as $e) {
if($x <= $e[0] && $x >= $e[1] && $y <= $e[2] && $y >= $e[3])
return $e[4];
}
with a small dataset this will work fine, if your dataset is bigger you should consider using a database.