# Cours 4 : tris

Un algorithme de **tri** répond au problème suivant :

Input : Un tableau de données comparables entre elles (par exemple : des nombres)
Ouput : Un tableau (parfois le même tableau) contenant ces mêmes données par ordre croissant
Lorsqu'un algorithme est proposé, on se pose les questions suivantes :

* l'algorithme fonctionne-t-il, pourquoi ? Comment le prouver ?
* Quelle est la complexité en temps de l'algorithme ? Dans le meilleur des cas ? Dans le pire des cas ? En moyenne ?
* Quelle est la complexité en espace de l'algorithme ?
* Le tri est-il effectué "sur place", c'est-à-dire à l'intérieur du tableau ?
* Le tri est-il "stable", c'est-à-dire est-ce qu'on modifie l'ordre des valeurs égales dans le tableau ?

## Mise en situation

Beaucoup d'entre vous ont déjà vu ou entrevu des algorithmes de tris. Nous allons donc commencer par écrire des algorithmes de la façon qui vous semble la plus naturelle et voir ce que l'on peut dire à partir des questions ci-dessus.

```python
from random import randint
t = [randint(1,100) for i in range(100)]
print(t)
```

    [72, 72, 75, 94, 42, 70, 63, 97, 7, 26, 79, 72, 12, 9, 12, 48, 75, 33, 78, 14, 44, 2, 19, 19, 11, 94, 72, 4, 14, 22, 69, 45, 8, 19, 99, 91, 48, 4, 9, 75, 8, 47, 19, 14, 98, 35, 95, 57, 5, 23, 80, 8, 37, 22, 61, 4, 49, 51, 62, 71, 48, 55, 5, 46, 10, 59, 25, 85, 7, 5, 63, 95, 97, 34, 24, 1, 11, 31, 21, 72, 11, 44, 56, 64, 12, 29, 53, 60, 92, 74, 99, 62, 56, 5, 16, 57, 100, 39, 68, 27]

```python
Tableaux_tests = [
    [5,4,3,2,1],
    [1,2,3,4,5],
    [6, 14, 2, 7, 10, 7, 11, 11, 17, 19, 5, 8, 9, 2, 14, 19, 13, 6, 1, 15],
    [100, 44, 84, 5, 99, 6, 26, 97, 39, 35, 50, 39, 22, 21, 81, 25, 30, 20, 45, 49, 33, 24, 38, 85, 32, 56, 7, 64, 18, 98, 62, 77, 91, 32, 4, 10, 96, 98, 73, 28, 27, 19, 22, 51, 52, 96, 87, 40, 19, 35, 55, 14, 91, 6, 67, 7, 60, 59, 86, 10, 41, 6, 88, 74, 76, 74, 48, 64, 69, 61, 26, 80, 3, 56, 35, 76, 84, 12, 13, 36, 78, 97, 19, 99, 39, 69, 75, 18, 24, 4, 2, 78, 15, 80, 50, 17, 99, 36, 63, 24],
    [72, 72, 75, 94, 42, 70, 63, 97, 7, 26, 79, 72, 12, 9, 12, 48, 75, 33, 78, 14, 44, 2, 19, 19, 11, 94, 72, 4, 14, 22, 69, 45, 8, 19, 99, 91, 48, 4, 9, 75, 8, 47, 19, 14, 98, 35, 95, 57, 5, 23, 80, 8, 37, 22, 61, 4, 49, 51, 62, 71, 48, 55, 5, 46, 10, 59, 25, 85, 7, 5, 63, 95, 97, 34, 24, 1, 11, 31, 21, 72, 11, 44, 56, 64, 12, 29, 53, 60, 92, 74, 99, 62, 56, 5, 16, 57, 100, 39, 68, 27]
]
```

```python
def testTri(algo):
    """
    Lance une série de tests sur un algorithme donné
    Input :
        - algo, un algorithme qui prend en paramètre un tableau et soit trie le tableau sur place, soit renvoie
        une version triée du tableau
    """
    non_trie = []

    surplace = 0
    for t in Tableaux_tests:
        tcopie = list(t) # on effectue une copie pour le test
        t2 = algo(tcopie)
        if t2 is None:
            surplace+=1
            t2 = tcopie
        if not sorted(t) == t2: # on compare au tri effectué par python
            non_trie.append(t)

    if len(non_trie) == 0:
        print("L'algoritme a trié tous les tableaux.")
        if surplace == len(Tableaux_tests):
            print("Le tri se fait sur place")
        else:
            print("Le tri ne se fait pas sur place")
    else:
        for t in non_trie:
            print("Le tri n'a pas fonctionné pour : ", t)

```

Ecrivez ci-dessous un algortihme de tri

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

## 3 algorithmes classiques mais inefficaces

### Un tri facile à écrire mais pas efficace : le tri par sélection

