Combinatoire - TP 3

Nombres de Stirling

Les nombres de Stirling de première espèce non signés $\left[n\atop k\right]$ comptent les permutations de $\mathfrak{S}_n$ ayant exactement $k$ cycles.

Ce sont aussi les coefficients des polynômes $$(t)^n = t^{\overline{n}} = t(t+1)(t+2)\cdots (t+n-1) = \sum_{k=1}^n \left[n\atop k\right]t^k.$$

Les nombres de Stirling de première espèce signés $s(n,k)$ sont les coefficients de

$$(t)_n = t^{\underline{n}} = t(t-1)(t-2)\cdots (t-n+1) = \sum_{k=1}^n s(n,k) t^k.$$

Coder une fonction stirling1(n,k,signed=True) calculant les deux versions à l'aide de la récurrence du triangle vue en cours.

In [1]:
def stirling1(n,k,signed=True):
    if k<=0 or k>n: return 0
    elif n==1 and k==1: return 1
    else:
        e = -1 if signed else 1
        return stirling1(n-1,k-1,signed)+e*(n-1)*stirling1(n-1,k,signed)
In [4]:
for n in range(10):
    for k in range(n+1): print(stirling1(n,k,signed=False), end=' ')
    print()
0 
0 1 
0 1 1 
0 2 3 1 
0 6 11 6 1 
0 24 50 35 10 1 
0 120 274 225 85 15 1 
0 720 1764 1624 735 175 21 1 
0 5040 13068 13132 6769 1960 322 28 1 
0 40320 109584 118124 67284 22449 4536 546 36 1 
In [6]:
for k in range(1,11): print (stirling1(10,k), stirling1(10,k,signed=False))
-362880 362880
1026576 1026576
-1172700 1172700
723680 723680
-269325 269325
63273 63273
-9450 9450
870 870
-45 45
1 1

Coder une fonction fallpow(n,variable=t) calculant $(t)_n$.

In [7]:
from sympy import *
var('x t')
Out[7]:
(x, t)
In [10]:
from functools import reduce
def fallpow(n,variable=t):
    return reduce(lambda a,b:a*b, [variable-i for i in range(n)])
In [11]:
fallpow(5)
Out[11]:
t*(t - 4)*(t - 3)*(t - 2)*(t - 1)

Les nombres de Stirling de deuxième espèce $S(n,k)$ comptent les partitions en $k$ blocs d'un ensemble de $n$ éléments.

Ce sont les coefficients des polynômes (dits de Bell ou de Touchard) $B_n(t)$ dont la série génératrice exponentielle est $$\sum_{n\ge 0}B_n(t)\frac{x^n}{n!}=e^{t(e^x-1)}.$$

Calculer les premiers $B_n(t)$ au moyen de cette série, puis coder une fonction stirling2(n,k) calculant directement les coefficients au moyen de la récurrence du triangle vue en cours.

In [12]:
def stirling2(n,k):
    if k<=0 or k>n: return 0
    elif n==1 and k==1: return 1
    else: return stirling2(n-1,k-1)+k*stirling2(n-1,k)
In [14]:
for n in range(10):
    for k in range(n+1): print (stirling2(n,k), end=' ')
    print()
0 
0 1 
0 1 1 
0 1 3 1 
0 1 7 6 1 
0 1 15 25 10 1 
0 1 31 90 65 15 1 
0 1 63 301 350 140 21 1 
0 1 127 966 1701 1050 266 28 1 
0 1 255 3025 7770 6951 2646 462 36 1 

En utilisant le développement $$t^n = \sum_{k=1}^n S(n,k)(t)_k$$ et la propriété $$\sum_{s=0}^{t-1}(s)_k = \frac{(t)_{k+1}}{k+1},$$ calculer les premiers polynômes $S_p(t)$ donnant les sommes $$0^p+1^p+\cdots+(n-1)^p = S_p(n).$$

In [15]:
def S(p):
    return sum([stirling2(p,j)*fallpow(j+1)/(j+1) for j in range(1,p+1)])
In [16]:
for i in range(1,9): print (S(i).factor())
t*(t - 1)/2
t*(t - 1)*(2*t - 1)/6
t**2*(t - 1)**2/4
t*(t - 1)*(2*t - 1)*(3*t**2 - 3*t - 1)/30
t**2*(t - 1)**2*(2*t**2 - 2*t - 1)/12
t*(t - 1)*(2*t - 1)*(3*t**4 - 6*t**3 + 3*t + 1)/42
t**2*(t - 1)**2*(3*t**4 - 6*t**3 - t**2 + 4*t + 2)/24
t*(t - 1)*(2*t - 1)*(5*t**6 - 15*t**5 + 5*t**4 + 15*t**3 - t**2 - 9*t - 3)/90

