QMUL Programming Competition 2016

Ryan Welch / January 30, 2016

11 min read– views

Last week, I along with two friends, Marcello and Chris, competed in the Queen Mary University programming competition. We did quite well. In fact, we won!

The competition was made up of six challenging problems to which we had to think up a solution, implement the solution and submit it to be tested against a ruthless online judge. We were competing against 19 teams, from first year students to fourth year students. I thought it would be interesting to go through the problems as well as the approaches we took.

The online judge

We incurred a lot of penalty time. Although we tested the entries our biggest problem was not handling all edge cases as well as strange bugs. Admittedly that was partly due to not reading the questions properly.

For each problem we also had to parse the input from standard input and parse the data to be able to solve it. I haven't included this in the solutions since it was specific to the competition.

Problem 1

This was a relatively simple problem, the task was to parse some HTML input and extract all the links in a specified format (ignoring duplicates) and then print them out in alphabetical order.

Our algorithm worked something like this:

problem1.java
1
String input = "<a href=\"B\"<a href=\"A\">"; // Test input
2
3
// Array of links found
4
ArrayList<String> links = new ArrayList<String>();
5
6
// Pattern to match any digit and any trailing zeroes
7
Pattern p = Pattern.compile("<a href=\"([^\"]+?)\">");
8
9
Matcher m = p.matcher(input);
10
while(m.find()) {
11
if(!links.contains(m.group(1))) {
12
links.add(m.group(1));
13
}
14
}
15
16
Collections.sort(links);
17
18
for(String s : links) {
19
System.out.println(s);
20
}
21