```python
def triSelection(t):

    for fin in range(len(t), 0, -1): # on décrémente un indice depuis la fin du tableau
        imax = 0
        # on cherche l'indice de l'élément maximum
        for i in range(fin):
            if t[i] > t[imax]:
                imax = i
        # on place l'élément maximal à la fin du tableau
        t[imax], t[fin-1] = t[fin-1], t[imax]

```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
```

```python
triSelection(t)
t
```

    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]

```python
testTri(triSelection)
```

    L'algoritme a trié tous les tableaux.
    Le tri se fait sur place

**Pourquoi ça fonctionne ?**

```python
def triSelectionAffichage(t):
    print(t)

    for fin in range(len(t), 0, -1): # on décrémente un indice depuis la fin du tableau
        imax = 0
        # on cherche l'indice de l'élément maximum
        for i in range(fin):
            if t[i] > t[imax]:
                imax = i
        # on place l'élément maximal à la fin du tableau
        t[imax], t[fin-1] = t[fin-1], t[imax]
        print(t)
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
triSelectionAffichage(t)
```

    [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
    [4, 3, 6, 1, 2, 4, 3, 5, 6, 9]
    [4, 3, 6, 1, 2, 4, 3, 5, 6, 9]
    [4, 3, 5, 1, 2, 4, 3, 6, 6, 9]
    [4, 3, 3, 1, 2, 4, 5, 6, 6, 9]
    [4, 3, 3, 1, 2, 4, 5, 6, 6, 9]
    [2, 3, 3, 1, 4, 4, 5, 6, 6, 9]
    [2, 1, 3, 3, 4, 4, 5, 6, 6, 9]
    [2, 1, 3, 3, 4, 4, 5, 6, 6, 9]
    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]
    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]

A la fin de chaque itération de boucle, qu'est-ce qui est vrai ? Pourquoi le tableau est-il "plus trié" ?

.

.

.

Peut-on le vérfier automatiquement ?

```python
def triSelectionCheck(t):
    # Rajouter un assert à chaque itération pour vérifier l'invariant de boucle
    tsorted = sorted(t) # la version triee par python pour "vérfier"

    # begin solution
    for fin in range(len(t), 0, -1): # on décrémente un indice depuis la fin du tableau
        imax = 0
        # on cherche l'indice de l'élément maximum
        for i in range(fin):
            if t[i] > t[imax]:
                imax = i
        # on place l'élément maximal à la fin du tableau
        t[imax], t[fin-1] = t[fin-1], t[imax]
        assert(t[fin-1:] == tsorted[fin-1:])
    # end solution
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
triSelectionCheck(t)
```

**Quelle est la complexité de l'algorithme ?**

Dans le meilleur des cas ?

Dans le pire des cas ?

En moyenne ?

**Quelle est la complexité en espace de l'algorithme ?**

**Le tri est-il effectué sur place ?**

**Le tri est-il stable ?**

**Conclusion sur le tri sélection**

Un tri peu efficace et jamais utilisé en pratique.

Avantages :

* facile à implater
* faible nombre d'écriture dans le tableau (cf TP)
* Tri sur place et stable (si bien écrit)

Inconvénient :

* Forte complexité quelque soit les données

### Un tri amusant mais toujours inefficace : le tri bulle

```python
def triBulle(t):
    for fin in range(len(t),0,-1): # on décrémente un indice depuis la fin du tableau
        inversions = False
        for i in range(fin-1):
            # les grands éléments avancent vers la fin comme des "bulles"
            if t[i] > t[i+1]:
                t[i],t[i+1] = t[i+1],t[i]
                inversions = True
        if not inversions:
            return
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
triBulle(t)
t
```

    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]

```python
testTri(triBulle)
```

    L'algoritme a trié tous les tableaux.
    Le tri se fait sur place

**Pourquoi ça fonctionne ?**

```python
def triBulleAffichage(t):
    print(t)
    for fin in range(len(t),0,-1): # on décrémente un indice depuis la fin du tableau
        inversions = False
        for i in range(fin-1):
            # les grands éléments avancent vers la fin comme des "bulles"
            if t[i] > t[i+1]:
                t[i],t[i+1] = t[i+1],t[i]
                print(t)
                inversions = True
        print("fin de boucle")
        if not inversions:
            return
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
triBulleAffichage(t)
```

    [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
    [3, 9, 6, 1, 2, 4, 3, 5, 6, 4]
    [3, 6, 9, 1, 2, 4, 3, 5, 6, 4]
    [3, 6, 1, 9, 2, 4, 3, 5, 6, 4]
    [3, 6, 1, 2, 9, 4, 3, 5, 6, 4]
    [3, 6, 1, 2, 4, 9, 3, 5, 6, 4]
    [3, 6, 1, 2, 4, 3, 9, 5, 6, 4]
    [3, 6, 1, 2, 4, 3, 5, 9, 6, 4]
    [3, 6, 1, 2, 4, 3, 5, 6, 9, 4]
    [3, 6, 1, 2, 4, 3, 5, 6, 4, 9]
    fin de boucle
    [3, 1, 6, 2, 4, 3, 5, 6, 4, 9]
    [3, 1, 2, 6, 4, 3, 5, 6, 4, 9]
    [3, 1, 2, 4, 6, 3, 5, 6, 4, 9]
    [3, 1, 2, 4, 3, 6, 5, 6, 4, 9]
    [3, 1, 2, 4, 3, 5, 6, 6, 4, 9]
    [3, 1, 2, 4, 3, 5, 6, 4, 6, 9]
    fin de boucle
    [1, 3, 2, 4, 3, 5, 6, 4, 6, 9]
    [1, 2, 3, 4, 3, 5, 6, 4, 6, 9]
    [1, 2, 3, 3, 4, 5, 6, 4, 6, 9]
    [1, 2, 3, 3, 4, 5, 4, 6, 6, 9]
    fin de boucle
    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]
    fin de boucle
    fin de boucle

Quel invariant de boucle peut-on définir ?

.

.

.

.

```python
def triBulleCheck(t):
    # Tri avec vérification de l'invariant de boucle

    # begin solution
    tsorted = sorted(t)
    for fin in range(len(t),0,-1): # on décrémente un indice depuis la fin du tableau
        inversions = False
        for i in range(fin-1):
            # les grands éléments avancent vers la fin comme des "bulles"
            if t[i] > t[i+1]:
                t[i],t[i+1] = t[i+1],t[i]
                inversions = True
        assert tsorted[fin:] == t[fin:]
        if not inversions:
            return
    # end solution
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
triBulleCheck(t)
```

```python
t
```

    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]

```python
# Un autre invariant possible
def nbInversions(t):
    return sum(sum(1 for v in t[i+1:] if v <t[i]) for i in range(len(t)))
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
nbInversions(t)
```

    20

```python
def triBulleCheck2(t):
    # Tri avec vérification de l'invariant par comptage des inversions
    nb = nbInversions(t)

    for fin in range(len(t),0,-1): # on décrémente un indice depuis la fin du tableau
        inversions = False
        for i in range(fin-1):
            # les grands éléments avancent vers la fin comme des "bulles"
            if t[i] > t[i+1]:
                t[i],t[i+1] = t[i+1],t[i]
                inversions = True
        if not inversions:
            return
        else:
            new_nb = nbInversions(t)
            assert new_nb < nb
            nb = new_nb
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
triBulleCheck2(t)
```

**Quelle est la complexité de l'algorithme ?**

Dans le meilleur des cas ?

Dans le pire des cas ?

En moyenne ?

Pour répondre, il faut calculer le nombre d'inversions moyen d'un "tableau aléatoire". Pour simplifier, on supposera que toutes les valeurs sont distinctes et que le tableau correspond à une permutation aléatoire choisie uniformément parmi les permutations d'un certain ensemble.

Soit $i$ et $j$, avec $i < j$, deux indices du tableau, la probabilité de $tab[i] < tab[j]$ est $\frac{1}{2}$ car pour deux valeurs $v$ et $w$, le nombre de tableaux tel que $t[i] = v$ et $t[j] = w$ est le même que le nombre de tableau tel que $t[i] = w$ et $t[j] = w$. Il existe donc $\frac{n(n-1)}{2}$ couples $i < j$ et chacun a une probabilité $\frac{1}{2}$ d'être une inversion. Par linéarité de l'espérance, on obtient donc que le nombre d'inversions est en moyenne de $I = \frac{n(n-1)}{4}$, et comme on sait que $I \leq C$, on en conclue que la **complexité en moyenne est toujours de $O(n^2)$**

**Quelle est la complexité en espace de l'algorithme ?**

**Le tri est-il effectué sur place ?**

**Le tri est-il stable ?**

**Conclusion pour le tri bulle**

Un tri inefficace et jamais utilisé en pratique.

Avantages :

* Implantation simple
* Tri sur place et stable
* Complexité linéaire dans le meilleur des cas

Inconvénients :

* Complexité quadratique dans le pire des cas et en moyenne
* Une seule valeur mal placée peut amener à une complexité quadratique

### Un tri intéressant : le tri par insertion

```python
def triInsertion(t):
    # on va insérer les valeurs une à une dans le début (trié du tableau)
    for i in range(1, len(t)): # l'indice de la valeur à insérer
        v = t[i]
        j = i-1
        while j >= 0 and t[j] > v: # on décale les valeurs plus grandes que v
            t[j+1] = t[j]
            j-=1
        t[j+1] = v # on place v à sa position dans cette partie du tableau
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
triInsertion(t)
t
```

    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]

```python
testTri(triInsertion)
```

    L'algoritme a trié tous les tableaux.
    Le tri se fait sur place

**Pourquoi ça fonctionne ?**

```python
def triInsertionAffichage(t):
    print(t)
    # on va insérer les valeurs une à une dans le début (trié du tableau)
    for i in range(1, len(t)): # l'indice de la valeur à insérer
        v = t[i]
        j = i-1
        while j >= 0 and t[j] > v: # on décale les valeurs plus grandes que v
            t[j+1] = t[j]
            print(t)
            j-=1
        t[j+1] = v # on place v à sa position dans cette partie du tableau
        print(t)
        print("Fin de boucle")
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
triInsertionAffichage(t)
```

    [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
    [9, 9, 6, 1, 2, 4, 3, 5, 6, 4]
    [3, 9, 6, 1, 2, 4, 3, 5, 6, 4]
    Fin de boucle
    [3, 9, 9, 1, 2, 4, 3, 5, 6, 4]
    [3, 6, 9, 1, 2, 4, 3, 5, 6, 4]
    Fin de boucle
    [3, 6, 9, 9, 2, 4, 3, 5, 6, 4]
    [3, 6, 6, 9, 2, 4, 3, 5, 6, 4]
    [3, 3, 6, 9, 2, 4, 3, 5, 6, 4]
    [1, 3, 6, 9, 2, 4, 3, 5, 6, 4]
    Fin de boucle
    [1, 3, 6, 9, 9, 4, 3, 5, 6, 4]
    [1, 3, 6, 6, 9, 4, 3, 5, 6, 4]
    [1, 3, 3, 6, 9, 4, 3, 5, 6, 4]
    [1, 2, 3, 6, 9, 4, 3, 5, 6, 4]
    Fin de boucle
    [1, 2, 3, 6, 9, 9, 3, 5, 6, 4]
    [1, 2, 3, 6, 6, 9, 3, 5, 6, 4]
    [1, 2, 3, 4, 6, 9, 3, 5, 6, 4]
    Fin de boucle
    [1, 2, 3, 4, 6, 9, 9, 5, 6, 4]
    [1, 2, 3, 4, 6, 6, 9, 5, 6, 4]
    [1, 2, 3, 4, 4, 6, 9, 5, 6, 4]
    [1, 2, 3, 3, 4, 6, 9, 5, 6, 4]
    Fin de boucle
    [1, 2, 3, 3, 4, 6, 9, 9, 6, 4]
    [1, 2, 3, 3, 4, 6, 6, 9, 6, 4]
    [1, 2, 3, 3, 4, 5, 6, 9, 6, 4]
    Fin de boucle
    [1, 2, 3, 3, 4, 5, 6, 9, 9, 4]
    [1, 2, 3, 3, 4, 5, 6, 6, 9, 4]
    Fin de boucle
    [1, 2, 3, 3, 4, 5, 6, 6, 9, 9]
    [1, 2, 3, 3, 4, 5, 6, 6, 6, 9]
    [1, 2, 3, 3, 4, 5, 6, 6, 6, 9]
    [1, 2, 3, 3, 4, 5, 5, 6, 6, 9]
    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]
    Fin de boucle

Quel invariant de boucle peut-on définir ?

.

.

.

.

```python
def triInsertionCheck(t):
    # vérifier que le début du tableau est trié
    t2 = list(t) # copie du tableau pour effectuer les tests
    # begin solution
    # on va insérer les valeurs une à une dans le début (trié du tableau)
    for i in range(1, len(t)): # l'indice de la valeur à insérer
        v = t[i]
        j = i-1
        while j >= 0 and t[j] > v: # on décale les valeurs plus grandes que v
            t[j+1] = t[j]
            j-=1
        t[j+1] = v # on place v à sa position dans cette partie du tableau
        assert(sorted(t2[:i+1]) == t[:i+1])
    # end solution
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
triInsertionCheck(t)
```

```python
t
```

    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]

**Quelle est la complexité de l'algorithme ?**

Dans le meilleur des cas ?

Dans le pire des cas ?

En moyenne ?

Le problème est assez similaires à celui du tri à bulles. En effet, à chaque insertion, on supprime un nombre d'inversion exactement égal au décalage que l'on fait subir au tableau trié. Observez par exemple l'insertion du nombre 2 dans l'exemple donné : on décale le 3, le 6 et le 9 et on supprime  les trois inversions de ces nombres avec le 2 lui-même (surtout, on en supprime pas plus). On en arrive donc à la même conclusion que pour le tri à bulle : en moyenne, il faudra au minimum $\frac{n(n-1)}{4}$ opérations, soit une **complexité en $O(n^2)$**.

Cependant, il est à noter que si l'on peut maîtriser le nombre d'inversions : par exemple en limitant la distance possible entre une valeur et sa position finale, on limite aussi à coup sûr le nombre d'opérations. En effet, en observant l'algorithme, on observe que le nombre de comparaisons est pratiquement égal au nombre d'inversions. Ainsi, si on exprime le problème, non plus seulement en fonction de $n$, mais en fonction de $n$ et $I$ où $I$ est le nombre d'inversions on trouve une complexité en $O(n +I)$. En particulier, si on travail sur des données dont le nombre d'inversions est en $O(n)$, alors on retrouve une complexité linéaire.

**Quelle est la complexité en espace de l'algorithme ?**

**Le tri est-il effectué sur place ?**

**Le tri est-il stable ?**

**Conclusion pour le tri insertion**

Un tri inefficace sur des données quelconque mais qui peut être très efficace sur des données "presque triées" (cf TP).

Avantages :

* Tri sur place et stable
* Complexité linéaire dans le meilleur des cas
* Complexité linéaire au nombre d'inversions

Inconvénients :

* Complexité quadratique dans le pire des cas et en moyenne

## Deux tris récursifs utilisant le principe du "diviser pour régner"

Le princie de *diviser pour régner* consiste à séparer le problème en deux problèmes plus petits de façon récursive. On voit ici deux algorithmes très classiques souvent à la base des algorithmes implantés "en vrai"

### Le tri rapide

A chaque étape, on choisit un pivot $p$ (ici la première valeur) et effectue un parcourt du tableau tel que toutes les valeurs $v \leq p$ soit au début du tableau et les autres à la fin. On place $p$ à la fin des petites valeurs, c'est-à-dire à son emplacement final dans le tableau trié. Puis on lance le tri récursivement sur les deux moitiés du tableau données par $p$.

Commençons par écrire la fonction *pivot* :
Input : un tableau de taille au moins 1
Output :
    - le tableau contenant les mêmes valeurs réorganisées en fonction de t[0] : t[0] doit être placé à un certain indice i tel que toutes les valeurs d'indices < i soit inférieures ou égales à t[0] et toutes les valeurs d'indice > i soit supérieures à t[0]
    - la fonction renvoie l'indice i du "pivot" t[0]
**Attention !** La complexité doit être de $O(n)$. Plus précisément, on ne veut parcourir le tableau qu'une seule fois.

Indication : l'ordre des valeurs plus petites que le pivot n'a pas besoin d'être respecté. Parcourez le tableau en mettant "au début" les plus petites et "à la fin" les plus grandes en maintenant les indices nécessaires.

```python
def pivot(t):
    # ecrire la fonction pivot
    # begin solution
    p = t[0]
    i = 1
    j = len(t)
    while i < j:
        if t[i] <= p:
            i+=1
        else:
            j-= 1
            t[i],t[j] = t[j],t[i]
    i-=1
    t[0],t[i] = t[i],t[0]
    return i
    # end solution
