This would be pretty easy without duplication . . . for
{ 1, 1, 2, 3, 4, 5, 6, 7, 8, 9 }
Since you only have 9 distinct choices (as opposed to the 10 in the original problem), the answer should be 9! / (9 - 4)!
( By the way, you can actually have more different 4-digit numbers if you allow repetition, i.e. 4456. Then the answer would just be 9^4 4-digit numbers. )
Similarly, {1, 1, 2, 2, 3, 4, 5, 6, 7, 8 } has 8 distinct choices, so the answer should be 8! / (8 - 4)! by your original math.
Edit and actual answer: Maybe you meant that if the 1 is duplicated in your set, you can use two 1's in the answer?
Edit 2: Working, tested python module provided
In that case, you might try calculating the distinct number of possibilities, and then adding the results with duplicates, as the following python code suggests):
import math
def set_exclude(a, b):
result = []
for i in a:
if not i in b:
result.append(i)
return result
def cnt_unique(aset, choices_picked, count_left, count_total):
# Sanity check
if count_left < 0:
return 0
if count_left == 0:
return math.factorial(count_total)
# Do non-duplicate combinations first
# Set unprocessed = (set without any elements in choices_picked)
unprocessed = set_exclude(aset, choices_picked)
temp = len(set(unprocessed))
# If we have no more valid items to choose from, this is impossible
if temp >= count_left:
result = math.factorial(temp) / math.factorial(temp - count_left) / \
math.factorial(count_left) * math.factorial(count_total)
else:
result = 0
# Now find duplicate-involving combinations
for member in set(unprocessed):
valid = True
for contest in choices_picked:
if contest >= member:
valid = False
break
if valid:
count = unprocessed.count(member)
new_choices = choices_picked + [ member ]
for i in range(2, count+1):
result_temp = cnt_unique(aset, new_choices, \
count_left - i, count_total)
if result_temp != 0:
result_temp //= math.factorial(i)
result += result_temp
return result
aset = [ 1, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
result_size = 4
combinations = cnt_unique(aset, [], result_size, result_size)
OK, I've verified by hand that algorithm works for all of your cases presented above. I'm fairly confident it works in more general cases, but I don't have time at the moment to do any additional test cases (for instance, if there were 3 1's, or 3 groups of duplicates). Note it would also blow up if there were no numbers in set which were not in choices_picked (ie you have one unique number duplicated 10 times).
Edit 3: Regarding how many function calls are made with this algorithm for large sets, I tested with the following function call, incrementing a variable once for each non-trivial (count_left >= 0) call to cnt_unique:
>>> def test():
b = [0]
c = time.time()
result = cnt_unique(range(1,51) + range(1,51), [], 4, 4, b)
c = time.time() - c
print("Result: " + str(result))
print("Time: " + str(c))
print("Calls: " + str(b[0]))
>>> test()
Result: 6240150
Time: 0.0150001049042
Calls: 1276
So, for a 100 element set with 2 entries for each number 1-50, there were 1276 calls. And it executes quite fast; one tick with time.time() is 15 ms, so it executes in typically less than 15 ms.