random-weight

::: info Prerequisites

Before reading this article, you should be familiar with:

:::

I got inspired to write this article while playing League of Legends mobile. A friend of mine was complaining about getting terrible teammates in ranked matches. I told him I thought my ranked teammates were pretty solid—they didn't seem that bad to me.

He gave me this knowing look and said: "Usually, when players with high MMR can't find teammates at their skill level, they get matched with... weaker players."

Wait, what? I thought about it for a second. Something didn't add up. Was he saying my MMR was low, or was he calling me the weak player?

I immediately challenged him to duo queue with me to prove I wasn't the weak link—he was. I'll let you guess how that went.

After that game, I came here to write this article because it got me thinking about how matchmaking systems work.

I don't know if "hidden MMR" is actually a thing. Matchmaking is the backbone of any competitive game, so it's probably way more complex than a few simple metrics.

But if we simplify this whole hidden MMR concept, it becomes an interesting algorithmic problem: how does the system randomly match players with different probabilities?

Or to put it more simply: how do you make weighted random selections?

Don't think it's trivial. Sure, if you have an array of length n and need to randomly pick an element with equal probability, that's easy—just generate a random index in [0, n-1] and each element has a 1/n chance of being selected.

But what if each element has a different weight, where the weight determines the probability of selecting that element? How would you write an algorithm to randomly pick elements based on those weights?

LeetCode problem 528, "Random Pick with Weightarrow-up-right," is exactly this problem:

LeetCode 528. Random Pick with Weight

You are given a 0-indexed array of positive integers w where w[i] describes the weight of the i^(th) index.

You need to implement the function pickIndex(), which randomly picks an index in the range [0, w.length - 1] (inclusive) and returns it. The probability of picking an index i is w[i] / sum(w).

  • For example, if w = [1, 3], the probability of picking index 0 is 1 / (1 + 3) = 0.25 (i.e., 25%), and the probability of picking index 1 is 3 / (1 + 3) = 0.75 (i.e., 75%).

Example 1:**

Example 2:**

Constraints:

  • 1 <= w.length <= 10^(4)

  • 1 <= w[i] <= 10^(5)

  • pickIndex will be called at most 10^(4) times.

Let's think through this problem and solve weighted random selection.

Approach

First, let's recap some previous articles on randomized algorithms:

In Designing a Data Structure for Random Deletionarrow-up-right, we focused on data structure design—moving elements to the end of the array before deleting them to avoid shifting data.

In Randomized Algorithms in Gamesarrow-up-right, we covered the classic reservoir sampling algorithm, which uses simple math to select elements with equal probability from an infinite sequence.

But neither of those articles solve the problem we're facing here. Instead, it's the combination of Prefix Sum Techniquesarrow-up-right and Binary Search Explainedarrow-up-right that unlocks weighted random selection.

How do random algorithms relate to prefix sums and binary search? Let me explain.

Say the input weight array is w = [1,3,2,1]. We want the selection probability to match the weights. Here's a way to visualize it—imagine drawing a colored line segment based on these weights:

If we randomly drop a stone onto this line segment, and we pick the index corresponding to whichever color the stone lands on, doesn't that mean each index gets selected with probability proportional to its weight?

Now take a closer look at this colored line segment. What does it look like? It's just a prefix sum arrayarrow-up-right!

Next question: how do we simulate dropping a stone onto the line segment?

With a random number, of course. For the prefix sum array preSum above, the range is [1, 7]. If we generate a random number target = 5 in that range, it's like randomly dropping a stone on the line segment:

But there's another issue: preSum doesn't actually contain the element 5. We need to find the smallest element greater than or equal to 5, which is 6, at index 3 of the preSum array:

How do we quickly find the smallest element in an array that's greater than or equal to our target? That's exactly what binary searcharrow-up-right is for.

That's the core idea. Here's the breakdown:

  1. Build a prefix sum array preSum from the weight array w.

  2. Generate a random number within the range of preSum, then use binary search to find the index of the smallest element greater than or equal to this random number.

  3. Subtract one from this index (because the prefix sum array has an index offset), and you get the index in the weight array—your final answer:

Solution Code

The approach itself is pretty straightforward, but the implementation is where things get tricky.

Problems involving open/closed intervals, index offsets, and binary search require extremely precise attention to detail. Otherwise, you'll run into all sorts of hard-to-debug issues.

Let's dig into the details, continuing with our previous example:

Take the preSum array. What range should the random number target be in? The closed interval [0, 7] or the half-open interval [0, 7)?

Neither. It should be in the closed interval [1, 7], because the 0 in the prefix sum array is essentially just a placeholder. Think about it:

So you should write the code like this:

Next, when searching for the index of the smallest element in preSum that's greater than or equal to target, which variant of binary search should you use? Left boundary search or right boundary search?

You should use left boundary binary search:

In Binary Search Explainedarrow-up-right, I focused on cases where the target element appears multiple times in the array, but didn't go into detail about when the target doesn't exist. Let me clarify that here.

When the target element target doesn't exist in array nums, the return value from left boundary binary search can be interpreted in these ways:

  1. It's the index of the smallest element in nums that's greater than or equal to target.

  2. It's the index position where target should be inserted into nums.

  3. It's the count of elements in nums that are less than target.

For example, if you search for target = 4 in the sorted array nums = [2,3,5,7], left boundary binary search returns 2. Try plugging that into each of the interpretations above—they all work.

All three interpretations are equivalent, so you can use whichever fits your problem best. Obviously, we need the first interpretation here.

Putting it all together, here's the final solution:

With all the groundwork we've laid, you should fully understand this code. That solves the weighted random selection problem.

I often get comments from readers joking that they just "cloud grind" problems by reading my articles—they understand everything without actually coding it themselves.

But here's the thing: many problems are easy to understand conceptually, but once you dig deeper, there are tons of subtle pitfalls. This problem is a perfect example. So I still recommend practicing hands-on and reflecting on what you learn.

Last updated