```

```python
t = [5, 7, 1, 3, 5, 4, 2, 2, 8]
pivot(t)
```

    6

```python
t
```

    [2, 2, 1, 3, 5, 4, 5, 8, 7]

```python
for t in Tableaux_tests:
    t = list(t)
    i = pivot(t)
    assert all(t[j] <= t[i] for j in range(i)) and all(t[j] > t[i] for j in range(i+1, len(t)))
```

Une fois qu'on a la fonction pivot, le principe du tri est de l'utiliser pour découper notre tableau en deux parties sur lesqueles on lance de nouveau l'algorithme de façon récursive.

```python
def triRapide1(t):
    if len(t) <= 1: # cas d'arret
        return t
    i = pivot(t) # on sépare les valeurs et on récupère l'indice "du milieu"
    return triRapide1(t[:i]) + [t[i]] + triRapide1(t[i+1:]) # l'appel récursif

```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
t = triRapide1(t)
t
```

    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]

```python
testTri(triRapide1)
```

    L'algoritme a trié tous les tableaux.
    Le tri ne se fait pas sur place

Tel qu'on l'a écrit, l'algorithme fait de recopie le tableau à chaque appel récursif, mais on peut corriger le problème. Voici une version "tout en un" ou le même tableau est passé en paramètre, dans ce cas, on doit aussi passer les bornes du tableau.

