1A - Enoncé 8 novembre 2023-2024

Exercice 1 : décodage façon Morse

Une langue étrangère s’écrit avec 10 lettres ABCDEFGHUIJ. Chacune est représentée par 4 bits.

[13]:
code_def = """
A 0000
B 0001
C 0010
D 0011
E 0100
F 0101
G 0110
H 0111
I 1000
J 1001
"""

Ou encore.

[14]:
code_def = {
    "A": "0000",
    "B": "0001",
    "C": "0010",
    "D": "0011",
    "E": "0100",
    "F": "0101",
    "G": "0110",
    "H": "0111",
    "I": "1000",
    "J": "1001",
}

Q1 : Ecrire une fonction qui code une séquence de lettres en une séquence de 0 et 1.

[25]:
def code(text):
    return "".join(code_def[c] for c in text)


assert code("AG") == "00000110"

Q2 : On s’intéresse au décodage d’un message.

Première étage : écrire une fonction qui retourne la première lettre correpondant au premier code qui commence un message codé.

[26]:
def first_letter(chaine):
    for k, v in code_def.items():
        if chaine.startswith(v):
            return k


assert first_letter("10010001") == "J"

Q3 : Ecrire une fonction qui reçoit une séquence de 0 et de 1 et retourne la séquence de lettres correspondante.

[27]:
def decode(chaine):
    res = ""
    while len(chaine) > 0:
        found = None
        for k, v in code_def.items():
            if chaine.startswith(v):
                found = k
        if found is None:
            return None
        res += found
        chaine = chaine[len(code_def[found]) :]
    return res


assert decode("00000110") == "AG"

On peut utiliser le fait que toutes les lettres sont représentées par des séquences de 4 bits mais la fonction est spécifique au premier code et ne marcherait pas dans les autres cas.

[17]:
code_def_inverse = {v: k for k, v in code_def.items()}


def decode_rapide(chaine):
    res = ""
    for i in range(0, len(chaine), 4):
        res += code_def_inverse[chaine[i : i + 4]]
    return res


assert decode_rapide("00000110") == "AG"

Q4 : On forme une classe avec les deux fonctions précédentes. Il faut compléter le code suivant.

[35]:
class Coding:
    def __init__(self):
        self.mapping = {
            "A": "0000",
            "B": "0001",
            "C": "0010",
            "D": "0011",
            "E": "0100",
            "F": "0101",
            "G": "0110",
            "H": "0111",
            "I": "1000",
            "J": "1001",
        }
        self.inverse = {v: k for k, v in self.mapping.items()}

    def code(self, text):
        return "".join(self.mapping[c] for c in text)

    def first_letter(self, chaine):
        for k, v in self.mapping.items():
            if chaine.startswith(v):
                return k
        return None

    def decode(self, chaine):
        if len(chaine) == 0:
            return ""
        letter = self.first_letter(chaine)
        if letter is None:
            return None
        suite = self.decode(chaine[len(self.mapping[letter]) :])
        if suite is None:
            return None
        return letter + suite


cl = Coding()
assert cl.code("AG") == "00000110"
assert cl.decode("00000110") == "AG"

Q5 : On veut réduire la taille du message codé.

Les lettres de A à G sont maintenant codées sur 3 bits et les suivantes sur 5. On change juste le dictionnaire de la classe.

[36]:
class Coding35(Coding):
    def __init__(self):
        self.mapping = {
            "A": "000",
            "B": "001",
            "C": "010",
            "D": "011",
            "E": "100",
            "F": "101",
            "G": "110",
            "H": "11100",
            "I": "11101",
            "J": "11110",
        }
        self.inverse = {v: k for k, v in self.mapping.items()}


cl = Coding35()
assert cl.code("AH") == "00011100"
assert cl.decode("00011100") == "AH"

Q6 : Que fait la fonction suivante ?

Que suppose-t-elle sur la méthode decode pour qu’elle fonctionne.

[38]:
def which_coding(text, codings):
    return [c for c in codings if c.decode(text) is not None]


