# ==========================================================
# Programmes Python du cours d'informatique BCPST
# Generation automatique : tous les programmes de plus de 5 lignes,
# extraits des blocs lstlisting des chapitres du cours.
# ==========================================================

# --- Chapitre1 : Types de base, variables et opérations / Entrées et sorties / Formatage des chaînes (f-strings) ---
n = 42
pi = 3.14159265
print(f"n = {n}, pi = {pi:.4f}")   # n = 42, pi = 3.1416

# Sans f-string, equivalent avec print :
print("n =", n, ", pi =", round(pi, 4))


# --- Chapitre1 : Types de base, variables et opérations / Fonctions natives ---
L = [3, 1, 4, 1, 5, 9, 2]
print(len(L))       # 7
print(sum(L))       # 25
print(sorted(L))    # [1, 1, 2, 3, 4, 5, 9]

for i, x in enumerate(["a", "b", "c"]):
    print(i, x)     # 0 a / 1 b / 2 c




# --- Chapitre2 : Instructions conditionnelles / L'instruction if ---
if condition:
    instruction_1   # executee si condition est True
    instruction_2
    ...
else:
    instruction_3   # executee si condition est False
    instruction_4
    ...


# --- Chapitre2 : Instructions conditionnelles / L'instruction if ---
if condition_1:
    ...          # si condition_1 est True
elif condition_2:
    ...          # si condition_1 est False et condition_2 est True
elif condition_3:
    ...          # si les deux precedentes sont False et condition_3 est True
else:
    ...          # si toutes les conditions precedentes sont False


# --- Chapitre2 : Instructions conditionnelles / Conditions composées ---
x = 3
if x > 0 and x < 10:
    print("x est compris entre 0 et 10 (exclus)")

if x < 0 or x > 100:
    print("x est hors de [0, 100]")

if not (x == 0):
    print("x est non nul")


# --- Chapitre2 : Instructions conditionnelles / Imbrication de conditions ---
x = 5
if x >= 0:
    if x == 0:
        print("x est nul")
    else:
        print("x est strictement positif")
else:
    print("x est strictement negatif")


# --- Chapitre2 : Instructions conditionnelles / Exemples algorithmiques / Signe d'un nombre ---
x = float(input("Entrer un nombre : "))
if x > 0:
    print("x est positif")
elif x < 0:
    print("x est negatif")
else:
    print("x est nul")


# --- Chapitre2 : Instructions conditionnelles / Exemples algorithmiques / Maximum de deux valeurs ---
a = float(input("Entrer a : "))
b = float(input("Entrer b : "))
if a >= b:
    m = a
else:
    m = b
print("Le maximum est", m)


# --- Chapitre2 : Instructions conditionnelles / Exemples algorithmiques / Maximum de trois valeurs ---
a = float(input("Entrer a : "))
b = float(input("Entrer b : "))
c = float(input("Entrer c : "))

# Approche 1 : comparaisons successives
m = a
if b > m:
    m = b
if c > m:
    m = c
print("Le maximum est", m)


# --- Chapitre2 : Instructions conditionnelles / Exemples algorithmiques / Maximum de trois valeurs ---
# Approche 2 : conditions combinees avec elif
if a >= b and a >= c:
    m = a
elif b >= c:
    m = b
else:
    m = c
print("Le maximum est", m)


# --- Chapitre2 : Instructions conditionnelles / Exemples algorithmiques / Discriminant d'un trinôme ---
a = float(input("Entrer a : "))
b = float(input("Entrer b : "))
c = float(input("Entrer c : "))
delta = b**2 - 4*a*c
if delta > 0:
    print("Deux racines :", (-b - delta**0.5)/(2*a),
                            (-b + delta**0.5)/(2*a))
elif delta == 0:
    print("Une racine double :", -b/(2*a))
else:
    print("Pas de racine reelle")


# --- Chapitre3 : Boucles / La boucle for ---
for i in range(4):
    print(i)          # affiche 0, 1, 2, 3

for i in range(1, 6):
    print(i)          # affiche 1, 2, 3, 4, 5

for i in range(0, 11, 2):
    print(i)          # affiche 0, 2, 4, 6, 8, 10


# --- Chapitre3 : Boucles / Algorithmes classiques / Algorithme d'Euclide ---
# Trouver le premier multiple de 7 superieur a 50
i = 51
while True:       # boucle a priori infinie
    if i % 7 == 0:
        print(i)
        break     # on sort des qu'on a trouve
    i = i + 1


# --- Chapitre4 : Fonctions / Définir et appeler une fonction ---
def maximum(a, b):
    if a >= b:
        return a
    return b

m = maximum(3, 7)   # appel : a vaut 3, b vaut 7
print(m)            # affiche 7
print(maximum(10, 2))  # affiche 10


# --- Chapitre4 : Fonctions / Valeur de retour ---
def signe(x):
    if x > 0:
        return "positif"
    if x < 0:
        return "negatif"
    return "nul"

print(signe(-3))   # affiche negatif
print(signe(0))    # affiche nul


# --- Chapitre4 : Fonctions / Portée des variables ---
x = 10          # variable globale

def f():
    x = 5       # variable locale : ne modifie pas le x global
    print(x)    # affiche 5

f()
print(x)        # affiche 10 (x global inchange)


# --- Chapitre4 : Fonctions / Paramètres par défaut ---
def puissance(x, n=2):
    p = 1
    for i in range(n):
        p = p * x
    return p

print(puissance(3))     # n vaut 2 par defaut : affiche 9
print(puissance(3, 4))  # n vaut 4 : affiche 81


# --- Chapitre4 : Fonctions / Exemples / Parité ---
def est_pair(n):
    """Renvoie True si l'entier n est pair, False sinon."""
    return n % 2 == 0

print(est_pair(4))   # True
print(est_pair(7))   # False


# --- Chapitre4 : Fonctions / Exemples / Factorielle ---
def factorielle(n):
    """Renvoie n! pour un entier n >= 0."""
    p = 1
    for i in range(1, n + 1):
        p = p * i
    return p

print(factorielle(5))   # 120


# --- Chapitre4 : Fonctions / Exemples / PGCD (algorithme d'Euclide) ---
def pgcd(a, b):
    """Renvoie le PGCD de deux entiers strictement positifs a et b."""
    while b != 0:
        a, b = b, a % b
    return a

print(pgcd(48, 18))   # 6


# --- Chapitre4 : Fonctions / Exemples / Test de primalité ---
def est_premier(n):
    """Renvoie True si n est premier, False sinon (n entier >= 2)."""
    if n < 2:
        return False
    for d in range(2, int(n**0.5) + 1):
        if n % d == 0:
            return False
    return True

print(est_premier(17))   # True
print(est_premier(15))   # False


# --- Chapitre4 : Fonctions / Exemples / Test de primalité ---
# Fonction pure : pas d'effet de bord
def double(x):
    return 2 * x

# Fonction avec effet de bord (affichage)
def double_affiche(x):
    print(2 * x)   # effet de bord : affichage


# --- Chapitre5 : Listes / Accès aux éléments ---
L = [10, 20, 30, 40, 50]
print(L[1:4])    # [20, 30, 40]
print(L[:3])     # [10, 20, 30]  (depuis le debut)
print(L[2:])     # [30, 40, 50]  (jusqu'a la fin)
print(L[:])      # [10, 20, 30, 40, 50]  (copie)
print(L[::2])    # [10, 30, 50]  (un element sur deux)
print(L[::-1])   # [50, 40, 30, 20, 10]  (liste inversee)


# --- Chapitre5 : Listes / Modification d'une liste ---
L = [1, 2, 3]
L[0] = 10          # L vaut [10, 2, 3]
L.append(4)        # L vaut [10, 2, 3, 4]
L.pop()            # renvoie 4, L vaut [10, 2, 3]
L.insert(1, 99)    # L vaut [10, 99, 2, 3]
del L[2]           # L vaut [10, 99, 3]


# --- Chapitre5 : Listes / Compréhension de liste ---
carres = [x**2 for x in range(1, 6)]
# [1, 4, 9, 16, 25]

pairs = [x for x in range(10) if x % 2 == 0]
# [0, 2, 4, 6, 8]

longueurs = [len(mot) for mot in ["chat", "chien", "oiseau"]]
# [4, 5, 6]


# --- Chapitre5 : Listes / Compréhension de liste ---
# Ces deux codes produisent le meme resultat :
carres = [x**2 for x in range(1, 6)]

carres = []
for x in range(1, 6):
    carres.append(x**2)


# --- Chapitre5 : Listes / Compréhension de liste ---
# Toutes les paires (x, y) avec x dans L1 et y dans L2
L1 = [1, 2, 3]
L2 = [10, 20]
paires = [(x, y) for x in L1 for y in L2]
# [(1,10),(1,20),(2,10),(2,20),(3,10),(3,20)]

# Equivalent avec des boucles for imbriquees :
paires = []
for x in L1:
    for y in L2:
        paires.append((x, y))


# --- Chapitre5 : Listes / Compréhension de liste ---
# Compréhension de liste (construit toute la liste, puis la somme)
s = sum([x**2 for x in range(1000)])

# Expression generatrice (calcule element par element, plus economique)
s = sum(x**2 for x in range(1000))

# Avec condition : compter les elements verifiant un critere
nb_pairs = sum(1 for x in range(100) if x % 2 == 0)   # 50


# --- Chapitre5 : Listes / P-uplets (tuples) / Renvoi de plusieurs valeurs par une fonction ---
def min_max(L):
    """Renvoie le minimum et le maximum de la liste L."""
    return min(L), max(L)

m, M = min_max([3, 1, 4, 1, 5, 9])
print(m, M)   # 1  9


# --- Chapitre5 : Listes / P-uplets (tuples) / Tuples dans les boucles for ---
points = [(0, 0), (1, 2), (3, -1)]
for x, y in points:
    print(f"x={x}, y={y}")

# Autre exemple : liste de paires (temps, valeur)
mesures = [(0.0, 20.1), (0.5, 21.3), (1.0, 22.8)]
for t, temp in mesures:
    print(t, temp)


# --- Chapitre5 : Listes / Ensembles (set) ---
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b)    # {1, 2, 3, 4, 5, 6}
print(a & b)    # {3, 4}
print(a - b)    # {1, 2}
print(3 in a)   # True


# --- Chapitre6 : Algorithmes sur les listes / Calculs sur une liste / Somme et produit ---
def somme(L):
    """Renvoie la somme des elements de L (L liste de nombres)."""
    s = 0
    for x in L:
        s = s + x
    return s

