1

This question is an extension of Java- Math.random(): Selecting an element of a 13 by 13 triangular array. I am selecting two numbers at random (0-12 inclusive) and I wanted the values to be equal.

But now, since this is a multiplication game, I want a way to bias the results so certain combinations come up more frequently (like if the Player does worse for 12x8, I want it to come up more frequently). Eventually, I would like to bias towards any of the 91 combinations, but once I get this down, that should not be hard.

My Thoughts: Add some int n to the triangular number and Random.nextInt(91 + n) to bias the results toward a combination.

private int[] triLessThan(int x, int[] bias) { // I'm thinking a 91 element array, 0 for no bias, positive for bias towards
    int i = 0;
    int last = 0;
    while (true) {
                    int sum = 0;
                    for (int a = 0; a < i * (i + 2)/2; a++){
                        sum += bias[a]
                    }
        int triangle = i * (i + 1) / 2;
        if (triangle + sum > x){
            int[] toReturn = {last,i};
            return toReturn;
        }
        last = triangle;
        i++;
    }
}

At the random number roll:

int sum = sumOfArray(bias); // bias is the array;
int roll = random.nextInt(91 + sum);
int[] triNum = triLessThan(roll);
int num1 = triNum[1];
int num2 = roll - triNum[0]; //now split into parts and make bias[] add chances to one number.

where sumOfArray just finds the sum (that formula is easy). Will this work?

Edit: Using Floris's idea:

At random number roll:

int[] bias = {1,1,1,...,1,1,1} // 91 elements
int roll = random.nextInt(sumOfBias());
int num1 = roll;
int num2 = 0;
while (roll > 0){
    roll -= bias[num2];
    num2++;
}
num1 = (int) (Math.sqrt(8 * num2 + 1) - 1)/2;
num2 -= num1 * (num1 + 1) / 2;
Community
  • 1
  • 1
Justin
  • 24,288
  • 12
  • 92
  • 142
  • Why **YOU NO** try yourself & verify if it works or not? (O_o) – Rahul Apr 02 '13 at 04:01
  • @R.J Well, I was asking a question and presenting a possible solution. If it works, then you just have to post it as the answer and you'll get accepted. – Justin Apr 02 '13 at 04:03

1 Answers1

3

You already know how to convert a number between 0 and 91 and turn it into a roll (from the answer to your previous question). I would suggest that you create an array of N elements, where N >> 91. Fill the first 91 elements with 0...90, and set a counter A to 91. Now choose a number between 0 and A, pick the corresponding element from the array, and convert to a multiplication problem. If the answer is wrong, append the number of the problem to the end of the array, and increment A by one.

This will create an array in which the frequencies of sampling will represent the number of times a problem was solved incorrectly - but it doesn't ever lower the frequency again if the problem is solved correctly the next time it is asked.

An alternative and better solution, and one that is a little closer to yours (but distinct) creates an array of 91 frequencies - each initially set to 1 - and keeps track of the sum (initially 91). But now, when you choose a random number (between 0 and sum) you traverse the array until the cumulative sum is greater then your random number - the number of the bin is the roll you choose, and you convert that with the formula derived earlier. If the answer is wrong you increment the bin and update the sum; if it is right, you decrement the sum but never to a value less than one, and update the sum. Repeat.

This should give you exactly what you are asking: given an array of 91 numbers ("bins"), randomly select a bin in such a way that the probability of that bin is proportional to the value in it. Return the index of the bin (which can be turned into the combination of numbers using the method you had before). This function is called with the bin (frequency) array as the first parameter, and the cumulative sum as the second. You look up where the cumulative sum of the first n elements first exceeds a random number scaled by the sum of the frequencies:

private int chooseBin(float[] freq, float fsum) {
// given an array of frequencies (probabilities) freq
// and the sum of this array, fsum
// choose a random number between 0 and 90
// such that if this function is called many times
// the frequency with which each value is observed converges
// on the frequencies in freq
    float x, cs=0; // x stores random value, cs is cumulative sum
    int ii=-1;     // variable that increments until random value is found

    x = Math.rand();

    while(cs < x*fsum && ii<90) { 
    // increment cumulative sum until it's bigger than fraction x of sum
        ii++;
        cs += freq[ii];
    }
return ii;
}

I confirmed that it gives me a histogram (blue bars) that looks exactly like the probability distribution that I fed it (red line): histogram

(note - this was plotted with matlab so X goes from 1 to 91, not from 0 to 90).