```python
def triRapideRec(t, deb, fin):
    if fin - deb <= 1: # condition d'arret (taille 0 ou 1)
        return
    # pivot
    p = t[deb]
    i = deb + 1
    j = fin
    while i < j:
        if t[i] <= p:
            i+=1
        else:
            j-= 1
            t[i],t[j] = t[j],t[i]
    i-=1
    t[deb],t[i] = t[i],t[deb]

    # appels récursifs
    triRapideRec(t,deb,i)
    triRapideRec(t, i+1, fin)

def triRapide2(t):
    triRapideRec(t, 0, len(t))
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
triRapide2(t)
t
```

    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]

```python
testTri(triRapide2)
```

    L'algoritme a trié tous les tableaux.
    Le tri se fait sur place

**Pourquoi ça fonctionne ?**

Observons sur l'exemple précédent

```python
def triRapideRecAffichage(t, deb, fin):
    print("Appel sur ", deb, fin)
    if fin - deb <= 1: # condition d'arret (taille 0 ou 1)
        return
    # pivot
    print(t)
    p = t[deb]
    i = deb + 1
    j = fin
    while i < j:
        if t[i] <= p:
            i+=1
        else:
            j-= 1
            t[i],t[j] = t[j],t[i]
    i-=1
    t[deb],t[i] = t[i],t[deb]
    print("pivot indice ", i)
    print(t)

    # appels récursifs
    triRapideRecAffichage(t,deb,i)
    triRapideRecAffichage(t, i+1, fin)

def triRapide2Affichage(t):
    triRapideRecAffichage(t, 0, len(t))
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
triRapide2Affichage(t)
```

    Appel sur  0 10
    [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
    pivot, indice  9
    [4, 3, 6, 1, 2, 4, 3, 5, 6, 9]
    Appel sur  0 9
    [4, 3, 6, 1, 2, 4, 3, 5, 6, 9]
    pivot, indice  5
    [4, 3, 3, 1, 2, 4, 5, 6, 6, 9]
    Appel sur  0 5
    [4, 3, 3, 1, 2, 4, 5, 6, 6, 9]
    pivot, indice  4
    [2, 3, 3, 1, 4, 4, 5, 6, 6, 9]
    Appel sur  0 4
    [2, 3, 3, 1, 4, 4, 5, 6, 6, 9]
    pivot, indice  1
    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]
    Appel sur  0 1
    Appel sur  2 4
    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]
    pivot, indice  3
    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]
    Appel sur  2 3
    Appel sur  4 4
    Appel sur  5 5
    Appel sur  6 9
    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]
    pivot, indice  6
    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]
    Appel sur  6 6
    Appel sur  7 9
    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]
    pivot, indice  8
    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]
    Appel sur  7 8
    Appel sur  9 9
    Appel sur  10 10

Comment prouver que ça marche ?

Qu'est-ce qui est vrai à la fin de chaque itération de boucle dans le pivot ?

Qu'est-ce qui est vrai à la fin de chaque appel avant les appels récursifs ?

```python
def triRapideRecCheck(t, deb, fin):
    if fin - deb <= 1: # condition d'arret (taille 0 ou 1)
        return
    # pivot
    p = t[deb]
    i = deb + 1
    j = fin
    while i < j:
        if t[i] <= p:
            i+=1
        else:
            j-= 1
            t[i],t[j] = t[j],t[i]
        assert all(t[k] <= p for k in range(deb,i)) and all(t[k] > p for k in range(j,fin))
    i-=1
    t[deb],t[i] = t[i],t[deb]
    assert t[i] == sorted(t)[i] # le pivot est placé à son emplacement final

    # appels récursifs
    triRapideRecCheck(t,deb,i)
    triRapideRecCheck(t, i+1, fin)

def triRapide2Check(t):
    triRapideRecCheck(t, 0, len(t))
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
triRapide2Check(t)
t
```

    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]

**Quelle est la complexité de l'algorithme ?**

A chaque étape, le tableau est divisé en deux sous-tableaux. La taille de ces sous-tableaux n'est pas constante, elle dépend du pivot. Si l'on choisit un tableau strictement décroissant ou croissant, à chaque étape $n$, il sera décomposé en un tableau de taille $O$ et un tableau de taille $n-1$. Soit $f$ la complexité de mon tri, on obtient la formule récursive

$\begin{align}
f(1) &= 1 \\
f(n) &= n + f(n-1).
\end{align}$

En dé-récursivant : $f$ est la somme des entiers de 1 à $n$. On a une **complexité quadratique** soit $O(n^2)$.

Le cas où le **tableau est trié** dans un sens ou dans l'autre est donc **le pire des cas** pour le tri rapide. Quel est le meilleur des cas ? Celui où le tableau est divisé en deux sous-parties de tailles équivalentes. Prenons par exemple la fonction récursive suivante

$\begin{align}
f( n \leq 1) &= 1 \\
f(n) &= n + 2 f \left(\frac{n}{2} \right) \\
     &= n + 2 \left( \frac{n}{2} + 2 \left( \frac{n}{ 4} + \dots \right) \right)
\end{align}$

En développant, les fractions se simplifient et on obtient $f(n) = n(\log(n) + 1)$, c'est-à-dire une complexité en $O(n \log(n))$. Donc la complexité du tri rapide est améliorée si on divise le tableau en parts égales.

On peut utiliser cette propriété pour le calcul de la complexité en moyenne. Sans entrer dans les détails du calcul, si le tableau est une permutation aléatoire d'un certain ensemble, le choix du pivot le divisera en sous-parties qui seront équilibrées en moyenne et elles-mêmes des permutations aléatoires. Par une étude probabiliste et des calculs un peu plus poussés, on obtient que **la complexité en moyenne est en $O(n \log(n))$**.

**Quelle est la complexité en espace de l'algorithme ?**

**Le tri est-il effectué sur place ?**

**Le tri est-il stable ?**

**Conclusion sur le tri rapide**

Tri considéré comme très efficace en pratique et donc souvent utilisé. Cependant, **attention** car la complexité du pire des cas reste quadratique !

**Avantages**

* Tri en place
* Complexité $O(n \log(n))$ en moyenne
* Bonne efficacité en pratique

**Inconvénients**

* Complexité quadratique dans le pire des cas (et sur le tableau trié)
* Peu efficace sur des données "presque triées"

### Le tri fusion

Là encore, on va utiliser le principe du *diviser pour régner*. L'idée est la suivante : si mon tableau est de taille 0 ou 1 alors, il est trié. S'il est plus grand, je peux le diviser en deux moitiés égales et les trier séparément. Mon problème est donc maintenant le suivant : **comment fusionner deux tableaux trier en un seul ?**

Implantez la fonction suivante. On veut une complexité **linéaire** chacun des tableaux `t1` et `t2` ne doit être parcouru d'une seule fois.