def produit(L):
    """Renvoie le produit des elements de L (L liste non vide de nombres)."""
    p = 1
    for x in L:
        p = p * x
    return p


# --- Chapitre6 : Algorithmes sur les listes / Calculs sur une liste / Recherche du maximum ---
def maximum(L):
    """Renvoie le plus grand element de L (L liste non vide)."""
    m = L[0]         # meilleur candidat courant
    for x in L:
        if x > m:
            m = x
    return m


# --- Chapitre6 : Algorithmes sur les listes / Calculs sur une liste / Recherche du maximum ---
def indice_max(L):
    """Renvoie l'indice du plus grand element de L (L liste non vide)."""
    i_max = 0
    for i in range(len(L)):
        if L[i] > L[i_max]:
            i_max = i
    return i_max


# --- Chapitre6 : Algorithmes sur les listes / Calculs sur une liste / Comptage d'occurrences ---
def compte(L, v):
    """Renvoie le nombre de fois que v apparait dans la liste L."""
    c = 0
    for x in L:
        if x == v:
            c = c + 1
    return c


# --- Chapitre6 : Algorithmes sur les listes / Recherche dans une liste / Présence d'un élément vérifiant une propriété ---
def contient_pair(L):
    """Renvoie True si L contient au moins un entier pair."""
    for x in L:
        if x % 2 == 0:
            return True
    return False


# --- Chapitre6 : Algorithmes sur les listes / Recherche dans une liste / Appartenance à une liste ---
def appartient(v, L):
    """Renvoie True si v est dans la liste L."""
    for x in L:
        if x == v:
            return True
    return False


# --- Chapitre6 : Algorithmes sur les listes / Tests sur les listes / Palindrome ---
def est_palindrome(L):
    """Renvoie True si la liste L est un palindrome."""
    n = len(L)
    for i in range(n // 2):
        if L[i] != L[n - 1 - i]:
            return False
    return True

print(est_palindrome([1, 2, 3, 2, 1]))   # True
print(est_palindrome([1, 2, 3]))          # False


# --- Chapitre6 : Algorithmes sur les listes / Tests sur les listes / Listes distinctes et doublons ---
def tous_distincts(L):
    """Renvoie True si tous les elements de L sont distincts."""
    for i in range(len(L)):
        for j in range(i + 1, len(L)):
            if L[i] == L[j]:
                return False
    return True


# --- Chapitre6 : Algorithmes sur les listes / Tests sur les listes / Listes distinctes et doublons ---
def sans_doublon(L):
    """Renvoie une liste contenant les elements de L sans repetition."""
    R = []
    for x in L:
        if x not in R:
            R.append(x)
    return R

print(sans_doublon([1, 3, 2, 1, 4, 3]))   # [1, 3, 2, 4]


# --- Chapitre6 : Algorithmes sur les listes / Tests sur les listes / Anagrammes ---
def sont_anagrammes(mot1, mot2):
    """Renvoie True si mot1 et mot2 sont des anagrammes."""
    return sorted(mot1) == sorted(mot2)

print(sont_anagrammes("chien", "niche"))   # True
print(sont_anagrammes("chat", "tache"))    # False


# --- Chapitre6 : Algorithmes sur les listes / Construction de listes / Filtrage ---
def multiples(L, k):
    """Renvoie la liste des elements de L divisibles par k."""
    R = []
    for x in L:
        if x % k == 0:
            R.append(x)
    return R

# equivalent en comprehension :
def multiples_comp(L, k):
    return [x for x in L if x % k == 0]


# --- Chapitre6 : Algorithmes sur les listes / Listes de listes (tableaux 2D) ---
# CORRECT : chaque ligne est un objet distinct
M = [[0] * 3 for i in range(2)]
M[0][1] = 99
print(M)    # [[0, 99, 0], [0, 0, 0]]  correct

# INCORRECT : toutes les lignes partagent la meme liste
M = [[0] * 3] * 2
M[0][1] = 99
print(M)    # [[0, 99, 0], [0, 99, 0]]  FAUX !


# --- Chapitre6 : Algorithmes sur les listes / Listes de listes (tableaux 2D) / Parcours d'un tableau 2D ---
def somme_tableau(M):
    """Renvoie la somme de tous les elements du tableau M."""
    s = 0
    for i in range(len(M)):
        for j in range(len(M[i])):
            s = s + M[i][j]
    return s


# --- Chapitre6 : Algorithmes sur les listes / Listes de listes (tableaux 2D) / Transposée d'une matrice ---
def transposee(M):
    """Renvoie la transposee de la matrice M."""
    n = len(M)
    p = len(M[0])
    T = [[0] * n for j in range(p)]
    for i in range(n):
        for j in range(p):
            T[j][i] = M[i][j]
    return T


# --- Chapitre6 : Algorithmes sur les listes / Listes de listes (tableaux 2D) / Produit de matrices ---
def produit_matrices(A, B):
    """Renvoie le produit matriciel A * B.
    A : n lignes x p colonnes, B : p lignes x q colonnes."""
    n = len(A)
    p = len(B)
    q = len(B[0])
    C = [[0] * q for i in range(n)]
    for i in range(n):
        for k in range(q):
            for j in range(p):
                C[i][k] = C[i][k] + A[i][j] * B[j][k]
    return C


# --- Chapitre6 : Algorithmes sur les listes / Application : nombres parfaits ---
def diviseurs_stricts(n):
    """Renvoie la liste des diviseurs stricts de n."""
    return [d for d in range(1, n) if n % d == 0]

def est_parfait(n):
    """Renvoie True si n est un nombre parfait."""
    return sum(diviseurs_stricts(n)) == n

# Liste des nombres parfaits inferieurs a 1000
parfaits = [n for n in range(1, 1001) if est_parfait(n)]
print(parfaits)   # [6, 28, 496]


# --- Chapitre7 : Fichiers texte / Lire un fichier ---
temperatures = []
with open("temperatures.txt", "r") as f:
    for ligne in f:
        t = float(ligne.strip())   # convertir la chaine en flottant
        temperatures.append(t)

print(temperatures)   # [15.2, 17.8, 14.5, 19.1, 16.3]
print("Moyenne :", sum(temperatures) / len(temperatures))


# --- Chapitre7 : Fichiers texte / Fichiers CSV / Lire un fichier CSV ---
temps = []
temperature = []

with open("mesures.csv", "r") as f:
    f.readline()   # ignorer la ligne d'en-tete
    for ligne in f:
        valeurs = ligne.strip().split(";")
        temps.append(int(valeurs[0]))
        temperature.append(float(valeurs[1]))

print(temps)          # [0, 10, 20, 30]
print(temperature)    # [15.2, 16.1, 17.3, 18.0]


# --- Chapitre7 : Fichiers texte / Fichiers CSV / Écrire un fichier CSV ---
resultats = [(0, 15.2), (10, 16.1), (20, 17.3), (30, 18.0)]

with open("sortie.csv", "w") as f:
    f.write("temps;temperature\n")   # en-tete
    for t, temp in resultats:
        f.write(str(t) + ";" + str(temp) + "\n")


# --- Chapitre7 : Fichiers texte / Exemple complet : analyse d'une série de mesures ---
longueur_onde;absorbance
400;0.12
450;0.34
500;0.87
550;1.23
600;0.56


# --- Chapitre7 : Fichiers texte / Exemple complet : analyse d'une série de mesures ---
longueurs = []
absorbances = []

with open("donnees.csv", "r") as f:
    f.readline()   # en-tete
    for ligne in f:
        vals = ligne.strip().split(";")
        longueurs.append(int(vals[0]))
        absorbances.append(float(vals[1]))

# Trouver l'indice du maximum
i_max = 0
for i in range(len(absorbances)):
    if absorbances[i] > absorbances[i_max]:
        i_max = i

print("Absorbance maximale :", absorbances[i_max])
print("Longueur d'onde :", longueurs[i_max], "nm")


# --- Chapitre7 : Fichiers texte / Exemple complet : analyse d'une série de mesures ---
import numpy as np

data = np.loadtxt("mesures.csv", delimiter=";", skiprows=1)
longueurs   = data[:, 0]   # premiere colonne
absorbances = data[:, 1]   # deuxieme colonne

print("Maximum :", np.max(absorbances))


# --- Chapitre8 : Modules / Le module math ---
from math import *

print(sqrt(2))          # 1.4142135623730951
print(pi)               # 3.141592653589793
print(exp(1))           # 2.718281828459045
print(log(exp(3)))      # 3.0
print(sin(pi / 6))      # 0.4999999999999999  (~ 0.5)
print(floor(3.7))       # 3
print(factorial(5))     # 120


# --- Chapitre8 : Modules / Le module random / Fonctions principales ---
import random as rd

print(rd.random())           # ex : 0.7342...  (dans [0,1[)
print(rd.randint(1, 6))      # ex : 4  (de de six faces)
print(rd.choice([10, 20, 30, 40]))   # ex : 20

L = [1, 2, 3, 4, 5]
rd.shuffle(L)
print(L)                     # ex : [3, 1, 5, 2, 4]


# --- Chapitre8 : Modules / Le module random / Exemple : simulation d'une expérience aléatoire ---
import random as rd

def simul_deux_des():
    """Renvoie True si au moins un des deux des vaut 6."""
    d1 = rd.randint(1, 6)
    d2 = rd.randint(1, 6)
    return d1 == 6 or d2 == 6

N = 100000
favorables = 0
for k in range(N):
    if simul_deux_des():
        favorables = favorables + 1

print("Probabilite estimee :", favorables / N)
# Valeur theorique : 1 - (5/6)^2 = 11/36 ~ 0.306


# --- Chapitre8 : Modules / Le module numpy ---
import numpy as np

a = np.array([1, 2, 3, 4, 5])
print(a * 2)      # [2  4  6  8  10]
print(a ** 2)     # [ 1  4  9 16 25]
print(a + 10)     # [11 12 13 14 15]


# --- Chapitre8 : Modules / Le module matplotlib.pyplot ---
import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 2 * np.pi, 200)
y = np.sin(x)

plt.plot(x, y)
plt.show()


# --- Chapitre8 : Modules / Le module matplotlib.pyplot ---
import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 2 * np.pi, 300)

plt.plot(x, np.sin(x), "b-", label="sin(x)")
plt.plot(x, np.cos(x), "r--", label="cos(x)")
plt.xlabel("x")
plt.ylabel("y")
plt.title("Fonctions trigonometriques")
plt.legend()
plt.grid()
plt.show()