Here is another idea (this is not really answering the question, but it's potentially even more interesting):

You can skew your probability of choosing a particular problem by sampling something other than a uniform distribution. For example, the square of a uniformly sampled random variate will favor smaller numbers. This gives us an interesting possibility:

First, shuffle your 91 numbers into a random order

Next, pick a number from a non-uniform distribution (one that favors smaller numbers). Since the numbers were randomly shuffled, they are in fact equally likely to be chosen. But now here's the trick: if the problem (represented by the number picked) is solved correctly, you move the problem number "to the top of the stack", where it is least likely to be chosen again. If the player gets it wrong, it is moved to the bottom of the stack, where it is most likely to be chosen again. Over time, difficult problems move to the bottom of the stack.

You can create random distributions with different degrees of skew using a variation of

roll = (int)(91*(asin(Math.rand()*a)/asin(a)))

As you make a closer to 1, the function tends to favor lower numbers with almost zero probability of higher numbers:

non uniform sampling example

I believe the following code sections do what I described:

private int[] chooseProblem(float bias, int[] currentShuffle) { 
// if bias == 0, we choose from uniform distribution
// for 0 < bias <= 1, we choose from increasingly biased distribution
// for bias > 1, we choose from uniform distribution
// array currentShuffle contains the numbers 0..90, initially in shuffled order
// when a problem is solved correctly it is moved to the top of the pile
// when it is wrong, it is moved to the bottom.
// return value contains number1, number2, and the current position of the problem in the list
    int problem, problemIndex;

    if(bias < 0 || bias > 1) bias = 0;

    if(bias == 0) {
        problem = random.nextInt(91);
        problemIndex = problem;
    }
    else {
        float x = asin(Math.random()*bias)/asin(bias);
        problemIndex = Math.floor(91*x);
        problem = currentShuffle[problemIndex];
    }

    // now convert "problem number" into two numbers:
    int first, last;    
    first = (int)((Math.sqrt(8*problem + 1)-1)/2);
    last = problem - first * (first+1) / 2;

    // and return the result:
    return {first, last, problemIndex};
}


private void shuffleProblems(int[] currentShuffle, int upDown) {
// when upDown==0, return a randomly shuffled array
// when upDown < 0, (wrong answer) move element[-upDown] to zero
// when upDown > 0, (correct answer) move element[upDown] to last position
// note - if problem 0 is answered incorrectly, don't call this routine!

    int ii, temp, swap;

    if(upDown == 0) {

        // first an ordered list:
        for(ii=0;ii<91;ii++) {
            currentShuffle[ii]=ii;
        }

        // now shuffle it:
        for(ii=0;ii<91;ii++) {
            temp = currentShuffle[ii];
            swap = ii + random.nextInt(91-ii);
            currentShuffle[ii]=currentShuffle[swap];
            currentShuffle[swap]=temp;
        }
        return;
    }

    if(upDown < 0) {
        temp = currentShuffle[-upDown];
        for(ii = -upDown; ii>0; ii--) {
            currentShuffle[ii]=currentShuffle[ii-1];
        }
        currentShuffle[0] = temp;
    }
    else {
        temp = currentShuffle[upDown];
        for(ii = upDown; ii<90; ii++) { 
            currentShuffle[ii]=currentShuffle[ii+1];
        }
        currentShuffle[90] = temp;
    }
    return;
}


// main problem posing loop:

int[] currentShuffle = new int[91];
int[] newProblem;
int keepGoing = 1;

// initial shuffle:
shuffleProblems( currentShuffle, 0); // initial shuffle

while(keepGoing) {
    newProblem = chooseProblem(bias, currentShuffle);
    // pose the problem, get the answer
    if(wrong) {
        if(newProblem > 0) shuffleProblems( currentShuffle, -newProblem[2]);
    }
    else shuffleProblems( currentShuffle, newProblem[2]);
    // decide if you keep going...
}
Floris
  • 45,857
  • 6
  • 70
  • 122
  • I think I figured out what you are saying; I made an edit to your post with the code I thought you were recommending. However, I think that it should be `int num2 = rollStore (int)(Math.sqrt(8*num1 + 1)-1)/2`. Here's what I understood: `int[] bias = {1,1,...,1,1};//91 elements` `roll = random.nextInt(sumOfBias());` `int num1 = 0` `for (;roll>0;num1++)roll -= bias[num1]` (made some mistakes in the edit.) – Justin Apr 02 '13 at 15:13
  • A few more things: for biasing away a certain number, I can increment every other element in the array. rollStore is unnecessary; I can -- at the place where rollStore is made -- make num2 and set it equal to roll, then do `num2 -= (int)(Math.sqrt(8*num1+1)-1)/2;` – Justin Apr 02 '13 at 15:26
  • I'm sorry - it appears I was editing the code at the same time that you were, and my edits somehow overwrote yours... I will add a bit of code to show how the last example (which I think is the best) would work. – Floris Apr 02 '13 at 16:29
  • Looks too complicated... I'll do another edit to my post to show what I got from your original post. – Justin Apr 02 '13 at 20:50
  • The first piece of code surely isn't too complicated? The entire function is just eight lines long. The second example was me having fun.... – Floris Apr 02 '13 at 20:57
  • I just did not understand the variable names. Will the code in my post work? – Justin Apr 02 '13 at 20:59
  • 1
    I think it will - you are basically doing the same thing as the first code snippet in my current answer. The only difference is that my code allows floating point numbers in the bias - so you can change probability smoothly. You are using integers, which is a little coarser. But it should work for your purpose. It's a game, after all... Thanks for the accept! PS - I have added some more comments to my first code snippet - maybe that helps with the understanding. – Floris Apr 02 '13 at 21:40
  • I'm running into problems where num2, the number incrementing in my while loop, gets to 91 when it is only supposed to get to 90 (`ArrayIndexOutOfBoundsException`) when I move num2 to the front of `roll -= bias[num2];` (When roll is > 90). If I leave in after, then 0x0 doesn't get increased bias, 1x0 does (bias[0] affects the second (1) combination) and I get an error if `bias[90] != 0` (one number gets to 13). I tried adding a `roll -= bias[num2]` before the loop, but it didn't work – Justin Apr 04 '13 at 03:02
  • @gangqinlaohu - if you get a "hit" on a number you must not increment `num2` afterwards. So initialize with `num2=-1;` before the loop, and add `num2++;` before `roll-=bias[num2]`. I think that should solve it for you. Note - that's how I do it in my first code snippet (with `ii=-1;`, and incrementing before updating the cumulative sum). I also check that I don't go out of bounds). – Floris Apr 04 '13 at 13:20