Partie II - Structures de donnees · Chapitre 8

Utilisation des Listes (Matrices)

3 sections de cours · fiche d'exercices corrigee

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.

Convention

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]                # 6
Le piege de la construction
A = [[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))]
Copie d'une matrice
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 general

Affichage 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]]
Complexite du produit

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
Pourquoi cela marche

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]
Pourquoi le pivot partiel ?

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

Produit matriciel : le coefficient \(c_{i,j}\) pas a pas
Cliquer pour calculer les coefficients de C = A x B un par un.

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]]
A retenir

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
Le piege des indices negatifs

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.
Fiche d'exercices — chapitre 8Enonces gradues, du calcul d'application au niveau concours, avec corriges detailles.
Ouvrir la fiche →