In-Class Exercise 6.1: Write pseudocode to solve the following problem. Suppose you are given a 2D array of integers and that each position in the array has the value 0 or 1. (Thus, a binary matrix). A sub-array of the matrix is considered a "chessboard" if the following properties hold: (1) the number of rows equals the number of columns (i.e. it is square); (2) there are at least two rows; (3) the 0's and 1's alternate as in a chessboard (i.e., none of the four neighboring positions of a 0 has a 0). Your goal is to find the largest sub-matrix that is a chessboard. Analyse the complexity of your algorithm when the array size is N x N. Note: If there's time in class to implement, this template provides a starting point.
Most programming languages include supporting tools such as: (Java example)
A profiler
A Java profiling example:
public class ProfileTest {
static int numTrials; // The number of trials to use in averaging.
static int size; // Data (array) size.
static int[] data; // The data.
// Find partial maximum: the max value in data[0],...,data[limit].
static int findMax (int limit)
{
int max = data[0];
for (int i=1; i < limit; i++)
if (data[i] > max)
max = data[i];
return max;
}
// Find partial sum: the sume of data[0],...,data[limit].
static double findSum (int limit)
{
double sum = 0;
for (int i=0; i < limit; i++)
sum += data[i];
return sum;
}
static void estimateMax()
{
// 1. Maintain total.
double total = 0;
// 2. Repeat numTrials times.
for (int n=0; n < numTrials; n++) {
// 2.1 Pick a sub-array randomly.
int limit = (int) UniformRandom.uniform (0, size-1);
// 2.2 Compute partial max.
int value = findMax (limit);
// 2.3 Accumulate.
total += value;
}
// 3. Compute average.
double avg = (double) total / (double) numTrials;
// 4. Output.
System.out.println ("Estimate of maximum=" + avg);
}
static void estimateSum()
{
// 1. Maintain total.
double total = 0;
// 2. Repeat numTrials times.
for (int n=0; n < numTrials; n++) {
// 2.1 Pick a sub-array randomly.
int limit = (int) UniformRandom.uniform (0, size-1);
// 2.2 Compute partial sum.
double value = findSum (limit);
// 2.3 Accumulate.
total += value;
}
// 3. Compute average.
double avg = (double) total / (double) numTrials;
// 4. Output.
System.out.println ("Estimate of sum=" + avg);
}
static void createData ()
{
// Draw the data randomly between 0 and "size":
data = new int [size];
for (int i=0; i < size; i++)
data[i] = (int) UniformRandom.uniform (1, size);
}
public static void main (String[] argv)
{
try {
// Command-line arguments contain the array size and number of trials.
if ( (argv == null) || (argv.length != 2) ) {
System.out.println ("Usage: java ProfileTest ");
System.exit(1);
}
// Obtain the size of the data.
size = Integer.parseInt (argv[0].trim());
// Obtain the number of trials used in averaging.
numTrials = Integer.parseInt (argv[1].trim());
// Create random data.
createData ();
// Estimate the average partial-maximum.
estimateMax ();
// Estimate the average partial-sum.
estimateSum ();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
java ProfileTest 1000 100000
java -Xrunhprof:cpu=samples,file=log.txt,depth=10 ProfileTest 1000 100000
java -Xrunhprof:help
CPU SAMPLES BEGIN (total = 37)
rank self accum count trace method
1 32.43% 32.43% 12 15 ProfileTest.findSum
2 18.92% 51.35% 7 13 ProfileTest.findMax
3 5.41% 56.76% 2 19 java.lang.StrictMath.floor
4 5.41% 62.16% 2 3 java.util.jar.Manifest.parseName
5 5.41% 67.57% 2 18 UniformRandom.uniform
6 2.70% 70.27% 1 8 java.util.HashMap.put
7 2.70% 72.97% 1 7 java.lang.String.< init >
8 2.70% 75.68% 1 2 java.lang.Character.toLowerCase
9 2.70% 78.38% 1 9 java.util.jar.JarFile.getManifest
10 2.70% 81.08% 1 10 java.util.jar.Attributes.read
11 2.70% 83.78% 1 6 java.lang.Object.clone
12 2.70% 86.49% 1 4 java.util.jar.Attributes.< init >
13 2.70% 89.19% 1 1 java.util.jar.Manifest.parseName
14 2.70% 91.89% 1 14 java.lang.FloatingDecimal.dtoa
15 2.70% 94.59% 1 5 java.util.jar.Attributes.< init >
16 2.70% 97.30% 1 16 UniformRandom.uniform
17 2.70% 100.00% 1 17 java.lang.Math.floor
CPU SAMPLES END
Thus, the most time was spent in findSum, according to
this estimate.
public class ProfileTest2 {
// ...
// findMax now returns the stored value.
static int findMax (int limit)
{
return max[limit];
}
// ...
static void estimateMax()
{
// 1. First find partial maximums.
max = new int [data.length];
for (int k=0; k < max.length; k++) {
max[0] = data[0];
int m = data[0];
// 1.1 Find the partial maximum for each value of k.
for (int i=1; i<=k; i++)
if (data[i] > m)
m = data[i];
// 1.2 Store as k-th partial maximum.
max[k] = m;
}
// 2. Now estimate.
double total = 0;
// 3. Repeat numTrials times.
for (int n=0; n < numTrials; n++) {
// 3.1 Pick a sub-array randomly.
int limit = (int) UniformRandom.uniform (0, size-1);
// 3.2 Compute partial max.
int value = findMax (limit);
// 3.3 Accumulate.
total += value;
}
// 4. Compute average.
double avg = (double) total / (double) numTrials;
// 5. Output.
System.out.println ("Estimate of maximum=" + avg);
}
// ...
}
CPU SAMPLES BEGIN (total = 28)
rank self accum count trace method
1 32.14% 32.14% 9 18 ProfileTest2.findSum
2 7.14% 39.29% 2 12 ProfileTest2.estimateMax
3 7.14% 46.43% 2 15 UniformRandom.uniform
4 3.57% 50.00% 1 3 java.lang.Object.clone
5 3.57% 53.57% 1 16 UniformRandom.uniform
6 3.57% 57.14% 1 20 UniformRandom.uniform
7 3.57% 60.71% 1 14 java.lang.StrictMath.floor
8 3.57% 64.29% 1 7 java.lang.StringBuffer.append
9 3.57% 67.86% 1 1 java.util.jar.Manifest.parseName
10 3.57% 71.43% 1 13 UniformRandom.uniform
11 3.57% 75.00% 1 19 UniformRandom.uniform
13 3.57% 82.14% 1 2 java.util.jar.Manifest.read
14 3.57% 85.71% 1 4 java.util.jar.Attributes.read
15 3.57% 89.29% 1 6 java.util.Properties.load
16 3.57% 92.86% 1 8 sun.net.www.protocol.file.Handler.openConnection
17 3.57% 96.43% 1 10 java.net.URL.equals
18 3.57% 100.00% 1 17 java.lang.StrictMath.floor
CPU SAMPLES END
public class ProfileTest3 {
// ...
static void estimateMax()
{
// 1. More efficient computation of partial maximums.
max = new int [data.length];
int currentMax = data[0];
max[0] = data[0];
for (int i=1; i < data.length; i++) {
// Track current maximum.
if (data[i] > currentMax)
currentMax = data[i];
// The current maximum is tracked in exactly the order we need it.
max[i] = currentMax;
}
// ...
}
// ...
}
A C profiling example:
double *data; // The data.
int N; // Size of the data.
int numTrials; // Number of trials to use in estimation.
// Random-number generator
static r_seed = 12345678L;
double uniform ()
{
static long m = 2147483647;
static long a = 48271;
static long q = 44488;
static long r = 3399;
long t, lo, hi;
hi = r_seed / q;
lo = r_seed - q * hi;
t = a * lo - r * hi;
if (t > 0)
r_seed = t;
else
r_seed = t + m;
return ( (double) r_seed / (double) m );
}
// Build random array
double* makeRandomArray (int length)
{
int i;
double *A = (double*) malloc (sizeof(double) * length);
for (i=0; i < length; i++) {
A[i] = floor (length*uniform ());
}
return A;
}
// Find partial maximum: the max value in data[0],...,data[limit].
int findMax (int limit)
{
int i;
double sum;
int max;
max = data[0];
for (i=1; i < limit; i++)
if (data[i] > max)
max = data[i];
return max;
}
// Find partial sum: the sum of data[0],...,data[limit].
double findSum (int limit)
{
int k;
double sum;
sum = 0;
for (k=0; k < limit; k++)
sum += data[k];
return sum;
}
void estimateMax ()
{
double total, avg;
int n, value, limit;
// 1. Maintain total.
total = 0;
// 2. Repeat numTrials times.
for (n=0; n < numTrials; n++) {
// 2.1 Pick a sub-array randomly.
limit = floor (N*uniform());
// 2.2 Compute partial max.
value = findMax (limit);
// 2.3 Accumulate.
total += value;
}
// 3. Compute average.
avg = total / (double) numTrials;
// 4. Output.
printf ("Estimate of maximum=%lf\n", avg);
}
void estimateSum ()
{
double total, avg;
int n, value, limit;
// 1. Maintain total.
total = 0;
// 2. Repeat numTrials times.
for (n=0; n < numTrials; n++) {
// 2.1 Pick a sub-array randomly.
limit = floor (N*uniform());
// 2.2 Compute partial sum.
value = findSum (limit);
// 2.3 Accumulate.
total += value;
}
// 3. Compute average.
avg = total / (double) numTrials;
// 4. Output.
printf ("Estimate of sum=%lf\n", avg);
}
int main ()
{
// Set data size and number of trials.
N = 1000;
numTrials = 100000;
// Create random data.
data = makeRandomArray (N);
// Estimate the average partial-maximum.
estimateMax();
// Estimate the average partial-sum.
estimateSum();
}
gcc -pg profiletest.c -oprofiletest -lm
profiletest
gprof
% cumulative self self total
time seconds seconds calls ms/call ms/call name
48.1 3.15 3.15 100000 0.03 0.03 findMax [4]
45.0 6.10 2.95 100000 0.03 0.03 findSum [6]
3.1 6.30 0.20 internal_mcount [7]
1.4 6.39 0.09 603012 0.00 0.00 .umul [9]
1.1 6.46 0.07 201000 0.00 0.00 .div [10]
0.6 6.50 0.04 1 40.00 3284.53 estimateMax [3]
0.3 6.52 0.02 201000 0.00 0.00 __floor [11]
0.3 6.54 0.02 1 20.00 3064.53 estimateSum [5]
0.2 6.55 0.01 201000 0.00 0.00 uniform [8]
0.0 6.55 0.00 32 0.00 0.00 _return_zero [299]
0.0 6.55 0.00 16 0.00 0.00 _mutex_lock [300]
0.0 6.55 0.00 16 0.00 0.00 mutex_unlock [21]
...
How profiling works:
Limitations of profiling:
Timing sections of code:
long startTime = System.currentTimeMillis();
// ... algorithm runs here ...
double timeTaken = System.currentTimeMillis() - startTime;
#include <sys/times.h>
// NOTE: times.h may lie in different directories in some systems.
// This example compiles on standard Linux distributions.
static struct tms t_record;
static double timer_start_time, timer_end_time;
void start_timer ()
{
times (&t_record);
timer_start_time = (double) (t_record.tms_utime + t_record.tms_stime);
}
double stop_timer ()
{
times (&t_record);
timer_end_time = (double) (t_record.tms_utime + t_record.tms_stime);
return (timer_end_time - timer_start_time);
}
double timer_difference ()
{
return (timer_end_time - timer_start_time);
}
int main ()
{
double elapsed_time;
// Start timing.
start_timer();
// ... compute ...
// Get time taken.
elapsed_time = stop_timer();
}
System performance:
Stepwise refinement not only applies to coding, but also to problem-solving.
We'll use an example to illustrate: the maximal rectangle problem
First attempt: the obvious algorithm
public class NaiveMaxRect implements MaxRectangleAlgorithm {
// ...
// See if the subrectangle (i,j,a,b) is filled with 1's.
boolean checkFilled (int i, int j, int a, int b)
{
for (int k1=i; k1 <= a; k1++) {
for (int k2=j; k2 <= b; k2++) {
if (A[k1][k2] == 0) {
// Quit as soon as a zero is detected.
return false;
}
}
}
return true;
}
// Compute the area of rectangle (i,j,a,b)
int computeArea (int i, int j, int a, int b)
{
// Bad input:
if (a < i)
return -1;
if (b < j)
return -1;
// Area.
return (a-i+1) * (b-j+1);
}
// The algorithm.
public int findMaxRectangleArea (int[][] A)
{
// ...
// 1. Initialize.
int maxArea = 0;
// 2. Outer double-for-loop to consider all possible positions
// for topleft corner.
for (int i=0; i < M; i++) {
for (int j=0; j < N; j++) {
// 2.1 With (i,j) as topleft, consider all possible bottom-right corners.
for (int a=i; a < M; a++) {
for (int b=j; b < N; b++) {
// 2.1.2 See if rectangle(i,j,a,b) is filled.
boolean filled = checkFilled (i, j, a, b);
// 2.1.3 If so, compute it's area.
if (filled) {
// Check area.
int area = computeArea (i, j, a, b);
// If the area is largest, adjust maximum and update coordinates.
if (area > maxArea) {
maxArea = area;
topLeftX = i; topLeftY = j;
botRightX = a; botRightY = b;
}
}
}
} // end-3rd-for
} // end-2nd-for
} // end-outermost-for
return maxArea;
}
// ...
}
Algorithm: naiveMaxRect (A)
Input: an array with m rows and n columns
1. for i=0 to ... // for each top-left corner
2. for j=0 to ...
3. for a=i to ... // for each bottom right corner
4. for b=j to ...
// Check if rectangle defined by i,j,a,b is filled
5. if checkFilled(i,j,a,b)
6. if larger than largest-so-far
7. record i,j,a,b as largest-so-far
8. endif
9. endif
10. endfor
11. endfor
12. endfor
13. endfor
Some improvements:
// ...
public int findMaxRectangleArea (int[][] A)
{
// ...
// 1. Check if array is all zeroes: this is O(mn) work.
boolean found = false;
outer:
for (int i=0; i < M; i++) {
for (int j=0; j < N; j++) {
if (A[i][j] == 1) {
found = true;
topLeftX = botRightX = i;
topLeftY = botRightY = j;
break outer;
}
}
}
// 2. If all zeroes, no further checks are required.
if (! found)
return 0;
// 3. We know there's at least one 1 x 1 rectangle (of area 1).
int maxArea = 1;
// 4. Outer double-for-loop to consider all possible positions
// for topleft corner.
for (int i=0; i < M; i++) {
for (int j=0; j < N; j++) {
// 4.1 With (i,j) as topleft, consider all possible bottom-right corners.
for (int a=i; a < M; a++) {
for (int b=j; b < N; b++) {
// 4.1.1 No need to check size-1 rectangles.
if ( (a == i) && (b == j) )
continue;
// 4.2.1 If a corner is zero, no need to check further.
if ( (A[i][j] == 0) || (A[a][b] == 0) )
continue;
// 4.2.2 First compute area to see if we should scan for 1's.
int area = computeArea (i, j, a, b);
if (area > maxArea) {
// 4.2.2.1 Only if area is larger should we bother checking.
boolean filled = checkFilled (i, j, a, b);
if (filled) {
maxArea = area;
topLeftX = i; topLeftY = j;
botRightX = a; botRightY = b;
}
} // endif-area
} // end-innermost-for
} // end-3rd-for
} // end-2nd-for
} // end-outermost-for
return maxArea;
}
// ...
Analysis (for both variations):
Let's see if we can avoid some unnecessary comparisons:

⇒ Small rectangles enclosed by larger ones are always scanned
when processing the larger ones.


// ...
// Start with top left at i,j and find largest rectangle of 1's.
// Use java.awt.Point to store and return two integers.
Point growRegion (int i, int j)
{
// 1. best_a and best_b will record the best bottom-right corner so far.
int best_a = i, best_b = j;
// 2. a and b will range over possible locations for the bottom-right corner.
int a = i, b = j;
// 3. There is no need to search below rowMax, which is updated
// as we proceed.
int rowMax = M-1;
// 4. Scan left to right along row i using index b as long as there are 1's.
while ( (b <= N-1) && (A[i][b]) != 0) {
// 4.1 Start at the highest possible row, row i.
a = i;
// 4.2 Descend into current column (column b) as far down as possible.
while ( (a <= rowMax) && (A[a][b] == 1) )
a = a + 1;
// 4.3 Back up to the last "1".
a = a - 1;
// 4.4 Update rowMax if we stopped at an earlier row.
if (a < rowMax)
rowMax = a;
// 4.5 Check to see if found a larger rectangle.
int area = computeArea (i, j, a, b);
// 4.6 If the rectangle is larger, update.
if (area > maxArea) {
best_a = a;
best_b = b;
maxArea = area;
topLeftX = i; topLeftY = j;
botRightX = best_a; botRightY = best_b;
}
// 4.7 Continue with next column.
b++;
} // endwhile
// 5. Return best bottom-right corner.
return new Point (best_a, best_b);
}
public int findMaxRectangleArea (int[][] A)
{
// ...
// 1. Check if array is all zeroes.
// ...
// 2. If all zeroes, no further checks are required.
// 3. We know there's at least one 1 x 1 rectangle (of area 1).
maxArea = 1;
// 4. Outer double-for-loop to consider all possible positions
// for topleft corner.
for (int i=0; i < M-1; i++) {
for (int j=0; j < N-1; j++) {
// 4.1 Find the largest possible rectangle with topleft at i,j.
Point p = growRegion (i, j);
// NOTE: growRegion itself updates the current largest rectangle,
// so there's no need to do it here.
}
} // end-outermost-for
// 5. Return value.
return maxArea;
}
// ...
An improvement:


// ...
// Start with top left at i,j and find largest rectangle of 1's.
Point growRegion (int i, int j)
{
// 1. best_a and best_b will record the best bottom-right corner so far.
int best_a = i, best_b = j;
// 2. a and b will range over possible locations for the bottom-right corner.
int a = i, b = j;
// 3. There is no need to search below rowMax, which is updated
// as we proceed.
int rowMax = M-1;
// 4. Scan left to right along row i using index b as long as there are 1's.
while ( (b <= N-1) && (A[i][b]) != 0) {
// Replace this:
// a = i;
// while ( (a <= rowMax) && (A[a][b] == 1) )
// a = a + 1;
// a = a - 1;
// with:
// 4.1 Descend into current column (column b) as far down as possible - in time O(1)!
a = i + cache[b] - 1;
// 4.2 Update rowMax if we stopped at an earlier row.
if (a < rowMax)
rowMax = a;
else
a = rowMax;
// 4.3 Check to see if found a larger rectangle.
int area = computeArea (i, j, a, b);
// 4.4 If the rectangle is larger, update.
if (area > maxArea) {
// ...
}
// 4.5 Continue with next column.
b++;
} // endwhile
// 5. Return best bottom-right corner.
return new Point (best_a, best_b);
}
// For each row, create the cache that's used repeatedly in the row.
void fillCache (int i)
{
// 1. Initialize, since cache is created just once.
Arrays.fill (cache, 0);
// 2. Walk across the columns.
for (int j=0; j < N; j++) {
// 2.1 For each column position (i.e., potential top-left corner),
// find the longest column of 1's.
for (int a=i; a < M; a++) {
if (A[a][j] == 0)
break;
else
cache[j] ++;
}
} // end-column-scan.
}
public int findMaxRectangleArea (int[][] A)
{
// ...
// Create space for cache - use maximum possible size.
cache = new int [N];
// ...
for (int i=0; i < M-1; i++) {
// Fill cache for row i.
fillCache (i);
// Scan columns in row.
for (int j=0; j < N-1; j++) {
// Find the largest possible rectangle with topleft at i,j.
Point p = growRegion (i, j);
}
} // end-outermost-for
// ...
}
// ...
Note: the material on the maximal rectangle problem is based on an article by D.Vanderwoode in Dr.Dobbs Journal, 1998. Much of the code is completely re-written here (in Java).
What did we learn in this module?
In-Class Exercise 6.2:
Solve the Community Problem.