# --- Chapitre8 : Modules / Le module matplotlib.pyplot / Tracer sans numpy : avec math et des listes ---
import matplotlib.pyplot as plt

def f(x):
    """ Renvoie la valeur de f(x)"""
    ...
    return ...

# Tracer de la représe'ntation de f sur [a,b]

n = 200
a, b = ..., ... 
x = [ a + k*(b-a)/n for k in range(n + 1)]
y = [f(xi) for xi in x]

plt.plot(x, y)
plt.show()


# --- Chapitre8 : Modules / Le module matplotlib.pyplot / Tracer sans numpy : avec math et des listes ---
from math import sin, pi
import matplotlib.pyplot as plt

def f(x):
    if x < pi:
        return sin(x)
    return - sin(x)


n = 200
x = [2 * pi * k / n for k in range(n + 1)]
y = [f(xi) for xi in x]

plt.plot(x, y)
plt.show()


# --- Chapitre8 : Modules / Exemple synthétique : intégration graphique ---
import numpy as np
import matplotlib.pyplot as plt

def f(x):
    return x**2

# Courbe exacte
x = np.linspace(0, 2, 200)
plt.plot(x, f(x), "b-", label="f(x) = x^2")

# Rectangles (n = 6 sous-intervalles)
n = 6
a, b = 0, 2
h = (b - a) / n
for k in range(n):
    xk = a + k * h
    plt.bar(xk, f(xk), width=h, align="edge",
            edgecolor="k", facecolor="lightblue", alpha=0.5)

plt.xlabel("x")
plt.title("Methode des rectangles a gauche, n = 6")
plt.legend()
plt.grid()
plt.show()


# --- Chapitre8 : Modules / Exemple synthétique : intégration graphique ---
import numpy as np

A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])

print(A + B)          # addition terme a terme
print(A @ B)          # produit matriciel
print(np.transpose(A)) # transposee

print(np.linalg.det(A))    # determinant
print(np.linalg.inv(A))    # inverse
print(np.linalg.eig(A))    # valeurs et vecteurs propres


# --- Chapitre9 : Fonctions récursives / Principe de la récursion ---
def factorielle(n):
    """Renvoie n! pour un entier n >= 0."""
    if n == 0:          # cas de base
        return 1
    return n * factorielle(n - 1)

print(factorielle(5))   # 120


# --- Chapitre9 : Fonctions récursives / Principe de la récursion / Déroulement des appels ---
factorielle(4)
  = 4 * factorielle(3)
  = 4 * (3 * factorielle(2))
  = 4 * (3 * (2 * factorielle(1)))
  = 4 * (3 * (2 * (1 * factorielle(0))))
  = 4 * (3 * (2 * (1 * 1)))
  = 4 * (3 * (2 * 1))
  = 4 * (3 * 2)
  = 4 * 6
  = 24


# --- Chapitre9 : Fonctions récursives / Exemples classiques / Suite de Fibonacci ---
def fibonacci(n):
    """Renvoie le n-ieme terme de la suite de Fibonacci (n >= 0)."""
    if n == 0:
        return 0
    if n == 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))   # 55


# --- Chapitre9 : Fonctions récursives / Exemples classiques / Suite de Fibonacci ---
def fibonacci_iter(n):
    """Version iterative : calcule F_n sans recalculs inutiles."""
    if n == 0:
        return 0
    a, b = 0, 1
    for i in range(n - 1):
        a, b = b, a + b
    return b


# --- Chapitre9 : Fonctions récursives / Exemples classiques / PGCD (algorithme d'Euclide) ---
def pgcd(a, b):
    """Renvoie le PGCD de a et b (a, b entiers positifs)."""
    if b == 0:              # cas de base
        return a
    return pgcd(b, a % b)   # appel recursif

print(pgcd(48, 18))   # 6