```python
def fusion(t1, t2):
    """
    Input : t1 et t2 deux tableaux déjà trié
    Output : un nouveau tableau t contenant les valeurs triées de t1 et t2
    """
    # begin solution
    i = 0
    j = 0
    t = []
    while i < len(t1) and j < len(t2):
        if t1[i] <= t2[j]:
            t.append(t1[i])
            i+=1
        else:
            t.append(t2[j])
            j+=1
    if i < len(t1):
        t.extend(t1[i:])
    if j < len(t2):
        t.extend(t2[j:])
    return t
    # end solution

```

```python
t1 = [1,1,5,6,8,9]
t2 = [2,3,5,8]
fusion(t1,t2)
```

    [1, 1, 2, 3, 5, 5, 6, 8, 8, 9]

```python
t1 = [2, 2, 3, 4, 4, 6, 6, 7, 8, 8, 9, 10, 11, 12, 12, 12, 15, 15, 15, 18]
t2 = [6, 6, 8, 9, 9, 9, 9, 10, 10, 10, 11, 12, 12, 13, 14, 18, 18, 18, 19, 20]
assert fusion(t1,t2) == sorted(t1 + t2)
```

On peut maintenant écrire de façon très simple le tri fusion

```python
def triFusion(t):
    if len(t) <= 1: # cas d'arret
        return t
    m = len(t)//2 # la moitié du tableau
    return fusion(triFusion(t[:m]), triFusion(t[m:]))
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
t = triFusion(t)
t
```

    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]

```python
testTri(triFusion)
```

    L'algoritme a trié tous les tableaux.
    Le tri ne se fait pas sur place

**Pourquoi ça fonctionne ?**

```python
def fusionAffiche(t1, t2):
    """
    Input : t1 et t2 deux tableaux déjà trié
    Output : un nouveau tableau t contenant les valeurs triées de t1 et t2
    """
    print("Fusion de ", t1, t2)
    i = 0
    j = 0
    t = []
    while i < len(t1) and j < len(t2):
        if t1[i] <= t2[j]:
            t.append(t1[i])
            i+=1
        else:
            t.append(t2[j])
            j+=1
    if i < len(t1):
        t.extend(t1[i:])
    if j < len(t2):
        t.extend(t2[j:])
    print("Résultat fusion :", t)
    return t

def triFusionAffiche(t):
    print(t)
    if len(t) <= 1: # cas d'arret
        return t
    m = len(t)//2 # la moitié du tableau
    return fusionAffiche(triFusionAffiche(t[:m]), triFusionAffiche(t[m:]))

```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
t = triFusionAffiche(t)
t
```

    [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
    [9, 3, 6, 1, 2]
    [9, 3]
    [9]
    [3]
    Fusion de  [9] [3]
    Résultat fusion : [3, 9]
    [6, 1, 2]
    [6]
    [1, 2]
    [1]
    [2]
    Fusion de  [1] [2]
    Résultat fusion : [1, 2]
    Fusion de  [6] [1, 2]
    Résultat fusion : [1, 2, 6]
    Fusion de  [3, 9] [1, 2, 6]
    Résultat fusion : [1, 2, 3, 6, 9]
    [4, 3, 5, 6, 4]
    [4, 3]
    [4]
    [3]
    Fusion de  [4] [3]
    Résultat fusion : [3, 4]
    [5, 6, 4]
    [5]
    [6, 4]
    [6]
    [4]
    Fusion de  [6] [4]
    Résultat fusion : [4, 6]
    Fusion de  [5] [4, 6]
    Résultat fusion : [4, 5, 6]
    Fusion de  [3, 4] [4, 5, 6]
    Résultat fusion : [3, 4, 4, 5, 6]
    Fusion de  [1, 2, 3, 6, 9] [3, 4, 4, 5, 6]
    Résultat fusion : [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]

    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]

La preuve se fait par récurrence :
* le cas de base (taille 0 et taille 1) est trivial
* Si le tri fonctionne sur les tableaux de taille $<n$ alors, les deux sous tableaux seront triés. Il reste à prouver que la fonction `fusion` répond bien au problème, en particulier que le tableau renvoyé est bien trié. On peut le voir avec un invariant de boucle, à chaque itération de la boucle, les valeurs déjà ajoutées au tableaux sont toutes inférieures ou égales à l'ensemble des valeurs restantes dans t1 t2.

```python
def fusionCheck(t1, t2):
    """
    Input : t1 et t2 deux tableaux déjà trié
    Output : un nouveau tableau t contenant les valeurs triées de t1 et t2
    """
    i = 0
    j = 0
    t = []
    while i < len(t1) and j < len(t2):
        if t1[i] <= t2[j]:
            t.append(t1[i])
            i+=1
        else:
            t.append(t2[j])
            j+=1
        assert all(v <= w for v in t for w in t1[i:]) and all(v <= w for v in t for w in t2[j:])
    if i < len(t1):
        t.extend(t1[i:])
    if j < len(t2):
        t.extend(t2[j:])
    return t

def triFusionCheck(t):
    if len(t) <= 1: # cas d'arret
        return t
    m = len(t)//2 # la moitié du tableau
    return fusionCheck(triFusionCheck(t[:m]), triFusionCheck(t[m:]))
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
t = triFusionCheck(t)
t
```

    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]

**Quelle est la complexité de l'algorithme ?**

On découpe systématiquement le tableau en deux parts égales (contrairement au tri rapide, cela ne dépend plus des valeurs). A chaque étape, il faut effectuer une copie du tableau (pour créer les sous-tableaux) et la fusion, cela se fait en $O(n)$. On obtient donc une formule récursive pour la complexité du type de

$\begin{align}
f( n \leq 1) &= 1 \\
f(n) &= n + 2 f \left(\frac{n}{2} \right) \\
\end{align}$

et donc une complexité en $O(n \log(n))$ **dans tous les cas.**

**Quelle est la complexité en espace de l'algorithme ?**

**Le tri est-il effectué sur place ?**

**Le tri est-il stable ?**

**Conclusion sur le tri Fusion**

C'est un tri avec une bonne efficacité et il est utilisé en pratique. Par exemple, l'algorithme de tri de python le *Timsort* est un dérivé du tri fusion (mélangé avec du tri par insertion)

**Avantages**

* Complexité $O(n \log(n))$ dans le pire des cas

**Inonvénients**

* Complecité $O(n \log(n))$ dans le meilleure des cas (non linéaire)
* Complexité mémoire $O(n)$ : nécessessite des allocations
* Pas de tri sur place

## Exercice : le tri pas rapide

Voici un algorithme

```python
def triPasRapideRec(t, i, j):
    if j-i == 1 and t[j] < t[i]:
        t[i], t[j] = t[j], t[i]
    if j-i <= 1:
        return
    k = (j+1-i)//3
    triPasRapideRec(t, i, j - k)
    triPasRapideRec(t, i+k, j)
    triPasRapideRec(t, i, j-k)

def triPasRapide(t):
    triPasRapideRec(t, 0, len(t) - 1)
