I have a function where i input a level, and it returns the XP:
this is gotten from runescape 1-99 formula:
function experience($L) {
$a=0;
for($x=1; $x<$L; $x++) {
$a += floor($x+300*pow(2, ($x/7)));
}
return floor($a/4);
}
this means:
level 54 would return 150872 XP.
But, how would i go the other way around, to input 150872 and make it return 54?
and, whats the way to go when xp might be 150873, but its still level 54 to return?
How would i approatch?
Wants:
experience(152439) -> 54
One inefficient but easy solution is to just continuously call the experience function in a loop, increasing the level each time, until you reach a level that returns an experience value above the one you are looking for, and then return the level before that:
function level($experience) {
$returned = 0;
$level = 0;
while ($returned <= $experience) {
$level++;
$returned = experience($level);
}
return $level - 1;
}
Demo: http://sandbox.onlinephpfunctions.com/code/820d659feb28a00dd87a21d01bd2414cbc66d300
Related
So, this question is more about needing an explanation of why certain code works than it is about needing the code problem answered. I recently did a Codility sample test where you had to write a function in any desired language that could accept an array of varying integers and return each index in the array that was an Equilibrium Index. I wrote my answer in PHP and got a pretty horrible score so I went looking at other people's solutions and found some that worked great and got perfect scores, but they did something very specific that I couldn't figure out the mathematical logic for making that leap.
First, here is my function I wrote to answer the question:
function solution($A) {
for($i=0; $i<=count($A); $i++){
$leftTotal = 0;
$rightTotal = 0;
for($j=0; $j<count($A); $j++){
if($j < $i && isset($A[$j])){
$leftTotal += $A[$j];
} else if($j > $i && isset($A[$j])) {
$rightTotal += $A[$j];
}
}
if($leftTotal === $rightTotal){
return $i;
}
}
return -1;
}
This answer received a 95% Correctness score but only an 18% Performance score. I knew nested for loops was going to kill me on Performance, but I couldn't think of any way to get rid of it until I went and looked at some other solutions.
This is an example that I found on this Github page:
function equi($A) {
$lower_elements_sum = 0;
$higher_elements_sum = array_sum($A);
for ($i = 0, $cnt = count($A); $i < $cnt; $i++) {
if (isset($A[$i - 1])) {
$lower_elements_sum += $A[$i - 1];
}
$higher_elements_sum -= $A[$i];
if ($lower_elements_sum == $higher_elements_sum) {
return $i;
}
}
return -1;
}
So, I understand everything that this code is doing, and structurally, it's not really THAT different from what I did. What is driving me crazy is how they knew that in order to get an Equilibrium Index they could do this whole function working from the outside in and starting their $higher_elements_sum and the array_sum value of the array passed into the function.
How did they know that would work that way? I literally had to write out their function's results on pen and paper to double check because I just couldn't make the leap from having to find the left and right values each time vs what they did and just kept building off of the same value for left and right totals.
Can anyone help me understand and fill in the gaps between my answer and theirs because I'm at a complete loss for how I could have gotten from A to B?
similar but different, the solution I found scored 100 and 100
function solution($A) {
// write your code in PHP7.0
$left_sum = 0;
$right_sum = array_sum($A);
foreach($A as $k => $v) {
if($k > 0) {
$left_sum += $A[($k -1)];
}
$right_sum -= $v;
if($left_sum === $right_sum) {
return $k;
}
}
return -1;
}
I am building a little game and got stuck in developing the leveling system. I created a function that will exponentially increase the experience required for the next level. However, I am not sure how to turn it around so that I can put in the amount of experience a user has gained and get the corresponding level.
PHP function
function experience($level, $curve = 300) {
// Preset value to prevent notices
$a = 0;
// Calculate level cap
for ($x = 1; $x < $level; $x++) {
$a += floor($x+$curve*pow(2, ($x/7)));
}
// Return amount of experience
return floor($a/4);
}
The issue
I am wondering how I can reverse engineer this function in order to return the correct level for a certain amount of experience.
Using the above function, my code would output the following:
Level 1: 0
Level 2: 83
Level 3: 174
Level 4: 276
Level 5: 388
Level 6: 512
Level 7: 650
Level 8: 801
Level 9: 969
Level 10: 1154
What I am looking for is a way to invert this function so that I can input a certain amount and it will return the corresponding level.
A 1000 experience should return level 9 for example.
Plugging the values into excel and creating a trend line, I got the following equation:
y = 1.17E-09x^3 - 4.93E-06x^2 + 1.19E-02x + 6.43E-02
So your reverse engineered equation would be
function level($xp) {
$a = 1.17e-9;
$b = -4.93e-6;
$c = 0.0119;
$d = 0.0643
return round($a*pow($xp, 3) + $b*pow($xp,2) + $c * $xp + $d);
}
Results are accurate to within 1dp, but if your $curve changes, you'd need to recalculate. I also haven't extended higher than level 10.
Other options include caching the results of the lookup:
$levelXpAmounts = array()
function populateLevelArray($curve=300) {
$levelXpAmounts[$curve] = array();
for($level = $minlevel; $level <= $maxLevel; $level++) {
$levelXpAmounts[$curve][$level] = experience($level);
}
}
//at game load:
populateLevelArray()
Then, your reverse lookup would be
function level($xp, $curve=300) {
if (!array_key_exists($levelXpAmounts, curve)
populateLevelArray($curve);
for($level = $minlevel; $ level <= $maxLevel; $level++) {
if ($xp < $levelXpAmounts[$curve][$level]) {
return $level - 1;
}
}
}
That way, the iteration through all the levels is only done once for each different value of $curve. You can also replace your old experience() function with a (quite likely faster) lookup.
Note: it's been a while since I've written any php, so my syntax may be a little rusty. I apologize in advance for any errors in that regard.
You can do another function called level which uses the experience function to find the level:
function level($experience)
{
for ($level = 1; $level <= 10; $level++) {
if ($experience <= experience($level)) {
return $level;
}
}
}
function experience($level, $curve = 300)
{
$a = 0;
for ($x = 1; $x < $level; $x++) {
$a += floor($x+$curve*pow(2, ($x/7)));
}
return floor($a/4);
}
var_dump(level(1000));
You can clearly work the math here and find a reverse formula. Not sure whether it will be a nice and easy formula, so I would suggest you an alternative approach which is easy to implement.
Precalculate the results for all the levels you realistically want your person to achieve (I highly doubt that you need more than 200 levels, because based on my estimation you will need tens of billions exp points).
Store all these levels in the array: $arr = [0, 83, 174, 276, 388, 512, 650, ...];. Now your array is sorted and you need to find a position where your level should fit.
If you are looking for 400 exp points, you see that it should be inserted after 5-th position - so it is 5-th level. Even a simple loop will suffice, but you can also write a binary search.
This task could be solved in other way. This is method of partial sums.
Let's assume, you have a class , which stores an array of exponential values calculated by function:
function formula($level, $curve){ return floor($level+$curve*pow(2, ($level/7)));}
$MAX_LEVEL = 90;
function calculateCurve($curve){
$array = [];
for($i =0; $i< $MAX_LEVEL; $i++) $array.push(formula($i, $curve));
return $array;
}
Now we can calculate experience, needed for a level:
$curve = calculateCurve(300);
function getExperienceForLevel($level, $curve){
$S = 0;
for($i =0; $i < level; $i++) $S += $curve[$i];
}
And calculate level for experience:
function getLevelForExperience($exp, $curve){
for($i =0; $i < $MAX_LEVEL; $i++){
$exp -= $curve[$i];
if($exp < 0) return $i-1;
}
return $MAX_LEVEL;
}
I assume there could index problems - I didn't tested the code, but I suppose that main idea is clearly explained.
Pros:
Code cleaner, There no magic numbers and interpolation coeficients.
You can easy change your learning curve.
Possibility to improve and make calculating functions as O(1);
Cons:
There is an $curve array to store, or calculate somewhere.
Also. you could make even more advanced version of this:
function calculateCurve($curve){
$array = [];
$exp = 0;
for($i =0; $i< $MAX_LEVEL; $i++) {
$exp += formula($i, $curve);
$array.push($exp);
}
return $array;
}
Now calculating experience have O(1) complexity;
function getExperienceForLevel($level, $curve){
return $curve[min($MAX_LEVEL, $level)];
}
Perhaps not the best way, but it's working.
function level($experience, $curve = 300)
{
$minLevel = 1;
$maxLevel = 10;
for($level = $minLevel; $level <= $maxLevel; $level++)
{
if(experience($level, $curve) <= $experience && $experience < experience($level + 1, $curve))
{
return $level;
}
}
return $maxLevel;
}
Using PHP >= 5.5 if we have a method that yielded values, what would be the best method in counting these values?
What I was expecting was to be able to convert a Generator to an array and count that, however it would return an empty array. Count() also does not work as the Generator is reported as empty despite not being empty.
I'm baffled with this. If you don't need to count a generators yields then it's a nice feature otherwise I don't see much of a point for it. There is a way to detect if a generator is empty, this is by using the key() method and if it returns NULL then there are either no yields or the generator has already been iterated through which would mean the current pointer is null.
If you have to do it, following as a on-liner of native functions:
count(iterator_to_array($generator, false));
However, take care: After this your $generator is executed and consumed. So if you would put that same $generator into a foreach in a following line, it would loop 0 times.
Generators are by design highly dynamic (in contrast to fixed data structures like arrays), thats why they don't offer ->count() or ->rewind().
You should understand, that generator isn't data structure - it's an instance of Generator class and, actually, it's special sort of Iterator. Thus, you can't count its items directly (to be precise - that's because Generator class implements only Iterator interface, and not Countable interface. To be honest, I can't imagine how can it implement that)
To count values with native generator you'll have to iterate through it. But that can not be done in common sense - because in most cases it's you who'll decide how many items will be yielded. Famous xrange() sample from manual:
function xrange($start, $limit, $step = 1) {
if ($start < $limit) {
if ($step <= 0) {
throw new LogicException('Step must be +ve');
}
for ($i = $start; $i <= $limit; $i += $step) {
yield $i;
}
} else {
if ($step >= 0) {
throw new LogicException('Step must be -ve');
}
for ($i = $start; $i >= $limit; $i += $step) {
yield $i;
}
}
}
-as you can see, it's you who must define borders. And final count will depend from that. Iterating through generator will have sense only with static-borders defined generator (i.e. when count of items is always static - for example, defined inside generator strictly). In any other case you'll get parameter-dependent result. For xrange():
function getCount(Generator $functor)
{
$count = 0;
foreach($functor as $value)
{
$count++;
}
return $count;
}
-and usage:
var_dump(getCount(xrange(1, 100, 10)));//10
var_dump(getCount(xrange(1, 100, 1)));//100
-as you can see, "count" will change. Even worse, generator hasn't to be finite. It may yield infinite set of values (and borders are defined in external loop, for example) - and this is one more reason which makes "counting" near senseless.
Actually, it depends in which case you are :
Case 1 : I can't count before iterating and I care about values
// The plain old solution
$count = 0;
foreach($traversable as $value) {
// Do something with $value, then…
++$count;
}
Case 2 : I can't count before iterating but I don't care about values
// let's iterator_count() do it for me
$count = iterator_count($traversable);
Case 3 : I can count before iterating but I don't care about values
I try not to use generators.
For example (with SQL backends) :
SELECT count(1) FROM mytable; // then return result
is better than
SELECT * FROM mytable; // then counting results
Other example (with xrange from Alma Do) :
// More efficient than counting by iterating
function count_xrange($start, $limit, $step = 1) {
if (0 === $step) throw new LogicException("Step can't be 0");
return (int)(abs($limit-$start) / $step) + 1;
}
Case 4 : I can count before iterating and I care about values
I can use a generator AND a count function
$args = [0,17,2];
$count = count_xrange(...$args);
$traversable = xrange(...$args);
Case 5 : Case 4, and I want all in one object
I can "decorate" an Iterator to make a Countable Iterator
function buildCountableIterator(...$args) {
$count = count_xrange(...$args);
$traversable = xrange(...$args);
return new class($count, $traversable) extends \IteratorIterator implements \Countable {
private $count;
public function __construct($count, $traversable) {
parent::__construct($traversable);
$this->count = $count;
}
public function count() {
return $this->count;
}
}
}
$countableIterator = buildCountableIterator(1, 24, 3);
// I can do this because $countableIterator is countable
$count = count($countableIterator);
// And I can do that because $countableIterator is also an Iterator
foreach($countableIterator as $item) {
// do something
}
Sources :
http://php.net/manual/en/function.iterator-count.php
http://php.net/manual/en/class.countable.php
http://php.net/manual/en/class.iteratoriterator.php
http://php.net/manual/en/language.oop5.anonymous.php
While you can't use count() you can use a reference to set the count to make it accessible to the outside world.
function generate(&$count = 0) {
// we have 4 things
$count = 4;
for($i = 0; $i < $count; $i++) {
yield $i;
}
}
$foo = generate($count);
echo $count; // 4
foreach ($foo as $i) {
echo $i;
}
Downside to this is it won't tell you how many remain but how many it started with.
function find_highest_prime_factor($n)
{
for ($i = 2; $i <= $n; $i++)
{
if (bcmod($n, $i) == 0) //its a factor
{
return max($i, find_highest_prime_factor(bcdiv($n,$i)));
}
}
if ($i == $n)
{
return $n; //it's prime if it made it through that loop
}
}
UPDATE: This is the correct answer, my bad!
Get rid of the final if statement otherwise if $i!=sqrt($n) because sqrt($n) is not an integer you have an undefined return value
function find_highest_prime_factor($n){
for ($i = 2; $i <= sqrt($n); $i++) //sqrt(n) is the upperbound
{
if (bcmod($n, $i) == 0) //its a factor
{
return max($i, find_highest_prime_factor(bcdiv($n,$i)));
}
}
return $n; //it's prime if it made it through that loop
}
Line 11 should be:
if ($i == ceil(sqrt($n)))
Starting at 2 and stepping by 1 is inefficient. At least check 2 separately and then loop from 3 stepping 2 each time. Using a 2-4 wheel would be even faster.
When you recurse your code starts again at trial factor 2. It would be better to pass a second parameter holding the factor you have reached so far. Then the recursion wouldn't have go back over old factors that have already been tested.
Given an array $array of N numbers and a key $key, write the binary search algorithm in plain English. If $array contains $key, return the index of the $key; otherwise, return -1.
Can someone show me how to do this?
Doesn't seem like I should give you the code here, but maybe this description can help?
Sort the list.
Let i = length / 2
Compare term at index i to your key.
a. If they are equal, return the index.
b. If key is greater than this term, repeat 3 (recurse) on upper half of list i = (i + length) / 2 (or (i + top) / 2 depending how you implement)
c. If key is less than this term, repeat 3 on lower half i = i/2 or (i + bottom)/2
Stop recursion if/when the new i is the same as the old i. This means you've exhausted the search. Return -1
Be careful for off-by-one errors, which can make you exclude certain terms by mistake, or cause infinite recursion, but this is the general idea. Pretty straightforward.
Think of it as playing 'Guess the number' for the numbers 1 through 100. You take a guess, I tell you higher or lower. You say 50, I say lower. You say 25, I say higher. You say 37...
I know this is little late :) ,but take it anyway.This also show that recursive function works faster than in_array()
function binarySearch($A,$value,$starting,$ending)
{
if($ending<$starting)
{
return -1;
}
$mid=intVal(($starting+$ending)/2);
if($value===$A[$mid])
{
return $mid;
}
else if($value<$A[$mid])
{
$ending=$mid-1;
}
else if($value>$A[$mid])
{
$starting=$mid+1;
}
return binarySearch($A,$value,$starting,$ending);
}
for($i;$i<1000000;$i++){
$arr[$i]=$i;
}
$value =99999;
$msc=microtime(true);
$pos = in_array($value,$arr);
$msc=microtime(true)-$msc;
echo "Time taken for in_array() : ".round($msc*1000,3).' ms <br>';
if($pos>0)
echo $value .' found.';
else
echo $value .' not found';
echo "<br><br>";
$msc=microtime(true);
$pos = binarySearch($arr,$value ,0,1000000);
$msc=microtime(true)-$msc;
echo "Time taken for recursive function : ".round($msc*1000,3).' ms<br>';
if($pos>=0)
echo $value .' found.';
else
echo $value .' not found';
Ouput:
Time taken for in_array() : 5.165 ms
99999 found.
Time taken for recursive function : 0.121 ms
99999 found.
Here is a better non recursive solution.
function fast_in_array($elem, $array){
$top = sizeof($array) -1;
$bot = 0;
while($top >= $bot)
{
$p = floor(($top + $bot) / 2);
if ($array[$p] < $elem) $bot = $p + 1;
elseif ($array[$p] > $elem) $top = $p - 1;
else return TRUE;
}
return FALSE;
}