# --- Chapitre9 : Fonctions récursives / Exemples classiques / Exponentiation rapide ---
def puissance(x, n):
    """Renvoie x**n pour un entier n >= 0."""
    if n == 0:
        return 1
    if n % 2 == 0:
        moitie = puissance(x, n // 2)
        return moitie * moitie
    return x * puissance(x, n - 1)

print(puissance(2, 10))   # 1024


# --- Chapitre9 : Fonctions récursives / Application : processus de branchement / Modèle ---
import random as rd

def temps_extinction():
    """Renvoie le temps d'extinction d'une lignee.
    A chaque generation : extinction (proba 1/2) ou deux descendants (proba 1/2)."""
    if rd.random() < 0.5:
        return 1                      # extinction des la generation suivante
    t1 = temps_extinction()           # temps d'extinction de la premiere lignee
    t2 = temps_extinction()           # temps d'extinction de la seconde lignee
    return 1 + max(t1, t2)


# --- Chapitre9 : Fonctions récursives / Application : processus de branchement / Terminaison probabiliste ---
def est_pair(n):
    if n == 0:
        return True
    return est_impair(n - 1)

def est_impair(n):
    if n == 0:
        return False
    return est_pair(n - 1)


# --- Chapitre9 : Fonctions récursives / Application : processus de branchement / Terminaison probabiliste ---
memo = {}

def fib_memo(n):
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    memo[n] = fib_memo(n - 1) + fib_memo(n - 2)
    return memo[n]


# --- Chapitre10 : Recherche dichotomique / Recherche dans une liste triée / Recherche linéaire : rappel ---
def recherche_lineaire(L, v):
    """Renvoie True si v est dans la liste L."""
    for x in L:
        if x == v:
            return True
    return False


# --- Chapitre10 : Recherche dichotomique / Recherche dans une liste triée / Version itérative ---
def recherche_dicho(L, v):
    """Renvoie True si v est dans la liste triee L, False sinon."""
    g = 0
    d = len(L) - 1
    while g <= d:
        m = (g + d) // 2
        if L[m] == v:
            return True
        if L[m] < v:
            g = m + 1
        else:
            d = m - 1
    return False

L = [1, 3, 5, 7, 9, 11, 13, 15]
print(recherche_dicho(L, 7))    # True
print(recherche_dicho(L, 6))    # False


# --- Chapitre10 : Recherche dichotomique / Recherche dans une liste triée / Version récursive ---
def recherche_dicho_rec(L, v, g, d):
    """Renvoie True si v est dans L[g:d+1] (liste triee)."""
    if g > d:               # zone vide : v absent
        return False
    m = (g + d) // 2
    if L[m] == v:
        return True
    if L[m] < v:
        return recherche_dicho_rec(L, v, m + 1, d)
    return recherche_dicho_rec(L, v, g, m - 1)

L = [1, 3, 5, 7, 9, 11, 13, 15]
print(recherche_dicho_rec(L, 7, 0, len(L) - 1))   # True


# --- Chapitre10 : Recherche dichotomique / Résolution approchée d'une équation ---
def f(x):
    return x**2 - 2

def dichotomie(f, a, b, eps):
    """Renvoie une valeur approchee a eps/2 pres de la racine de f sur [a,b].
    Condition : f(a) et f(b) de signes opposes."""
    g, d = a, b
    while d - g > eps:
        m = (g + d) / 2
        if f(g) * f(m) < 0:
            d = m
        else:
            g = m
    return (g + d) / 2

racine = dichotomie(f, 1, 2, 1e-6)
print(racine)          # 1.4142136...  (valeur approchee de sqrt(2))


# --- Chapitre10 : Recherche dichotomique / Résolution approchée d'une équation / Exemple : recherche d'un zéro sur plusieurs intervalles ---
def f(x):
    return x**3 - 3*x + 1

def dichotomie(f, a, b, eps):
    g, d = a, b
    while d - g > eps:
        m = (g + d) / 2
        if f(g) * f(m) < 0:
            d = m
        else:
            g = m
    return (g + d) / 2

# Localisation des changements de signe par pas de 0.1
n = 40
pas = 4 / n
racines = []
for k in range(n):
    a = -2 + k * pas
    b = a + pas
    if f(a) * f(b) < 0:
        racines.append(dichotomie(f, a, b, 1e-9))

print(racines)   # trois racines approchees


# --- Chapitre11 : Algorithmes de tri / Tri par sélection / Mise en œuvre ---
def indice_min(L, k):
    """Renvoie l'indice du minimum de L[k:]."""
    i_min = k
    for i in range(k + 1, len(L)):
        if L[i] < L[i_min]:
            i_min = i
    return i_min

def tri_selection(L):
    """Trie la liste L en place par selection."""
    n = len(L)
    for k in range(n - 1):
        i_min = indice_min(L, k)
        L[k], L[i_min] = L[i_min], L[k]

L = [5, 3, 8, 1, 9, 2]
tri_selection(L)
print(L)   # [1, 2, 3, 5, 8, 9]


# --- Chapitre11 : Algorithmes de tri / Tri par insertion / Mise en œuvre ---
def insere(L, j):
    """Insere L[j] dans L[0:j] supposee triee, de sorte que L[0:j+1] soit triee."""
    val = L[j]
    i = j
    while i > 0 and L[i - 1] > val:
        L[i] = L[i - 1]
        i = i - 1
    L[i] = val

def tri_insertion(L):
    """Trie la liste L en place par insertion."""
    for j in range(1, len(L)):
        insere(L, j)

L = [5, 3, 8, 1, 9, 2]
tri_insertion(L)
print(L)   # [1, 2, 3, 5, 8, 9]


# --- Chapitre11 : Algorithmes de tri / Tri par comptage / Mise en œuvre ---
def occurrences(L, k):
    """Renvoie la liste des occurrences des entiers 0, 1, ..., k-1 dans L."""
    occ = [0] * k
    for x in L:
        occ[x] = occ[x] + 1
    return occ

def tri_comptage(L, k):
    """Trie en place la liste L dont les valeurs sont dans [[0, k-1]]."""
    occ = occurrences(L, k)
    i = 0
    for v in range(k):
        for c in range(occ[v]):
            L[i] = v
            i = i + 1

L = [3, 1, 4, 1, 5, 2, 3, 2]
tri_comptage(L, 6)
print(L)   # [1, 1, 2, 2, 3, 3, 4, 5]


# --- Chapitre11 : Algorithmes de tri / Comparaison des algorithmes ---
def segmente(L, i, j):
    """Place L[j-1] (pivot) a sa position definitive dans L[i:j].
    Renvoie l'indice final du pivot."""
    pivot = L[j - 1]
    place = i
    for k in range(i, j - 1):
        if L[k] < pivot:
            L[place], L[k] = L[k], L[place]
            place = place + 1
    L[place], L[j - 1] = L[j - 1], L[place]
    return place

def tri_rapide(L, i, j):
    """Trie L[i:j] en place par tri rapide."""
    if j - i <= 1:
        return
    place = segmente(L, i, j)
    tri_rapide(L, i, place)
    tri_rapide(L, place + 1, j)

L = [5, 3, 8, 1, 9, 2]
tri_rapide(L, 0, len(L))
print(L)   # [1, 2, 3, 5, 8, 9]


# --- Chapitre12 : Tris avancés / Tri fusion (\textit{mergesort}) / Fusion de deux listes triées ---
def fusion(L1, L2):
    """Fusionne deux listes triees, renvoie une nouvelle liste triee."""
    res = []
    i, j = 0, 0
    while i < len(L1) and j < len(L2):
        if L1[i] <= L2[j]:
            res.append(L1[i])
            i = i + 1
        else:
            res.append(L2[j])
            j = j + 1
    return res + L1[i:] + L2[j:]

print(fusion([1, 3, 5], [2, 4, 6]))   # [1, 2, 3, 4, 5, 6]


# --- Chapitre12 : Tris avancés / Tri fusion (\textit{mergesort}) / Tri fusion récursif ---
def tri_fusion(L):
    """Renvoie une nouvelle liste triee contenant les elements de L."""
    n = len(L)
    if n <= 1:
        return L[:]
    m = n // 2
    gauche = tri_fusion(L[:m])
    droite = tri_fusion(L[m:])
    return fusion(gauche, droite)

L = [5, 3, 8, 1, 9, 2]
print(tri_fusion(L))   # [1, 2, 3, 5, 8, 9]
print(L)               # [5, 3, 8, 1, 9, 2]  (L non modifiee)


# --- Chapitre13 : Mutabilité et effets de bord / Aliasing / Deux variables, un seul objet ---
L1 = [1, 2, 3]
L2 = L1          # L2 et L1 designent le meme objet

L2[0] = 99
print(L1)        # [99, 2, 3]  -- L1 est modifiee !
print(L2)        # [99, 2, 3]


# --- Chapitre13 : Mutabilité et effets de bord / Aliasing / Deux variables, un seul objet ---
L1 = [1, 2, 3]
L2 = L1
L3 = [1, 2, 3]   # meme contenu, objet different

print(L1 is L2)   # True  -- meme objet
print(L1 is L3)   # False -- objets distincts
print(L1 == L3)   # True  -- meme contenu


# --- Chapitre13 : Mutabilité et effets de bord / Aliasing / Copier une liste ---
L1 = [1, 2, 3]
L2 = L1[:]        # copie superficielle
L3 = list(L1)     # copie superficielle

L2[0] = 99
print(L1)   # [1, 2, 3]  -- L1 n'est pas modifiee
print(L2)   # [99, 2, 3]


# --- Chapitre13 : Mutabilité et effets de bord / Effets de bord dans les fonctions / Passage par objet ---
def ajoute_zero(L):
    """Ajoute un zero en fin de L (modifie L en place)."""
    L.append(0)

L = [1, 2, 3]
ajoute_zero(L)
print(L)   # [1, 2, 3, 0]  -- L a ete modifiee


# --- Chapitre13 : Mutabilité et effets de bord / Effets de bord dans les fonctions / Passage par objet ---
# Fonction en place (effet de bord)
tri_selection(L)    # modifie L, renvoie None
L.sort()            # idem

# Fonction pure (pas d'effet de bord)
L_triee = tri_fusion(L)   # L inchangee, renvoie une nouvelle liste
L_triee = sorted(L)       # idem


# --- Chapitre13 : Mutabilité et effets de bord / Conséquences pratiques ---
a = [1, 2, 3]
b = a
c = [1, 2, 3]

print(id(a), id(b), id(c))   # id(a) == id(b) != id(c)
print(a is b)   # True
print(a is c)   # False
print(a == c)   # True


# --- Chapitre13 : Mutabilité et effets de bord / Conséquences pratiques ---
a = 5
b = 5
print(a is b)     # True  (optimisation interne)

a = 1000
b = 1000
print(a is b)     # False (en dehors de la plage optimisee)


# --- Chapitre14 : Dictionnaires / Modifier un dictionnaire ---
d = {"a": 1, "b": 2}

d["c"] = 3        # ajout d'une nouvelle entree
d["a"] = 10       # modification d'une entree existante
del d["b"]        # suppression d'une entree

print(d)   # {"a": 10, "c": 3}


# --- Chapitre14 : Dictionnaires / Parcourir un dictionnaire ---
d = {"a": 1, "b": 2, "c": 3}

# Parcours des cles
for cle in d:
    print(cle, d[cle])

# Parcours des cles et valeurs simultanement
for cle, val in d.items():
    print(cle, "->", val)

# Listes des cles, valeurs, paires
print(list(d.keys()))     # ["a", "b", "c"]
print(list(d.values()))   # [1, 2, 3]
print(list(d.items()))    # [("a", 1), ("b", 2), ("c", 3)]


# --- Chapitre14 : Dictionnaires / Applications / Comptage de fréquences ---
liste = ["chat", "chien", "chat", "oiseau", "chien", "chat"]

occurrences = {}
for valeur in liste:
    if valeur in occurrences:
        occurrences[valeur] = occurrences[valeur] + 1
    else:
        occurrences[valeur] = 1

print(occurrences)   # {'chat': 3, 'chien': 2, 'oiseau': 1}


# --- Chapitre14 : Dictionnaires / Applications / Comptage de fréquences ---
liste = ["chat", "chien", "chat", "oiseau", "chien", "chat"]

occurrences = {}
for valeur in liste:
    occurrences[valeur] = occurrences.get(valeur, 0) + 1

print(occurrences)   # {'chat': 3, 'chien': 2, 'oiseau': 1}


# --- Chapitre14 : Dictionnaires / Applications / Représentation de données structurées ---
etudiant = {
    "nom": "Dupont",
    "prenom": "Marie",
    "notes": [14, 16, 12, 18],
    "absent": False
}

moyenne = sum(etudiant["notes"]) / len(etudiant["notes"])
print(etudiant["nom"], ":", moyenne)   # Dupont : 15.0


# --- Chapitre14 : Dictionnaires / Applications / Mémoïsation ---
memo = {}

def fibonacci(n):
    """Renvoie le n-ieme terme de Fibonacci (avec memorisation)."""
    if n in memo:
        return memo[n]
    if n == 0:
        memo[0] = 0
        return 0
    if n == 1:
        memo[1] = 1
        return 1
    resultat = fibonacci(n - 1) + fibonacci(n - 2)
    memo[n] = resultat
    return resultat

print(fibonacci(50))   # calcul instantane


# --- Chapitre15 : Bases de données et SQL / Utilisation avec Python ---
import sqlite3

# Connexion (crée le fichier si inexistant)
conn = sqlite3.connect("animaux.db")
curseur = conn.cursor()

# Creation de la table
curseur.execute("""
    CREATE TABLE IF NOT EXISTS especes (
        id INTEGER PRIMARY KEY,
        nom TEXT,
        famille TEXT,
        masse_kg REAL
    )
""")

# Insertion de donnees
curseur.execute("INSERT INTO especes VALUES (1, 'Lion', 'Felidae', 190)")
curseur.execute("INSERT INTO especes VALUES (2, 'Elephant', 'Elephantidae', 5000)")
conn.commit()

# Requete
curseur.execute("SELECT nom, masse_kg FROM especes WHERE masse_kg > 500")
resultats = curseur.fetchall()
for ligne in resultats:
    print(ligne)    # ('Elephant', 5000.0)

conn.close()


# --- Chapitre15 : Bases de données et SQL / Utilisation avec Python ---
# Requete avec parametre (eviter les injections SQL)
espece_cherchee = "Felidae"
curseur.execute(
    "SELECT nom FROM especes WHERE famille = ?",
    (espece_cherchee,)
)
resultats = curseur.fetchall()


# --- Chapitre17 : Graphes --- bases / Représentations en Python / Dictionnaire de listes de voisins ---
# Graphe de l'exemple ci-dessus (non oriente)
graphe = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'E'],
    'D': ['B', 'E'],
    'E': ['C', 'D', 'B']
}

print(graphe['A'])          # ['B', 'C']  -- voisins de A
print(len(graphe['B']))     # 3           -- degre de B


# --- Chapitre17 : Graphes --- bases / Représentations en Python / Dictionnaire de listes de voisins ---
graphe_oriente = {
    'A': ['B', 'C'],
    'B': ['D'],
    'C': ['D'],
    'D': []
}


# --- Chapitre17 : Graphes --- bases / Représentations en Python / Matrice d'adjacence ---
# Graphe a 4 sommets : 0-1, 0-2, 1-3, 2-3
n = 4
M = [[0]*n for _ in range(n)]

def ajoute_arete(M, i, j):
    """Ajoute l'arete {i, j} dans la matrice M (graphe non oriente)."""
    M[i][j] = 1
    M[j][i] = 1

ajoute_arete(M, 0, 1)
ajoute_arete(M, 0, 2)
ajoute_arete(M, 1, 3)
ajoute_arete(M, 2, 3)

# Voisins du sommet 0
voisins_0 = [j for j in range(n) if M[0][j] == 1]
print(voisins_0)   # [1, 2]


# --- Chapitre17 : Graphes --- bases / Parcours d'un graphe / Parcours en largeur (BFS) ---
def bfs(graphe, depart):
    """Renvoie la liste des sommets visites depuis depart en largeur d'abord."""
    visites = [depart]
    file = [depart]
    while file:
        sommet = file.pop(0)
        for voisin in graphe[sommet]:
            if voisin not in visites:
                visites.append(voisin)
                file.append(voisin)
    return visites

print(bfs(graphe, 'A'))   # ['A', 'B', 'C', 'D', 'E']


# --- Chapitre17 : Graphes --- bases / Parcours d'un graphe / Parcours en profondeur (DFS) ---
def dfs(graphe, depart, visites=None):
    """Renvoie la liste des sommets visites depuis depart en profondeur d'abord."""
    if visites is None:
        visites = []
    visites.append(depart)
    for voisin in graphe[depart]:
        if voisin not in visites:
            dfs(graphe, voisin, visites)
    return visites

print(dfs(graphe, 'A'))   # ['A', 'B', 'D', 'E', 'C']  (ordre possible)


