2026-10-21 11:05:36 +02:00
|
|
|
|
# Compte rendu du TD de complexité algorithmique
|
|
|
|
|
|
|
2026-10-21 11:11:28 +02:00
|
|
|
|
Ce compte rendu traite le palier 1 et l'étape 4 du TD
|
|
|
|
|
|
(page 68 de `R5A04_Algorithmique_MM.pdf`) : analyser la version initiale,
|
|
|
|
|
|
puis atteindre les objectifs avec un commit par optimisation.
|
2026-10-21 11:05:36 +02:00
|
|
|
|
|
|
|
|
|
|
Les mesures ont été réalisées le 21 octobre 2026 sur Linux x86_64, avec
|
2026-10-21 11:11:28 +02:00
|
|
|
|
GCC 16.2.1 et GNU gprof 2.47. Les sections 1 à 3 décrivent la version initiale.
|
2026-10-21 11:05:36 +02:00
|
|
|
|
Les données sont reproductibles grâce à la graine fixe du générateur.
|
|
|
|
|
|
|
|
|
|
|
|
| Paramètre | Valeur |
|
|
|
|
|
|
|---|---:|
|
|
|
|
|
|
| Transactions, `n` | 30 000 |
|
|
|
|
|
|
| Clients, `C` | 2 000 |
|
|
|
|
|
|
| Jours, `J` | 1 000 |
|
|
|
|
|
|
| Requêtes par identifiant, `Q` | 30 000 |
|
|
|
|
|
|
| Somme à rendre, `S` | 400 centimes |
|
|
|
|
|
|
| Types de pièces, `k` | 8 |
|
|
|
|
|
|
|
|
|
|
|
|
## 1. Mesurer la version initiale
|
|
|
|
|
|
|
|
|
|
|
|
Le Makefile compile le programme avec
|
|
|
|
|
|
`gcc -std=c11 -Wall -Wextra -Wpedantic -O2 banque.c -o build/banque`.
|
|
|
|
|
|
La compilation s'est terminée sans avertissement. Les commandes utilisées
|
|
|
|
|
|
pour compiler et enregistrer une mesure sont les suivantes, depuis la racine :
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
make -B all
|
|
|
|
|
|
(cd build && { time -p ./banque > sortie.txt; } 2> temps_avant.txt)
|
|
|
|
|
|
cat build/sortie.txt build/temps_avant.txt
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Résultat de l'exécution mesurée :
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
Clients distincts : 2000
|
|
|
|
|
|
Somme des requêtes : -2624578
|
|
|
|
|
|
Meilleure période : jours 650 à 986 (3548830)
|
|
|
|
|
|
Paires opposées : 5832
|
|
|
|
|
|
Façons de rendre 400 : 1960497
|
|
|
|
|
|
Empreinte : ff68d260a7a58441
|
|
|
|
|
|
real 2.43
|
|
|
|
|
|
user 2.39
|
|
|
|
|
|
sys 0.00
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Le temps réel est de **2,43 s**. Le temps CPU passé dans le programme est de
|
|
|
|
|
|
2,39 s ; le temps système est arrondi à 0,00 s. Cette mesure porte sur une
|
|
|
|
|
|
exécution et peut varier selon la charge de la machine.
|
|
|
|
|
|
|
|
|
|
|
|
L'empreinte obtenue est bien **`ff68d260a7a58441`**, celle attendue dans le TD.
|
|
|
|
|
|
Le rapport produit contient 6 017 lignes, soit 120 348 octets. Les résultats
|
|
|
|
|
|
sont conservés dans [sortie.txt](build/sortie.txt) et
|
|
|
|
|
|
[temps_avant.txt](build/temps_avant.txt).
|
|
|
|
|
|
|
|
|
|
|
|
## 2. Profiler et comparer aux complexités
|
|
|
|
|
|
|
|
|
|
|
|
Le profil a été généré avec les commandes suivantes :
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
make profile
|
|
|
|
|
|
cp build/profil/profil.txt build/profil/profil_avant.txt
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
La cible `profile` utilise une compilation en `-O0 -g -pg`, exécute le programme,
|
|
|
|
|
|
puis lance `gprof -b ./banque gmon.out` dans `build/profil`. Le profil initial
|
|
|
|
|
|
est conservé dans [profil_avant.txt](build/profil/profil_avant.txt).
|
|
|
|
|
|
|
|
|
|
|
|
Cette exécution produit également l'empreinte attendue. Les fichiers
|
|
|
|
|
|
`build/rapport.txt` et `build/profil/rapport.txt` ont été comparés : ils sont
|
|
|
|
|
|
identiques octet par octet.
|
|
|
|
|
|
|
|
|
|
|
|
### Résultats de gprof
|
|
|
|
|
|
|
|
|
|
|
|
Le tableau reprend les temps propres du profil plat : ils correspondent au
|
|
|
|
|
|
temps passé dans chaque fonction, hors fonctions appelées. Les pourcentages
|
|
|
|
|
|
portent sur environ **6,44 s de temps CPU échantillonné**.
|
|
|
|
|
|
|
|
|
|
|
|
| Fonction | Appels | Temps propre (s) | Part du temps |
|
|
|
|
|
|
|---|---:|---:|---:|
|
|
|
|
|
|
| F1 — `trier_par_montant` | 1 | 3,13 | 48,60 % |
|
|
|
|
|
|
| F6 — `rechercher_par_id` | 30 000 | 0,85 | 13,20 % |
|
|
|
|
|
|
| F7 — `nb_paires_opposees` | 1 | 0,76 | 11,80 % |
|
|
|
|
|
|
| F8 — `nb_facons_rendre` | 1 appel initial | 0,66 | 10,17 % |
|
|
|
|
|
|
| `longueur`, appelée par F10 | 6 017 | 0,41 | 6,37 % |
|
|
|
|
|
|
| F5 — `meilleure_periode` | 1 | 0,32 | 4,97 % |
|
|
|
|
|
|
| F2 — `nb_clients_distincts` | 1 | 0,11 | 1,71 % |
|
|
|
|
|
|
| F3 — `total_par_client` | 1 | 0,11 | 1,71 % |
|
|
|
|
|
|
| F4 — `total_par_jour` | 1 | 0,05 | 0,78 % |
|
|
|
|
|
|
| F9 — `categorie` | 30 000 | 0,03 | 0,39 % |
|
|
|
|
|
|
| F10 — `ajouter_ligne` | 6 017 | 0,00 | 0,00 % |
|
|
|
|
|
|
|
|
|
|
|
|
Le graphe d'appels précise que F8 effectue **266 723 738 appels récursifs**,
|
|
|
|
|
|
en plus de l'appel initial. Le compteur du profil plat ne montre que cet
|
|
|
|
|
|
appel initial. Les écarts entre les pourcentages et les temps affichés
|
|
|
|
|
|
s'expliquent par les arrondis ; quelques échantillons sont aussi attribués
|
|
|
|
|
|
à `_init`.
|
|
|
|
|
|
|
|
|
|
|
|
Le temps cumulé de F10, en incluant `longueur`, est d'environ **0,41 s**.
|
|
|
|
|
|
Son temps propre affiché à 0,00 s ne signifie donc pas que la construction
|
|
|
|
|
|
du rapport est gratuite. Plus généralement, une valeur nulle dans ce profil
|
|
|
|
|
|
ne prouve pas l'absence de coût : la période d'échantillonnage est de 0,01 s.
|
|
|
|
|
|
|
|
|
|
|
|
Ces temps ne se comparent pas directement aux 2,43 s de la première étape :
|
|
|
|
|
|
le profil utilise `-O0` et l'instrumentation `-pg`, tandis que la mesure
|
|
|
|
|
|
initiale utilise `-O2`. Le profil décrit le comportement de cette version
|
|
|
|
|
|
instrumentée, pour ces données et sur cette machine.
|
|
|
|
|
|
|
|
|
|
|
|
### Les fonctions les plus coûteuses ont-elles les pires complexités ?
|
|
|
|
|
|
|
|
|
|
|
|
**Non, pas systématiquement.** La complexité décrit la croissance du nombre
|
|
|
|
|
|
d'opérations lorsque la taille des entrées augmente. Le profil mesure le coût
|
|
|
|
|
|
d'une exécution précise. Ce coût dépend aussi des tailles effectivement
|
|
|
|
|
|
utilisées, du nombre d'appels, des données et du travail effectué à chaque
|
|
|
|
|
|
itération.
|
|
|
|
|
|
|
|
|
|
|
|
- **F1 : le tri quadratique domine ici.** Le tri à bulles effectue
|
|
|
|
|
|
`n(n - 1) / 2`, soit **449 985 000 comparaisons de paires voisines**, avec
|
|
|
|
|
|
des échanges de structures lorsque les transactions sont mal ordonnées.
|
|
|
|
|
|
Sa complexité est `O(n²)` et il représente 48,60 % du temps échantillonné.
|
|
|
|
|
|
|
|
|
|
|
|
- **F5 : une complexité cubique ne suffit pas à en faire la fonction la plus
|
|
|
|
|
|
lente.** Ses trois boucles donnent une complexité `O(J³)`, mais elles portent
|
|
|
|
|
|
sur **1 000 jours**, alors que F1 traite 30 000 transactions. F5 effectue
|
|
|
|
|
|
`J(J + 1)(J + 2) / 6`, soit **167 167 000 additions**, et prend environ
|
|
|
|
|
|
0,32 s, contre 3,13 s pour F1. Les variables et les tailles diffèrent :
|
|
|
|
|
|
comparer uniquement les exposants serait trompeur.
|
|
|
|
|
|
|
|
|
|
|
|
- **F6 : le nombre d'appels compte.** Une recherche par identifiant est en
|
|
|
|
|
|
`O(n)` au pire cas, mais `main` l'effectue **30 000 fois**. Le coût de
|
|
|
|
|
|
l'ensemble des recherches est donc `O(Q × n)`, soit `O(n²)` lorsque `Q = n`.
|
|
|
|
|
|
Avec 0,85 s au total, F6 arrive devant F5 malgré sa complexité linéaire
|
|
|
|
|
|
par appel.
|
|
|
|
|
|
|
|
|
|
|
|
- **F7 : une même classe de complexité ne donne pas un même temps.** F7
|
|
|
|
|
|
examine elle aussi **449 985 000 paires**, en `O(n²)`, mais se limite à
|
|
|
|
|
|
additionner deux montants, comparer le résultat à zéro et éventuellement
|
|
|
|
|
|
incrémenter un compteur. Elle prend 0,76 s, environ quatre fois moins que
|
|
|
|
|
|
F1, dont les itérations peuvent déplacer des transactions entières.
|
|
|
|
|
|
|
|
|
|
|
|
- **F8 : les sous-problèmes recalculés multiplient les appels.** Le rendu de
|
|
|
|
|
|
monnaie utilise une récursion sans mémorisation et dépasse 266 millions
|
|
|
|
|
|
d'appels récursifs. Son coût dépend de `S` et de `k`, ici 400 et 8. Malgré
|
|
|
|
|
|
cet arbre d'appels volumineux, son temps de 0,66 s reste inférieur à celui
|
|
|
|
|
|
du tri ; le nombre d'appels ne suffit pas non plus à classer les temps.
|
|
|
|
|
|
|
|
|
|
|
|
- **F10 : il faut tenir compte des fonctions appelées et des appels répétés.**
|
|
|
|
|
|
Chaque ajout parcourt tout le rapport existant avec `longueur`, puis copie
|
|
|
|
|
|
la nouvelle ligne. Pour un préfixe de longueur `p` et une ligne de longueur
|
|
|
|
|
|
`l`, un appel coûte `O(p + l)`. Sur `m` ajouts de lignes de taille bornée,
|
|
|
|
|
|
les parcours successifs coûtent `O(m²)`, soit `O(L²)` dans ce cas, où `L`
|
|
|
|
|
|
est la longueur finale du rapport. Pour le rapport obtenu, les recherches
|
|
|
|
|
|
de fin de chaîne relisent **340 702 496 caractères** de préfixes, ce qui
|
|
|
|
|
|
explique les 0,41 s attribuées à `longueur`.
|
|
|
|
|
|
|
|
|
|
|
|
Pour cette exécution instrumentée, F1, F6 et F7 représentent ensemble
|
|
|
|
|
|
**73,60 %** du temps échantillonné : ce sont les premières pistes
|
|
|
|
|
|
d'optimisation indiquées par le profil.
|
|
|
|
|
|
|
|
|
|
|
|
## 3. Calculer les complexités de F1 à F10
|
|
|
|
|
|
|
|
|
|
|
|
Le tableau reprend l'analyse initiale de `notes.md`, avec la complexité
|
|
|
|
|
|
cyclomatique de chaque fonction. Les complexités temporelles sont données
|
|
|
|
|
|
au pire cas, en faisant varier les tailles des données, même si leurs valeurs
|
|
|
|
|
|
sont fixées par des constantes dans le programme. Les objectifs sont ceux
|
2026-10-21 11:23:07 +02:00
|
|
|
|
du tableau de la page 67 du TD ; la section 4 présente leur réalisation.
|
2026-10-21 11:05:36 +02:00
|
|
|
|
|
|
|
|
|
|
| ID | Fonction | Calcule | Actuelle | M | Objectif |
|
|
|
|
|
|
|:---:|---|---|---|:---:|---|
|
|
|
|
|
|
| F1 | `trier_par_montant` | tri des transactions | `O(n²)` | 6 | `O(n log n)` |
|
|
|
|
|
|
| F2 | `nb_clients_distincts` | clients différents | `O(n²)` | 5 | `O(n + C)` |
|
|
|
|
|
|
| F3 | `total_par_client` | solde par client | `O(n × C)` | 4 | `O(n + C)` |
|
|
|
|
|
|
| F4 | `total_par_jour` | solde par jour | `O(n × J)` | 4 | `O(n + J)` |
|
|
|
|
|
|
| F5 | `meilleure_periode` | période la plus rentable | `O(J³)` | 5 | `O(J)` |
|
|
|
|
|
|
| F6 | `rechercher_par_id` | recherche par identifiant | `O(n)` par requête | 3 | `O(1)` par requête |
|
|
|
|
|
|
| F7 | `nb_paires_opposees` | paires `a + b = 0` | `O(n²)` | 4 | `O(n log n)` |
|
|
|
|
|
|
| F8 | `nb_facons_rendre` | rendu de monnaie | `O(2^(S + k))`, borne générale | 4 | `O(S × k)` |
|
|
|
|
|
|
| F9 | `categorie` | catégorie d'une transaction | `O(1)` | 14 | `M ≤ 10` |
|
|
|
|
|
|
| F10 | `ajouter_ligne` | construction du rapport | `O(p + l)` par ajout ; `O(L²)` au total pour des lignes de taille bornée | 2 | `O(L)` au total |
|
|
|
|
|
|
|
|
|
|
|
|
`n` : transactions ; `C` : clients ; `J` : jours ; `Q` : requêtes ;
|
|
|
|
|
|
`S` : somme à rendre ; `k` : types de pièces ; `L` : longueur finale du
|
|
|
|
|
|
rapport ; `p` : longueur du rapport avant un ajout ; `l` : longueur de la
|
|
|
|
|
|
ligne ajoutée. **F5, F7 et F8** sont signalées comme les plus difficiles
|
|
|
|
|
|
dans le TD.
|
|
|
|
|
|
|
|
|
|
|
|
### Justification des complexités temporelles
|
|
|
|
|
|
|
|
|
|
|
|
- **F1 et F7** : les deux boucles parcourent `n(n - 1) / 2` paires.
|
|
|
|
|
|
Le travail par paire est constant, d'où `O(n²)`.
|
|
|
|
|
|
- **F2** : pour chaque transaction d'indice `i`, la recherche d'un client
|
|
|
|
|
|
déjà rencontré peut parcourir les `i` transactions précédentes.
|
|
|
|
|
|
La somme `0 + 1 + … + (n - 1)` donne la borne `O(n²)`, atteinte si
|
|
|
|
|
|
les clients sont tous différents (`C ≥ n`).
|
|
|
|
|
|
- **F3 et F4** : chaque client, puis chaque jour, provoque un parcours
|
|
|
|
|
|
complet des `n` transactions : respectivement `O(C × n)` et `O(J × n)`.
|
|
|
|
|
|
- **F5** : chaque intervalle de jours est essayé, et sa somme est recalculée
|
|
|
|
|
|
par une troisième boucle. Le nombre d'additions est
|
|
|
|
|
|
`J(J + 1)(J + 2) / 6`, d'où `O(J³)`.
|
|
|
|
|
|
- **F6** : une recherche peut lire les `n` transactions, soit `O(n)`.
|
|
|
|
|
|
Les `Q` recherches de `main` coûtent donc `O(Q × n)` au pire cas.
|
|
|
|
|
|
- **F8** : sans mémorisation, le coût suit la récurrence
|
|
|
|
|
|
`T(S, k) = T(S - pièce[k - 1], k) + T(S, k - 1) + O(1)`.
|
|
|
|
|
|
Chaque appel non terminal engendre deux appels ; chaque branche diminue
|
|
|
|
|
|
`S` ou `k` d'au moins 1. La profondeur est donc au plus `S + k`, ce qui
|
|
|
|
|
|
donne la borne générale `O(2^(S + k))`. Cette borne est large : pour
|
|
|
|
|
|
`k` fixé, on peut affiner en `O(S^k)`. La notation `O(Sⁿ)` des notes
|
|
|
|
|
|
initiales ne convient pas, car `n` désigne les transactions.
|
|
|
|
|
|
- **F9** : le nombre de tests est borné, indépendamment de `n`, d'où `O(1)`.
|
|
|
|
|
|
L'objectif porte ici sur la complexité cyclomatique, pas sur le temps.
|
|
|
|
|
|
- **F10** : `longueur` lit les `p` caractères du rapport, puis la fonction
|
|
|
|
|
|
copie les `l` caractères de la ligne : `O(p + l)` par ajout.
|
|
|
|
|
|
Pour `m` lignes non vides de taille bornée, les préfixes relus ont des
|
|
|
|
|
|
longueurs proportionnelles à `0, 1, …, m - 1` : le coût total est
|
|
|
|
|
|
`O(m²)`, soit `O(L²)` dans ce cas.
|
|
|
|
|
|
|
|
|
|
|
|
### Calcul manuel de la complexité cyclomatique
|
|
|
|
|
|
|
|
|
|
|
|
La convention du cours (pages 31–32) est **`M = nombre de décisions + 1`**.
|
|
|
|
|
|
Chaque `if`, `else if`, `for`, `while`, ainsi que chaque opérateur `&&` ou
|
|
|
|
|
|
`||`, ajoute une décision. `else`, `return` et `break` n'en ajoutent pas.
|
|
|
|
|
|
Les décisions d'une fonction appelée se comptent dans cette fonction.
|
|
|
|
|
|
|
|
|
|
|
|
- **F1** : 2 `for` + 1 `if` + 1 `||` + 1 `&&` = 5 décisions, donc **M = 6**.
|
|
|
|
|
|
- **F2** : 2 `for` + 2 `if` = 4 décisions, donc **M = 5**.
|
|
|
|
|
|
- **F3, F4 et F7** : 2 `for` + 1 `if` = 3 décisions chacune, donc **M = 4**.
|
|
|
|
|
|
- **F5** : 3 `for` + 1 `if` = 4 décisions, donc **M = 5**.
|
|
|
|
|
|
- **F6** : 1 `for` + 1 `if` = 2 décisions, donc **M = 3**.
|
|
|
|
|
|
- **F8** : 2 `if` + 1 `||` = 3 décisions, donc **M = 4**.
|
|
|
|
|
|
- **F9** : 11 `if` ou `else if` + 1 `||` + 1 `&&` = 13 décisions,
|
|
|
|
|
|
donc **M = 14**. C'est la plus grande valeur de F1 à F10.
|
|
|
|
|
|
- **F10** : 1 `while` = 1 décision, donc **M = 2**.
|
|
|
|
|
|
La fonction auxiliaire `longueur` contient elle aussi un `while` et a
|
|
|
|
|
|
**M = 2**, compté séparément.
|
2026-10-21 11:11:28 +02:00
|
|
|
|
|
|
|
|
|
|
## 4. Atteindre les objectifs
|
|
|
|
|
|
|
|
|
|
|
|
Chaque optimisation est vérifiée avant son commit : compilation avec les
|
|
|
|
|
|
avertissements du Makefile, tests de cas limites via `make test`, exécution
|
|
|
|
|
|
en `-O2`, empreinte **`ff68d260a7a58441`** et comparaison du rapport complet
|
|
|
|
|
|
avec celui de la version initiale. Les constantes, `alea`,
|
|
|
|
|
|
`generer_transactions` et le format du rapport sont conservés.
|
|
|
|
|
|
|
|
|
|
|
|
### F1 — Tri fusion
|
|
|
|
|
|
|
|
|
|
|
|
Le tri à bulles est remplacé par un tri fusion : chaque moitié est triée,
|
|
|
|
|
|
puis les deux moitiés sont fusionnées en un passage. La comparaison porte
|
|
|
|
|
|
sur le montant, puis sur l'identifiant, sans soustraction susceptible de
|
|
|
|
|
|
déborder. La récurrence `T(n) = 2T(n/2) + O(n)` donne **`O(n log n)` au pire
|
|
|
|
|
|
cas**, avec `O(n)` de mémoire auxiliaire et `O(log n)` de pile.
|
|
|
|
|
|
|
|
|
|
|
|
La complexité cyclomatique de F1 passe de **6 à 3** ; les auxiliaires
|
|
|
|
|
|
`comparer_transactions` et `trier_intervalle` ont respectivement **M = 2**
|
|
|
|
|
|
et **M = 6**. Les tests comparent le résultat à un tri de référence pour
|
|
|
|
|
|
les tailles 0 à 128, des doublons, des entrées déjà triées ou inversées,
|
|
|
|
|
|
et les bornes des types `int` et `long`.
|
2026-10-21 11:11:56 +02:00
|
|
|
|
|
|
|
|
|
|
### F2 — Tableau des clients rencontrés
|
|
|
|
|
|
|
|
|
|
|
|
Un tableau de `C` indicateurs, initialisé à zéro, remplace la recherche
|
|
|
|
|
|
dans les transactions précédentes. Une transaction marque son client à son
|
|
|
|
|
|
premier passage et incrémente le compteur. Le coût devient **`O(n + C)`**
|
|
|
|
|
|
au lieu de `O(n²)`, avec `O(C)` de mémoire. Les identifiants de clients sont
|
2026-10-21 11:23:07 +02:00
|
|
|
|
compris entre 0 et `C - 1`, comme le garantit le générateur.
|
2026-10-21 11:11:56 +02:00
|
|
|
|
La complexité cyclomatique passe de **5 à 3**. Les tests couvrent l'absence
|
|
|
|
|
|
de transactions, les doublons, les identifiants extrêmes et tous les clients.
|
2026-10-21 11:12:21 +02:00
|
|
|
|
|
|
|
|
|
|
### F3 — Accumulation directe par client
|
|
|
|
|
|
|
|
|
|
|
|
Après la remise à zéro des `C` soldes, un seul parcours des transactions
|
|
|
|
|
|
ajoute chaque montant dans `totaux[t[i].client]`. Les deux boucles sont
|
|
|
|
|
|
successives : **`O(n + C)`** au lieu de `O(n × C)`, avec `O(1)` de mémoire
|
|
|
|
|
|
auxiliaire en dehors du tableau de sortie. La complexité cyclomatique passe
|
|
|
|
|
|
de **4 à 3**. Les tests vérifient les clients sans transaction, les débits,
|
|
|
|
|
|
les crédits, les annulations et la réinitialisation entre deux appels.
|
2026-10-21 11:12:46 +02:00
|
|
|
|
|
|
|
|
|
|
### F4 — Accumulation directe par jour
|
|
|
|
|
|
|
|
|
|
|
|
Le même principe s'applique aux jours : initialisation des `J` soldes,
|
|
|
|
|
|
puis ajout de chaque montant dans `totaux[t[i].jour]`. Le coût passe de
|
|
|
|
|
|
`O(n × J)` à **`O(n + J)`**, avec `O(1)` de mémoire auxiliaire hors sortie.
|
|
|
|
|
|
La complexité cyclomatique passe de **4 à 3**. Les tests comparent chaque
|
|
|
|
|
|
solde à un calcul de référence, y compris aux bornes du tableau, puis
|
|
|
|
|
|
vérifient qu'un appel sans transaction remet tous les soldes à zéro.
|
2026-10-21 11:13:19 +02:00
|
|
|
|
|
|
|
|
|
|
### F5 — Meilleure période en un passage (Kadane)
|
|
|
|
|
|
|
|
|
|
|
|
La somme de la meilleure période se terminant au jour courant est conservée.
|
|
|
|
|
|
Si cette somme est négative, le prochain jour démarre une nouvelle période ;
|
|
|
|
|
|
sinon, elle est prolongée. Le coût passe de `O(J³)` à **`O(J)`**, avec
|
|
|
|
|
|
**`O(1)`** de mémoire auxiliaire, et la complexité cyclomatique de **5 à 4**.
|
|
|
|
|
|
|
|
|
|
|
|
La somme initiale est celle du premier jour : une série entièrement négative
|
|
|
|
|
|
renvoie bien un intervalle non vide. On redémarre uniquement pour une somme
|
|
|
|
|
|
strictement négative et on remplace le meilleur résultat uniquement pour
|
|
|
|
|
|
une somme strictement supérieure. Une somme nulle conserve ainsi le début
|
|
|
|
|
|
le plus ancien ; à égalité, le premier début puis la première fin sont
|
|
|
|
|
|
conservés comme dans les boucles initiales. Les **97 655 séries** de longueur
|
|
|
|
|
|
1 à 7 à valeurs dans `[-2, 2]` sont comparées à la version cubique, sur la
|
|
|
|
|
|
somme et sur les deux bornes : elles couvrent notamment les zéros, les
|
|
|
|
|
|
égalités et les séries entièrement négatives.
|
2026-10-21 11:14:00 +02:00
|
|
|
|
|
|
|
|
|
|
### F6 — Index direct par identifiant
|
|
|
|
|
|
|
|
|
|
|
|
Les identifiants sont une permutation de `[0, n - 1]`. Un index de pointeurs
|
|
|
|
|
|
est construit en **`O(n)`** avant la boucle des requêtes. Chaque recherche
|
|
|
|
|
|
contrôle les bornes et renvoie `index_ids[id]` en **`O(1)`**, au lieu de
|
|
|
|
|
|
parcourir les transactions en `O(n)`. Le coût des `Q` requêtes, construction
|
|
|
|
|
|
comprise, devient **`O(n + Q)`**, avec `O(n)` de mémoire supplémentaire.
|
|
|
|
|
|
|
|
|
|
|
|
Les recherches ont toutes lieu avant le tri des transactions : un déplacement
|
|
|
|
|
|
des transactions imposerait de reconstruire l'index. F6 garde **M = 3** et
|
|
|
|
|
|
`indexer_par_id` a **M = 2**. Les tests comparent les pointeurs renvoyés
|
|
|
|
|
|
à une recherche linéaire sur des identifiants permutés pour les tailles
|
|
|
|
|
|
1 à 128, et vérifient les recherches hors bornes et dans un tableau vide.
|
2026-10-21 11:15:03 +02:00
|
|
|
|
|
|
|
|
|
|
### F7 — Tri et deux pointeurs pour les paires opposées
|
|
|
|
|
|
|
|
|
|
|
|
Une copie des transactions est triée avec F1 pour préserver l'ordre du
|
|
|
|
|
|
tableau d'entrée. Deux pointeurs partent ensuite des extrémités : une somme
|
|
|
|
|
|
négative fait avancer le pointeur gauche, une somme positive fait reculer
|
|
|
|
|
|
le droit. Pour deux montants opposés, on compte leurs multiplicités `a` et
|
|
|
|
|
|
`b` et on ajoute `a × b`. Pour un groupe de `z` zéros, on ajoute
|
|
|
|
|
|
`z(z - 1) / 2` : chaque paire d'indices distincts est comptée une seule fois.
|
|
|
|
|
|
|
|
|
|
|
|
Le tri coûte `O(n log n)` et chaque pointeur traverse au plus `n` éléments,
|
|
|
|
|
|
soit **`O(n log n)`** au total au lieu de `O(n²)`, avec `O(n)` de mémoire.
|
|
|
|
|
|
F7 passe de **M = 4 à M = 3** ; l'auxiliaire `compter_paires_triees` a
|
|
|
|
|
|
**M = 9**. Les tests comparent le compteur à l'énumération de toutes les
|
|
|
|
|
|
paires sur les **97 655 tableaux** de longueur 1 à 7 à montants dans
|
|
|
|
|
|
`[-2, 2]`, ainsi que sur une entrée vide. Ils vérifient aussi que les
|
|
|
|
|
|
transactions d'origine restent inchangées, notamment en présence de
|
|
|
|
|
|
doublons et de plusieurs zéros.
|
2026-10-21 11:15:43 +02:00
|
|
|
|
|
|
|
|
|
|
### F8 — Programmation dynamique pour le rendu de monnaie
|
|
|
|
|
|
|
|
|
|
|
|
`facons[s]` contient le nombre de façons de rendre `s` avec les pièces déjà
|
|
|
|
|
|
traitées. Il vaut initialement 1 pour la somme nulle et 0 ailleurs. Pour
|
|
|
|
|
|
chaque pièce, les sommes sont parcourues dans l'ordre croissant et l'on
|
|
|
|
|
|
ajoute `facons[s - piece]` à `facons[s]`. Ce sens permet la réutilisation
|
|
|
|
|
|
d'une pièce ; la boucle des pièces à l'extérieur compte des combinaisons,
|
|
|
|
|
|
sans distinguer l'ordre des pièces.
|
|
|
|
|
|
|
|
|
|
|
|
Chaque état est calculé une seule fois par type de pièce : **`O(S × k)`**
|
|
|
|
|
|
pour `k ≥ 1` (et `O(S × k + S)` en incluant l'initialisation si `k = 0`),
|
|
|
|
|
|
avec **`O(S)`** de mémoire. La récursion et ses sous-problèmes répétés
|
|
|
|
|
|
disparaissent. F8 passe de **M = 4 à M = 5** : l'amélioration temporelle
|
|
|
|
|
|
n'implique pas une baisse de M. Les tests comparent les résultats à la
|
|
|
|
|
|
récursion initiale pour les sommes de -2 à 40 et de 0 à 8 types de pièces,
|
|
|
|
|
|
y compris la somme nulle et l'absence de pièces.
|
2026-10-21 11:16:34 +02:00
|
|
|
|
|
|
|
|
|
|
### F9 — Séparer les règles des débits et des crédits
|
|
|
|
|
|
|
|
|
|
|
|
`categorie` répartit les transactions entre débit, montant nul et crédit.
|
|
|
|
|
|
Les règles propres aux débits et aux crédits sont regroupées dans deux
|
|
|
|
|
|
fonctions auxiliaires, avec des retours directs. Le test du week-end
|
|
|
|
|
|
`jour % 7 >= 5` remplace les deux comparaisons reliées par `||`.
|
|
|
|
|
|
|
|
|
|
|
|
Le temps reste **`O(1)`** et la complexité cyclomatique de F9 passe de
|
|
|
|
|
|
**14 à 3**, sous l'objectif de 10. Les auxiliaires `categorie_debit`
|
|
|
|
|
|
et `categorie_credit` ont respectivement **M = 5** et **M = 7** : les
|
|
|
|
|
|
décisions ne sont pas simplement déplacées vers une autre fonction
|
|
|
|
|
|
dépassant l'objectif. Les tests confrontent les 20 montants situés aux
|
|
|
|
|
|
seuils et à leurs voisins, y compris `LONG_MIN` et `LONG_MAX`, à la fonction
|
|
|
|
|
|
initiale, pour 20 clients et 210 jours (cycle commun des modulos 7 et 30),
|
|
|
|
|
|
soit **84 000 classifications**.
|
2026-10-21 11:23:07 +02:00
|
|
|
|
|
|
|
|
|
|
### F10 — Conserver la fin du rapport
|
|
|
|
|
|
|
|
|
|
|
|
`ajouter_ligne` reçoit le pointeur sur le caractère nul final, copie la ligne
|
|
|
|
|
|
et renvoie le nouveau pointeur de fin. `main` le conserve entre les appels.
|
|
|
|
|
|
La fonction `longueur` disparaît : les caractères déjà écrits ne sont plus
|
|
|
|
|
|
relus. Un ajout coûte **`O(l)`** pour une ligne de longueur `l`, et la
|
|
|
|
|
|
construction complète **`O(L)`** au lieu de `O(L²)` pour des lignes de taille
|
|
|
|
|
|
bornée. La mémoire auxiliaire reste **`O(1)`** et F10 garde **M = 2**.
|
|
|
|
|
|
|
|
|
|
|
|
Les tests vérifient la concaténation et le pointeur renvoyé après 200 ajouts,
|
|
|
|
|
|
les lignes vides et une ligne remplissant exactement le tampon disponible.
|
|
|
|
|
|
Le rapport complet garde ses **6 017 lignes et 120 348 octets**.
|
|
|
|
|
|
|
|
|
|
|
|
### Bilan des complexités
|
|
|
|
|
|
|
|
|
|
|
|
| Fonction | Avant | Après | M avant → après |
|
|
|
|
|
|
|---|---|---|:---:|
|
|
|
|
|
|
| F1 | `O(n²)` | `O(n log n)` au pire cas | 6 → 3 |
|
|
|
|
|
|
| F2 | `O(n²)` | `O(n + C)` | 5 → 3 |
|
|
|
|
|
|
| F3 | `O(n × C)` | `O(n + C)` | 4 → 3 |
|
|
|
|
|
|
| F4 | `O(n × J)` | `O(n + J)` | 4 → 3 |
|
|
|
|
|
|
| F5 | `O(J³)` | `O(J)` | 5 → 4 |
|
|
|
|
|
|
| F6 | `O(n)` par requête | `O(1)` par requête après indexation `O(n)` | 3 → 3 |
|
|
|
|
|
|
| F7 | `O(n²)` | `O(n log n)` | 4 → 3 |
|
|
|
|
|
|
| F8 | `O(2^(S + k))`, borne générale | `O(S × k)` pour `k ≥ 1` | 4 → 5 |
|
|
|
|
|
|
| F9 | `O(1)` | `O(1)` | 14 → 3 |
|
|
|
|
|
|
| F10 | `O(L²)` au total, lignes de taille bornée | `O(L)` au total | 2 → 2 |
|
|
|
|
|
|
|
|
|
|
|
|
M des auxiliaires ajoutés : `comparer_transactions` **2**,
|
|
|
|
|
|
`trier_intervalle` **6**, `indexer_par_id` **2**,
|
|
|
|
|
|
`compter_paires_triees` **9**, `categorie_debit` **5**,
|
|
|
|
|
|
`categorie_credit` **7**. Avec **M = 9** pour `main`, le maximum hors
|
|
|
|
|
|
génération est désormais **9**, contre **14** initialement.
|
|
|
|
|
|
|
|
|
|
|
|
### Validation à chaque commit et mesure finale
|
|
|
|
|
|
|
|
|
|
|
|
Le journal [validations_optimisations.csv](build/validations_optimisations.csv)
|
|
|
|
|
|
conserve les dix vérifications successives : fonction optimisée, empreinte,
|
|
|
|
|
|
égalité du rapport, temps d'une exécution et SHA-256 du fichier `banque.c`
|
|
|
|
|
|
testé. Chaque ligne correspond au code du commit de cette optimisation.
|
|
|
|
|
|
Les dix empreintes sont **`ff68d260a7a58441`**, et les dix rapports sont
|
|
|
|
|
|
identiques octet par octet à celui de la version initiale.
|
|
|
|
|
|
|
|
|
|
|
|
La validation finale comprend :
|
|
|
|
|
|
|
|
|
|
|
|
- `make test` : tests de cas limites et comparaisons aux versions de référence ;
|
|
|
|
|
|
- `make check` : compilation en `-O2` et vérification de l'empreinte ;
|
|
|
|
|
|
- `make profile` : compilation en `-O0 -g -pg`, même sortie et même rapport ;
|
|
|
|
|
|
- les tests et une exécution complète compilés avec
|
|
|
|
|
|
`-O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer` : aucune erreur
|
|
|
|
|
|
mémoire ou de comportement indéfini signalée ;
|
|
|
|
|
|
- comparaison du code : constantes, génération des données et chaînes de
|
|
|
|
|
|
formatage du rapport inchangées.
|
|
|
|
|
|
|
|
|
|
|
|
La commande pour relancer les tests avec les vérifications mémoire est :
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
make test CFLAGS='-std=c11 -Wall -Wextra -Wpedantic -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer'
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Une nouvelle exécution de la version initiale prend **2,437683 s**. Pour
|
|
|
|
|
|
la version finale compilée avec les mêmes options en `-O2`, les cinq temps
|
|
|
|
|
|
réels sont **0,010910 ; 0,011111 ; 0,010734 ; 0,011242 ; 0,010525 s**,
|
|
|
|
|
|
soit une médiane de **0,010910 s**, environ **11 ms**. Les mesures utilisent
|
|
|
|
|
|
`time.perf_counter()` autour du processus entier et incluent l'écriture du
|
|
|
|
|
|
rapport ; elles sont conservées dans [temps_apres.txt](build/temps_apres.txt).
|
|
|
|
|
|
La référence étant une seule exécution et les temps dépendant de la machine,
|
|
|
|
|
|
cette comparaison donne un ordre de grandeur du gain, pas un classement.
|
|
|
|
|
|
|
|
|
|
|
|
### Profil gprof après optimisation
|
|
|
|
|
|
|
|
|
|
|
|
Le profil d'une exécution finale est conservé dans
|
|
|
|
|
|
[profil_apres.txt](build/profil/profil_apres.txt), ainsi que dans le fichier
|
|
|
|
|
|
courant [profil.txt](build/profil/profil.txt). Il a été obtenu avec :
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
make profile
|
|
|
|
|
|
cp build/profil/profil.txt build/profil/profil_apres.txt
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Cette exécution ne fournit qu'environ **0,01 s échantillonnée**, attribuée
|
|
|
|
|
|
au tri fusion. Un seul échantillon ne permet pas d'interpréter les parts de
|
|
|
|
|
|
temps. Pour mieux distinguer les coûts restants, le programme complet est
|
|
|
|
|
|
donc répété **200 fois dans le même processus instrumenté**, avec remise à
|
|
|
|
|
|
la graine initiale entre les passages. La sortie standard est redirigée,
|
|
|
|
|
|
le rapport est toujours écrit, et le dernier rapport est comparé à la
|
|
|
|
|
|
référence. Cette répétition concerne uniquement le pilote de mesure ;
|
|
|
|
|
|
`banque.c` et ses constantes ne sont pas modifiés.
|
|
|
|
|
|
|
|
|
|
|
|
Le profil cumulé est enregistré dans
|
|
|
|
|
|
[profil_apres_repete.txt](build/profil/profil_apres_repete.txt). Il représente
|
|
|
|
|
|
environ **2,07 s de temps CPU échantillonné pour 200 traitements** : les
|
|
|
|
|
|
nombres d'appels et les temps du tableau suivant sont ceux du lot entier.
|
|
|
|
|
|
|
|
|
|
|
|
| Fonction | Appels hors récursion | Temps propre cumulé (s) | Part du temps |
|
|
|
|
|
|
|---|---:|---:|---:|
|
|
|
|
|
|
| `trier_intervalle` | 400 | 1,19 | 57,25 % |
|
|
|
|
|
|
| `comparer_transactions` | 163 486 000 | 0,29 | 14,01 % |
|
|
|
|
|
|
| `generer_transactions` | 200 | 0,14 | 6,76 % |
|
|
|
|
|
|
| `compter_paires_triees` | 200 | 0,08 | 3,86 % |
|
|
|
|
|
|
| `ajouter_ligne` | 1 203 400 | 0,04 | 1,93 % |
|
|
|
|
|
|
| `rechercher_par_id` | 6 000 000 | 0,01 | 0,48 % |
|
|
|
|
|
|
| `meilleure_periode` | 200 | 0,00 | 0,00 % |
|
|
|
|
|
|
| `nb_facons_rendre` | 200 | 0,00 | 0,00 % |
|
|
|
|
|
|
|
|
|
|
|
|
Les deux tris de chaque traitement, celui de F7 sur une copie et celui du
|
|
|
|
|
|
rapport, expliquent les 400 appels initiaux de `trier_intervalle`. Le tri
|
|
|
|
|
|
fusion et ses comparaisons représentent désormais l'essentiel du coût.
|
|
|
|
|
|
Les centaines de millions d'appels récursifs de F8 ont disparu, les
|
|
|
|
|
|
recherches F6 coûtent très peu et F10 ne rescane plus les préfixes du
|
|
|
|
|
|
rapport. Les valeurs à 0,00 s restent des arrondis de l'échantillonnage.
|
|
|
|
|
|
Les durées brutes de ce lot ne se comparent pas à celles du profil initial
|
|
|
|
|
|
sur une seule exécution, ni aux temps réels mesurés en `-O2`.
|
|
|
|
|
|
|
|
|
|
|
|
Pour reproduire ce profil cumulé depuis la racine :
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
tmp_profil=$(mktemp -d)
|
|
|
|
|
|
gcc -std=c11 -Wall -Wextra -Wpedantic -O0 -g -pg -x c -o "$tmp_profil/banque" - <<EOF
|
|
|
|
|
|
#define main banque_main
|
|
|
|
|
|
#include "$(pwd)/banque.c"
|
|
|
|
|
|
#undef main
|
|
|
|
|
|
int main(void) {
|
|
|
|
|
|
for (int i = 0; i < 200; i++) {
|
|
|
|
|
|
etat_alea = GRAINE;
|
|
|
|
|
|
if (banque_main() != 0) return EXIT_FAILURE;
|
|
|
|
|
|
}
|
|
|
|
|
|
return EXIT_SUCCESS;
|
|
|
|
|
|
}
|
|
|
|
|
|
EOF
|
|
|
|
|
|
(cd "$tmp_profil" && ./banque > /dev/null && gprof -b ./banque gmon.out) > build/profil/profil_apres_repete.txt
|
|
|
|
|
|
rm -rf "$tmp_profil"
|
|
|
|
|
|
```
|