# IOI 2010: Quality of living

The 2010 International Olympiad of Informatics (IOI) finished today. Probably the highest high school level programming competition in the world, with participants from many countries. Anyways.

An interesting problem was Task 3 of Day 1, titled Quality of Living. In a grid of $R$ rows and $C$ columns, each cell is given an unique quality rank, as in 1 being the best, 2 being the second best, and $R \times C$ being the worst.

Given $H$ and $W$ as odd numbers with $H \leq R$ and $W \leq C$, the task is to choose a rectangle of height $H$ and width $W$ so that the median quality rank of the rectangle is at a minimum.

For example, given $R=5$, $C=5$, $H=3$, $W=3$: The best, or smallest median of all possible $3 \times 3$ rectangles in the configuration is the one shown, with a median of 9. Since no $3 \times 3$ rectangle has a smaller median, 9 is the answer to this problem.

For an arbitrary array and parameters $H$ and $W$, how should a program calculate the smallest median?

Let’s consider brute force. There are $(R-H) \times (C-W)$ possible positions for the rectangle to be placed, and we will check each of them. Checking for the median requires sorting, which is of complexity $n \log n$.

In order to earn full points for this problem, the program needs to handle 3000 x 3000 grids in under 10 seconds. In the worst case that $H=1500$ and $W=1500$, this amounts to over 2 million sorts, or many billion operations, which is clearly too much.

### A binary search approach

An efficient solution takes a completely different approach. There is no way to possibly calculate the median for every position.

Instead, we conduct a binary search on $m$, the smallest median. Let us arbitrarily ‘guess’ a value of $m$. For each element on the grid, we assign it -1 if it’s smaller than $m$, 1 if it’s greater than $m$, or 0 if it is equal to (and therefore is) $m$.

In the example above, if we choose 12 for $m$, we get: Now for any sub-rectangle, we can determine if its median is smaller or larger than our guess by summing it up.

If its sum is negative, then its median is smaller than our guess; if it’s positive then the median is larger than our guess. If the sum is exactly 0, it means that the number of elements smaller is equal to the number of elements bigger, implying that the median of this sub-rectangle is our guess.

So if we look at all the sub-rectangles, we can determine if our guess is correct or not. Furthermore, if it’s not correct, we know whether it’s too large, or too small:

If there is any sub-rectangle having a negative sum, then our guess is too big. That particular sub-rectangle has a smaller median, so our median can’t be the smallest anymore.

On the other hand, if all sub-rectangles have a positive sum, then our guess is too small.

We know we’ve got it when none of the sub-rectangles are negative, and at least one sub-rectangle has a 0 sum.

It turns out that going through the sub-rectangles can be done very efficiently:

The plan is, we start from the top of the first column and go down it, and when we reach the end we start at the top of the next column, and so on. We keep doing this until we reach the end.

As we are dealing with $W$ columns at a time, let us keep the sums of the relevant columns in an array wsum[]. For example, in the $m=12$ grid: Now to find the sum of the first sub-rectangle, we add up -2 and 1 and 1, getting 0.

Then we can use this result to find the sum of the next sub-rectangle down. Removing -2 and adding 3, we get 5, which is the sum of it. Thus moving down a space can be done with only 2 additions.

We use a similar idea when moving right a column after having finished our current one. Rather than calculate wsum[] over from scratch, we can take the old wsum[], and for each row, subtract the removed element and add the new element.

This combination of ideas allow us to implement this task very efficiently, well able to solve the 3000 x 3000 subtask within 10 seconds.

I have an implementation in Java:


import java.util.*;
import java.io.*;

public class Main{

static int R = 0;
static int C = 0;
static int W = 0;
static int H = 0;
static int[][] a = null;

public static void main(String[] args) throws Throwable {

// Initialize constants
StringTokenizer l1tok = new StringTokenizer(l1);
R = Integer.parseInt(l1tok.nextToken());
C = Integer.parseInt(l1tok.nextToken());
H = Integer.parseInt(l1tok.nextToken());
W = Integer.parseInt(l1tok.nextToken());

// Parse data into array. Each cell has its own line.
a = new int[R][C];
for(int i=0; i<R; i++)
for(int j=0; j<C; j++)

// Seperate the calculations from the parsing
run();
}

// Try m as the new smallest median, and decide if m should be larger or
// smaller. Return -1 if it should be smaller, 1 if it should be bigger.
// If we have found the smallest median then we return 0.
static int tryMedian(int m){

// Does a median m exist at all in this configuration?
boolean exists = false;

// Individual row sums of the column we're on
int wsum[] = new int[R];

// Initiate wsum for the first column layer
for(int i=0;i<R;i++) {
for(int j=0;j<W;j++) {
int temp=0;
if(a[i][j]>m) temp=1;
if(a[i][j]<m) temp=-1;
if(j==0) wsum[i]=temp;
else wsum[i]+=temp;
}
}

// Outer loop: goes through the columns
for(int i=0;i<=C-W;i++) {

// Sum for the rectangle we're looking at
int block=0;

// First block in the column
for(int j=0;j<H;j++)
block+=wsum[j];

// Go through the rest of the rows
for(int j=0;j<=R-H;j++) {

// Found a negative block: m is too big!
if(block<0) return -1;

// The median exists, so this is it (unless we find a negative)
if(block==0) exists=true;

// As long as we're not at the very end, adjust the sum
if(j!=R-H) block+=wsum[j+H]-wsum[j];
}

// If not at the last column, adjust the wsum
if(i!=C-W)
for(int j=0;j<R;j++) {
int temp=0;

// Remove element at the head
if(a[j][i]>m) temp=1;
if(a[j][i]<m) temp=-1;
wsum[j]-=temp; temp=0;

// Add the new element at the end
if(a[j][i+W]>m) temp=1;
if(a[j][i+W]<m) temp=-1;
wsum[j]+=temp;
}
}

// No negatives; return based on if we've found m as a median or not
if(exists) return 0;
return 1;
}

static void run() {

// Min and max for binary search
int min = 1;
int max = R*C;

while(true) {

// Perform new iteration on binary search
int mid = (max+min)/2;
int result = tryMedian(mid);

// Found lowest median, stop here
if(result == 0){
System.out.println(mid);
return;
}

// Keep searching; direction is determined by the result
if(result == 1) min = mid+1;
else max = mid-1;
}
}
}