# --- Chapitre17 : Graphes --- bases / Parcours d'un graphe / Connexité ---
def est_connexe(graphe):
    """Renvoie True si le graphe est connexe."""
    if len(graphe) == 0:
        return True
    depart = next(iter(graphe))   # premier sommet du dictionnaire
    visites = bfs(graphe, depart)
    return len(visites) == len(graphe)

print(est_connexe(graphe))   # True


# --- Chapitre17 : Graphes --- bases / Application : distance entre sommets ---
def distance(graphe, depart, arrivee):
    """Renvoie la distance (nb d'aretes) entre depart et arrivee, ou -1 si inaccessible."""
    dist = {depart: 0}
    file = [depart]
    while file:
        sommet = file.pop(0)
        if sommet == arrivee:
            return dist[sommet]
        for voisin in graphe[sommet]:
            if voisin not in dist:
                dist[voisin] = dist[sommet] + 1
                file.append(voisin)
    return -1

print(distance(graphe, 'A', 'E'))   # 2
print(distance(graphe, 'A', 'D'))   # 2


# --- Chapitre17 : Graphes --- bases / Énumération : exploration d'un arbre de recherche / Suites avec répétition ---
# Version iterative (parcours en largeur de l'arbre)
def suites(p, n):
    """Renvoie la liste de toutes les suites de p entiers dans {1,...,n}."""
    L = [[]]
    for i in range(p):
        S = []
        for x in L:
            for k in range(1, n + 1):
                S.append(x + [k])
        L = S
    return L

print(suites(2, 3))   # [[1,1],[1,2],[1,3],[2,1],[2,2],[2,3],[3,1],[3,2],[3,3]]
print(len(suites(3, 5)))   # 125  (= 5^3)


# --- Chapitre17 : Graphes --- bases / Énumération : exploration d'un arbre de recherche / Suites avec répétition ---
# Version iterative (parcours en largeur de l'arbre)
def suites(p, n):
    """Renvoie la liste de toutes les suites de p entiers dans {1,...,n}."""
    L = [[]]
    for i in range(p):
        L = [x + [k] for x in L  for k in range(1, n + 1)]
    return L



# --- Chapitre17 : Graphes --- bases / Énumération : exploration d'un arbre de recherche / Suites avec répétition ---
# Version recursive (parcours en profondeur de l'arbre)
def suites(p, n):
    """Renvoie la liste de toutes les suites de p entiers dans {1,...,n}."""
    if p == 0:
        return [[]]
    return [x + [k] for x in suites(p - 1, n) for k in range(1, n + 1)]


# --- Chapitre17 : Graphes --- bases / Énumération : exploration d'un arbre de recherche / Élagage de l'arbre ---
def listes(p, n):
    """Renvoie la liste de toutes les listes de p entiers distincts dans {1,...,n}."""
    L = [[]]
    for i in range(p):
        S = []
        for x in L:
            for k in range(1, n + 1):
                if k not in x:          # elagage : k deja present -> branche coupee
                    S.append(x + [k])
        L = S
    return L

print(len(listes(2, 4)))   # 12  (= 4 * 3)


# --- Chapitre17 : Graphes --- bases / Énumération : exploration d'un arbre de recherche / Élagage de l'arbre ---
def strict_croissantes(p, n):
    """Renvoie la liste de toutes les suites strictement croissantes
    de p entiers dans {1,...,n}."""
    L = [[k] for k in range(1, n + 1)]
    for i in range(p - 1):
        L = [x + [k] for x in L for k in range(x[-1] + 1, n + 1)]
    return L

print(len(strict_croissantes(4, 30)))   # 27405  (= C(30,4))


# --- Chapitre17 : Graphes --- bases / Énumération : exploration d'un arbre de recherche / Élagage de l'arbre ---
def parmi(p, n):
    """Renvoie le nombre de combinaisons de p elements parmi n (C(n,p))."""
    if not (0 <= p <= n):
        return 0
    num, den = 1, 1
    for i in range(p):
        num, den = num * (n - i), den * (i + 1)
    return num // den

print(parmi(4, 30))   # 27405


# --- Chapitre17 : Graphes --- bases / Énumération : exploration d'un arbre de recherche / Élagage de l'arbre ---
def occurrences(c, mot):
    """Renvoie le nombre d'occurrences de c dans mot."""
    return sum(1 for t in mot if t == c)

def lettres_distinctes(mot):
    """Renvoie la liste des lettres distinctes de mot (sans doublon)."""
    S = []
    for t in mot:
        if t not in S:
            S.append(t)
    return S

def anagrammes(mot):
    """Renvoie la liste de tous les anagrammes de mot."""
    L = ['']
    for _ in range(len(mot)):
        S = []
        for x in L:
            for k in lettres_distinctes(mot):
                if occurrences(k, x) < occurrences(k, mot):   # lettre encore disponible
                    S.append(x + k)
        L = S
    return L

print(len(anagrammes('sarah')))     # 60
print(len(anagrammes('thimothe')))  # 10080


# --- Chapitre17 : Graphes --- bases / Énumération : exploration d'un arbre de recherche / Élagage de l'arbre ---
L = [[]]                         # racine : sequence vide
for i in range(profondeur):
    S = []
    for x in L:                  # pour chaque noeud courant
        for k in candidats:      # pour chaque fils possible
            if valide(x, k):     # elagage : on ne suit que les branches valides
                S.append(x + [k])
    L = S
return L                         # feuilles = solutions


# --- Chapitre18 : Graphes avancés / Graphes pondérés ---
# Graphe pondere non oriente
# Chaque entree : sommet -> [(voisin, poids), ...]
graphe = {
    'A': [('B', 4), ('C', 2)],
    'B': [('A', 4), ('C', 1), ('D', 5)],
    'C': [('A', 2), ('B', 1), ('D', 8), ('E', 10)],
    'D': [('B', 5), ('C', 8), ('E', 2)],
    'E': [('C', 10), ('D', 2)]
}


# --- Chapitre18 : Graphes avancés / Algorithme de Dijkstra / Implémentation ---
def dijkstra(graphe, depart):
    """Renvoie un dictionnaire des distances minimales depuis depart.
    graphe : dict {sommet: [(voisin, poids), ...]}
    Les poids doivent etre positifs ou nuls."""
    dist = {s: float('inf') for s in graphe}
    dist[depart] = 0
    non_traites = list(graphe.keys())

    while non_traites:
        # Sommet non traite de distance minimale
        u = min(non_traites, key=lambda s: dist[s])
        if dist[u] == float('inf'):
            break
        non_traites.remove(u)
        for voisin, poids in graphe[u]:
            nouvelle_dist = dist[u] + poids
            if nouvelle_dist < dist[voisin]:
                dist[voisin] = nouvelle_dist

    return dist

print(dijkstra(graphe, 'A'))
# {'A': 0, 'B': 3, 'C': 2, 'D': 8, 'E': 10}


# --- Chapitre18 : Graphes avancés / Algorithme de Dijkstra / Reconstruction du chemin ---
def dijkstra_chemin(graphe, depart, arrivee):
    """Renvoie (distance, chemin) du plus court chemin de depart a arrivee."""
    dist = {s: float('inf') for s in graphe}
    pred = {s: None for s in graphe}
    dist[depart] = 0
    non_traites = list(graphe.keys())

    while non_traites:
        u = min(non_traites, key=lambda s: dist[s])
        if dist[u] == float('inf'):
            break
        non_traites.remove(u)
        for voisin, poids in graphe[u]:
            if dist[u] + poids < dist[voisin]:
                dist[voisin] = dist[u] + poids
                pred[voisin] = u

    # Reconstitution du chemin
    chemin = []
    s = arrivee
    while s is not None:
        chemin.append(s)
        s = pred[s]
    chemin.reverse()
    return dist[arrivee], chemin

d, c = dijkstra_chemin(graphe, 'A', 'E')
print(d, c)   # 10  ['A', 'C', 'B', 'D', 'E']


# --- Chapitre18 : Graphes avancés / Détection de cycles ---
def a_cycle(graphe):
    """Renvoie True si le graphe oriente contient un cycle."""
    visites = []
    en_cours = []

    def explore(sommet):
        en_cours.append(sommet)
        for voisin in graphe[sommet]:
            if voisin in en_cours:
                return True
            if voisin not in visites:
                if explore(voisin):
                    return True
        en_cours.remove(sommet)
        visites.append(sommet)
        return False

    for sommet in graphe:
        if sommet not in visites:
            if explore(sommet):
                return True
    return False

g_sans_cycle = {'A': ['B'], 'B': ['C'], 'C': []}
g_avec_cycle = {'A': ['B'], 'B': ['C'], 'C': ['A']}
print(a_cycle(g_sans_cycle))   # False
print(a_cycle(g_avec_cycle))   # True


# --- Chapitre18 : Graphes avancés / Détection de cycles ---
def kruskal(sommets, aretes):
    """Renvoie les aretes de l'arbre couvrant minimal.
    aretes : liste de (poids, u, v)"""
    aretes_triees = sorted(aretes)
    parent = {s: s for s in sommets}

    def trouve(s):
        if parent[s] != s:
            parent[s] = trouve(parent[s])
        return parent[s]

    def union(u, v):
        parent[trouve(u)] = trouve(v)

    acm = []
    for poids, u, v in aretes_triees:
        if trouve(u) != trouve(v):
            union(u, v)
            acm.append((poids, u, v))
    return acm

sommets = ['A', 'B', 'C', 'D', 'E']
aretes = [(4,'A','B'),(2,'A','C'),(1,'B','C'),(5,'B','D'),
          (8,'C','D'),(10,'C','E'),(2,'D','E')]
print(kruskal(sommets, aretes))
# [(1,'B','C'), (2,'A','C'), (2,'D','E'), (5,'B','D')]  -- cout total 10


# --- Chapitre19 : Méthodes numériques / Intégration numérique / Méthode des rectangles ---
def integrale_rect_gauche(f, a, b, n):
    """Approximation de l'integrale de f sur [a,b] par n rectangles (point gauche)."""
    h = (b - a) / n
    somme = 0
    for k in range(n):
        somme = somme + f(a + k * h)
    return h * somme

def integrale_rect_milieu(f, a, b, n):
    """Approximation de l'integrale de f sur [a,b] par n rectangles (point milieu)."""
    h = (b - a) / n
    somme = 0
    for k in range(n):
        somme = somme + f(a + (k + 0.5) * h)
    return h * somme

