Nombre de façons de regrouper les étudiants - Programmation compétitive

19 Apr 2022 19 Apr 2022 2272 vues ESSADDOUKI Mostafa 6 min de lecture

Problème de regroupement — Solo ou Binôme

Lors de la préparation de la première épreuve éliminatoire organisée par les Olympiades Marocaines d'Informatique, le comité donne le choix aux étudiants de travailler seuls ou en binôme avec un autre étudiant.

Le problème se réduit à savoir de combien de manières les étudiants peuvent rester seuls ou en binôme.

Définition — Problème de regroupement Étant donné n étudiants, déterminer le nombre de façons de les regrouper en paires (binômes) ou en solo, où chaque étudiant appartient soit à un binôme (avec un autre étudiant) soit reste seul.

Ce problème est un classique de la programmation dynamique et correspond au nombre de couplages dans un graphe complet (connu sous le nom de "nombres de téléphone" ou "involution numbers").

Énoncé du problème

Entrée
  • Un entier n représentant le nombre d'étudiants.
Sortie
  • Le nombre de façons de regrouper les étudiants en solo ou en binômes.
Exemple 1
Entrée
3
Sortie
4
Explication :Pour 3 étudiants, les regroupements possibles sont :
  • {1}, {2}, {3}
  • {1, 2}, {3}
  • {1, 3}, {2}
  • {1}, {2, 3}
Exemple 2
Entrée
4
Sortie
10
Explication : Pour 4 étudiants, il y a 10 façons différentes.
Exemple 3
Entrée
40
Sortie
8166103632745757696
Explication : Pour 40 étudiants, le nombre devient très grand.
Contraintes
  • 1 ≤ n ≤ 50

Solution 1 — Approche récursive (Diviser pour régner)

Le problème peut être résolu avec la récursivité en considérant les choix que nous avons pour le n-ième étudiant :

  • Cas 1 : Le n-ième étudiant reste seul → on récure pour les (n-1) étudiants restants.
  • Cas 2 : Le n-ième étudiant se met en binôme avec n'importe lequel des (n-1) élèves restants, puis on récure pour les (n-2) étudiants restants. Le nombre de façons de faire est (n-1) × Rec(n-2).
Relation de récurrence

f(n) = f(n-1) + (n-1) × f(n-2)

Avec les cas de base :

  • f(1) = 1 (un seul étudiant reste seul)
  • f(2) = 2 (deux étudiants peuvent rester seuls ou former un binôme)
   
Cas de base de la récursion C++/Python
si n ≤ 2 → retourner n
def CompterGroupes(n):
    if n <= 2:
        return n
    else:
        return CompterGroupes(n-1) + (n-1) * CompterGroupes(n-2)


n = int(input())
print(CompterGroupes(n))
#include <iostream>
using namespace std;

unsigned long long CompterGroupes(int n)
{
    if (n <= 2) return n;
    else return CompterGroupes(n-1) + (n-1) * CompterGroupes(n-2);
}

int main()
{
    int n;
    cin >> n;
    cout << CompterGroupes(n) << endl;
    return 0;
}
Sortie (n=3)
4
Complexité — Solution récursive naïve La récursion recalcule plusieurs fois les mêmes sous-problèmes.
Complexité temporelle : exponentielle (similaire à Fibonacci mais avec multiplications).
Complexité spatiale : O(n) — profondeur de la pile d'appels.

Pour n=50, cette solution est beaucoup trop lente.

Solution 2 — Programmation Dynamique

Puisque la formule récursive ci-dessus (solution 1) a des sous-problèmes qui se chevauchent, nous pouvons la résoudre en utilisant la programmation dynamique.

Définition — Tableau DP On crée un tableau dp[n+1] où :

dp[i] = nombre de façons de regrouper i étudiants.
Remplissage du tableau
  • Initialisation :
    • dp[0] = 1 (ensemble vide, une seule façon)
    • dp[1] = 1 (un seul étudiant reste seul)
  • Récurrence :Pour i de 2 à n
    • dp[i] = dp[i-1] + (i-1) × dp[i-2]

Illustration des premières valeurs

ndp[n]Explication
01ensemble vide
11{1}
22{1,2} ou {1},{2}
34exemple 1
410exemple 2
526= 10 + 4×4
def CompterGroupes(n):
    # Tableau DP
    dp = [0] * (n + 1)
    
    for i in range(n + 1):
        if i <= 2:
            dp[i] = i
        else:
            dp[i] = dp[i-1] + (i-1) * dp[i-2]
    
    return dp[n]


n = int(input())
print(CompterGroupes(n))
#include <iostream>
using namespace std;

unsigned long long CompterGroupes(int n)
{
    unsigned long long dp[n + 1];
    
    for (int i = 0; i <= n; i++) {
        if (i <= 2)
            dp[i] = i;
        else
            dp[i] = dp[i - 1] + (i - 1) * dp[i - 2];
    }
    
    return dp[n];
}

int main()
{
    int n;
    cin >> n;
    cout << CompterGroupes(n) << endl;
    return 0;
}
Sortie (n=40)
8166103632745757696
Complexité — Solution Programmation Dynamique
CritèreSolution récursive naïveProgrammation dynamique
TempsExponentiel (∼ φⁿ)O(n) — linéaire
EspaceO(n) — pile d'appelsO(n) — tableau 1D
Sous-problèmes recalculésOui — nombreux recalculsNon — mémoïsation
Optimisation mémoire

On peut réduire l'espace à O(1) en ne gardant que les deux dernières valeurs :

unsigned long long CompterGroupes(int n) {
    if (n <= 2) return n;
    
    unsigned long long prev2 = 1;  // dp[1]
    unsigned long long prev1 = 2;  // dp[2]
    unsigned long long curr;
    
    for (int i = 3; i <= n; i++) {
        curr = prev1 + (i - 1) * prev2;
        prev2 = prev1;
        prev1 = curr;
    }
    
    return curr;
}
Points clés à retenir
  • Ce problème correspond au nombre de façons de partitionner un ensemble en paires et éléments seuls.
  • La relation de récurrence est : f(n) = f(n-1) + (n-1) × f(n-2).
  • Les cas de base sont f(0)=1, f(1)=1, f(2)=2.
  • La solution récursive naïve a une complexité exponentielle.
  • La programmation dynamique permet de résoudre le problème en temps linéaire O(n).
  • Pour n=50, les résultats deviennent très grands (dépassent 64 bits pour n>50).
  • Ce problème est connu sous le nom de "nombres de téléphone" (telephone numbers) ou "involution numbers".
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.