codings = [Coding(), Coding35()]
assert which_coding("0000", codings) == codings[:1]

La fonction retourne la liste des codes qui peuvent décoder un message. Elle suppose que la méthode decode retourne None lorsqu’elle ne peut décoder un message.

Q7 : Dans ce langage, les lettres sont toutes équiprobables

Quel code est le plus court pour un texte aléatoire très grand et quantifier le gain ? Que se passe-t-il si la lettre J a une probabilité de 0.3 et toutes les autres lettres ont la même probabilité d’apparition ? Que suggérez-vous pour optimiser le Coding en terme de longueur ?

La probabilité des lettes est uniforme, donc égale 0.1. Avec le premier code, la longueur moyenne d’une lettre codé est 4. Avec le second c’est (7 * 3  + 3 * 5) * 0.1 = 3.6. Donc le second est plus efficace. Si J a une probabilité d’apparition de 0.3, le calcule devient : 7 * 3 * \frac{0.5}{9}  + 2 * 5 * \frac{0.5}{9} + 5 * 0.3 \sim 4.22.

[20]:
7 * 3 * 0.5 / 9 + 2 * 5 * 0.5 / 9 + 5 * 0.5
[20]:
4.222222222222222

Pour optimiser, il faut donner le code le plus court aux lettres les plus probables.

[21]:
6 * 3 * 0.5 / 9 + 3 * 5 * 0.5 / 9 + 3 * 0.5
[21]:
3.3333333333333335

Q8 : On change le Coding des lettres A et B

A  00 et B 01.

Il faut créer une troisième classe héritant de la première. Que valent c.code("BGBB") et c.code("DEF") ? Que retourne votre méthode decode ?

[22]:
class Coding235(Coding):
    def __init__(self):
        self.mapping = {
            "A": "00",
            "B": "01",
            "C": "010",
            "D": "011",
            "E": "100",
            "F": "101",
            "G": "110",
            "H": "11100",
            "I": "11101",
            "J": "11110",
        }
        self.inverse = {v: k for k, v in self.mapping.items()}


c = Coding235()
assert c.code("BGBB") == "011100101"
assert c.code("DEF") == "011100101"  # c'est la même chose

Q9 : Dans le cas précédent, la première lettre peut être soit B soit D.

Ecrire une méthode qui retourne toutes les options pour la première lettre d’un message codé.

[23]:
class Coding235(Coding):
    def __init__(self):
        self.mapping = {
            "A": "00",
            "B": "01",
            "C": "010",
            "D": "011",
            "E": "100",
            "F": "101",
            "G": "110",
            "H": "11100",
            "I": "11101",
            "J": "11110",
        }
        self.inverse = {v: k for k, v in self.mapping.items()}

    def first_letters(self, chaine):
        found = []
        for k, v in self.mapping.items():
            if chaine.startswith(v):
                found.append(k)
        return set(found)


c = Coding235()
assert c.first_letters("011100101") == {"B", "D"}

Q10 : Ecrire une méthode…

decode qui retourne toutes les solutions par récurrence.

[24]:
class Coding235(Coding):
    def __init__(self):
        self.mapping = {
            "A": "00",
            "B": "01",
            "C": "010",
            "D": "011",
            "E": "100",
            "F": "101",
            "G": "110",
            "H": "11100",
            "I": "11101",
            "J": "11110",
        }
        self.inverse = {v: k for k, v in self.mapping.items()}

    def first_letters(self, chaine):
        found = []
        for k, v in self.mapping.items():
            if chaine.startswith(v):
                found.append(k)
        return set(found)

    def decode(self, chaine):
        solutions = []
        found = self.first_letters(chaine)
        for f in found:
            end = chaine[len(self.mapping[f]) :]
            if end == "":
                solutions.append(f)
                continue
            suites = self.decode(end)
            if len(suites) == 0:
                continue
            for s in suites:
                solutions.append(f + s)

        return set(solutions)


c = Coding235()
assert c.decode("011100101") == {"BGBB", "DEF"}
{'BGBB', 'DEF'}
---
[ ]:


Notebook on github