```

```python
t = [9, 3, 6, 1, 2, 4, 3, 5, 6, 4]
triPasRapide(t)
t
```

    [1, 2, 3, 3, 4, 4, 5, 6, 6, 9]

```python
testTri(triPasRapide)
```

    L'algoritme a trié tous les tableaux.
    Le tri se fait sur place

* Ajouter des affichages pour comprendre le fonctionnement de l'algorithme sur l'exemple `[9,3,6,1]`
* On veut prouver par récurence que l'algorithme fonctionne.
  - Quel est le cas de base ?
  - Soit $u < v$ dans le tableau, si $u$ apparait avant $v$ dans le tableau, peuvent-ils être échangés ?
  - Supposons que $v$ apparait avant $u$ et qu'ils appartiennent tous les deux aux deux premiers tiers, que se passe-t-il ?
  - même question mais ils appartiennent aux deux derniers tiers
  - Et si $v$ est dans le premier tiers et $u$ dans le dernier tiers ?
* Pour un tableau de taille $n$, exprimer sous forme d'une fonction récursive $f(n)$ le nombre d'appels effectués
* Un théorème d'analyse d'algorithme, le *master theorem* nous dit que si la complexité d'un algorithme récursif s'exprime sous la forme

$\begin{equation}
f(n) = a f\left( \frac{n}{b} \right)
\end{equation}
$

avec $a \geq 1$ et $b > 1$, alors la complexité de l'algorithme est de $n^{\frac{\log(a)}{\log(b)}}$. En comparant avec les algorithmes connus, l'algorithme `TriPasRapide` est-il efficace ?

```python

```


# Project "Training a cat to catch a Mouse using Q-learning and Qiskit"

Participants:

## 1. Intoduction
### 1.1 Idea
The idea of this project is to train a novice(single agent) to catch a mouse in a grid environment 3*3.

We placed the cat in the lower right corner and the mouse in the upper left corner. Our task is to find the best way for the cat to catch a motionless (and unsuspecting danger) mouse. We want to test whether it is possible to solve such a problem using quantum computing.

    MOUSE
    EMPTY
    EMPTY

    EMPTY
    EMPTY
    EMPTY

    EMPTY
    EMPTY
    CAT

### 1.2 Q-Learning
Q-Learning is a reinforcement learning algorithm to learn the value of an action in a particular state.
When Q-learning is performed we create what’s called a **q-table** or matrix that follows the shape of *[state, action]* and we initialize our values to zero. We then update and store our q-values after an episode. This q-table becomes a reference table for our agent to select the best action based on the q-value.

An agent interacts with the environment in 1 of 2 ways. The first is to use the q-table as a reference and view all possible actions for a given state. The agent then selects the action based on the max value of those actions. This is known as **exploiting** since we use the information we have available to us to make a decision.

The second way to take action is to act randomly. This is called **exploring**. Instead of selecting actions based on the max future reward we select an action at random. Acting randomly is important because it allows the agent to explore and discover new states that otherwise may not be selected during the exploitation process. You can balance exploration/exploitation using epsilon (ε) and setting the value of how often you want to explore vs exploit. Here’s some rough code that will depend on how the state and action space are setup.

Q-table is updated in every episode of training via **Bellman Equation**:
$$
\begin{align}
\underbrace{\text{New}Q(s,a)}_{\scriptstyle\text{New Q-Value}}=Q(s,a)+\mkern-34mu\underset{\text{New Q-Value}}{\underset{\Bigl|}{\alpha}}\mkern-30mu \underbrace{R(s,a)}_{\scriptstyle\text{Reward}}+ \mkern-30mu\underset{\text{Discount rate}}{\underset{\Biggl |}{\gamma}}\mkern-75mu * \overbrace{\max Q'(s',a')}^{\scriptstyle\substack{\text{Maximum predicted reward, given} \\ \text{new state and all possible actions}}} * \mkern-45mu-Q(s,a)
\end{align}
$$

### 1.3 Variational Quantum Eigensolver (VQE)

VQE is an application of the variational method of quantum mechanics.
$$
\begin{align}
    \lambda_{min} \le \langle H \rangle_{\psi} = \langle \psi | H | \psi \rangle = \sum_{i = 1}^{N} \lambda_i | \langle \psi_i | \psi\rangle |^2
\end{align}
$$
The above equation is known as the **variational method**.

When the Hamiltonian of a system is described by the Hermitian matrix $H$ the ground state energy of that system, $E_{gs}$, is the smallest eigenvalue associated with $H$. By arbitrarily selecting a wave function $|\psi \rangle$ (called an *ansatz*) as an initial guess approximating $|\psi_{min}\rangle$, calculating its expectation value, $\langle H \rangle_{\psi}$, and iteratively updating the wave function, arbitrarily tight bounds on the ground state energy of a Hamiltonian may be obtained.

A systematic approach to varying the ansatz is required to implement the variational method on a quantum computer. VQE does so through the use of a parameterized circuit with a fixed form. Such a circuit is often called a *variational form*, and its action may be represented by the linear transformation $U(\theta)$. A variational form is applied to a starting state $|\psi\rangle$ (such as the vacuum state $|0\rangle$, or the Hartree Fock state) and generates an output state $U(\theta)|\psi\rangle\equiv |\psi(\theta)\rangle$. Iterative optimization over $|\psi(\theta)\rangle$ aims to yield an expectation value $\langle \psi(\theta)|H|\psi(\theta)\rangle \approx E_{gs} \equiv \lambda_{min}$. Ideally, $|\psi(\theta)\rangle$ will be close to $|\psi_{min}\rangle$ (where 'closeness' is characterized by either state fidelity, or Manhattan distance) although in practice, useful bounds on $E_{gs}$ can be obtained even if this is not the case.

## 2. Solution scheme via Q-learning & Quantum Calculations

The scheme of our solution a as follows: Each point of our medium of size 3*3 corresponds to a quantum circuit with configurable parameters. Each such scheme consists of several U3-gates, the type of which is shown below.

$$
\begin{align}
    U3(\theta, \phi, \lambda) = \begin{pmatrix}\cos(\frac{\theta}{2}) & -e^{i\lambda}\sin(\frac{\theta}{2}) \\ e^{i\phi}\sin(\frac{\theta}{2}) & e^{i\lambda + i\phi}\cos(\frac{\theta}{2}) \end{pmatrix}
\end{align}
$$

One of the options for implementing such a quantum circuit looks like this:

```python
from copy import deepcopy
import numpy as np
import random
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
from qiskit import Aer, transpile, assemble
from qiskit.providers import backend
from qiskit.aqua.components.optimizers import COBYLA
import matplotlib.pyplot as plt
import itertools
```

### 2.3 Basic Classes

**State**

This class is needed for describing the position of the cat and operating with it.

**GridWorld**

We use this for operating with out grid where the cat and the mouse are located.

**QNet**

Here we can perfrom all the quantum computation needed for updating the q-tables in each point.

**Cat**

This class describes the cat's actions and update q-circuits accordingly.

**PetSchool**

In this class we can train our model and show the results.

```python
# TYPES:
CAT = "c"
MOUSE = "m"
EMPTY = "emp"

# ACTIONS:
UP = "00"
DOWN = "01"
LEFT = "10"
RIGHT = "11"
ACTIONS = [UP, DOWN, LEFT, RIGHT]

# random seed
random.seed(10)
np.random.seed(10)
```

```python
# state of cat
class State:
    def __init__(self, catP):
        #self.row = catP[0]
        #self.column = catP[1]
        self.catP = catP

    def __eq__(self, other):
        return isinstance(other, State) and self.catP == other.catP

    def __hash__(self):
        return hash(str(self.catP))

    def __str__(self):
        return f"State(cat_pos={self.catP})"
