202609171030 Leetcode Sum of Floored Pairs
Problem
Given an integer array nums, return the sum of floor(nums[i] / nums[j]) for all pairs of indices 0 <= i, j < nums.length in the array. Since the answer may be too large, return it modulo 10^9 + 7.
The floor() function returns the integer part of the division.
Example 1:
Input: nums = [2,5,9]
Output: 10
Explanation:
floor(2 / 5) = floor(2 / 9) = floor(5 / 9) = 0
floor(2 / 2) = floor(5 / 5) = floor(9 / 9) = 1
floor(5 / 2) = 2
floor(9 / 2) = 4
floor(9 / 5) = 1
We calculate the floor of the division for every pair of indices in the array then sum them up.
Example 2:
Input: nums = [7,7,7,7,7,7,7]
Output: 49
Constraints:
1 <= nums.length <= 10^51 <= nums[i] <= 10^5
Other solutions
Obviously this works but it's too slow.
const MODULO: i64 = 1_000_000_007;
So what's the trick? When we look at one denominator j we know that the floor-ed value will be constant for a range of numerators. For instance, with nums = [1, 2, 3, 10, 11, 12, 13, 20] and j = 10 we have [1, 2, 3], [10, 11, 12, 13], and [20] as the buckets corresponding to 0, 1, and 2. So the sum (for only j = 10) is 0*3 + 1*4 + 2*1. This also shows us that we can sort and ignore all numbers i < j.
I started with a frequency map, knowing that it probably wouldn't be fast enough (e.g., does it help when all numbers are unique?).
And we were right that it was still too slow. The next thing we need to do is to take out the inner hot loop.
If we ask ourselves, is there a faster way to look up the sum of an array from i..j, we recall (or perhaps research and find) the pattern we're searching for — a prefix sum array.
We can build that for 1..maxNum with
let mut prefixes = vec!;
for i in 1..=max
and then use that like usual to find the sum of our range [bi, bj - 1]
sum += block * ;
and putting it all together, we get our final answer.
This version was a success but landed us in the middle of the performance curve on submissions, so I went digging for improvements.
- The
numsintoi64is wasteful and I just wanted that to not have to do as many casts as I was working on the problem. Removed that. - The
HashMapis unnecessary. We can just use an array for frequencies with the index as the "hash key". - We have two data structures (
freqsandprefixes) when we can compute the prefix sums in place after computing the frequencies. Importantly, we can still recover the number ofcopies(frequency) for a number byprefixes[i] - prefixes[i - 1](which we'll need later). - At the cost of a little readability, we can get some cache efficiencies by checking sequentially over all
1..=maxand skipping out if there are no copies. This checks more iterations but the sequential cache efficiencies are worth it and the earlyif copy == 0bailout is highly predictable to the CPU. - We can also rewrite our
leftandrightblockbounds iterations to use addition instead of multiplication which is a small but worthwhile win at our current depths of optimization.
Here's I could come up with without going crazy on things.