def integrale_rect_droit(f, a, b, n):
    """Approximation de l'integrale de f sur [a,b] par n rectangles (point droit)."""
    h = (b - a) / n
    somme = 0
    for k in range(1, n + 1):
        somme = somme + f(a + k * h)
    return h * somme


# --- Chapitre19 : Méthodes numériques / Intégration numérique / Méthode des rectangles ---
from math import exp

def f(x):
    return exp(-x**2)

print(integrale_rect_milieu(f, 0, 1, 1000))   # 0.7468241...


# --- Chapitre19 : Méthodes numériques / Intégration numérique / Méthode des trapèzes ---
def integrale_trapezes(f, a, b, n):
    """Approximation de l'integrale de f sur [a,b] par la methode des trapezes."""
    h = (b - a) / n
    somme = (f(a) + f(b)) / 2
    for k in range(1, n):
        somme = somme + f(a + k * h)
    return h * somme

print(integrale_trapezes(f, 0, 1, 1000))   # 0.7468241...


# --- Chapitre19 : Méthodes numériques / Intégration numérique / Erreur et convergence ---
def g(x):
    return x**2

for n in [10, 100, 1000]:
    rg = integrale_rect_gauche(g, 0, 1, n)
    tr = integrale_trapezes(g, 0, 1, n)
    print(f"n={n:5d}  rect_gauche={rg:.6f}  trapezes={tr:.6f}")

# n=   10  rect_gauche=0.285000  trapezes=0.335000
# n=  100  rect_gauche=0.328500  trapezes=0.333350
# n= 1000  rect_gauche=0.332850  trapezes=0.333334


# --- Chapitre19 : Méthodes numériques / Intégration à partir de données discrètes ---
def integrale_trapezes_discret(X, Y):
    """Approximation de l'integrale a partir de points (X[i], Y[i]).
    X doit etre trie par ordre croissant."""
    somme = 0
    for i in range(len(X) - 1):
        somme = somme + (X[i+1] - X[i]) * (Y[i] + Y[i+1]) / 2
    return somme

# Exemple : absorbance mesuree a differentes longueurs d'onde
longueurs = [400, 420, 450, 500, 550, 600]
absorbances = [0.1, 0.3, 0.8, 1.2, 0.6, 0.2]

print(integrale_trapezes_discret(longueurs, absorbances))   # 92.5


# --- Chapitre19 : Méthodes numériques / Méthode de Newton / Implémentation ---
def newton(f, fprime, u0, eps, n_max=100):
    """Renvoie une valeur approchee d'un zero de f par la methode de Newton.
    f      : fonction dont on cherche un zero
    fprime : derivee de f
    u0     : valeur initiale (doit etre proche de la racine)
    eps    : precision souhaitee (arret quand |f(u)| < eps)
    n_max  : nombre maximal d'iterations (securite contre la divergence)"""
    u = u0
    for _ in range(n_max):
        if abs(f(u)) < eps:
            return u
        u = u - f(u) / fprime(u)
    return u   # renvoie la meilleure approximation apres n_max iterations


# --- Chapitre19 : Méthodes numériques / Méthode de Newton / Implémentation ---
def f(x):
    return x**2 - 2

def fprime(x):
    return 2 * x

print(newton(f, fprime, 2.0, 1e-10))   # 1.4142135623730951


# --- Chapitre19 : Méthodes numériques / Méthode de Newton / Convergence ---
# Newton : affichage des iterees
u = 2.0
for k in range(5):
    u = u - f(u) / fprime(u)
    print(f"k={k}  u={u:.15f}  |f(u)|={abs(f(u)):.2e}")

# k=0  u=1.500000000000000  |f(u)|=2.50e-01
# k=1  u=1.416666666666667  |f(u)|=6.94e-03
# k=2  u=1.414215686274510  |f(u)|=6.01e-06
# k=3  u=1.414213562374690  |f(u)|=4.51e-12
# k=4  u=1.414213562373095  |f(u)|=0.00e+00


# --- Chapitre19 : Méthodes numériques / Méthode d'Euler / Implémentation ---
def euler(f, t0, y0, T, n):
    """Resolution approchee de y' = f(t, y), y(t0) = y0, par la methode d'Euler.
    Renvoie deux listes (temps, valeurs) sur [t0, T] avec n pas."""
    h = (T - t0) / n
    t = t0
    y = y0
    temps = [t]
    valeurs = [y]
    for k in range(n):
        y = y + h * f(t, y)
        t = t + h
        temps.append(t)
        valeurs.append(y)
    return temps, valeurs


# --- Chapitre19 : Méthodes numériques / Méthode d'Euler / Exemples ---
from math import exp

def f(t, y):
    return -y

temps, valeurs = euler(f, 0, 1, 3, 100)

# Comparaison avec la solution exacte en t = 3
print(valeurs[-1])        # 0.0476...  (Euler, n=100)
print(exp(-3))            # 0.0498...  -- valeur exacte

# Avec plus de pas :
temps, valeurs = euler(f, 0, 1, 3, 1000)
print(valeurs[-1])        # 0.0496...  (encore plus proche)


# --- Chapitre19 : Méthodes numériques / Méthode d'Euler / Exemples ---
import matplotlib.pyplot as plt

r = 0.5    # taux de croissance
K = 1000   # capacite d'accueil

def f_logistique(t, y):
    return r * y * (1 - y / K)

temps, valeurs = euler(f_logistique, 0, 10, 30, 500)

plt.plot(temps, valeurs)
plt.xlabel("Temps")
plt.ylabel("Population")
plt.title("Croissance logistique")
plt.show()


# --- Chapitre19 : Méthodes numériques / Méthode d'Euler / Exemples ---
r, K = 0.5, 1000
y_star = 391.0   # population connue en t* = 5

def f_logistique(t, y):
    return r * y * (1 - y / K)

# Euler vers l'avenir : t* -> 10  (h > 0)
temps_av,  val_av  = euler(f_logistique, 5, y_star, 10, 200)

# Euler vers le passe : t* -> 0   (h < 0  car T < t0)
temps_ret, val_ret = euler(f_logistique, 5, y_star,  0, 200)

import matplotlib.pyplot as plt
plt.plot(temps_av,  val_av,        label="Euler vers l'avenir")
plt.plot(temps_ret, val_ret, '--', label="Euler vers le passe")
plt.scatter([5], [y_star], color='black', zorder=5, label="condition en $t^*$")
plt.xlabel("Temps")
plt.ylabel("Population")
plt.legend()
plt.show()


# --- Chapitre19 : Méthodes numériques / Méthode d'Euler / Précision et pas de temps ---
# Exemple : y' = 2*y + t,  y(0) = 1,  sur [0, 2]
def F(y, t):
    return 2*y + t

t = np.linspace(0, 2, 1000)    # tableau des instants
y = si.odeint(F, [1], t)       # y[k] contient la solution en t[k]

plt.plot(t, y)
plt.xlabel("t")
plt.ylabel("y")
plt.show()


# --- Chapitre19 : Méthodes numériques / Méthode d'Euler / Précision et pas de temps ---
# Modele de Lotka-Volterra (proies x, predateurs y)
# x' = (a - b*y)*x
# y' = -(c - d*x)*y
def F(Y, t):
    a, b, c, d = 0.8, 0.4, 0.9, 0.1
    x = Y[0]
    y = Y[1]
    return [(a - b*y)*x, -(c - d*x)*y]

t = np.linspace(0, 30, 2000)
Y = si.odeint(F, [5, 3], t)   # Y[k] = [x_k, y_k]

x = Y[:, 0]   # composante x pour tous les instants
y = Y[:, 1]   # composante y pour tous les instants

plt.plot(t, x, label="proies")
plt.plot(t, y, label="predateurs")
plt.legend()
plt.show()


# --- Chapitre19 : Méthodes numériques / Méthode d'Euler / Précision et pas de temps ---
# Pendule : theta'' + w0^2 * sin(theta) = 0
# Systeme : Y = [theta, v]  avec  theta' = v,  v' = -w0^2 * sin(theta)
def F(Y, t):
    theta = Y[0]
    v     = Y[1]
    w0 = 2 * np.pi
    return [v, -w0**2 * np.sin(theta)]

t = np.linspace(0, 5, 1000)
Y = si.odeint(F, [0.3, 0], t)   # theta(0) = 0.3 rad, theta'(0) = 0
plt.plot(t, Y[:, 0])
plt.xlabel("t")
plt.ylabel("theta")
plt.show()


# --- Chapitre19 : Méthodes numériques / Méthode d'Euler / Précision et pas de temps ---
def integrale_simpson(f, a, b, n):
    """Approximation de l'integrale de f sur [a,b] par la methode de Simpson.
    n doit etre pair."""
    h = (b - a) / n
    somme = f(a) + f(b)
    for k in range(1, n):
        if k % 2 == 1:
            somme = somme + 4 * f(a + k * h)
        else:
            somme = somme + 2 * f(a + k * h)
    return (h / 3) * somme


# --- Chapitre20 : Statistiques et simulations / Simuler des lois de probabilité / Simuler une loi discrète quelconque ---
import random

def simulation(valeurs, probabilites):
    """Simule la loi qui a valeurs[i] associe probabilites[i].
    La somme des probabilites doit etre egale a 1."""
    r = random.random()
    cumul = 0
    for i in range(len(valeurs)):
        cumul = cumul + probabilites[i]
        if r < cumul:
            return valeurs[i]
    return valeurs[-1]   # cas limite numerique

# Exemple : X vaut 1 avec p=0.3, 2 avec p=0.5, 3 avec p=0.2
valeurs = [1, 2, 3]
proba   = [0.3, 0.5, 0.2]
print(simulation(valeurs, proba))


# --- Chapitre20 : Statistiques et simulations / Simuler des lois de probabilité / Simuler une loi à densité ---
from math import log
import random

def simul_expo(lam):
    """Simule la realisation d'une loi exponentielle E(lam)."""
    u = random.random()
    return -log(1 - u) / lam

# Estimation de l'esperance (doit etre proche de 1/lam)
N = 100000
moyenne = sum(simul_expo(2) for _ in range(N)) / N
print(moyenne)   # ~0.5  (esperance de E(2) est 1/2)


# --- Chapitre20 : Statistiques et simulations / Modèles d'urnes / Tirages avec et sans remise ---
import random as rd

def tirage_avec_remise(urne, n):
    """Renvoie une liste de n tirages successifs avec remise dans urne."""
    resultats = []
    for _ in range(n):
        i = int(rd.random() * len(urne))
        resultats.append(urne[i])
    return resultats

