Le nombre de façons pour construire un mur de dimension 4*N - Programmation compétitive

25 Apr 2022 25 Apr 2022 2571 vues ESSADDOUKI Mostafa 6 min de lecture
Définition On considère un mur de dimensions 4 × N et des briques de dimensions 4 × 1. Chaque brique peut être placée soit verticalement, soit horizontalement. Le problème consiste à déterminer le nombre de façons de construire entièrement le mur.

Une brique verticale occupe une seule colonne de hauteur 4, tandis qu’une brique horizontale occupe une seule ligne sur 4 colonnes. Comme la hauteur du mur est 4, poser des briques horizontalement impose d’en placer quatre en même temps pour couvrir complètement les 4 lignes.

Idée essentielle À chaque étape, deux choix sont possibles pour terminer le mur : soit ajouter une brique verticale sur la dernière colonne, soit ajouter quatre briques horizontales couvrant les quatre dernières colonnes.

Problème

Écrire un programme qui lit un entier N et affiche le nombre de façons de construire un mur de dimensions 4 × N à l’aide de briques 4 × 1.

Contraintes
  • N ≥ 0
  • Chaque brique peut être placée verticalement ou horizontalement
  • Le mur doit être entièrement recouvert, sans chevauchement ni espace vide

Entrée

Une ligne contient un entier N.

Sortie

Afficher le nombre de façons de construire un mur de dimensions 4 × N.

Exemples

Exemple 1
Entrée
5
Sortie
3
Explication : il existe 3 façons différentes de construire un mur de largeur 5.
Exemple 2
Entrée
8
Sortie
7
Explication : pour N = 8, le nombre total de constructions possibles vaut 7.
Exemple 3
Entrée
50
Sortie
5453761
Exemple 4
Entrée
35
Sortie
43371

Relation de récurrence

Notons nbFacons(N) le nombre de façons de construire un mur de dimensions 4 × N. En observant la dernière partie du mur, on obtient deux cas :

  • si la dernière brique est verticale, il reste à construire un mur de taille 4 × (N - 1) ;
  • si les dernières briques sont horizontales, il faut placer 4 briques horizontales, et il reste à construire un mur de taille 4 × (N - 4).

Récurrence Math
nbFacons(N) = 1                  si N < 4
nbFacons(N) = nbFacons(N-1) + nbFacons(N-4)    si N ≥ 4
Interprétation Pour N < 4, une seule disposition est possible : toutes les briques sont verticales. À partir de N = 4, on peut commencer à placer aussi des briques horizontalement.

Solution 1 : approche récursive

La solution la plus directe consiste à appliquer la relation de récurrence telle quelle. On calcule récursivement le nombre de façons de construire un mur plus petit jusqu’à atteindre un cas de base.

Attention Cette approche est simple à comprendre, mais elle recalcule plusieurs fois les mêmes sous-problèmes. Elle devient donc inefficace lorsque N devient grand.
#include <iostream>
using namespace std;

int nbFacons(int N) {
    if (N < 4)
        return 1;

    return nbFacons(N - 1) + nbFacons(N - 4);
}

int main() {
    int n;
    cin >> n;
    cout << nbFacons(n) << endl;
    return 0;
}
def nbFacons(N):
    if N < 4:
        return 1

    return nbFacons(N - 1) + nbFacons(N - 4)

n = int(input())
print(nbFacons(n))
Limite de cette méthode L’arbre récursif devient très grand, car les mêmes valeurs de nbFacons(k) sont recalculées plusieurs fois. La complexité temporelle est donc très mauvaise.

Pourquoi optimiser ?

Dans la solution récursive naïve, plusieurs appels récursifs demandent à nouveau le calcul des mêmes sous-problèmes. Par exemple, nbFacons(N-1) et nbFacons(N-4) peuvent eux-mêmes avoir besoin de nbFacons(N-5), etc.

 Observation

Chevauchement des sous-problèmes

 Niveau : Débutant

Le problème possède une structure typique de programmation dynamique : la solution d’un grand problème dépend de solutions plus petites, et plusieurs sous-problèmes identiques apparaissent plusieurs fois.

Solution optimisée : programmation dynamique top-down

L’approche top-down consiste à conserver la structure récursive, mais à mémoriser les résultats déjà calculés dans un tableau DP. Ainsi, dès qu’un sous-problème a déjà été résolu, on renvoie immédiatement sa valeur sans recalcul.

Principe On utilise un tableau DP tel que DP[i] contient la valeur de nbFacons(i). Si DP[i] est déjà connue, on la retourne directement.
#include <iostream>
#include <cstring>
using namespace std;

int DP[1000];

int nbFacons(int N) {
    if (DP[N] >= 0)
        return DP[N];

    if (N < 4)
        return 1;

    DP[N] = nbFacons(N - 1) + nbFacons(N - 4);
    return DP[N];
}

int main() {
    int n;
    cin >> n;
    memset(DP, -1, sizeof(DP));
    cout << nbFacons(n) << endl;
    return 0;
}
def nbFacons(N):
    global DP

    if DP[N] >= 0:
        return DP[N]

    if N < 4:
        return 1

    DP[N] = nbFacons(N - 1) + nbFacons(N - 4)
    return DP[N]

n = int(input())
DP = [-1 for _ in range(n + 1)]

print(nbFacons(n))

Solution optimisée : programmation dynamique bottom-up

L’approche bottom-up construit les résultats dans l’ordre croissant, depuis les plus petites valeurs jusqu’à N. Elle évite complètement la récursivité.

On commence par remplir les cas de base, puis on applique la relation :

$$ DP[i] = DP[i-1] + DP[i-4] \quad \text{pour } i \ge 4 $$

Avantage Cette méthode est souvent la plus robuste en pratique : elle évite les appels récursifs profonds et calcule chaque valeur une seule fois.
#include <iostream>
using namespace std;

int nbFacons(int N) {
    int DP[N + 1];

    for (int i = 0; i <= N; i++) {
        if (i < 4)
            DP[i] = 1;
        else
            DP[i] = DP[i - 1] + DP[i - 4];
    }

    return DP[N];
}

int main() {
    int n;
    cin >> n;
    cout << nbFacons(n) << endl;
    return 0;
}
def nbFacons(N):
    DP = [-1 for _ in range(N + 1)]

    for i in range(N + 1):
        if i < 4:
            DP[i] = 1
        else:
            DP[i] = DP[i - 1] + DP[i - 4]

    return DP[N]

n = int(input())
print(nbFacons(n))

Complexité

MéthodeComplexité temporelleComplexité spatialeRemarque
Récursive naïveExponentielleDépend de la profondeur de récursionTrès coûteuse pour les grandes valeurs de N
DP Top-DownO(N)O(N)Mémorisation des sous-problèmes
DP Bottom-UpO(N)O(N)Version itérative souvent préférable
À retenir En programmation dynamique, chaque sous-problème est calculé une seule fois. On passe ainsi d’une solution très lente à une solution linéaire en temps.
Sortie
// La sortie apparaîtra ici…
Prêt · Ctrl+Entrée pour exécuter

Discussion (0)

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Votre commentaire sera visible après modération.