An Optimal Solution of a Problem in Java – Imperative or Functional ?

Since March 2005 I participate in the TopCoder programming and data science contests, which allows to train programming and problem solving skills. One of those contests, TopCoder Single Round Match 670 (taking 1.5 hour), brought a very interesting problem. It turned out to be not straightforward – over 30% of the submitted solutions were incorrect. The lessons learned during that contest are very useful for every Java developer. They emphasize the importance of an optimal solution:
What is the optimal solution of the problem? Why is it optimal?

This article will provide answers to the mentioned questions. It demonstrates an optimal solution in Java implemented with an imperative approach and with a functional approach. It discusses complexity, performance, safety and maintainability of both implementations. First, let us take a look into the problem description.

Problem Statement

A plus/minus string is a string in which each character is either a ‘+’ or a ‘-‘.
The balance of a plus/minus string is computed as the number of ‘+’ characters minus the number of ‘-‘ characters.

For example, the balance of the string “++-+” is 3-1 = 2, and the balance of the string "---" is 0-3 = -3.

The prefix of a string S is any string that can be obtained by removing some (possibly none, possibly all) characters from the end of S. For example, the prefixes of the string “++-+” are the strings “++-+”, “++-“, “++”, “+”, and “”.

Given a plus/minus string, its negativity is the number of its prefixes that have a negative balance. For example, the negativity of the string “++-+” is 0, as none of its prefixes have a negative balance. The negativity of the string "---" is 3. Its three prefixes with a negative balance are "-", "--", and "---".

You are given a String s that is a plus/minus string. You are also given an int k. Your goal is to change s into a string with negativity at most k. In other words, you want to change s into a string that has at most k prefixes that have a negative balance.

In order to change s you are going to perform a sequence of zero or more steps. In each step you can change a single ‘-‘ character in s into a ‘+’ or vice versa. Compute and return the smallest number of steps needed.

Definition

Constraints

s will contain between 1 and 50 characters, inclusive.

k will be between 0 and the length of s, inclusive.

Each character in s will be either ‘+’ or ‘-‘.

Example

s: "---"
k: 1

Returns: 1

One step is sufficient. If we change character 0 of s into a ‘+’, we will obtain the string "+--". This string has only one prefix with a negative balance – namely, the entire string "+--". As k=1, we have reached our goal.

Optimal solution

What is an optimal solution?Typically the optimal solution is the simplest one – easiest to understand by other developer and easiest to maintain. Its performance and memory usage must be good enough for the use cases. In case of the constrains described above, it must fit within mentioned limits. But first of all, the solution must be correct. The simpler is the solution, the less are chances for a mistake and the easier is its future maintenance.

Solution explanation

The simplest solution, which is easiest to implement, understand and maintain, looks as follows:

(1) represent every ‘+’ and ‘-‘ as +1 and -1, respectively

(2) create an array with sub-sums related to each position of s;
in case of the given example (s="---"), the array of sub-sums will be: (-1, -2, -3)

(3) main loop: as long as the total number of sub-sums is greater than k (initially 3 in the given example), change next -1 to +1:

(4) increase the changes counter

(5) logically replace one -1 with +1
– it means, increase value of every sub-sum by 2 (as 1 -(-1) == 2);
in case of the given example, after first change the array of sub-sums will be:
(1, 0, -1)

(6) return the changes counter

The solution has square complexity – in other words, the algorithm complexity is approximated O(n2). In the main loop we may perform up to about 2×502 == 5,000 operations, because we may have up to 50 elements. It fully fits within the time limit (2s) – within that time we could perform about 2 billions (2×109) of operations.

From the perspective of memory usage, the solution is very effective. The data is stored into a table of int, which in the worst case can use 50×4 == 200 bytes (in fact, the table consumes a little bit more memory, but we can ignore internal data here).

Based on mentioned characteristics we can say, that the described solution is the optimal one.

Let us take a look into two different approaches while implementing it.

Imperative approach

Since Java version 1, the solution can be implemented with imperative approach – most common for Java developers. The following implementation in Java 5 is using such approach:

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

publicclassPositiveNegativeBalance{

publicintcalculateRequiredChanges(Strings,intk){

intchangesCount=0;

intsubSum=0;

int[]subSumsArr=newint[s.length()];// (2)

// (2) Initialize an array of sub-sums

for(inti=0;i<subSumsArr.length;i++){

intv=(s.charAt(i)=='+')?1:-1;// (1)

subSum+=v;

subSumsArr[i]=subSum;

}

// (3a) Check if number of negative sub-sums is greater than "k"

while(negativeSubSumsCount(subSumsArr)>k){

++changesCount;// (4)

// (5) Recalculate sub-sums after change

for(inti=0;i<subSumsArr.length;i++){

subSumsArr[i]+=2;

}

}

returnchangesCount;// (6)

}

// (3b) Count the number of negative sub-sums

privateintnegativeSubSumsCount(int[]subSumsArr){

intcount=0;

for(intsubSum:subSumsArr){if(subSum<0){++count;}}

returncount;

}

}

