Dynamic programming table - Finding the minimal cost to break a string - string

A certain string-processing language offers a primitive operation
which splits a string into two pieces. Since this operation involves
copying the original string, it takes n units of time for a string of
length n, regardless of the location of the cut. Suppose, now, that
you want to break a string into many pieces.
The order in which the breaks are made can affect the total running
time. For example, suppose we wish to break a 20-character string (for
example "abcdefghijklmnopqrst") after characters at indices 3, 8, and
10 to obtain for substrings: "abcd", "efghi", "jk" and "lmnopqrst". If
the breaks are made in left-right order, then the first break costs 20
units of time, the second break costs 16 units of time and the third
break costs 11 units of time, for a total of 47 steps. If the breaks
are made in right-left order, the first break costs 20 units of time,
the second break costs 11 units of time, and the third break costs 9
units of time, for a total of only 40 steps. However, the optimal
solution is 38 (and the order of the cuts is 10, 3, 8).
The input is the length of the string and an ascending-sorted array with the cut indexes. I need to design a dynamic programming table to find the minimal cost to break the string and the order in which the cuts should be performed.
I can't figure out how the table structure should look (certain cells should be the answer to certain sub-problems and should be computable from other entries etc.). Instead, I've written a recursive function to find the minimum cost to break the string: b0, b1, ..., bK are the indexes for the cuts that have to be made to the (sub)string between i and j.
totalCost(i, j, {b0, b1, ..., bK}) = j - i + 1 + min {
totalCost(b0 + 1, j, {b1, b2, ..., bK}),
totalCost(i, b1, {b0 }) + totalCost(b1 + 1, j, {b2, b3, ..., bK}),
totalCost(i, b2, {b0, b1 }) + totalCost(b2 + 1, j, {b3, b4, ..., bK}),
....................................................................................
totalCost(i, bK, {b0, b1, ..., b(k - 1)})
} if k + 1 (the number of cuts) > 1,
j - i + 1 otherwise.
Please help me figure out the structure of the table, thanks!

For example we have a string of length n = 20 and we need to break it in positions cuts = [3, 8, 10]. First of all let's add two fake cuts to our array: -1 and n - 1 (to avoid edge cases), now we have cuts = [-1, 3, 8, 10, 19]. Let's fill table M, where M[i, j] is a minimum units of time to make all breaks between i-th and j-th cuts. We can fill it by rule: M[i, j] = (cuts[j] - cuts[i]) + min(M[i, k] + M[k, j]) where i < k < j. The minimum time to make all cuts will be in the cell M[0, len(cuts) - 1]. Full code in python:
# input
n = 20
cuts = [3, 8, 10]
# add fake cuts
cuts = [-1] + cuts + [n - 1]
cuts_num = len(cuts)
# init table with zeros
table = []
for i in range(cuts_num):
table += [[0] * cuts_num]
# fill table
for diff in range(2, cuts_num):
for start in range(0, cuts_num - diff):
end = start + diff
table[start][end] = 1e9
for mid in range(start + 1, end):
table[start][end] = min(table[start][end], table[
start][mid] + table[mid][end])
table[start][end] += cuts[end] - cuts[start]
# print result: 38
print(table[0][cuts_num - 1])

Just in case you may feel easier to follow when everything is 1-based (same as DPV Dasgupta Algorithm book problem 6.9, and same as UdaCity Graduate Algorithm course initiated by GaTech), following is the python code that does the equivalent thing with the previous python code by Jemshit and Aleksei. It follows the chain multiply (binary tree) pattern as taught in the video lecture.
import numpy as np
# n is string len, P is of size m where P[i] is the split pos that split string into [1,i] and [i+1,n] (1-based)
def spliting_cost(P, n):
P = [0,] + P + [n,] # make sure pos list contains both ends of string
m = len(P)
P = [0,] + P # both C and P are 1-base indexed for easy reading
C = np.full((m+1,m+1), np.inf)
for i in range(1, m+1): C[i, i:i+2] = 0 # any segment <= 2 does not need split so is zero cost
for s in range(2, m): # s is split string len
for i in range(1, m-s+1):
j = i + s
for k in range(i, j+1):
C[i,j] = min(C[i,j], P[j] - P[i] + C[i,k] + C[k,j])
return C[1,m]
spliting_cost([3, 5, 10, 14, 16, 19], 20)
The output answer is 55, same as that with split points [2, 4, 9, 13, 15, 18] in the previous algorithm.

