Min Average Two Slice Codility
100% score with C++ based on Kadane's algorithm
int solution(vector<int> &A) {
// Find prefix sum.
int N = A.size();
vector<int> ps(N + 1, 0);
for (int i = 1; i <= N; i++) {
ps[i] = A[i - 1] + ps[i - 1];
}
int lft_idx, min_lft_idx;
double avg_here, min_avg, avg_of_two, avg_with_prev;
// Initialize variables at the first possible slice (A[0:1]).
lft_idx = min_lft_idx = 0;
avg_here = min_avg = (A[0] + A[1]) / 2.0;
// Find min average of every slice that ends at ith element,
// starting at i = 2.
for (int i = 2; i < N; i ++) {
// average of A[lft_idx : i]
avg_with_prev = ((double) ps[i + 1] - ps[lft_idx]) /
(i - lft_idx + 1);
// average of A[i - 1 : i]
avg_of_two = (A[i - 1] + A[i]) / 2.0;
// Find minimum and update lft_idx of slice
// (previous lft_idx or i - 1).
if (avg_of_two < avg_with_prev) {
avg_here = avg_of_two;
lft_idx = i - 1;
}
else
avg_here = avg_with_prev;
// Keep track of minimum so far and its left index.
if (avg_here < min_avg) {
min_avg = avg_here;
min_lft_idx = lft_idx;
}
}
return min_lft_idx;
}
I wasn't satisfied with the 2/3-element subslices solution (come on! who would think of that in the duration of an interview?), nor with the explanations (or lack of them), so I went on searching for other approaches. Then I found about Kadane's algorithm for the maximum subarray problem (MSP) which led me to a different solution.
The basic question is (similar to that of the MSP): What is the minimal average of a slice that includes the i-th element?
In order to answer it, we'll look for slices that end in the i-th element, only updating their left index. That is, we have to check for the slices A[lft_idx : i]
.
Assuming we know the lft_idx
of the slice A[lft_idx : i - 1]
with a minimal average, then we have two possibilities:
- The average of
A[lft_idx : i]
is minimal. - The average of
A[i - 1 : i]
is minimal (the shortest possible slice has 2 elements).
What happens in case 1 is that we continue growing the slice that starts at lft_idx
.
In case 2, however, we find that growing the previous slice actually increases the average. So we start over again and "reset" the start of the slice (lft_idx
) to the previous element (i - 1
). Now we have a new best slice of size 2 to grow from this point.
Finally, we want the global minimal average, so we need to keep track of the minimum so far and where it started (the question only asks for this, but we could as well save the right index).
Note: I'm using prefix sums here to calculate slice averages because that's where the question appears in Codility, but it could easily be replaced by a variable with the size of the previous slice and another multiplication.
This is a mathematical problem... and to solve it you have to understand the relationship that exists between the averages of the slices.
We know from the problem description that the slices are a minimum length of 2. The trick to this problem is that the min average slice also cannot be longer than 3. So we only have to calculate the avg of the slices of length 2 and 3.
To understand why the min average slice cannot be longer than 3, consider the case where it is longer than 3...
ex. [-10, 3, 4, -20]
avg(0,3) = -23 / 4 = -5.75 // entire array is -5.75 average
avg(0,1) = -7 / 2 = -3.5 // subslice (0,1)
avg(2,3) = -16 / 2 = -8 // subslice (2,3)
Notice that (avg(0,1) + avg(2,3)) / 2 = avg(0,3)
Therefore, if avg(0,1) != avg(2,3)
then one of them must be smaller than the other.
No matter which way we split up this array, if the slices aren't exactly the same, then one of them must have a lower average than the full slice. Play around with it and you'll see that it's true. There are mathematical proofs out there.
The tricky part is to figure out even before you start coding that there is for sure an average min slice of length 2 or 3. From there it is easier, but I have a few important notes:
You don't need division at all, you can instead multiply, so that you can get the same average over a slice of length 6 and avoid float operations altogether
You don't need division (or in my case multiplication) in the loop, once in the end it is enough.
If you had to actually do it, you should always compare two floating point numbers like so: EPSILON = 0.0000001 (depending on the precision you look for this can be a different number) and if Math.abs(averageTwo - averageThree) < EPSILON it means they are equal. And you don't need double precision, float is enough.
Here is my solution in Java, it got 100% on Codility:
public int solution(int[] A) {
if (A.length == 2) return 0;
int minSliceTwo = A[0] + A[1];
int minTwoIndex = 0;
int minSliceThree = Integer.MAX_VALUE;
int minThreeIndex = 0;
for (int i = 2; i < A.length; i++) {
int sliceTwo = A[i - 1] + A[i];
if (sliceTwo < minSliceTwo) {
minSliceTwo = sliceTwo;
minTwoIndex = i - 1;
}
int sliceThree = sliceTwo + A[i - 2];
if (sliceThree < minSliceThree) {
minSliceThree = sliceThree;
minThreeIndex = i - 2;
}
}
int averageMinTwo = minSliceTwo*3;
int averageMinThree = minSliceThree*2;
if (averageMinTwo == averageMinThree) return Math.min(minTwoIndex, minThreeIndex);
else return averageMinTwo < averageMinThree ? minTwoIndex : minThreeIndex;
}
I had posted this some days ago:
Check this out:
http://codesays.com/2014/solution-to-min-avg-two-slice-by-codility/
In there, they explain with great detail why their solution works. I haven't implemented it myself yet, but I will definitely try it.
Hope it helps!
but I just saw it was deleted by a moderator. They say the link is dead, but I just tried it and it works fine. I'm posting it once again, hoping it can be validated that the link is good.
And now I can also provide my implementation, based on the codesays link that I provided before: https://codility.com/demo/results/demoERJ4NR-ETT/
class Solution {
public int solution(int[] A) {
int minAvgIdx=0;
double minAvgVal=(A[0]+A[1])/2; //At least two elements in A.
double currAvg;
for(int i=0; i<A.length-2; i++){
/**
* We check first the two-element slice
*/
currAvg = ((double)(A[i] + A[i+1]))/2;
if(currAvg < minAvgVal){
minAvgVal = currAvg;
minAvgIdx = i;
}
/**
* We check the three-element slice
*/
currAvg = ((double)(A[i] + A[i+1] + A[i+2]))/3;
if(currAvg < minAvgVal){
minAvgVal = currAvg;
minAvgIdx = i;
}
}
/**
* Now we have to check the remaining two elements of the array
* Inside the for we checked ALL the three-element slices (the last one
* began at A.length-3) and all but one two-element slice (the missing
* one begins at A.length-2).
*/
currAvg = ((double)(A[A.length-2] + A[A.length-1]))/2;
if(currAvg < minAvgVal){
minAvgVal = currAvg;
minAvgIdx = A.length-2;
}
return minAvgIdx;
}
}