3

Found this algorithm online, I can't work out how to do the maths to find its complexity. I do understand its worst case is 2^n

// A simple recursive function to check whether C is an interleaving of A and B
bool isInterleaved(char *A, char *B, char *C)
{
    // Base Case: If all strings are empty
    if (!(*A || *B || *C))
        return true;

    // If C is empty and any of the two strings is not empty
    if (*C == '\0')
        return false;

    // If any of the above mentioned two possibilities is true,
    // then return true, otherwise false
    return ( (*C == *A) && isInterleaved(A+1, B, C+1))
           || ((*C == *B) && isInterleaved(A, B+1, C+1));
}
Dummy00001
  • 16,630
  • 5
  • 41
  • 63
  • @PaulS. But there is a `OR` between two statements, means either first statement is going to be true or another – surajs1n Dec 03 '15 at 12:13
  • what do you mean?? @PaulS. –  Dec 03 '15 at 12:13
  • 1
    @psyco oh yeah, I was thinking in an `if..else` mentality, that `OR` does make it `2^n`, you're right – Paul S. Dec 03 '15 at 12:14
  • 1
    I understand its 2^n but I don't understand how to do them math @PaulS. –  Dec 03 '15 at 12:16
  • Possible duplicate of [Big O, how do you calculate/approximate it?](http://stackoverflow.com/questions/3255/big-o-how-do-you-calculate-approximate-it) , `There is no mechanical procedure that can be used to get the BigOh.` – Paul S. Dec 03 '15 at 12:21
  • @PaulS. But you make assumptions for given algorithms, you can calculate the BigOh, but each algorithm is different. This is not a duplicate. –  Dec 03 '15 at 12:29
  • I have read through that article multiple times @PaulS.helps me without a doubt for all other algorithms apart from this one –  Dec 03 '15 at 12:30
  • @PaulS. I believe the complexity of this solution is O(n+m), if len(A)=n and len(B)=m as len(C)=n+m. I've written an ans justifying this, correct me if I'm wrong. – vish4071 Dec 03 '15 at 12:58

3 Answers3

1

See if you can reduce the problem by splitting the return into more lines,

// A simple recursive function to check whether C is an interleaving of A and B
bool isInterleaved(char *A, char *B, char *C)
{
    // Base Case: If all strings are empty
    if (!(*A || *B || *C))
        return true;

    // If C is empty and any of the two strings is not empty
    if (*C == '\0')
        return false;

    // If any of the above mentioned two possibilities is true,
    // then return true, otherwise false
    if ((*C == *A) && isInterleaved(A+1, B, C+1))
        return true;
    return (*C == *B) && isInterleaved(A, B+1, C+1);
}

"Factor out" B to produce two methods

bool isInterleavedA(char *A, char *C)
{
    // Base Case: If all strings are empty
    if (!(*A || *C))
        return true;

    // If C is empty and any of the two strings is not empty
    if (*C == '\0')
        return false;

    // If any of the above mentioned two possibilities is true,
    // then return true, otherwise false
    return (*C == *A) && isInterleavedA(A+1, C+1);
}

bool isInterleavedB(char *A, char *B, char *C)
{
    bool result = isInterleavedA(A, C);
    if (!(*B) && result)
        return true;
    return (*C == *B) && isInterleavedB(A, B+1, C+1);
}

Now we can see isInterleavedA is O(n) and the BigOh isInterleavedB will be the same as isInterleaved, which will be.. N + MN?

Paul S.
  • 64,864
  • 9
  • 122
  • 138
  • I'm not confident at this, please edit if you see mistakes! – Paul S. Dec 03 '15 at 12:53
  • So enter values such as a=xxxx and b = xxxx and c = xxxxxxxy That is definitely not a complexity of O(n+m) –  Dec 03 '15 at 13:00
1

Consider the complexity as a function of the number n of characters in C. Let's call that f(n).

The first if blocks are just doing simple checks no matter what, so we can ignore them for now (constant complexity).

The meat of the algorithm is of course these lines:

((*C == *A) && isInterleaved(A+1, B, C+1)) || ((*C == *B) && isInterleaved(A, B+1, C+1));

Checks (*C == ...) are again constant time complexity. Now, isInterleaved(..., ..., C+1) is calling the algorithm with a C shorter by 1: it's complexity is therefore f(n-1).

We can then put it all together as:

f(n) = k1 + (k2 + f(n-1)) + (k3 + f(n-1))

With k1, k2 and k3 being some constants. Reorder the terms, we get: f(n) = 2 * f(n-1) + k

Where k is, again, some constant. Now, expanding the recursion, we get:

f(n) = 2 * (2 * ( 2 * (... f(0) + k0) + k1) + k2) + ... + k_n) = 2 * (2 * (2 * (... 2*(2*(f(0) + k0) + k1) + k2) + ... + k_n) = 2 * (2 * (2 * (... 2*(2^2*(f(0) + k0) + 2*k1) + k2) + ... + k_n) = 2 * (2 * (2 * (... 2*(2^3*(f(0) + k0) + 2^2*k1 + 2*k2) + ... + k_n) f(n) = 2^n * (f(0) + k0) + 2^(n-1) * k1 + 2^(n-2) * k2 + ... Dividing that all by 2^n, we get:

f(n) / 2^n = (f(0) + k0) + k1 / 2 + k2 / 4 + ... + k_n / 2^n All of these terms are bounded: It is a property of the sum of 2^{-n} that it will approach 2 without ever reaching it, no matter how many terms you sum. Now, since all your k constants are just simple checks, it makes sense that they are also all bounded. Therefore, we know that there exist some constant K such that k0 < K, k1 < K, ..., k_n < K. Your f(n)/2^n then becomes:

f(n) / 2^n < f(0) + K * (1 + 1/2 + 1/4 + ... + 1/2^n)

Now, the first two checks ensure that f(0) is constant as well, so you have proven that the complexity of that function divided by 2^n is indeed bounded, which is sufficient to say that f(n) is O(2^n).

You can skim over most of these 'there exists a constant such that...'; the main observation to make is (using the 'handwavily equivalent-ish' symbol ~):

f(n) ~ f(n-1) + f(n-1) f(n) ~ 2 * f(n-1) f(n) ~ O(2^n) I have also slightly cheated by starting from the assumption that the length of C is the only parameter that matters for computing the complexity, but how you can rigorously show that the complexity is equivalent for various lengths of A and B is left as an exercise to the reader !

val
  • 8,459
  • 30
  • 34
  • how would you compute it considering a and b matter for calculating the complexity? –  Dec 03 '15 at 13:20
  • @user3667111: rigorously, you would write your complexity `f(n_a, n_b, n_c)` and end up with `f(n_a, n_b, n_c) ~ f(n_a-1, n_b, n_c-1) + f(n_a, n_b-1, n_c-1)`, with limit conditions of `f(0, ., .) = k_a`, `f(., 0, .) = k_b` and `f(., ., 0) = k_c` (ignoring the laundry-eating which might occur if either `A` or `B` is empty but not `C`), and proceed similarly. More intuitively (and hand-waveily), we will only consider the first `n_a` and `n_b` characters of `A` and `B` respectively, such that `n_a + n_b = n`, so `n` is really the only important parameter. – val Dec 03 '15 at 13:31
  • I should say, the above holds true only for the computation of the worst-case complexity. If you need to compute average or best-case complexity, doing the rigorous derivation might actually very well be required. – val Dec 03 '15 at 13:32
  • I don't understand the expanding the recursion bit? you suddenly jumped to f(n) = 2 * (2 * ( 2 * (... f(0) + k0) + k1) + k2 + ... + k_n) = 2^n * (f(0) + k0) + 2^(n-1) * k1 + 2^(n-2) * k2 + ... –  Dec 03 '15 at 13:39
  • You simply distribute the terms `2*(a + b) => (2*a + 2*b)`, `2*(2*(a + b) + c) => 2*2*(a+b) + 2*c` etc... starting from the center. Edited the post to make that a bit clearer – val Dec 03 '15 at 13:52
  • Ah okay, do you know what values would have to be entered to get a worst-case scenario? –  Dec 03 '15 at 14:08
  • Passing in matching strings which differ in size should try all combinations until it has exhausted `C` every time. Doing it with `A=B=C='aaaa...aaaa'` `n` times results in `2 * 2^n - 1` calls, which is `O(2^n)` – val Dec 03 '15 at 14:43
0

Consider the following case.

A = "ab"
B = "aa"
C = "aa"

now the recursion will be executed as below,

fn(ab, aa, aa)
=> fn(b, aa, a) + fn(ab, a, a)
=> false + fn(b, a, null) + fn(b, null, a) + fn(ab, null, null)

number of function call is 6.

Now if you add a single a at the first for all of them, like

A = "aab"
B = "aaa"
C = "aaa"

you will see this time number of function call will be 14.

add similarly one a more. then it will be 30.. then 62 then ... you can see its simply 2^n - 2. or like so.

So the actual complexity here is 2^n not n^2.

This is happening because you are not using memorization here.

Sumsuddin Shojib
  • 3,583
  • 3
  • 26
  • 45
  • Your case is not a valid input case. len(C)=len(A)+len(B) – vish4071 Dec 03 '15 at 12:57
  • This problem can be solved by much easier and less costly way, but I think you are interested in the complexity calculation here. So, you can still make the same code O(n^2) by just adding memorization. This approach is known as dynamic programming. So if you are interested I can help you on that. – Sumsuddin Shojib Dec 03 '15 at 12:59
  • Are there any constraints stated here, like this? – Sumsuddin Shojib Dec 03 '15 at 13:02
  • @Md.SumsuddinShojib How did you get to the solution 2^n-2? –  Dec 03 '15 at 13:07
  • just add a count++ on the top of the function and you will see that the series is like 6, 14, 30, 62, 126 .... which is 2^n-2. or see carefully my case(which is worst case) and add one 'a' each time. you will see that it simply double the root function calls. I need time to visualize the senario. – Sumsuddin Shojib Dec 03 '15 at 13:11