def tirage_sans_remise(urne, n):
    """Renvoie une liste de n tirages successifs sans remise dans urne."""
    copie = list(urne)
    resultats = []
    for _ in range(n):
        i = int(rd.random() * len(copie))
        resultats.append(copie.pop(i))
    return resultats

urne = ["rouge"]*5 + ["verte"]*10
print(tirage_avec_remise(urne, 3))    # ex. ['verte', 'rouge', 'verte']
print(tirage_sans_remise(urne, 3))    # 3 boules distinctes


# --- Chapitre20 : Statistiques et simulations / Modèles d'urnes / Tirage jusqu'à l'obtention d'une valeur ---
def tirage_jusque(urne, a):
    """Repete des tirages avec remise jusqu'a obtenir a.
    Renvoie la liste de tous les tirages effectues."""
    resultats = []
    tirage = None
    while tirage != a:
        i = int(rd.random() * len(urne))
        tirage = urne[i]
        resultats.append(tirage)
    return resultats

urne = list(range(1, 7))   # les 6 faces d'un de
tirages = tirage_jusque(urne, 6)
print(f"Nombre de lancers avant le premier 6 : {len(tirages)}")


# --- Chapitre20 : Statistiques et simulations / Modèles d'urnes / Urne avec règle de remplacement ---
def experience_urne_polya(nb_tirages):
    """Simule nb_tirages dans l'urne avec regle de remplacement.
    Renvoie la liste des boules tirees."""
    urne = ["verte"]*7 + ["rouge"]*5
    resultats = []
    for _ in range(nb_tirages):
        i = int(rd.random() * len(urne))
        boule = urne[i]
        resultats.append(boule)
        if boule == "rouge":
            urne.append("rouge")   # on ajoute une rouge supplementaire
    return resultats

# Estimation des probabilites demandees
N = 100000
tirages = [experience_urne_polya(4) for _ in range(N)]

p_toutes_rouges = sum(1 for t in tirages if all(b == "rouge" for b in t)) / N
p_toutes_vertes = sum(1 for t in tirages if all(b == "verte" for b in t)) / N
p_plus_rouges   = sum(1 for t in tirages
                      if t.count("rouge") > t.count("verte")) / N

print(f"P(toutes rouges) ~ {p_toutes_rouges:.4f}")
print(f"P(toutes vertes) ~ {p_toutes_vertes:.4f}")
print(f"P(plus rouges)   ~ {p_plus_rouges:.4f}")


# --- Chapitre20 : Statistiques et simulations / Modèles d'urnes / Fabrication d'une permutation aléatoire ---
def permutation_aleatoire(L):
    """Renvoie une copie de L melangee aleatoirement (Fisher-Yates)."""
    p = list(L)
    n = len(p)
    for i in range(n - 1, 0, -1):
        j = int(rd.random() * (i + 1))   # j dans {0, 1, ..., i}
        p[i], p[j] = p[j], p[i]
    return p

print(permutation_aleatoire([1, 2, 3, 4, 5]))
# ex. [3, 1, 5, 2, 4]


# --- Chapitre20 : Statistiques et simulations / Estimer par simulation / Estimer une probabilité ---
import random

def estime_proba(experience, N):
    """Estime par simulation la probabilite que experience() renvoie True."""
    succes = 0
    for _ in range(N):
        if experience():
            succes = succes + 1
    return succes / N

# Exemple : probabilite d'obtenir deux rouges en tirant 2 boules
# dans une urne de 5 rouges et 10 vertes (avec remise)
def experience():
    urne = ["rouge"]*5 + ["verte"]*10
    return random.choice(urne) == "rouge" and random.choice(urne) == "rouge"

print(estime_proba(experience, 100000))   # ~0.111  (theorique : (5/15)^2)


# --- Chapitre20 : Statistiques et simulations / Estimer par simulation / Estimer une espérance et la loi empirique ---
def estime_esperance(simul_X, N):
    """Estime l'esperance de X par la moyenne de N simulations."""
    return sum(simul_X() for _ in range(N)) / N

def affiche_loi_empirique(simul_X, N):
    """Affiche la frequence empirique de chaque valeur sur N simulations."""
    resultats = [simul_X() for _ in range(N)]
    # Valeurs distinctes (sans dictionnaire)
    valeurs = []
    for v in resultats:
        if v not in valeurs:
            valeurs.append(v)
    valeurs.sort()
    for v in valeurs:
        print(f"P(X={v}) ~ {resultats.count(v) / N:.4f}")

# Exemple : X = nombre de 6 en 10 lancers de de
def simul_X():
    return sum(1 for _ in range(10) if random.randint(1, 6) == 6)

print(estime_esperance(simul_X, 100000))   # ~1.667  (theorique : 10/6)
affiche_loi_empirique(simul_X, 10000)


# --- Chapitre20 : Statistiques et simulations / Estimer par simulation / Estimer une espérance et la loi empirique ---
def loi_empirique(simul_X, N):
    """Renvoie le dictionnaire {valeur: frequence} sur N simulations."""
    freq = {}
    for _ in range(N):
        v = simul_X()
        freq[v] = freq.get(v, 0) + 1
    return {v: freq[v] / N for v in freq}


# --- Chapitre20 : Statistiques et simulations / Statistiques descriptives / Indicateurs numériques ---
def moyenne(L):
    """Renvoie la moyenne de la liste L."""
    return sum(L) / len(L)

def variance(L):
    """Renvoie la variance de la liste L."""
    m = moyenne(L)
    return sum((x - m)**2 for x in L) / len(L)

def ecart_type(L):
    """Renvoie l'ecart-type de la liste L."""
    return variance(L)**0.5

# Exemple : serie de 10000 simulations de la loi N(0,1) via numpy
# np.random.normal(mu, sigma, n) renvoie un tableau de n valeurs N(mu, sigma)
import numpy as np
L = list(np.random.normal(0, 1, 10000))
print(f"Moyenne   : {moyenne(L):.4f}")     # ~0
print(f"Ecart-type: {ecart_type(L):.4f}")  # ~1