Related

Rotate String to find Hamming distance equal to K

We are given 2 binary strings (A and B) both of length N and an integer K.
We need to check if there is a rotation of string B present where hamming distance between A and the rotated string is equal to K. We can just remove one character from front and put it at back in single operation.
Example : Let say we are given these 2 string with values as A="01011" and B="01110" and also K=4.
Note : Hamming distance between binary string is number of bit positions in which two corresponding bits in strings are different.
In above example answer will be "YES" as if we rotate string B once it becomes "11100", which has hamming distance of 4, that is equal to K.
Approach :
For every rotated string of B
check that hamming distance with A
if hamming distance == K:
return "YES"
return "NO"
But obviously above approach will execute in O(Length of string x Length of string) times. Is there better approach to solve this. As we don't need to find the actual string, I am just wondering there is some better algorithm to get this answer.
Constraints :
Length of each string <= 2000
Number of test cases to run in one file <=600
First note that we can compute the Hamming distance as the sum of a[i]*(1-b[i]) + b[i]*(1-a[i]) for all i. This simplifies to a[i] + b[i] - 2*a[i]*b[i]. Now in O(n) we can compute the sum of a[i] and b[i] for all i, and this doesn't change with bit rotations, so the only interesting term is 2*a[i]*b[i]. We can compute this term efficiently for all bit rotations by noting that it is equivalent to a circular convolution of a and b. We can efficiently compute such convolutions using the Discrete Fourier transform in O(n log n) time.
For example in Python using numpy:
import numpy as np
def hdist(a, b):
return sum(bool(ai) ^ bool(bi) for ai, bi in zip(a, b))
def slow_circular_hdist(a, b):
return [hdist(a, b[i:] + b[:i]) for i in range(len(b))]
def circular_convolution(a, b):
return np.real(np.fft.ifft(np.fft.fft(a)*np.fft.fft(b[::-1])))[::-1]
def fast_circular_hdist(a, b):
hdist = np.sum(a) + np.sum(b) - 2*circular_convolution(a, b)
return list(np.rint(hdist).astype(int))
Usage:
>>> a = [0, 1, 0, 1, 1]
>>> b = [0, 1, 1, 1, 0]
>>> slow_circular_hdist(a, b)
[2, 4, 2, 2, 2]
>>> fast_circular_hdist(a, b)
[2, 4, 2, 2, 2]
Speed and large correctness test:
>>> x = list((np.random.random(5000) < 0.5).astype(int))
>>> y = list((np.random.random(5000) < 0.5).astype(int))
>>> s = time.time(); slow_circular_hdist(x, y); print(time.time() - s)
6.682933807373047
>>> s = time.time(); fast_circular_hdist(x, y); print(time.time() - s)
0.008500814437866211
>>> slow_circular_hdist(x, y) == fast_circular_hdist(x, y)
True

How to check a list of lists against a list of lists and count their overlaps? (Python)

I have one list a containing 100 lists and one list x containing 4 lists (all of equal length). I want to test the lists in a against those in x. My goal is to find out how often numbers in a "touch" those in x. Stated differently, all the lists are points on a line and the lines in a should not touch (or cross) those in x.
EDIT
In the code, I am testing each line in a (e.g. a1, a2 ... a100) first against x1, then against x2, x3 and x4. A condition and a counter check whether the a's touch the x's. Note: I am not interested in counting how many items in a1, for example, touch x1. Once a1 and x1 touch, I count that and can move on to a2, and so on.
However, the counter does not properly update. It seems that it does not tests a against all x. Any suggestions on how to solve this? Here is my code.
EDIT
I have updated the code so that the problem is easier to replicate.
x = [[10, 11, 12], [14, 15, 16]]
a = [[11, 10, 12], [15, 17, 20], [11, 14, 16]]
def touch(e, f):
e = np.array(e)
f = np.array(f)
lastitems = []
counter = 0
for lst in f:
if np.all(e < lst): # This is the condition
lastitems.append(lst[-1]) # This allows checking the end values
else:
counter += 1
c = counter
return c
touch = touch(x, a)
print(touch)
The result I get is:
2
But I expect this:
1
2
I'm unsure of what exactly is the result you expect, your example and description are still not clear. Anyway, this is what I guess you want. If you want more details, you can uncomment some lines i.e. those with #
i = 0
for j in x:
print("")
#print(j)
counter = 0
for k in a:
inters = set(j).intersection(k)
#print(k)
#print(inters)
if inters:
counter += 1
#print("yes", counter)
#else:
#print("nope", counter)
print(i, counter)
i += 1
which prints
0 2
1 2