The interesting part here is the regular expression which does all the heavy lifting by finding the links in the text. ([^"]+?) is our capture group, which captures the link it finds which must be inside the a tag. The [^"] means match any character that isn't a speech mark, the + means at least 1 or more characters and the ? stops the regular expression from being greedy and matching everything between the first tag and the last tag.

Problem 2

A little more complex than the last problem, it required taking a string of bits where each 8 bits formed a character in a secret string. Each 8 bits was XOR-ed against a given set of bits.

Here's a snippet of java code:

problem2.java
1
String output = "";
2
3
int bit_mask = Integer.parseInt("01101110", 2); // Parse as base 2
4
5
int position = 0;
6
while(position < message.length()) {
7
String bitStr = message.substring(position, position + 8);
8
9
int num = Integer.parseInt(bitStr, 2); // Parse as base 2
10
num = num ^ bit_mask; // XOR
11
12
output += (char) num;
13
14
position += 8;
15
}
16
17
System.out.println(output);
18

We tried a few things to convert a string of binary into something we could work with in java before we found that Integer.parseInt had a radix (base) parameter we could use.

Problem 3

The classic, finding prime numbers but with a twist. In this case we had to find the nth prime number after a prime number which we were given.

For example if n is 2, and our starting prime number is 11, then the answer is 17 as it is the second prime number after 11.

Here's the relveant code:

problem3.java
1
public static void main(String[] args) {
2
int n = 2; // Test values
3
int m = 11;
4
5
int count = 0;
6
while(count < n) { // Keep going until we have enough primes
7
if(checkPrime(++m)) count++; // Count the number of primes we find
8
}
9
10
System.out.println(m);
11
}
12
13
// Really simple prime check
14
public static boolean checkPrime(int num) {
15
int sqrt = (int) Math.floor(Math.sqrt(num));
16
17
for(int i = 2; i <= sqrt; i++) {
18
if(num % i == 0) return false;
19
}
20
21
return true;
22
}
23

The prime checking algorithm could probably be improved on, but since we had limited time we were just focused on getting the problems done.

Problem 4

The challenge for this problem was to split a string of digits into numbers where each numbers is between 1 and 10. Therefore each digit is a number unless it has a trailing 0, in which case it is the digit plus the 0.

For example, given the input "310110", the output would be "3 10 1 10". Then we had to calculate the average of these numbers and return the result to two decimal places.

You guessed it, here's the code:

problem4.java
1
String input = "310110"; // Test input
2
3
// Pattern to match any digit and any trailing zeroes
4
Pattern p = Pattern.compile("([0-9]0*)");
5
6
Matcher m = p.matcher(input);
7
8
float sum = 0;
9
float count = 0;
10
while(m.find()) {
11
sum += Integer.parseInt(m.group(0));
12
count++;
13
}
14
15
if(count != 0) System.out.printf("%.02f", sum / count);
16

The regex again is the most important part, in this case it would be trivial to use a loop and check if the next digit is a zero since the only number possible with two digits is a "10". However since we had already used regex in problem 1, I chose to go with the above solution also using regex. This solution also has the added bonus of being more flexible against longer inputs of trailing 0s.

The built-in java formatting in printf was very useful and definitely worthwhile knowing how to use.

Problem 5

We spent a while working on the algorithm for this problem, longer than the others, and it took some trial and error. We had some annoying bugs in our code which took a while to track down. In this problem we are asked to calculate the surface area of the walls in a 3d arrangement of cubes. Our input consists of a 2d matrix which contains height data for each position.

Here's the java solution similar to the one we submitted, it's very rough but it works which is what's important when you're under pressure!

problem5.java
1
int[][] map = { // Test data
2
{ 3, 2, 2 },
3
{ 1, 3, 2 }
4
};
5
6
int rows = map.length;
7
int columns = map[0].length;
8
9
int total = 0;
10
11
// First: Add the EAST and WEST walls
12
// Iterate loop along rows
13
for(int i = 0; i < rows; i++) {
14
// Get first and last cubes in the row
15
total += map[i][0];
16
total += map[i][columns - 1];
17
}
18
19
// Second: Add the NORTH and SOUTH walls
20
// Iterate loop along columns
21
for(int i = 0; i < columns; i++) {
22
// Get top and bottom cubes in a column
23
total += map[0][i];
24
total += map[rows - 1][i];
25
}
26
27
// Third: Find all EAST to WEST inner walls
28
for(int i = 0; i < rows; i++) {
29
for(int j = 0; j < columns - 1; j++) {
30
if(map[i][j] > map[i][j + 1]) {
31
total += map[i][j] - map[i][j + 1];
32
} else {
33
total += map[i][j + 1] - map[i][j];
34
}
35
}
36
}
37
38
// Fourth: Find all NORTH to SOUTH inner walls
39
for(int j = 0; j < columns; j++) {
40
for(int i = 0; i < rows - 1; i++) {
41
if(map[i][j] > map[i + 1][j]) {
42
total += map[i][j] - map[i + 1][j];
43
} else {
44
total += map[i + 1][j] - map[i][j];
45
}
46
}
47
}
48
49
System.out.println(total);
50

There's plenty of room for improvement in this code, and after playing about with it now, I came up with the following improved code:

problem5.java
1
int[][] map = { // Test data
2
{ 3, 2, 2 },
3
{ 1, 3, 2 }
4
};
5
6
int rows = map.length;
7
int columns = map[0].length;
8
9
int total = 0;
10
11
for(int i = -1; i < rows; i++) {
12
for(int j = -1; j < columns; j++) {
13
14
// EAST to WEST
15
if((j == -1 || j == columns - 1) && i != -1) {
16
total += map[i][(j == -1 ? 0 : columns - 1)];
17
} else if(i != -1) {
18
total += Math.abs(map[i][j] - map[i][j + 1]);
19
}
20
21
// NORTH to SOUTH
22
if((i == -1 || i == rows - 1) && j != -1) {
23
total += map[(i == -1 ? 0 : rows - 1)][j];
24
} else if(j != -1) {
25
total += Math.abs(map[i][j] - map[i + 1][j]);
26
}
27
}
28
}
29
30
System.out.println(total);
31

The code is a bit obscure and certainly some of the expressions and in-line if statements make it hard to follow however it's much smaller than the previous code. While it's hard to come up with this code straight away, it would certainly be much faster to write.

Problem 6

Unfortunately we did not get time to implement the solution to this during the competition, however for the sake of completion I've decided to write a solution for it. In my opinion this is the most interesting question in the programming competition, it is based on a NP complete problem, specifically the question is a graph colouring problem.

We are given two variables, M and N, where the former is the number of rooms and the latter is the number of people. Then we are given a series of lines with lists of numerical IDs, each line corresponds to one person and the numerical IDs in the line are the people that they cannot be in a room with.

For example:

1
3
2
7
3
2 5 7
4
3 4
5
5 6
6
7
2 3
8
4
9
2
10

Means that there are 3 rooms and 7 people. Person 1 cannot share a room with person 2, 5 or 7. Person 2 cannot share a room with person 3 or 4, and so on.

This is a graph colouring problem, we create a graph where each person is a node and each line connecting the nodes is a relationship where those two people cannot be in the same room. We then attempt to colour the graph so that two connected nodes do not have the same colour using only a certain amount of colours. The amount of colours we can use are the number of rooms in the problem.

Below is a commented example program that will solve the above problem:

problem6.java
1
class Main {
2
// Number of rooms
3
private int numColors = 3;
4
// Number of people
5
private int numNodes = 7;
6
// People and who they cannot share a room with
7
private int[][] nodes = {
8
{2, 5, 7},
9
{3, 4},
10
{5, 6},
11
{},
12
{2, 3},
13
{4},
14
{2}
15
};
16
17
// Keep track of assigned colors, 0 means unassigned
18
private int[] solution = new int[numNodes];
19
20
public static void main(String[] args) {
21
// Solve it
22
if((new Main()).solve(0)) {
23
System.out.println("YES");
24
} else {
25
System.out.println("NO");
26
}
27
}
28
29
public boolean solve(int node) {
30
// We solved all nodes!
31
if(node == numNodes) {
32
return true;
33
}
34
35
// Try all colors for this node
36
for(int color = 1; color <= numColors; color++) {
37
if(checkNode(node, color)) {
38
// We found a possible solution for this node, lets check the rest...
39
40
// Our possible color
41
solution[node] = color;
42
43
// Let's check the rest of the nodes
44
if(solve(node + 1)) {
45
// If it works, then we have a solution
46
return true;
47
} else {
48
// If not, let's reset our color and keep looking
49
solution[node] = 0;
50
}
51
}
52
}
53
54
return false; // No solution found
55
}
56
57
private boolean checkNode(int node, int color) {
58
// Iterate over all connections to node
59
for(int i = 0; i < nodes[node].length; i++) {
60
int adjacent = nodes[node][i] - 1;
61
// If connected node has the same color, it's not valid
62
if(solution[adjacent] == color) return false;
63
}
64
65
// No clashes found, all good
66
return true;
67
}
68
}
69

This solution can be applied to any graph coloring problem. During the competition we did not realise that the problem was a graph colouring problem and so we spent time trying to think of efficient solutions to no avail.

That was it, all 6 problems. We never thought we would do so well, but we proved ourselves wrong, it was a fun experience and I hope to enter more competitions in the future. Feel free to use any of the solutions from this post.

© 2023 Ryan Welch. All rights reserved.