1Representer une matrice par une liste de listes
Python n'a pas de type « matrice » natif : on represente une matrice par une liste de listes, chaque sous-liste etant une ligne. Cette representation est simple, mais elle a des pieges qu'il faut connaitre avant d'ecrire la moindre ligne de code.
Une matrice \(A \in \mathcal{M}_{n,p}(\mathbb{R})\) est codee par une liste
A de \(n\) listes de \(p\) nombres. Le coefficient \(a_{i,j}\) (ligne \(i\), colonne
\(j\), indices a partir de 0) est A[i][j].
A = [[1, 2, 3],
[4, 5, 6]] # matrice 2 x 3
n = len(A) # 2 : nombre de lignes
p = len(A[0]) # 3 : nombre de colonnes
A[1][2] # 6A = [[0] * 3] * 2 # PIEGE : une seule ligne, referencee deux fois
A[0][0] = 9
print(A) # [[9, 0, 0], [9, 0, 0]]
A = [[0] * 3 for _ in range(2)] # CORRECT
A[0][0] = 9
print(A) # [[9, 0, 0], [0, 0, 0]]
liste * n repete la reference. La comprehension, elle, evalue
[0]*3 a chaque tour et cree donc \(n\) listes distinctes. Toujours utiliser
la comprehension.
Constructions usuelles
def matrice_nulle(n, p):
"""Matrice n x p remplie de zeros."""
return [[0] * p for _ in range(n)]
def identite(n):
"""Matrice identite d'ordre n."""
return [[1 if i == j else 0 for j in range(n)] for i in range(n)]
def matrice_depuis(f, n, p):
"""Matrice de terme general a[i][j] = f(i, j)."""
return [[f(i, j) for j in range(p)] for i in range(n)]
H = matrice_depuis(lambda i, j: 1 / (i + j + 1), 3, 3) # matrice de Hilbert
assert identite(2) == [[1, 0], [0, 1]]
Parcours
A = [[1, 2, 3], [4, 5, 6]]
n, p = len(A), len(A[0])
# par indices : indispensable des qu'on modifie
for i in range(n):
for j in range(p):
A[i][j] *= 2
# par valeurs : quand on ne fait que lire
for ligne in A:
for x in ligne:
print(x, end=" ")
# une colonne
colonne_1 = [A[i][1] for i in range(n)]
# la diagonale d'une matrice carree
diagonale = [A[i][i] for i in range(min(n, p))]
B = A # alias : aucune copie
B = A[:] # copie SUPERFICIELLE : les lignes sont partagees !
B = [ligne[:] for ligne in A] # copie correcte
import copy; B = copy.deepcopy(A) # equivalent, plus generalAffichage lisible
def affiche(A, largeur=7, decimales=2):
"""Affiche une matrice en colonnes alignees."""
for ligne in A:
print(" ".join(f"{x:{largeur}.{decimales}f}" if isinstance(x, float)
else f"{x:{largeur}d}" for x in ligne))
affiche([[1, 20, 300], [4000, 5, 6]])
1 20 300
4000 5 6
2Operations matricielles et pivot de Gauss
Operations matricielles
def dimensions(A):
"""(nombre de lignes, nombre de colonnes)."""
return len(A), (len(A[0]) if A else 0)
def somme(A, B):
"""A + B. Precondition : memes dimensions. Theta(np)."""
n, p = dimensions(A)
return [[A[i][j] + B[i][j] for j in range(p)] for i in range(n)]
def scalaire(k, A):
"""k.A"""
n, p = dimensions(A)
return [[k * A[i][j] for j in range(p)] for i in range(n)]
def transposee(A):
"""A^T : la ligne i devient la colonne i. Theta(np)."""
n, p = dimensions(A)
return [[A[i][j] for i in range(n)] for j in range(p)]
def produit(A, B):
"""A x B. Precondition : colonnes(A) = lignes(B). Theta(n m p)."""
n, m = dimensions(A)
m2, p = dimensions(B)
assert m == m2, "dimensions incompatibles"
C = [[0] * p for _ in range(n)]
for i in range(n):
for j in range(p):
s = 0
for k in range(m):
s += A[i][k] * B[k][j]
C[i][j] = s
return C
A = [[1, 2], [3, 4]]
I = [[1, 0], [0, 1]]
assert produit(A, I) == A
assert transposee(transposee(A)) == A
assert produit([[1, 2, 3]], [[1], [2], [3]]) == [[14]]
Le produit de deux matrices \(n\times n\) par la definition \(c_{i,j} = \sum_{k} a_{i,k} b_{k,j}\) demande \(n^3\) multiplications et \(n^2(n-1)\) additions, soit \(\Theta(n^3)\). Doubler \(n\) multiplie le temps par 8.
L'algorithme de Strassen (1969) abaisse ce cout a \(\Theta(n^{\log_2 7}) \approx \Theta(n^{2{,}807})\) en remplacant 8 multiplications de blocs par 7, au prix d'additions supplementaires. Le meilleur exposant connu a ce jour est d'environ \(2{,}37\), mais ces algorithmes ne sont pas utilises en pratique avant des tailles enormes.
Puissance rapide d'une matrice
def puissance(A, k):
"""A^k par exponentiation rapide. Theta(n^3 log k) au lieu de Theta(n^3 k)."""
n = len(A)
R = identite(n)
P = [ligne[:] for ligne in A]
while k > 0:
if k % 2 == 1:
R = produit(R, P)
P = produit(P, P)
k //= 2
return R
# suite de Fibonacci par puissance de matrice
F = [[1, 1], [1, 0]]
assert puissance(F, 10)[0][1] == 55 # F(10) = 55
On ecrit \(k\) en base 2 : \(k = \sum_i b_i 2^i\). Alors \(A^k = \prod_{i\,:\,b_i=1} A^{2^i}\). Les carres successifs \(A, A^2, A^4, \dots\) s'obtiennent en elevant au carre le precedent : il y en a \(\lfloor\log_2 k\rfloor + 1\). Le nombre total de produits matriciels est donc \(O(\log k)\) au lieu de \(k-1\). Pour \(k = 10^6\), on passe d'un million de produits a une vingtaine.
Appliquee a \(F = \begin{pmatrix}1&1\\1&0\end{pmatrix}\), la relation \(F^k = \begin{pmatrix}F_{k+1}&F_k\\F_k&F_{k-1}\end{pmatrix}\) donne le \(k\)-ieme terme de Fibonacci en \(O(\log k)\) operations.
Systemes lineaires : le pivot de Gauss
def gauss(A, b):
"""Resout A x = b par elimination de Gauss avec pivot partiel.
A : matrice carree n x n (non modifiee), b : liste de n reels.
Renvoie la liste solution, ou None si la matrice est singuliere.
"""
n = len(A)
# matrice augmentee, copiee pour ne pas modifier les arguments
M = [A[i][:] + [b[i]] for i in range(n)]
for k in range(n):
# 1. recherche du pivot de plus grande valeur absolue (stabilite numerique)
imax = max(range(k, n), key=lambda i: abs(M[i][k]))
if abs(M[imax][k]) < 1e-12:
return None # colonne nulle : pas de solution unique
M[k], M[imax] = M[imax], M[k]
# 2. elimination sous le pivot
for i in range(k + 1, n):
f = M[i][k] / M[k][k]
for j in range(k, n + 1):
M[i][j] -= f * M[k][j]
# 3. remontee (substitution arriere)
x = [0.0] * n
for i in range(n - 1, -1, -1):
s = M[i][n] - sum(M[i][j] * x[j] for j in range(i + 1, n))
x[i] = s / M[i][i]
return x
A = [[2, 1, -1], [-3, -1, 2], [-2, 1, 2]]
b = [8, -11, -3]
x = gauss(A, b)
print([round(v, 10) for v in x]) # [2.0, 3.0, -1.0]
Diviser par un pivot tres petit amplifie les erreurs d'arrondi. Choisir a chaque etape le coefficient de plus grande valeur absolue dans la colonne garantit que tous les facteurs \(f = M_{i,k}/M_{k,k}\) sont de module \(\le 1\) : l'algorithme devient numeriquement stable. Sans ce choix, un systeme parfaitement bien conditionne peut donner un resultat faux.
Complexite : l'elimination demande \(\sum_{k}(n-k)(n-k+2) \sim \frac{2n^3}{3}\) operations, soit \(\Theta(n^3)\) ; la remontee \(\Theta(n^2)\).
3Produit interactif, images et grilles
Matrices et images
Une image en niveaux de gris est exactement une matrice d'entiers de 0 (noir) a 255 (blanc). Les traitements du chapitre 19 sont donc des operations matricielles.
def negatif(img):
"""Inverse les niveaux de gris."""
return [[255 - x for x in ligne] for ligne in img]
def rotation_90(A):
"""Rotation d'un quart de tour dans le sens horaire."""
n, p = len(A), len(A[0])
return [[A[n-1-i][j] for i in range(n)] for j in range(p)]
def symetrie_horizontale(A):
return [ligne[::-1] for ligne in A]
A = [[1, 2], [3, 4]]
assert rotation_90(A) == [[3, 1], [4, 2]]
assert symetrie_horizontale(A) == [[2, 1], [4, 3]]
La rotation de 90 degres est la composee de la transposition et d'une symetrie :
rotation_90(A) == symetrie_horizontale(transposee(A)). Verifier cette identite est
un bon exercice.
Grilles et voisinages
De nombreux problemes (jeu de la vie, labyrinthes, plus court chemin) manipulent une grille et les voisins d'une case. Le motif standard :
def voisins(A, i, j, diagonales=False):
"""Liste des coordonnees des voisins valides de la case (i, j)."""
n, p = len(A), len(A[0])
if diagonales:
deplacements = [(-1,-1), (-1,0), (-1,1), (0,-1),
(0,1), (1,-1), (1,0), (1,1)]
else:
deplacements = [(-1,0), (1,0), (0,-1), (0,1)]
res = []
for di, dj in deplacements:
i2, j2 = i + di, j + dj
if 0 <= i2 < n and 0 <= j2 < p: # test de bord INDISPENSABLE
res.append((i2, j2))
return res
Oublier le test 0 <= i2 ne provoque pas d'IndexError :
A[-1] designe la derniere ligne. La grille se comporte alors comme un tore
et le resultat est silencieusement faux. C'est un bug tres difficile a reperer.
L'essentiel du chapitre
- Construire une matrice avec
[[0]*p for _ in range(n)], jamais avec[[0]*p]*n. A[i][j]: \(i\) est la ligne, \(j\) la colonne, indices a partir de 0.- Copier une matrice :
[ligne[:] for ligne in A]. - Le produit matriciel naif coute \(\Theta(n^3)\) ; l'exponentiation rapide, \(\Theta(n^3\log k)\).
- Le pivot de Gauss coute \(\Theta(n^3)\) ; le pivot partiel est indispensable a la stabilite numerique.
- Toujours tester les bornes avant d'acceder a un voisin dans une grille.