## Nombre de chemins

def chemins_memo(n: int, p: int)-> int:
    D = {}

    def chemins(i, j):
        if (i, j) in D:
            return D[(i,j)]

        if i == 0 or j == 0:
            r = 1
        else:
            r = chemins(i-1, j) + chemins(i, j-1) + chemins(i-1, j-1)
        D[(i, j)] = r

        return r

    return chemins(n, p)

def chemins_bottom_up(n: int, p: int)-> int:
    D = {}

    for i in range(n+1):
        D[(i, 0)] = 1
    for j in range(p+1):
        D[(0, j)] = 1

    for i in range(1, n+1):
        for j in range(1, p+1):
            D[(i, j)] = D[(i-1, j)] + D[(i, j-1)] + D[(i-1, j-1)]

    return D[(n, p)]


assert chemins_memo(8, 10) == 1256465
assert chemins_bottom_up(8, 10) == 1256465


## Somme maximale

import numpy as np

def somme_max_memo(T: np.array)-> int:
    D = {}

    def somme_max(i, j):
        if (i,j) in D:
            return D[(i,j)]

        n = len(T)
        if i == len(T) - 1:
            r = T[i, j]
        else:
            r = T[i, j] + max(somme_max(i+1, j), somme_max(i+1, j+1))
        D[(i, j)] = r

        return r

    return somme_max(0, 0)

def sommes_bottom_up(T: np.array)-> dict:
    D = {}

    n = len(T)
    for j in range(len(T)):
        D[(n-1, j)] = T[n-1, j]

    for i in range(n-2, -1, -1):
        for j in range(0, i+1):
            D[(i, j)] = T[i, j] + max(D[(i+1, j)], D[(i+1, j+1)])

    return D



def chemin_optimal(T: np.array)-> list:
    D = sommes_bottom_up(T)

    chemin = [T[0, 0]]
    i = j = 0

    while i < len(T) - 1:
        if D[(i+1, j+1)] >= D[(i+1, j)]:
            j += 1
        i += 1
        chemin.append(T[i, j])

    return chemin


T1 = np.array([
    [3, 0, 0, 0],
    [7, 4, 0, 0],
    [2, 4, 6, 0],
    [8, 5, 9, 3]
])

T2 = np.array([
    [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [8, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [13, 4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [10, 10, 19, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [5, 14, 3, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [6, 19, 14, 14, 16, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [20, 13, 11, 15, 5, 10, 9, 0, 0, 0, 0, 0, 0, 0, 0],
    [12, 14, 5, 6, 7, 10, 6, 1, 0, 0, 0, 0, 0, 0, 0],
    [16, 7, 12, 19, 3, 11, 10, 19, 5, 0, 0, 0, 0, 0, 0],
    [13, 12, 15, 3, 8, 12, 16, 19, 18, 1, 0, 0, 0, 0, 0],
    [13, 6, 9, 9, 15, 6, 18, 16, 16, 8, 11, 0, 0, 0, 0],
    [2, 7, 18, 10, 20, 6, 3, 18, 11, 13, 15, 20, 0, 0, 0],
    [15, 19, 19, 19, 9, 18, 12, 16, 10, 17, 16, 18, 10, 0, 0],
    [20, 14, 3, 17, 9, 15, 12, 14, 20, 13, 17, 3, 18, 6, 0],
    [15, 10, 6, 2, 16, 7, 10, 2, 15, 7, 16, 9, 1, 9, 14],
])

assert somme_max_memo(T1) == 23
assert somme_max_memo(T2) == 199
assert chemin_optimal(T2) == [2, 8, 13, 10, 14, 19, 13, 14, 12, 15, 9, 18, 19, 17, 16]


## Distance d'édition

def dist_naif(a: str, b: str)-> int:
    if len(a) == 0 or len(b) == 0:
        return max(len(a), len(b))
    elif a[-1] != b[-1]:
        return min(dist_naif(a[:-1], b), dist_naif(a, b[:-1]), dist_naif(a[:-1], b[:-1])) + 1
    else:
        return min(dist_naif(a[:-1], b) + 1, dist_naif(a, b[:-1]) + 1, dist_naif(a[:-1], b[:-1]))

def dist_memo(a: str, b: str)-> int:
    D = {}

    def dist(i, j):
        if (i, j) in D:
            return D[(i, j)]

        if i == 0 or j == 0:
            r = max(i, j)
        elif a[i-1] != b[j-1]:
            r = min(dist(i-1, j), dist(i, j-1), dist(i-1, j-1)) + 1
        else:
            r = min(dist(i-1, j) + 1, dist(i, j-1) + 1, dist(i-1, j-1))
        D[(i, j)] = r

        return r

    return dist(len(a), len(b))

def dist_bottom_up(a: str, b: str)-> int:
    D = {}

    for i in range(len(a)+1):
        D[(i, 0)] = i

    for j in range(len(b)+1):
        D[(0, j)] = j

    for i in range(1, len(a) + 1):
        for j in range(1, len(b) + 1):
            if a[i-1] != b[j-1]:
                D[(i, j)] = min(D[(i-1, j)], D[(i, j-1)], D[(i-1, j-1)]) + 1
            else:
                D[(i, j)] = min(D[(i-1, j)] + 1, D[(i, j-1)] + 1, D[(i-1, j-1)])

    return D[(len(a), len(b))]

def distances(a: str, b: str)-> dict:
    D = {}

    for i in range(len(a)+1):
        D[(i, 0)] = i

    for j in range(len(b)+1):
        D[(0, j)] = j

    for i in range(1, len(a) + 1):
        for j in range(1, len(b) + 1):
            if a[i-1] != b[j-1]:
                D[(i, j)] = min(D[(i-1, j)], D[(i, j-1)], D[(i-1, j-1)]) + 1
            else:
                D[(i, j)] = min(D[(i-1, j)] + 1, D[(i, j-1)] + 1, D[(i-1, j-1)])

    return D

def chemin_optimal(a: str, b: str)-> list:
    D = distances(a, b)

    chemin = [a]
    mot = a
    i = len(a)
    j = len(b)


    while mot != b:
        if D[(i, j)] == D[(i-1, j)] + 1:
            mot = mot[:i-1] + mot[i:]
            i -= 1
            chemin.append(mot)
        elif D[(i, j)] == D[(i, j-1)] + 1:
            mot = mot[:i] + b[j-1] + mot[i:]
            j -= 1
            chemin.append(mot)
        elif D[(i, j)] == D[(i-1, j-1)] + 1:
            mot = mot[:i-1] + b[j-1] + mot[i:]
            i -= 1
            j -= 1
            chemin.append(mot)
        else:
            i -= 1
            j -= 1

    return chemin


assert dist_naif("NICHER", "CHIENS") == 5
assert dist_memo("Mémoïsation", "Calcul de bas en haut") == 19
assert dist_bottom_up("Mémoïsation", "Calcul de bas en haut") == 19

##

for i in range(2, 7, 2):
        print(i)