On va maintenant vérifier que le nombre de partitions d'un ensemble à n éléments en k blocs est bien $S(n,k)$. Pour engendrer récursivement les partitions d'une liste ll, on pourra utiliser deux fonctions auxiliaires qui insérent son dernier élément dans les partitions du reste :

In [33]:
p =((1,3,6),(2,),(4,5)) # une partition de [6], représentée par un tuple de tuples
In [34]:
def insert_all(x,p): # p partition ensembliste, on insère x dans un bloc existant de toutes les manières possibles
    return [p[:i] + ((x,)+p[i],) + p[i+1:] for i in range(len(p))]
In [35]:
insert_all(7,p)
Out[35]:
[((7, 1, 3, 6), (2,), (4, 5)),
 ((1, 3, 6), (7, 2), (4, 5)),
 ((1, 3, 6), (2,), (7, 4, 5))]
In [37]:
def add_block(x,p): # ajout d'un nouveau bloc {x}
    return ((x,),)+p
In [38]:
add_block(7,p)
Out[38]:
((7,), (1, 3, 6), (2,), (4, 5))
In [39]:
from functools import reduce
def set_partitions(ll): 
    if not ll: return [tuple()]
    else:
        mm = set_partitions(ll[1:]); x=ll[0]
        return [add_block(x,p) for p in mm] + reduce(lambda a,b:a+b,  [insert_all(x,p) for p in mm])
In [40]:
set_partitions(range(1,5))# Et voilà le travail ...
Out[40]:
[((1,), (2,), (3,), (4,)),
 ((1,), (2,), (3, 4)),
 ((1,), (2, 3), (4,)),
 ((1,), (3,), (2, 4)),
 ((1,), (2, 3, 4)),
 ((1, 2), (3,), (4,)),
 ((2,), (1, 3), (4,)),
 ((2,), (3,), (1, 4)),
 ((1, 2), (3, 4)),
 ((2,), (1, 3, 4)),
 ((1, 2, 3), (4,)),
 ((2, 3), (1, 4)),
 ((1, 3), (2, 4)),
 ((3,), (1, 2, 4)),
 ((1, 2, 3, 4),)]

On peut maintenant les dénombrer par nombre de blocs :

In [44]:
def block_lengths(n):
    pp = set_partitions(range(1,n+1))
    ll = list(map(len,pp))
    return [ll.count(i) for i in range(1,n+1)]

On retrouve bien les S(n,k) ...

In [45]:
for i in range(1,10): print (block_lengths(i))
[1]
[1, 1]
[1, 3, 1]
[1, 7, 6, 1]
[1, 15, 25, 10, 1]
[1, 31, 90, 65, 15, 1]
[1, 63, 301, 350, 140, 21, 1]
[1, 127, 966, 1701, 1050, 266, 28, 1]
[1, 255, 3025, 7770, 6951, 2646, 462, 36, 1]

Evidemment, notre algorithme n'est pas très efficace. On trouvera ici le meilleur algorithme connu pour engendrer les partitions ensemblistes. Le programmer en Cython, et comparer ses performances avec l'algorithme naif codé précédemment.

In [46]:
# Version pur Python
def d2w(d):
    return ''.join([str(d[i+1]) for i in range(len(d))])

def SetPartitions(n):
    ll = []
    c = {}
    def SP(m,p):
        if p<n:
            for i in range(1,m+1):
                c[p] = i
                SP(m,p+1)
            c[p] = m+1
            SP(m+1,p+1)
        elif p==n:
            for i in range(1,m+2):
                c[p] = i
                ll.append(d2w(c))
    SP(0,1)
    return ll
In [47]:
# d'abord, la méthode bête et brutale
from time import time
t = time(); print (len(set_partitions(range(1,11))));print (time()-t)
115975
15.619646310806274
In [49]:
# ensuite, l'algorithme subtil
t = time(); print (len(SetPartitions(10)));print (time()-t)
115975
0.7810494899749756
In [51]:
# Et maintenant, la version Cython
from sp import SetPartitions as setp
t = time(); print (len(setp(10)));print (time()-t)
115975
0.028479576110839844

Voilà la marche à suivre ...

$ cat sp.pyx
#def d2w(d):
#    return ''.join([str(d[i+1]) for i in range(len(d))])

def SetPartitions(int n):                                                                                                                                                   
    ll = []                                                                                                                                                                 
    c = [0]*n
    def SP(int m,int p):
        if p<n:
            for i in range(1,m+1):
                c[p-1] = i
                SP(m,p+1)
            c[p-1] = m+1
            SP(m+1,p+1)
        elif p==n:
            for i in range(1,m+2):
                c[p-1] = i
                ll.append(tuple(c))
    SP(0,1)
    return ll

$ cat setup.py
from distutils.core import setup
from Cython.Build import cythonize

setup( ext_modules = cythonize("sp.pyx"))


$ python setup.py build_ext --inplace

$ ls sp.*
sp.c  sp.py  sp.pyx  sp.pyx~  sp.so*