3

I'm having problem with optimizing a function AltOper. In set {a, b, c}, there is a given multiplication (or binomial operation, whatsoever) which does not follow associative law. AltOper gets string consists of a, b, c such as "abbac", and calculates any possible answers for the operation, such as ((ab)b)(ac) = c, (a(b(ba)))c = a. AltOper counts every operation (without duplication) which ends with a, b, c, and return it as a triple tuple.

Though this code runs well for small inputs, it takes too much time for bit bulky ones. I tried memoization for some small ones, but apparently it's not enough. Struggling some hours, I finally figured out that its time complexity is basically too large. But I couldn't find any better algorithm for calculating this. Can anyone suggest idea for enhancing (significantly) or rebuilding the code? No need to be specific, but just vague idea would also be helpful.

public long[] AltOper(String str){
    long[] triTuple = new long[3]; // result: {number-of-a, number-of-b, number-of-c}

    if (str.length() == 1){ // Ending recursion condition
        if (str.equals("a")) triTuple[0]++;
        else if (str.equals("b")) triTuple[1]++;
        else triTuple[2]++;
        return triTuple;
    }

    String left = "";
    String right = str;

    while (right.length() > 1){ 
        // splitting string into two, by one character each
        left = left + right.substring(0, 1);
        right = right.substring(1, right.length());
        long[] ltemp = AltOper(left);
        long[] rtemp = AltOper(right);

        // calculating possible answers from left/right split strings
        triTuple[0] += ((ltemp[0] + ltemp[1]) * rtemp[2] + ltemp[2] * rtemp[0]);
        triTuple[1] += (ltemp[0] * rtemp[0] + (ltemp[0] + ltemp[1]) * rtemp[1]);
        triTuple[2] += (ltemp[1] * rtemp[0] + ltemp[2] * (rtemp[1] + rtemp[2]));
    }
    return triTuple;
}
Al Sel
  • 33
  • 3

1 Answers1

1

One comment ahead: I would modify the signature to allow for a binary string operation, so you can easiely modify your "input operation".

java public long[] AltOper(BiFunction<long[], long[], long[]> op, String str) {

I recommend using some sort of lookup table for subportions you have already answered. You hinted that you tried this already:

I tried memoization for some small ones, but apparently it's not enough

I wonder what went wrong, since this is a good idea, especially since your input is strings, which are both quickly hashable and comparable, so putting them in a map is cheap. You just need to ensure, that the map does not block the entire memory by ensuring, that old, unused entries are dropped. Cache-like maps can do this. I leave it to you to find one that suites your personal preferences.

From there, I would run any recursions through the cache check, to find precalculated results in the map. Small substrings that would otherwise be calculated insanely often are then looked up quickly, which cheapens your algorithm drastically.

I rewrote your code a bit, to allow for various inputs (including different operations):

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiFunction;

import org.junit.jupiter.api.Test;

public class StringOpExplorer {

    @Test
    public void test() {
        BiFunction<long[], long[], long[]> op = (left, right) -> {
            long[] r = new long[3];
            r[0] += ((left[0] + left[1]) * right[2] + left[2] * right[0]);
            r[1] += (left[0] * right[0] + (left[0] + left[1]) * right[1]);
            r[2] += (left[1] * right[0] + left[2] * (right[1] + right[2]));
            return r;
        };
        long[] result = new StringOpExplorer().opExplore(op, "abcacbabc");
        System.out.println(Arrays.toString(result));
    }

    @SuppressWarnings("serial")
    final LinkedHashMap<String, long[]> cache = new LinkedHashMap<String, long[]>() {
        @Override
        protected boolean removeEldestEntry(final Map.Entry<String, long[]> eldest) {
            return size() > 1_000_000;
        }
    };

    public long[] opExplore(BiFunction<long[], long[], long[]> op, String input) {
        // if the input is length 1, we return.
        int length = input.length();
        if (length == 1) {
            long[] result = new long[3];
            if (input.equals("a")) {
                ++result[0];
            } else if (input.equals("b")) {
                ++result[1];
            } else if (input.equals("c")) {
                ++result[2];
            }
            return result;
        }

        // This will check, if the result is already known.
        long[] result = cache.get(input);
        if (result == null) {
            // This will calculate the result, if it is not yet known.
            result = applyOp(op, input);
            cache.put(input, result);
        }
        return result;
    }

    public long[] applyOp(BiFunction<long[], long[], long[]> op, String input) {
        long[] result = new long[3];
        int length = input.length();
        for (int i = 1; i < length; ++i) {
            // This might be easier to read...
            String left = input.substring(0, i);
            String right = input.substring(i, length);

            // Subcalculation.
            long[] leftResult = opExplore(op, left);
            long[] rightResult = opExplore(op, right);

            // apply operation and add result.
            long[] operationResult = op.apply(leftResult, rightResult);
            for (int d = 0; d < 3; ++d) {
                result[d] += operationResult[d];
            }
        }
        return result;
    }
}

The idea of the rewrite was to introduce caching and to isolate the operation from the exploration. After all, your algorithm is in itself an operation, but not the 'operation under test'. So now you colud (theoretically) test any operation, by changing the BiFunction parameter.

This result is extremely fast, though I really wonder about the applicability...

TreffnonX
  • 2,924
  • 15
  • 23