Examples
In this lecture we give some example computational problems.
These are not well known computational problems, but rather
examples of what could we get into while analyzing data.
They show useful techniques such as cumulative sums
and bitmasks, though.
Example Problem 1: counting points in many overlapping rectangles
INPUT:
Arrays x and y of size n of integers, arrays x0, y0, x1, y1 of size k.
We know that 0≤x[i],x0[i],x1[i]<xM and 0≤y[i],y0[i],y1[i]<yM.
OUTPUT:
An array res of size k, such that res[i]
is the number of j such that x0[i]≤x[j]<[i] and y0[i]≤y[j]<y1[i].
Geometrically, you are given n points and k rectangles, and you have to
count the number of points in each rectangle.
Our "intended" application:
you are writing an eye-tracking system, and you analyze which parts of the
image people are looking at for the most time. The image is of size 1000x1000
(xM=yM=1000), there are n=1000000 points (multiple people looking at our
picture for some time), and k=1000000 queries (we ask about
each rectangle of some size).
However, we state the problem in the
abstract form, which could be used in many similar applications.
Solution 1: a Python program such as
[sum(x0[i]<=x[j]<x1[i] and y0[i]<=y[j]<y1[i] for j in range(n)) for k in range(k)]
Or a C++ program such as: (untested)
vector<int> res(k, 0);
for(int i=0; i<k; i++)
for(int j=0; j<n; j++)
if(x0[i]<x[j]<x1[i] && y0[i]<y[j]<y1[i])
res[i]++;
We will analyze the time complexity of the C++ program.
The last two lines run in time O(1) (a constant number of
comparisons and additions). The last three lines run in
time n*O(1), which is O(n). The last four lines run in
k*O(n), which is O(kn). The first line needs to fill
a vector with k zeros, which is O(k). The total running
time is O(k) + O(kn) = O(kn). Our program uses memory
O(1) (assuming that we do not count the size of the input
and output into our memory complexity).
Parts of the Python program correspond to the parts of the
C++ program -- it is a bit harder to see the steps, and where memory
is used, but the complexity is the same.
For the size of data given in our "intended application", we
get 1000000000000 -- it would need some time to finish.
Solution 2: We could try counting how many times each point
appears in the data: (untested)
vector<vector<int>> v(yM, vector<int> (xM, 0));
for(int i=0; i<n; i++) counts[y[i]][x[i]]++;
vector<int> res;
for(int i=0; i<k; i++) {
int sum = 0;
for(int y=y0[i]; y<y1[i]; y++)
for(int x=x0[i]; x<x1[i]; x++)
sum += counts[y][x];
res.push_back(sum);
}
First line runs in time O(xM*yM), the second -- O(n),
and counting runs in time O(k*xM*yM). Total time complexity is
O(k*xM*yM+n), and memory complexity is O(xM*yM). For our data,
it is roughly the same as Solution 1, though the constant hidden in the O notation
might be a bit lower.
Solution 3: However, we could use the counting solution given above
to create a much better solution.
We will show the idea on a one-dimensional variant of our problem: we are given
arrays c[n], x0[k], and x1[k]; for each k, compute res[k], the sum of c[i] for
x0[k]≤i<x1[k]. This can be done in time O(n*k) as above, or in time
O(n+k) in the following way: compute the vector cumsum of size n+1, where cumsum[i] is the
sum of c[j] for j<i. It is easy to show that cumsum can be computed in O(n),
and once we have computed cumsum, res[i] can be computed in O(1), using the
formula res[k] = cumsum[x1[k]] - cumsum[x0[k]]!
Our problem is solved in the same way, but in two dimensions: (untested)
vector<vector<int>> v(yM+1, vector<int> (xM+1, 0));
for(int i=0; i<n; i++) counts[y[i]+1][x[i]+1]++;
// compute cumulative sums in each row
for(int y=0; y<=yM; y++) {
int cs = 0;
for(int x=0; x<=xM; x++) {
int& x = counts[y][x];
cs += x; x = cs;
}
}
// compute cumulative sums in each column
for(int x=0; x<=xM; x++) {
int cs = 0;
for(int y=0; y<=yM; y++) {
int& x = counts[y][x];
cs += x; x = cs;
}
}
vector<int> res;
for(int i=0; i<k; i++)
res[i].push_back(
counts[y1[i]][x1[i]] + counts[y0[i]][x0[i]]
- counts[y1[i]][x0[i]] + counts[y0[i]][x1[i]];
This works in time O(xM*yM+k+n), thus, for our "intended application", much faster than the
previous algorithms. Memory complexity is still O(xM*yM).
This technique is called cumulative sums.
In Python, cumulative sums can be computed with
the function cumsum in the library numpy: (untested)
import numpy as np
counts = np.zeros([yM+1, xM+1])
for i in range(n):
counts[y[i]+1][x[i]+1]+=1
np.cumsum(counts, axis=0, out=counts)
np.cumsum(counts, axis=1, out=counts)
res = [counts[y1[i]][x1[i]] + counts[y0[i]][x0[i]] - counts[y0[i]][x1[i]] - counts[y1[i]][x0[i]] for i in range(k)]
Example Problem 2: correlations
INPUT:
Array data of size n times k of 0/1s
OUTPUT:
An array res of size k times k, such that res[i][j]
is the number of rows r of data such that r[i] and r[j] are both 1
Our intended application: we have information about a group of
n=100000000 people, each of them may have or not k=15 binary properties
(young/old, male/female, rich/poor, etc.) We want to see whether
these properties are correlated, and as an intermediary step we
need to compute the above.
As above, there are several solutions:
Solution 1: "Brute force" -- runs in time O(n*k*k).
Could work, although it would take some time.
Solution 2: Use the counting technique. Note that
there are 2k = 32768 possible sets of properties that a given
person could have. We enumerate these sets with numbers:
a given row r corresponds to the set whose index is the sum of
2ir[i] for all i.
For each possible set index s, compute count[s], the number of
people who have that set. Then, for each pair (i,j), sum the appropriate
count[s] of sets s which include both i and j. This runs in time
O(n+2k*k*k), which is much faster. The details are left for
the reader.
Solution 3: The solution above can be improved to O(n+2k*k),
using the technique similar to cumulative sums. Again, the details are left
for the reader. Solutions 2 and 3 run significantly faster than Solution 1
(however, to be honest -- if we used it just for one instance,
implementing them probably would take us much more time than we saved).
Corollary: It is sometimes said that algorithms with exponential
running time are bad. Not necessarily -- we can use them very well if
the data is small, or where the particular aspect of our data that
causes exponential growup is small!