String concatenation queries

I have a list of characters, say x in number, denoted by b[1], b[2], b[3] ... b[x]. After x,
b[x+1] is the concatenation of b[1],b[2].... b[x] in that order. Similarly,
b[x+2] is the concatenation of b[2],b[3]....b[x],b[x+1].
So, basically, b[n] will be concatenation of last x terms of b[i], taken left from right.
Given parameters as p and q as queries, how can I find out which character among b[1], b[2], b[3]..... b[x] does the qth character of b[p] corresponds to?
Note: x and b[1], b[2], b[3]..... b[x] is fixed for all queries.
I tried brute-forcing but the string length increases exponentially for large x.(x<=100).
Example:
When x=3,
b[] = a, b, c, a b c, b c abc, c abc bcabc, abc bcabc cabcbcabc, //....
//Spaces for clarity, only commas separate array elements
So for a query where p=7, q=5, answer returned would be 3(corresponding to character 'c').
I am just having difficulty figuring out the maths behind it. Language is no issue
I wrote this answer as I figured it out, so please bear with me.
As you mentioned, it is much easier to find out where the character at b[p][q] comes from among the original x characters than to generate b[p] for large p. To do so, we will use a loop to find where the current b[p][q] came from, thereby reducing p until it is between 1 and x, and q until it is 1.
Let's look at an example for x=3 to see if we can get a formula:
p N(p) b[p]
- ---- ----
1 1 a
2 1 b
3 1 c
4 3 a b c
5 5 b c abc
6 9 c abc bcabc
7 17 abc bcabc cabcbcabc
8 31 bcabc cabcbcabc abcbcabccabcbcabc
9 57 cabcbcabc abcbcabccabcbcabc bcabccabcbcabcabcbcabccabcbcabc
The sequence is clear: N(p) = N(p-1) + N(p-2) + N(p-3), where N(p) is the number of characters in the pth element of b. Given p and x, you can just brute-force compute all the N for the range [1, p]. This will allow you to figure out which prior element of b b[p][q] came from.
To illustrate, say x=3, p=9 and q=45.
The chart above gives N(6)=9, N(7)=17 and N(8)=31. Since 45>9+17, you know that b[9][45] comes from b[8][45-(9+17)] = b[8][19].
Continuing iteratively/recursively, 19>9+5, so b[8][19] = b[7][19-(9+5)] = b[7][5].
Now 5>N(4) but 5<N(4)+N(5), so b[7][5] = b[5][5-3] = b[5][2].
b[5][2] = b[3][2-1] = b[3][1]
Since 3 <= x, we have our termination condition, and b[9][45] is c from b[3].
Something like this can very easily be computed either recursively or iteratively given starting p, q, x and b up to x. My method requires p array elements to compute N(p) for the entire sequence. This can be allocated in an array or on the stack if working recursively.
Here is a reference implementation in vanilla Python (no external imports, although numpy would probably help streamline this):
def so38509640(b, p, q):
"""
p, q are integers. b is a char sequence of length x.
list, string, or tuple are all valid choices for b.
"""
x = len(b)
# Trivial case
if p <= x:
if q != 1:
raise ValueError('q={} out of bounds for p={}'.format(q, p))
return p, b[p - 1]
# Construct list of counts
N = [1] * p
for i in range(x, p):
N[i] = sum(N[i - x:i])
print('N =', N)
# Error check
if q > N[-1]:
raise ValueError('q={} out of bounds for p={}'.format(q, p))
print('b[{}][{}]'.format(p, q), end='')
# Reduce p, q until it is p < x
while p > x:
# Find which previous element character q comes from
offset = 0
for i in range(p - x - 1, p):
if i == p - 1:
raise ValueError('q={} out of bounds for p={}'.format(q, p))
if offset + N[i] >= q:
q -= offset
p = i + 1
print(' = b[{}][{}]'.format(p, q), end='')
break
offset += N[i]
print()
return p, b[p - 1]
Calling so38509640('abc', 9, 45) produces
N = [1, 1, 1, 3, 5, 9, 17, 31, 57]
b[9][45] = b[8][19] = b[7][5] = b[5][2] = b[3][1]
(3, 'c') # <-- Final answer
Similarly, for the example in the question, so38509640('abc', 7, 5) produces the expected result:
N = [1, 1, 1, 3, 5, 9, 17]
b[7][5] = b[5][2] = b[3][1]
(3, 'c') # <-- Final answer
Sorry I couldn't come up with a better function name :) This is simple enough code that it should work equally well in Py2 and 3, despite differences in the range function/class.
I would be very curious to see if there is a non-iterative solution for this problem. Perhaps there is a way of doing this using modular arithmetic or something...