Execution time, measured by TopCoder, varies between 2-8ms – depending on a case.

Functional approach

The solution can be implemented in an alternative way – with a functional approach. In Java 8 it can be done much easier as before – thanks to new extensions of the Java language (i.e. lambda expressions) and the standard API (i.e. streams). The implementation using a functional approach looks as follows:

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

publicclassPositiveNegativeBalance{

publicintcalculateRequiredChanges(Strings,intk){

intchangesCount=0;

AtomicInteger subSumAccumulator=newAtomicInteger();

// (1)(2) Initialize an array of sub-sums

int[]subSumsArr=s.chars().map(c->c=='+'?1:-1)

.map(v->subSumAccumulator.addAndGet(v)).toArray();

// (3) Check if number of negative sub-sums is greater than "k"

while(Arrays.stream(subSumsArr)

.filter(subSum->subSum<0).count()>k){

++changesCount;// (4)

// (5) Recalculate sub-sums after change

subSumsArr=Arrays.stream(subSumsArr)

.map(subSum->subSum+2).toArray();

}

returnchangesCount;// (6)

}

}

Execution time, measured by TopCoder, varies between 65-122ms – depending on a case.

Comparison of implementations

What can we say about code complexity of both implementations? Let us compare them step by step.

Step

Imperative approach

Functional approach

(1) (2)

1 loop + 4 statements

1 statement
(including 1 functional expression)

(3)

1 function + 2 loops + 4 statements

1 loop
(including 1 functional expression)

(4)

1 statement

1 statement

(5)

1 loop + 1 statement

1 statement
(including 1 functional expression)

(6)

1 statement

1 statement

We can notice that the functional approach is about 33% shorter than the imperative one. In the functional approach, each step is simplified into a single statement.

What can we say about the safety of both implementations?

In the imperative approach, we change the state of subSumsArr partially. If the loop from step (2) or from step (5) would be interrupted, we may end up with an inconsistent state of subSumsArr.

In the functional approach, we always have a consistent state of subSumsArr.

What can we say about the performance of both implementations?

We can notice, that the functional approach is about 16-32 times slower (depending on a case), than the imperative one. The main difference is in step (5): In the functional approach, we always create a modified a copy of the subSumsArr, which finally replaces the original. It shows, that typically the higher code safety, the lower its performance. Our functional implementation is a good trade-off – it is safe enough and it perfectly fits into the time limit (2s).

Finally, what can we say about the maintainability of both implementations?

The imperative one does not contain any limitations – another developer can extend or change the behaviour at every place. The code contains many atomic statements. Adding or moving them can introduce a tricky mistake or bug, difficult to spot.

The functional implementation, however, introduce some limitations. Statements are more composite and each of them serves for a specific purpose. The functional approach makes it more difficult to introduce a tricky mistake or bug; suspicious or tricky constructs are more difficult to create and easier to spot.

During modifications there may occur a need to debug the code. How easy is it to debug each of the implementations?
The imperative implementation can be easily debugged step-by-step, breakpoints can be set at every atomic statement.
In case of the functional implementation, debugging is different. We are usually not focused on every minor value, but rather on states. Some values or states, however, cannot be examined. Can we check, for example, if point (1) of our solution works correctly (if ‘+’ and ‘-‘ are mapped correctly to +1 and -1)? Unfortunately, we cannot.

Solutions on programming contests are not modified afterwards, but commercial software evolves continuously. Debug possibility, related to software maintainability, can be an important point which should not be overlooked.

Summary

What can we learn from this article?

First, we read the problem, which we are solving. Next we came up with the optimal solution – the one which is safest to implement, easiest to understand by others and easiest to maintain. Then we implemented it in two ways: imperative and functional.

Then we took a closer look into both implementations – we compared their code complexity, safety, performance and maintainability. We observed, that the implementation with functional approach is:

simpler than the imperative one – it is more compact and expressive and is about 33% shorter

safer than the imperative one – data always stays in the consistent state (no partial data updates)

slower than the imperative one – it is about 16-32 times slower (depending on a case); that is the “price” of the higher safety

easier maintainable than the imperative one – it can be easier and safer modified or extended;
on the other hand, debug of the functional implementation is more difficult – some expressions cannot be easily debugged

The optimal solution of a problem is usually not the easiest one to create. But the effort is worthwhile: once designed and created, it makes the software cheaper – less chances to introduce a mistake or bug and easier maintenance in the future.

Never miss an update by following us and subscribing to our monthly newsletter!