```

```python
# GridWorld
# e.g.
#  MOUSE | EMPTY | EMPTY
#  EMPTY | EMPTY | EMPTY
#  EMPTY | EMPTY | CAT
class GridWorld:
    def __init__(self, s, catP, mouseP):
        self.numRows = s[0]
        self.numColumns = s[1]
        self.catP = catP
        self.mouseP = mouseP
        # self.dogP = dogP
        assert(not self.compaireList(self.catP, self.mouseP))

    def getItem(self, p):
        if p[0]>=self.numRows or p[0]<0:
            return None
        if p[1]>=self.numColumns or p[1]<0:
            return None
        if self.compaireList(p, catP):
            return CAT
        elif self.compaireList(p, mouseP):
            return MOUSE
        # elif self.compaireList(p, DOG):
        #     return DOG
        else:
            return EMPTY

    def compaireList(self, l1,l2):
        for i, j in zip(l1, l2):
            if i!=j:
                return False
        return True

    def getNumRows(self):
        return self.numRows

    def getNumColumns(self):
        return self.numColumns

    def getMouse(self):
        return self.mouse

    def getCatP(self):
        return self.catP

    def setCatP(self, p):
        self.catP = p

    def setMouseP(self, p):
        self.mouseP = p

    def initCatState(self, rd = False):
        # init cat position
        if not rd:
            catP = [self.getNumRows() - 1, self.getNumColumns() - 1]
        else:
            catP = [random.randint(0, self.getNumRows()), random.randint(0, self.getNumColumns())]
            while self.getItem(catP) != EMPTY and self.getItem(catP) != CAT:
                catP = [random.randint(0, self.getNumRows()), random.randint(0, self.getNumColumns())]
        self.setCatP(catP)
        return State(catP)

    def show(self):
        output = ""
        for i in range(self.numRows):
            for j in range(self.numColumns):
                if self.compaireList([i,j], self.catP):
                    output += CAT + " "
                if self.compaireList([i,j], self.mouseP):
                    output += MOUSE + " "
                if not self.compaireList([i,j], self.catP) and not self.compaireList([i,j], self.mouseP):
                    output += EMPTY + " "
            output += "\n"
        print(output)
```

```python
# QNet
class QNet:

    def __init__(self, qTable, gridWorld:GridWorld, alpha=0.1, gamma=1.0, eps=0.2, actions=[UP, DOWN, LEFT, RIGHT], numParams=6):
        self.gw = gridWorld
        self.qt = qTable
        self.eps = eps
        self.backend = Aer.get_backend("qasm_simulator")
        self.NUM_SHOTS = 1000 # number of measurements
        self.optimizer = COBYLA(maxiter=500, tol=0.0001) # off the shelf
        self.gamma = gamma
        self.alpha = alpha
        self.ACTIONS = actions

        # self.rets = {(0,0):([0,..,0],0.0,0), ...}
        self.rets = dict() # resulting parameters after optimization for all points in the grid

        self.state = None

        for i in range(self.gw.getNumRows()):
            for j in range(self.gw.getNumColumns()):
                self.rets[i, j] = (np.random.rand(numParams), 0.0, 0)

    def qcMaker(self, params):
        qr = QuantumRegister(2, name="q")
        cr = ClassicalRegister(2, name="c")
        qc = QuantumCircuit(qr, cr)
        qc.u3(params[0], params[1], params[2], qr[0])
        qc.u3(params[3], params[4], params[5], qr[1])
        # qc.cx(qr[0], qr[1]) # create entangelment
        qc.measure(qr, cr)
        return qc

    def newPosition(self, state, action):
        p = deepcopy(state.catP)
        if action == UP:
            p[0] = max(0, p[0] - 1)
        elif action == DOWN:
            p[0] = min(self.gw.getNumRows() - 1, p[0]+1)
        elif action == LEFT:
            p[1] = max(0, p[1] - 1)
        elif action == RIGHT:
            p[1] = min(self.gw.getNumColumns() - 1, p[1] + 1)
        else:
            raise ValueError(f"Unkown action {action}")
        return p

    def getReward(self, p):
        grid = self.gw.getItem(p)
        if grid == EMPTY:
            reward = -1
        elif grid == MOUSE:
            reward = 1000
        elif grid == CAT:
            reward = -1
        else:
            raise ValueError(f"Unknown grid item {grid}")
        return reward

    def selectAction(self, state, training):
        if random.uniform(0, 1) < self.eps:
            return random.choice(self.ACTIONS)
        else:
            if training:
                self.state = deepcopy(state)
                self.updateCircuit(state)
            return self.ACTIONS[np.argmax(self.qt[self.state.catP[0], self.state.catP[1]])]

    def lossFunction(self, params):
        action = ""
        qc = self.qcMaker(params=params)
        t_qc = transpile(qc, self.backend)
        job = assemble(t_qc, shots=self.NUM_SHOTS)
        rlt = self.backend.run(job).result()
        counts = rlt.get_counts(qc)
        # speedup training, cross the ravine
        if random.uniform(0, 1) < self.eps:
            action = random.choice(self.ACTIONS)
        else:
            action = max(counts, key = counts.get)

        nextPosition = self.newPosition(self.state, action) # handle the
        reward = self.getReward(nextPosition)
        targetQvalue = reward + self.gamma *  np.max(self.qt[nextPosition[0],nextPosition[1]])
        predictedQvalue = self.calculateQvalue(action, nextPosition, reward, targetQvalue, self.state)

        # update q-table
        self.updateQtable(predictedQvalue, action)
        return targetQvalue - self.qt[self.state.catP[0],self.state.catP[1]][int(action,2)]

    def updateQtable(self, predictedQvalue, action):
        if self.qt[(self.state.catP[0],self.state.catP[1])][int(action,2)] < predictedQvalue:
            self.qt[self.state.catP[0],self.state.catP[1]][int(action,2)] = predictedQvalue

    def calculateQvalue(self, action, nextPosition, reward, targetQvalue, state:State):
        targetQvalue = reward + self.gamma *  np.max(self.qt[nextPosition[0],nextPosition[1]])
        return self.qt[state.catP[0], state.catP[1]][int(action,2)] + self.alpha * (targetQvalue - self.qt[state.catP[0],state.catP[1]][int(action,2)]) # update q-table

    def updateCircuit(self, state:State):
        self.rets[state.catP[0], state.catP[1]] = self.optimizer.optimize(num_vars=6, objective_function=self.lossFunction, initial_point=self.rets[state.catP[0], state.catP[1]][0])

    def setAlpha(self, alpha):
        self.alpha = alpha

    # @Daniel-Molpe
    def drawVectors(self, hasdiagonals):
        # Draw vectors representing the cat's desired direction for each place in the grid based on the Qtable
        x = np.linspace(0, self.gw.getNumColumns()-1, self.gw.getNumColumns())
        y = np.linspace(0, self.gw.getNumRows()-1, self.gw.getNumRows())
        vecx=np.zeros([len(x),len(y)])
        vecy=np.zeros([len(x),len(y)])
        for i in range(len(x)):
            for j in range(len(y)):
                vecx[i,j] = self.qt[(x[i], y[j])][3]-self.qt[(x[i], y[j])][2]
                vecy[i,j] = self.qt[(x[i], y[j])][0]-self.qt[(x[i], y[j])][1]
                norm = np.sqrt(vecx[i,j]**2 + vecy[i,j]**2)
                vecx[i,j]=-vecx[i,j]/norm
                vecy[i,j]=-vecy[i,j]/norm
        pts = itertools.product(x, y)
        plt.scatter(*zip(*pts), marker='o', s=30, color='red')
        X, Y = np.meshgrid(x, y)
        QP = plt.quiver(X, Y, vecx, vecy)
        plt.grid()
        plt.show()
