I have read the other suggestions with interest, and, as a person who is somewhat new to Python, I was wondering whether I was understanding them all correctly. The suggestions I see are clearly significantly better than your current code, but are missing an alternative algorithm that would make the performance significantly faster.
The solution is attainable in \$O(n)\$ time.
Alternative
Conceptually, the problem is simple:
- get a collection of all the actual values in the set. This requires a single scan of the entire input array.
- find the size of the left-most portion of the array that contains the entire set.
- find the size of the right-most portion that also contains the entire set.
- if the left and right spans do not overlap, then there is at least one solution. The actual number of solutions is the number of members between the left and right spans (plus 1).
By using a dict
to contain the collection of distinct values used in the input array, you get O(1) performance (on average, slightly worse in the event that the dictionary key values become congested - which in this input example is very unlikely.
By combining the identification of the left-portion with the full scan, you can reduce the number of iterations (though it is still \$O(n)\$).
Here's the resulting code:
def countParts(num):
distinctVal = {}
size = 0
left = 0
for idx, val in enumerate(num):
distinctVal[val] = 1
if len(distinctVal) > size:
left = idx
size = len(distinctVal)
for right in xrange(len(num) - 1, left, -1):
distinctVal.pop(num[right], None)
if len(distinctVal) == 0:
return right - left
return 0
Note that, in the above code, the key features are that it uses the enumerate mechanism to get both the index and value from the input array. This allows us to identify the left-most index of the data that contains the complete set. That set is found by resetting the left
pointer each time there's a new distinct value.
Once it has fully scanned the input, it then starts from the right side, and removes each distinct value it finds from the distinctVal
. When the dict is empty, it means we have covered the entire distinct set of values.
The difference between the end of the left set, and the start of the right set, is the number of solutions that are possible.
Performance
I compared the performance of my algorithm with that of Ashwini's. The solution I propose is about 40 times faster, for larger inputs, and about 10 times faster for smaller ones. This makes me believe that the time-complexity of my solution has a reduced dimension somewhere, compared to the other. I am not exactly sure why it is so much faster, or what that dimension is.... Here's how I tested them all.... note that your solution effectively does not complete the larger test cases... it's simply not scalable enough.
First, though, the results:
AP We have 1 splits from source 10
RL We have 1 splits from source 10
AP We have 6 splits from source 15
RL We have 6 splits from source 15
AP We have 8922 splits from source 10000
RL We have 8922 splits from source 10000
AP We have 98848 splits from source 100000
RL We have 98848 splits from source 100000
AP We have 987404 splits from source 1000000
RL We have 987404 splits from source 1000000
AP We have 0 splits from source 100000
RL We have 0 splits from source 100000
[27.986757040023804, 27.898667097091675, 27.68735098838806]
[0.6999490261077881, 0.7145471572875977, 0.701956033706665]
AP is Ashwini's post/solution, and RL is my solution.
The actual code that produced the output is:
import random
import timeit
from collections import Counter
def solve_counter(seq):
count = 0
full_counter = Counter(seq)
current_counter = Counter()
full_keys = set(seq)
current_keys = set()
for item in seq:
current_counter[item] += 1
full_counter[item] -= 1
current_keys.add(item)
if full_counter[item] == 0:
full_keys.remove(item)
count += full_keys == current_keys
return count
def countParts(num):
distinctVal = {}
size = 0
left = 0
for idx, val in enumerate(num):
distinctVal[val] = 1
if len(distinctVal) > size:
left = idx
size = len(distinctVal)
for right in xrange(len(num) - 1, left, -1):
distinctVal.pop(num[right], None)
if len(distinctVal) == 0:
return right - left
return 0
cases = [
[1,2,3,4,5,5,4,3,2,1],
[1,2,3,4,5,1,2,3,4,5,5,4,3,2,1],
[random.randint(0, 100) for _ in xrange(10**4)],
[random.randint(0, 100) for _ in xrange(10**5)],
[random.randint(0, 1000) for _ in xrange(10**6)],
[random.randint(0, 10000) for _ in xrange(10**5)]
]
for todo in cases:
# print "OP We have {} splits from source {}".format(solve(todo), len(todo))
print "AP We have {} splits from source {}".format(solve_counter(todo), len(todo))
print "RL We have {} splits from source {}".format(countParts(todo), len(todo))
print timeit.repeat("for todo in cases: solve_counter(todo)", "from __main__ import cases,solve_counter", number=3)
print timeit.repeat("for todo in cases: countParts(todo)", "from __main__ import cases,countParts", number=3)
Review