How to sort 4 integers using only min() and max()? Python

I am trying to sort 4 integers input by the user into numerical order using only the min() and max() functions in python. I can get the highest and lowest number easily, but cannot work out a combination to order the two middle numbers? Does anyone have an idea?
So I'm guessing your input is something like this?
string = input('Type your numbers, separated by a space')
Then I'd do:
numbers = [int(i) for i in string.strip().split(' ')]
amount_of_numbers = len(numbers)
sorted = []
for i in range(amount_of_numbers):
x = max(numbers)
numbers.remove(x)
sorted.append(x)
print(sorted)
This will sort them using max, but min can also be used.
If you didn't have to use min and max:
string = input('Type your numbers, separated by a space')
numbers = [int(i) for i in string.strip().split(' ')]
numbers.sort() #an optional reverse argument possible
print(numbers)
LITERALLY just min and max? Odd, but, why not. I'm about to crash, but I think the following would work:
# Easy
arr[0] = max(a,b,c,d)
# Take the smallest element from each pair.
#
# You will never take the largest element from the set, but since one of the
# pairs will be (largest, second_largest) you will at some point take the
# second largest. Take the maximum value of the selected items - which
# will be the maximum of the items ignoring the largest value.
arr[1] = max(min(a,b)
min(a,c)
min(a,d)
min(b,c)
min(b,d)
min(c,d))
# Similar logic, but reversed, to take the smallest of the largest of each
# pair - again omitting the smallest number, then taking the smallest.
arr[2] = min(max(a,b)
max(a,c)
max(a,d)
max(b,c)
max(b,d)
max(c,d))
# Easy
arr[3] = min(a,b,c,d)
For Tankerbuzz's result for the following:
first_integer = 9
second_integer = 19
third_integer = 1
fourth_integer = 15
I get 1, 15, 9, 19 as the ascending values.
The following is one of the forms that gives symbolic form of the ascending values (using i1-i4 instead of first_integer, etc...):
Min(i1, i2, i3, i4)
Max(Min(i4, Max(Min(i1, i2), Min(i3, Max(i1, i2))), Max(i1, i2, i3)), Min(i1, i2, i3, Max(i1, i2)))
Max(Min(i1, i2), Min(i3, Max(i1, i2)), Min(i4, Max(i1, i2, i3)))
Max(i1, i2, i3, i4)
It was generated by a 'bubble sort' using the Min and Max functions of SymPy (a python CAS):
def minmaxsort(v):
"""return a sorted list of the elements in v using the
Min and Max functions.
Examples
========
>>> minmaxsort(3, 2, 1)
[1, 2, 3]
>>> minmaxsort(1, x, y)
[Min(1, x, y), Max(Min(1, x), Min(y, Max(1, x))), Max(1, x, y)]
>>> minmaxsort(1, y, x)
[Min(1, x, y), Max(Min(1, y), Min(x, Max(1, y))), Max(1, x, y)]
"""
from sympy import Min, Max
v = list(v)
v0 = Min(*v)
for j in range(len(v)):
for i in range(len(v) - j - 1):
w = v[i:i + 2]
v[i:i + 2] = [Min(*w), Max(*w)]
v[0] = v0
return v
I have worked it out.
min_integer = min(first_integer, second_integer, third_integer, fourth_integer)
mid_low_integer = min(max(first_integer, second_integer), max(third_integer, fourth_integer))
mid_high_integer = max(min(first_integer, second_integer), min(third_integer, fourth_integer))
max_integer = max(first_integer, second_integer, third_integer, fourth_integer)

Time Complexity of String Subsequence Recursion