```

```python
# Agent: cat
class Cat:
    def __init__(self, qNet: QNet, training=True, eps = 0.2, actions = [UP, DOWN, LEFT, RIGHT]):
        self.eps = eps
        self.training = training
        self.qNet = qNet
        self.ACTIONS = actions
        self.state = None

    def newPosition(self, state, action):
            p = deepcopy(state.catP)
            if action == UP:
                p[0] = max(0, p[0] - 1)
            elif action == DOWN:
                p[0] = min(self.qNet.gw.getNumRows() - 1, p[0] + 1)
            elif action == LEFT:
                p[1] = max(0, p[1] - 1)
            elif action == RIGHT:
                p[1] = min(self.qNet.gw.getNumColumns() - 1, p[1] + 1)
            else:
                raise ValueError(f"Unkown action {self.ACTIONS[action]}")
            return p

    def getReward(self, p):
        grid = self.qNet.gw.getItem(p)
        if grid == MOUSE:
            reward = 1000
            end = True
        elif grid == EMPTY:
            reward = -1
            end = False
        elif grid == CAT:
            reward = -1
            end = False
        else:
            raise ValueError(f"Unknown grid item {grid}")
        return reward, end

    def act(self, state, action):
        p = self.newPosition(state, action)
        reward, end = self.getReward(p)
        return p, reward, end

    def updateQtable(self, action, p, reward, state):
        pqv = self.qNet.calculateQvalue(action, p, reward, state)
        self.qNet.updateQtable(pqv, action)

    def setTraining(self, training):
        self.Training = training
```

```python
# The pet school
class PetSchool:
    def __init__(self, cat:Cat, numEpisodes, maxEpisodeSteps, training=True, minAlpha = 0.02, eps = 0.2):
        self.cat = cat
        self.training = training
        self.NUM_EPISODES = numEpisodes
        self.MAX_EPISODE_STEPS = maxEpisodeSteps
        self.alphas = np.linspace(1.0, minAlpha, self.NUM_EPISODES)
        self.eps = eps

    def train(self):
        a=[]
        b=[]
        counter = 0
        rd = True
        for e in range(self.NUM_EPISODES): #  episode: a rund for agent
            print("episode: ", e)
            if e > int(self.NUM_EPISODES/2):
                rd = False
            state = self.cat.qNet.gw.initCatState(rd=rd) # default is rd = False
            self.cat.qNet.setAlpha(self.alphas[e])
            total_reward  = 0
            step = 0
            end = False
            for _ in range(self.MAX_EPISODE_STEPS): # step: a time step for agent
                action = self.cat.qNet.selectAction(deepcopy(state), self.training)
                p, reward, end = self.cat.act(state, action)
                self.catMoveTo(p)
                # self.cat.updateQtable(action, p, reward, state) # speedup learning
                total_reward += reward
                step += 1
                counter += 1
                if end:
                    print("catch the mouse!!!")
                    print("total reward: ", total_reward, "steps: ", step)
                    break
            a.append(step)
            b.append(e)
            print("counter: ", counter)
        plt.plot(b,a)

    def catMoveTo(self, p):
        self.cat.qNet.gw.setCatP(p)

    def show(self):
        self.cat.qNet.gw.show()
        print("qTable: ", self.cat.qNet.qt)
        print("\nparams: ", self.cat.qNet.rets)
        self.cat.qNet.drawVectors(False)

    def result(self):
        return self.cat.qNet.qt, self.cat.qNet.rets

    def initqTable(self, actions, size):
        d = {}
        for i in range(size[0]):
            for j in range(size[1]):
                d[i,j] = np.zeros(len(actions))
        return d

    # @Daniel-Molpe
    def mouseMove(p,oldPos): # goal (mouse) moves randomly with prob p every time the cat moves
        side = min(self.cat.qNet.gw.getNumColumns(), self.cat.qNet.gw.getNumRows()) # Number of cells per side of the grid
        if np.random.random() < p:
            n = np.random.random()
            if n < 0.25:
                newPos = (max(0, oldPos[0]-1),oldPos[1])
            elif n < 0.5:
                newPos = (min(side - 1, oldPos[0]+1),oldPos[1])
            elif n < 0.75:
                newPos = (oldPos[0],max(0, oldPos[1]-1))
            else:
                newPos = (oldPos[0],min(side - 1, oldPos[1]+1))
        else:
            newPos = oldPos
        return newPos
```

```python
# Super Pparameter
gridSize = [3, 3]
catP = [gridSize[0]-1, gridSize[0]-1]
mouseP = [0, 0]
EPS = 50 # 500 for more training time
MAX_EPS_STEP = 50 # 1000 for more training time
sizeOfParams = 6
gamma = 0.98
```

```python
def initqTable(size, actions=[UP, DOWN, LEFT, RIGHT]):
    d = {}
    for i in range(size[0]):
        for j in range(size[1]):
            d[i,j] = np.zeros(len(actions))
    return d

# initGridWorld
gridWorld = GridWorld(gridSize, catP=catP, mouseP=mouseP)
# init q Table
qt = initqTable(gridSize)
# init q Circuit
qNet = QNet(qt, gridWorld, gamma=gamma)
# init cat
cat = Cat(qNet=qNet)
# init pet school
petSchool = PetSchool(cat, EPS, MAX_EPS_STEP)
# start training
petSchool.train()
```

```python
# show what have been learned
petSchool.show()
```

```python
# @Vasily Bokov
backend = Aer.get_backend("qasm_simulator")
NUM_SHOTS = 1000 # number of measurements
optimizer = COBYLA(maxiter=500, tol=0.0001) # off the shelf

def qcMaker(params):
    qr = QuantumRegister(2, name="q")
    cr = ClassicalRegister(2, name="c")
    qc = QuantumCircuit(qr, cr)
    qc.u3(params[0], params[1], params[2], qr[0])
    qc.u3(params[3], params[4], params[5], qr[1])
    qc.measure(qr, cr)
    return qc

def calcActions(d):

    actions = dict()
    for key in d.keys():
        qc = qcMaker(d[key][0])
        t_qc = transpile(qc, backend)
        job = assemble(t_qc, shots=NUM_SHOTS)
        rlt = backend.run(job).result()
        counts = rlt.get_counts(qc)
        action = max(counts, key = counts.get)
        actions[key] = action

    return actions

# qt is a dict with all q-values
def calcPerformance(d, actions: dict, qt: dict):
    diff = 0
    # the difference between two action is defined as the steps difference between two action
    # e.g. left needs two steps turn to right.
    def difference(a, b):
        if a == UP:
            if b == DOWN:
                return (2*2)/5
            else:
                return 1/5
        elif a == DOWN:
            if b == UP:
                return (2*2)/5
            else:
                return 1/5
        elif a == LEFT:
            if b == RIGHT:
                return (2*2)/5
            else:
                return 1/5
        elif a == RIGHT:
            if b == LEFT:
                return (2*2)/5
            else:
                return 1/5

    for key in d.keys():
        action = ACTIONS[int(actions[key],2)] # "00","01" ..
        targetAction = ACTIONS[np.argmax(qt[key])] # "00","01" ..
        if action != targetAction:
            diff += difference(action, targetAction)
    return diff/len(d.keys())

qt, rets = petSchool.result()

print("MSE of trained circuit: ", calcPerformance(rets, calcActions(rets), qt))
```

    MSE of trained circuit:  0.19999999999999998