Let's label the students according to their round-1 team:
0000 1111 2222 3333 4444 5555 6666 7777 8888 9999 AAAA BBBB CCCC DDDD EEEE FFFF
The number of ways to assign round-2 teams, without restrictions, is
64! / ((4! ** 16) * (4! ** 16)) == 864285371844932000669608560277139342915848604
^ ^ ^
| | ways of rearranging each round-2 team
| indistinguishable arrangements for round-1 team members
raw number of permutations
The number of ways to assign round-2 teams without per-team duplicates is complicated because how we assign the first team changes the combinations available for the second team (and so on). But it should still be tractable with clever math and careful memoization!
from functools import lru_cache
# lookup table for `choose(n, k)` for n up to 16 and k up to 4
ncr = {}
for n in range(17):
nn = n
ntok = 1
ktok = 1
ncr[n, 0] = 1
for k in range(1, min(n, 4) + 1):
ntok *= nn
nn -= 1
ktok *= k
ncr[n, k] = ntok // ktok
@lru_cache(maxsize=None)
def team_combos(zeros, ones, twos, threes, fours):
"""
Calculate the number of unique second-round 4-person
team combinations such that no team has members from
the same first-round team.
"""
if ones or twos or threes or fours:
total_ways = 0
# number of members to take from teams having one person remaining
for b in range(min(ones, 4) + 1):
zeros_ = zeros + b
b_ways = ncr[ones, b]
b_rem = 4 - b # number of members yet to be chosen
# number of members to take from teams having two persons remaining
for c in range(min(twos, b_rem) + 1):
ones_ = ones - b + c
bc_ways = b_ways * ncr[twos, c]
bc_rem = b_rem - c # number of members yet to be chosen
# number of members to take from teams having three persons remaining
for d in range(min(threes, bc_rem) + 1):
e = bc_rem - d # number of members yet to be chosen
# ... all of which _must_ now come from
# teams having four persons remaining
if e <= fours:
bcde_ways = bc_ways * ncr[threes, d] * ncr[fours, e]
total_ways += bcde_ways * team_combos(zeros_, ones_, twos - c + d, threes - d + e, fours - e)
return total_ways
else:
return 1
then
>>> team_combos(0, 0, 0, 0, 16) # start with 16 four-person teams
6892692735539278753058456514221737762215000
then the final probability is exactly
>>> 6892692735539278753058456514221737762215000 / 864285371844932000669608560277139342915848604
0.0079750195480295