How to calculate the time complexity of the algorithm below? Can someone please explain to me briefly:
public static void print(String prefix, String remaining, int k) {
if (k == 0) {
StdOut.println(prefix);
return;
}
if (remaining.length() == 0) return;
print(prefix + remaining.charAt(0), remaining.substring(1), k-1);
print(prefix, remaining.substring(1), k);
}
public static void main(String[] args) {
String s = "abcdef";
int k = 3;
print("", s, k);
}
Suppose m is the length of prefix, and n is the length of remaining. Then the complexity is given by
T(m, n, k) = Θ(m + n) + T(m + 1, n - 1, k - 1) + T(m, n - 1, k).
The Θ(m + n) term stems from
prefix + remaining.charAt(0), remaining.substring(1)
which, in general will require creating two new strings of lengths about m and n, respectively (this might differ among various implementations).
Beyond that, it's pretty difficult to solve (at least for me), except for some very simple bounds. E.g., it's pretty clear that the complexity is at least exponential in the minimum of the length of the prefix and k, since
T(m, n, k) ≥ 2 T(m, n - 1, k - 1) &Rarr; T(m, n, k) = Ω(2min(n, k)).
Introduction
As the body is O(1), or at least can be rewritten as O(1), we only have to look for how many time the function is called. So the time complexity of this algorithm will be how many times the function will be called in relation to length of input word and length of output prefix.
n - length of input word
k - length of prefix being searched for
I have never done something like this before and common methods for finding time complexity for recursive methods that I know of don't work in this case. I started off by looking how many calls to the function were made depending on n and k to see if I can spot any patterns that might help me.
Gathering data
Using this code snippet (sorry for ugly code):
public static String word = "abcdefghij";
public static int wordLength = word.length();
public static int limit = 10;
public static int access = 0;
System.out.printf("Word length : %6d %6d %6d %6d %6d %6d %6d %6d %6d %6d %6d\n",0,1,2,3,4,5,6,7,8,9,10);
System.out.printf("-----------------------------------------------------------------------------------------------\n");
for(int k = 0; k <= limit; k++) {
System.out.printf("k : %2d - results :", k);
for(int i = 0; i <= limit; i++) {
print("", word.substring(0,i), k);
System.out.printf(", %5d", access);
access=0;
}
System.out.print("\n");
}
print(prefix, remaining, k) {
access++;
... rest of code...
}
From this I got :
Word length : 0 1 2 3 4 5 6 7 8 9 10
-----------------------------------------------------------------------------------------------
k : 0 - results :, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
k : 1 - results :, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21
k : 2 - results :, 1, 3, 7, 13, 21, 31, 43, 57, 73, 91, 111
k : 3 - results :, 1, 3, 7, 15, 29, 51, 83, 127, 185, 259, 351
k : 4 - results :, 1, 3, 7, 15, 31, 61, 113, 197, 325, 511, 771
k : 5 - results :, 1, 3, 7, 15, 31, 63, 125, 239, 437, 763, 1275
k : 6 - results :, 1, 3, 7, 15, 31, 63, 127, 253, 493, 931, 1695
k : 7 - results :, 1, 3, 7, 15, 31, 63, 127, 255, 509, 1003, 1935
k : 8 - results :, 1, 3, 7, 15, 31, 63, 127, 255, 511, 1021, 2025
k : 9 - results :, 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2045
k : 10 - results :, 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047
At least one call is made in every case as it is the initial call. From here we see that everything on and below main diagonal is called 2min(n, k) + 1 - 1 times, like mentioned by others. That is number of nodes in binary tree.
Each time a the print method is called, two new ones will be called - making a binary tree.
Things get more confusing above the main diagonal though and I failed to see any common pattern.
Graph representation of algorithm
To make it more visual I used graphviz (online version).
Here is code snippet that generates code for given n and k for graphviz (green nodes are ones from where solution was found):
public static String word = "abcde";
public static int wordLength = word.length();
public static int limit = 3;
public static void main(String[] args) {
String rootNode = "\"prefix|remaining|k\"";
StringBuilder graph = new StringBuilder("digraph G { \n node [style=filled];");
print("", word, limit, graph, rootNode);
graph.append("\n\"prefix|remaining|k\" [shape=Mdiamond];\n}");
System.out.println(graph);
}
public static void print(String prefix, String remaining, int k, StringBuilder sb, String parent) {
String currentNode = "\"" + prefix + "|" + (remaining.isEmpty() ? 0 : remaining) + "|" + k + "\"";
sb.append("\n " + parent + "->" + currentNode + ";");
if(k == 0) {
sb.append("\n " + currentNode + "[color=darkolivegreen3];");
return;
}
if (remaining.length() == 0)return;
print(prefix + remaining.charAt(0), remaining.substring(1), k - 1, sb,currentNode);
print(prefix, remaining.substring(1), k, sb, currentNode);
}
Example of graph (n=5, k=3):
digraph G {
node [style=filled];
"prefix|remaining|k"->"|abcde|3";
"|abcde|3"->"a|bcde|2";
"a|bcde|2"->"ab|cde|1";
"ab|cde|1"->"abc|de|0";
"abc|de|0"[color=darkolivegreen3];
"ab|cde|1"->"ab|de|1";
"ab|de|1"->"abd|e|0";
"abd|e|0"[color=darkolivegreen3];
"ab|de|1"->"ab|e|1";
"ab|e|1"->"abe|0|0";
"abe|0|0"[color=darkolivegreen3];
"ab|e|1"->"ab|0|1";
"a|bcde|2"->"a|cde|2";
"a|cde|2"->"ac|de|1";
"ac|de|1"->"acd|e|0";
"acd|e|0"[color=darkolivegreen3];
"ac|de|1"->"ac|e|1";
"ac|e|1"->"ace|0|0";
"ace|0|0"[color=darkolivegreen3];
"ac|e|1"->"ac|0|1";
"a|cde|2"->"a|de|2";
"a|de|2"->"ad|e|1";
"ad|e|1"->"ade|0|0";
"ade|0|0"[color=darkolivegreen3];
"ad|e|1"->"ad|0|1";
"a|de|2"->"a|e|2";
"a|e|2"->"ae|0|1";
"a|e|2"->"a|0|2";
"|abcde|3"->"|bcde|3";
"|bcde|3"->"b|cde|2";
"b|cde|2"->"bc|de|1";
"bc|de|1"->"bcd|e|0";
"bcd|e|0"[color=darkolivegreen3];
"bc|de|1"->"bc|e|1";
"bc|e|1"->"bce|0|0";
"bce|0|0"[color=darkolivegreen3];
"bc|e|1"->"bc|0|1";
"b|cde|2"->"b|de|2";
"b|de|2"->"bd|e|1";
"bd|e|1"->"bde|0|0";
"bde|0|0"[color=darkolivegreen3];
"bd|e|1"->"bd|0|1";
"b|de|2"->"b|e|2";
"b|e|2"->"be|0|1";
"b|e|2"->"b|0|2";
"|bcde|3"->"|cde|3";
"|cde|3"->"c|de|2";
"c|de|2"->"cd|e|1";
"cd|e|1"->"cde|0|0";
"cde|0|0"[color=darkolivegreen3];
"cd|e|1"->"cd|0|1";
"c|de|2"->"c|e|2";
"c|e|2"->"ce|0|1";
"c|e|2"->"c|0|2";
"|cde|3"->"|de|3";
"|de|3"->"d|e|2";
"d|e|2"->"de|0|1";
"d|e|2"->"d|0|2";
"|de|3"->"|e|3";
"|e|3"->"e|0|2";
"|e|3"->"|0|3";
"prefix|remaining|k" [shape=Mdiamond];
}
Number of nodes cut from binary tree
From the example where n = 5 and k = 3 we can see that tree of height 3 and three trees of height 2 were cut off. As we visited each of these trees root nodes we get number of nodes cut from complete binary tree to be 1*(23 - 2) + 3*(22 - 2) = 12
If it were a full binary tree : 25 + 1 - 1 = 63
Number of nodes(calls made to function "print") then comes to 63 - 12 = 51
Result matches one we got by calculating number of calls made to function when n = 5 and k = 3.
Now we have to find how many and how big parts of tree are cut off for every n and k.
From here on I will refer to method call print(prefix + remaining.charAt(0), remaining.substring(1), k-1); as left path or left node (as it is in graphviz graphs) and to print(prefix, remaining.substring(1), k); as right path or right node.
We can see the first, and biggest tree is cut when we go left k times and the height of the tree will be n - k + 1. ( + 1 because we visit the root of the tree we cut).
We can see that every time we take left path k times we get to result no matter how many right paths we took before (or in what order). This is unless the word runs out of letters before we get the k left paths. So we can make maximum of n - k right turns.
Lets take a closer look at example where n = 5 and k = 3:
L - left path
R - right path
The first tree cut we took the paths :
LLL
The next highest trees that will be cut will be ones where we take only one right node, possible combinations are :
RLLL, LRLL, LLRL, LLLR -> three trees of height 2 cut
Here we must note that LLLR is already cut as LLL gave solution in previous step.
To get number of next trees (height 1 -> 0 nodes cut) we'll calculate the possible combinations of two rights and three lefts subtracting already visited paths.
Combinations(5,3) - Combinations(4,3) = 10 - 4 = 6 nodes of height 1
We can see the numbers match green nodes on the example graph.
C(n,k) - combinations of k from n
f(n,k) - number binary tree nodes not visited by algorithm
f(n,k) = (2n-k+1-2) + Σn-ki=1(2n-k-i+1-2)(C(k+i,k) - C(k+i-1,k))
Explanation:
(2n-k+1-2) - the highest tree cut, have to bring it out of summation or we'll have to take negative factorials
Σn-ki=1 - sum of all nodes cut, excluding highest tree as it is already added. (We start adding larger trees first)
(2n-k-i+1-2) - number of nodes cut per tree. n-k+1 is the largest tree, then working down from there up to tree with height "n-k-(n-k)+1 = 1"
(C(k+i,k) - C(k+i-1,k)) - find how many trees of given height there is. First find all possible paths (lefts and rights) and then subtract already visited ones(in previous steps).
Looks awful, but it can be simplified if we assume that k != 0 (If we don't assume that there will be factorials of negative numbers - which is undefined)
Simplified function:
f(n,k) = Σn-ki=0(2n-k-i+1-2)*C(k+i-1,k-1)
Evaluation of accurate time complexity
The time complexity of the function:
O(2n- Σn-ki=0(2n-k-i+1-2)*C(k+i-1,k-1))
Now this looks awful and doesn't give much information. I don't know how to simplify it any further. I've asked about it here. No answer so far though.
But is it even worth considering the f(n,k) part? Probably depends on particular application where it is applied. We can see from the data table that it can considerably affect the algorithms calls depending on the choice of n and k.
To see more visually how much the extra part affects complexity I plotted best time complexity and real complexity on graph.
O(2n- Σn-ki=0(2n-k-i+1-2)*C(k+i-1,k-1)) is the colorful surface.
B(2min(n,k)) is the green surface.
We can see that B(2min(n,k)) overestimates (tells it works much better than it actually does) the function complexity by quite much. It is usually useful to look algorithms worst case complexity which is W(2max(n,k))
O(2n- Σn-ki=0(2n-k-i+1-2)*C(k+i-1,k-1)) is the colorful surface.
B(2min(n,k)) is the green surface.
W(2max(n,k)) is the yellow surface.
Conclusion
Best case complexity: B(2min(n,k))
Accurate complexity : O(2n- Σn-ki=0(2n-k-i+1-2)*C(k+i-1,k-1))
Worst case complexity: W(2max(n,k)) -> often noted as O(2max(n,k))
In my opinion worst case complexity should be used to evaluate the function as accurate is too complex to understand what it means without analyzing it further. I wouldn't use best case complexity because it leaves too much to chance. Unfortunately I can't calculate the average complexity for this. Depending on application of the algorithm, using average might be better for algorithm evaluation.
Suppose m is the length of prefix, and n is the length of remaining. Then the complexity is given by
T(m, n, k) = 1 + n + 1 + T(m + 1, n - 1, k - 1) + T(m, n - 1, k)
Obviously, the function stop when n=0 or k=0. So,
T(r, n, 0) = 1 + r
T(m, 0, k) = 1 + 1 + 1 = 3
Reform equation 1, we got
T(m, n, k) - T(m, n - 1, k) = 2 + n + T(m + 1, n - 1, k - 1)
Replace n by n-1 in equation 1
T(m, n - 1, k) - T(m, n - 2, k) = 2 + (n - 1) + T(m + 1, n - 2, k - 1)
... continue ...
T(m, 1, k) - T(m, 0, k) = 2 + (1) + T(m + 1, 0, k - 1)
Sum them up
T(m, n, k) - T(m, 0, k) = 2(n) + (n-1)(n)/2 + {Summation of a from 0 to n - 1 on T(m + 1, a, k - 1)}
Reform
T(m, n, k) = n2/2 + 3n/2 +3 + {Summation of a from 0 to n - 1 on T(m + 1, a, k - 1)}
I guess we can get the answer by solving the Summation by using the last equation and the leading factor of the equation would be something like nk+1

Resources