# --- Chapitre20 : Statistiques et simulations / Statistiques descriptives / Médiane et quartiles ---
def mediane(L):
    """Renvoie la mediane de la liste L."""
    L_tri = sorted(L)
    n = len(L_tri)
    if n % 2 == 1:
        return L_tri[n // 2]
    return (L_tri[n // 2 - 1] + L_tri[n // 2]) / 2

def quartiles(L):
    """Renvoie (Q1, Q2, Q3) de la liste L."""
    L_tri = sorted(L)
    n = len(L_tri)
    Q2 = mediane(L_tri)
    Q1 = mediane(L_tri[:n // 2])
    Q3 = mediane(L_tri[(n + 1) // 2:])
    return Q1, Q2, Q3

import numpy as np
L = list(np.random.normal(0, 1, 1000))
print(f"Mediane : {mediane(L):.3f}")
Q1, Q2, Q3 = quartiles(L)
print(f"Q1={Q1:.3f}  Q2={Q2:.3f}  Q3={Q3:.3f}")


# --- Chapitre20 : Statistiques et simulations / Statistiques descriptives / Histogramme ---
import matplotlib.pyplot as plt

# Histogramme d'une serie de donnees
plt.hist(L, bins=30, edgecolor="black")
plt.xlabel("Valeurs")
plt.ylabel("Effectifs")
plt.title("Histogramme")
plt.show()

# Avec density=True, l'aire totale vaut 1 (histogramme de densite)
plt.hist(L, bins=30, density=True, edgecolor="black")
plt.show()


# --- Chapitre20 : Statistiques et simulations / Statistiques descriptives / Courbe des fréquences cumulées ---
import matplotlib.pyplot as plt

def trace_freq_cumulees(L, titre=""):
    """Trace la courbe des frequences cumulees de la liste L."""
    L_tri = sorted(L)
    n = len(L_tri)
    freq_cum = [(i + 1) / n for i in range(n)]
    plt.plot(L_tri, freq_cum)
    plt.xlabel("Valeurs")
    plt.ylabel("Frequences cumulees")
    plt.title(titre)
    plt.grid()
    plt.show()

trace_freq_cumulees(L, "Loi N(0,1)")


# --- Chapitre20 : Statistiques et simulations / Statistiques descriptives / Intervalle empirique à 95\,% ---
def proportion_dans_intervalle(L, a, b):
    """Renvoie la proportion d'elements de L dans [a, b]."""
    return sum(1 for x in L if a <= x <= b) / len(L)

m = moyenne(L)
s = ecart_type(L)
p = proportion_dans_intervalle(L, m - 2*s, m + 2*s)
print(f"Proportion dans [m-2s, m+2s] : {p:.3f}")   # ~0.954


# --- Chapitre20 : Statistiques et simulations / Régression linéaire / Droite des moindres carrés ---
def regression_lineaire(X, Y):
    """Renvoie (a, b) de la droite de regression y = a*x + b."""
    n = len(X)
    moy_x  = sum(X) / n
    moy_y  = sum(Y) / n
    moy_xy = sum(X[i]*Y[i] for i in range(n)) / n
    moy_x2 = sum(X[i]**2   for i in range(n)) / n
    a = (moy_xy - moy_x * moy_y) / (moy_x2 - moy_x**2)
    b = moy_y - a * moy_x
    return a, b

X = [2,  4,  6,  8,  10, 12, 14, 16, 18, 20]
Y = [8, 13, 20, 27,  32, 38, 44, 50, 56, 62]
a, b = regression_lineaire(X, Y)
print(f"y = {a:.3f} x + {b:.3f}")   # y = 3.012 x + 1.867


# --- Chapitre20 : Statistiques et simulations / Régression linéaire / Avec NumPy ---
import numpy as np
import matplotlib.pyplot as plt

X = np.array([2,  4,  6,  8, 10, 12, 14, 16, 18, 20], dtype=float)
Y = np.array([8, 13, 20, 27, 32, 38, 44, 50, 56, 62], dtype=float)

# np.polyfit(X, Y, 1) renvoie [a, b] pour y = a*x + b
coeffs = np.polyfit(X, Y, 1)
a, b = coeffs[0], coeffs[1]
print(f"y = {a:.3f} x + {b:.3f}")

# Trace
plt.plot(X, Y, "o", label="donnees")
plt.plot(X, a*X + b, label=f"y = {a:.2f}x + {b:.2f}")
plt.legend()
plt.show()


# --- Chapitre20 : Statistiques et simulations / Régression linéaire / Avec NumPy ---
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm

def simul_X():
    return np.random.randint(0, 3)   # loi U({0,1,2})

mu    = 1.0     # esperance de U({0,1,2})
sigma = (2/3)**0.5  # ecart-type

for n in [1, 5, 30]:
    Mn = [sum(simul_X() for _ in range(n)) / n for _ in range(5000)]
    Zn = [(m - mu) / (sigma / n**0.5) for m in Mn]   # normalisation
    plt.figure()
    plt.hist(Zn, bins=40, density=True, label=f"n={n}")
    t = np.linspace(-4, 4, 200)
    plt.plot(t, norm.pdf(t), "r-", label="N(0,1)")
    plt.legend()
    plt.title(f"Distribution de la moyenne normalisee (n={n})")
    plt.show()


# --- Chapitre21 : Intervalles de fluctuation et tests statistiques / Intervalle de fluctuation d'une fréquence / Vérification par simulation ---
import random
from math import sqrt

def simule_fn(n, p):
    """Renvoie la frequence de succes sur n tirages de Bernoulli(p)."""
    succes = sum(1 for _ in range(n) if random.random() < p)
    return succes / n

def verifie_fluctuation(n, p, N):
    """Estime la probabilite que fn soit dans l'intervalle de fluctuation."""
    borne = 1 / sqrt(n)
    dans_intervalle = 0
    for _ in range(N):
        fn = simule_fn(n, p)
        if abs(fn - p) <= borne:
            dans_intervalle += 1
    return dans_intervalle / N

print(verifie_fluctuation(100, 0.5, 100000))   # ~0.95
print(verifie_fluctuation(400, 0.3, 100000))   # ~0.95


# --- Chapitre21 : Intervalles de fluctuation et tests statistiques / Intervalle de confiance pour une proportion / Implémentation ---
def intervalle_confiance(fn, n):
    """Renvoie (borne_inf, borne_sup) de l'IC a 95% pour une proportion p.
    fn : frequence observee  /  n : nombre d'observations"""
    borne = 1 / sqrt(n)
    return fn - borne, fn + borne

# Exemple : sur 200 individus, 94 presentent un caractere
n  = 200
fn = 94 / 200
ic = intervalle_confiance(fn, n)
print(f"Frequence observee : {fn:.3f}")
print(f"IC a 95% : [{ic[0]:.3f}, {ic[1]:.3f}]")
# Frequence observee : 0.470
# IC a 95% : [0.400, 0.541]


# --- Chapitre21 : Intervalles de fluctuation et tests statistiques / Intervalle de confiance pour une proportion / Vérification par simulation ---
def verifie_confiance(n, p, N):
    """Estime la probabilite que l'IC construite contienne p."""
    contient_p = 0
    borne = 1 / sqrt(n)
    for _ in range(N):
        fn = simule_fn(n, p)
        if fn - borne <= p <= fn + borne:
            contient_p += 1
    return contient_p / N

print(verifie_confiance(100, 0.4, 100000))   # ~0.95
print(verifie_confiance(400, 0.7, 100000))   # ~0.95


# --- Chapitre21 : Intervalles de fluctuation et tests statistiques / Test de conformité d'une proportion / Implémentation ---
def test_conformite(fn, n, p0):
    """Teste H0 : p = p0 au niveau 5%.
    Renvoie True si on rejette H0, False sinon."""
    borne = 1 / sqrt(n)
    return abs(fn - p0) > borne

# Exemple : 63 succes sur 100 epreuves -- la proportion vaut-elle 0.5 ?
n  = 100
fn = 63 / 100
p0 = 0.5
if test_conformite(fn, n, p0):
    print("On rejette H0 : la proportion semble differente de 0.5")
else:
    print("On ne rejette pas H0")
# -> On rejette H0  (0.63 est hors de [0.40, 0.60])


# --- Chapitre21 : Intervalles de fluctuation et tests statistiques / Test de conformité d'une proportion / Simulation du risque de première espèce ---
def risque_alpha(n, p0, N):
    """Estime le risque de rejeter H0 a tort (quand p = p0 est vraie)."""
    rejets = 0
    for _ in range(N):
        fn = simule_fn(n, p0)
        if test_conformite(fn, n, p0):
            rejets += 1
    return rejets / N

print(risque_alpha(100, 0.5, 100000))   # ~0.05
print(risque_alpha(400, 0.3, 100000))   # ~0.05


# --- Chapitre21 : Intervalles de fluctuation et tests statistiques / Applications / Génétique mendélienne ---
# Observation : 740 individus dominants sur 1000
n  = 1000
fn = 740 / 1000
p0 = 3 / 4

ic = intervalle_confiance(fn, n)
print(f"IC a 95% : [{ic[0]:.4f}, {ic[1]:.4f}]")
# IC a 95% : [0.7083, 0.7717]

if test_conformite(fn, n, p0):
    print(f"Au seuil de 95 %, l'échantillon est incompatible avec la loi de Mendel (p0 = {p0})")
    print("(on rejette H0 : l'écart observé est trop important pour être dû au seul hasard d'échantillonnage)")
else:
    print(f"Au seuil de 95 %, l'échantillon est compatible avec la loi de Mendel (p0 = {p0})")
    print("(on ne rejette pas H0 : l'écart observé est compatible avec un simple effet du hasard d'échantillonnage)")


# --- Chapitre21 : Intervalles de fluctuation et tests statistiques / Applications / Estimation d'une fréquence allélique ---
# Sur 500 individus genotypes, 320 portent l'allele A
n  = 500
fn = 320 / 500

ic = intervalle_confiance(fn, n)
print(f"Frequence estimee de A : {fn:.3f}")
print(f"IC a 95% : [{ic[0]:.3f}, {ic[1]:.3f}]")
# Frequence estimee de A : 0.640
# IC a 95% : [0.595, 0.685]


# --- Chapitre21 : Intervalles de fluctuation et tests statistiques / Applications / Synthèse : démarche d'un test ---
def p_valeur(fn, n, p0, N=100000):
    """Estime par simulation la p-valeur du test bilateral H0 : p = p0."""
    ecart_obs = abs(fn - p0)
    aussi_extreme = sum(
        1 for _ in range(N) if abs(simule_fn(n, p0) - p0) >= ecart_obs
    )
    return aussi_extreme / N

print(p_valeur(0.63, 100, 0.5))   # ~0.009  -> tres significatif
print(p_valeur(0.55, 100, 0.5))   # ~0.32   -> non significatif


# --- Chapitre22 : Annexe : numpy pour l'algèbre linéaire / Rappel : tableaux à deux dimensions ---
import numpy as np

A = np.array([[1, 2, 3],
              [4, 5, 6]])

print(A.shape)   # (2, 3)  -> 2 lignes, 3 colonnes


# --- Chapitre22 : Annexe : numpy pour l'algèbre linéaire / Rappel : tableaux à deux dimensions ---
B = np.array([[1, 2, 3, 4],
              [5, 6, 7, 8]])

print(B.shape)         # (2, 4)   -> 2 lignes, 4 colonnes
print(np.shape(B))     # (2, 4)   -> syntaxe fonction, meme resultat

print(B.size)           # 8        -> nombre total d'elements
print(np.size(B))       # 8        -> syntaxe fonction, meme resultat

print(np.size(B, 0))    # 2        -> nombre de lignes, comme B.shape[0]
print(np.size(B, 1))    # 4        -> nombre de colonnes, comme B.shape[1]


# --- Chapitre22 : Annexe : numpy pour l'algèbre linéaire / Produit terme à terme et produit matriciel ---
import numpy as np

A = np.array([[1, 2],
              [3, 4]])
B = np.array([[5, 6],
              [7, 8]])

print(A * B)
# [[ 5 12]
#  [21 32]]     -> terme a terme : (1*5, 2*6), (3*7, 4*8)

print(A @ B)
# [[19 22]
#  [43 50]]     -> produit matriciel usuel

print(np.dot(A, B))
# [[19 22]
#  [43 50]]     -> meme resultat, syntaxe equivalente a @


# --- Chapitre22 : Annexe : numpy pour l'algèbre linéaire / Extraire des lignes et des colonnes ---
import numpy as np

A = np.array([[ 1,  2,  3,  4],
              [ 5,  6,  7,  8],
              [ 9, 10, 11, 12]])

print(A[1, :])        # [5 6 7 8]         -> ligne d'indice 1
print(A[:, 2])         # [ 3  7 11]        -> colonne d'indice 2
print(A[0:2, 1:3])
# [[2 3]
#  [6 7]]              -> sous-matrice (lignes 0-1, colonnes 1-2)
print(A[-1, :])         # [ 9 10 11 12]     -> derniere ligne


# --- Chapitre22 : Annexe : numpy pour l'algèbre linéaire / Extraire des lignes et des colonnes ---
ligne = A[0, :]
ligne[0] = 99

print(A)
# [[99  2  3  4]
#  [ 5  6  7  8]
#  [ 9 10 11 12]]   -> A a ete modifie, alors qu'on n'a touche qu'a "ligne" !


# --- Chapitre22 : Annexe : numpy pour l'algèbre linéaire / Déterminant, inverse, valeurs propres ---
A = np.array([[1, 2], [3, 4]])

print(np.linalg.det(A))    # -2.0
print(np.linalg.inv(A))    # [[-2.   1. ]
                            #  [ 1.5 -0.5]]

print(np.linalg.eigvals(A))
# [-0.37228132  5.37228132]

valeurs_propres, vecteurs_propres = np.linalg.eig(A)
print(valeurs_propres)
# [-0.37228132  5.37228132]     -> memes valeurs que eigvals
print(vecteurs_propres)
# [[-0.82456484 -0.41597356]
#  [ 0.56576746 -0.90937671]]   -> vecteurs propres normes, en colonnes


# --- Chapitre22 : Annexe : numpy pour l'algèbre linéaire / Le sous-module numpy.random : incertitudes-type en physique ---
import numpy as np

np.random.seed(0)

# Mesures et incertitudes-type (donnees d'un TP)
L, u_L = 5.00, 0.02   # longueur (cm), mesures repetees -> loi normale
l, u_l = 3.00, 0.01   # largeur (cm), mesures repetees -> loi normale

N = 100_000
L_sim = np.random.normal(L, u_L, N)
l_sim = np.random.normal(l, u_l, N)

S_sim = L_sim * l_sim   # une aire par tirage : produit terme a terme

print(f"Aire moyenne     : {np.mean(S_sim):.3f} cm^2")
print(f"Incertitude-type : {np.std(S_sim):.3f} cm^2")

