Connexion élèves

Choisir le(s) module(s) à installer :

Programmation dynamique : applications

Optimisation de la découpe de planches

Présentation du problème

Nous allons considérer qu'une scierie reçoit des planches de longueur l entière.
La scierie découpe ces planches en morceaux de longueur également entière et les revend selon selon une grille tarifaire donnée :
longueur 0 1 2 3 4 5 6 7 8 9 10
prix 0 1 5 8 9 10 17 17 20 24 30
planche en bois
L'objectif est donc de découper chaque planche de façon optimale pour en tirer un prix maximum.

RésolutionS du problème

Afin de bien appréhender le problème répondez aux questions ci dessous :
  1. Présenter Toutes les découpes possibles pour une planche de longueur 4 sous forme d'un arbre.
  2. Déterminer le revenu de chaque découpe possible
  3. En déduire la découpe optimale pour une planche de longueur 4

SOLUTION

Solution récursive

Nous allons tenter de coder une fonction coupe_recurs ayant pour argument la longueur n de la planche et le tableau p des prix et qui renverra la revenu optimal que l'on peut tirer de la planche.
  1. Si l'on décide de faire une première découpe de longueur i dans la planche, quelle sera la longueur de la planche restante
  2. Si l'on décide de faire une première découpe de longueur i dans la planche, quel sera le revenu optimal en fonction de p[i] et de coupe_recurs(p, n-i) ?
  3. Si l'on appelle q le revenu optimal d'une précédente hypothèse. Comment choisir entre q et la réponse à la question 2 de façon optimale ?
  4. Déduire des questions précédentes un algorithme récursif résolvant le problème, et coder cet algorithme.
def coupe_recurs(n: int, p: list)->int: pass p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]

SOLUTION

Solutions dynamiques

Comme d'habitude, la solution récursive est simple à coder mais n'est pas très efficace....
Nous pouvons constater que nous sommes en présence d'un problème d'optimisation.
Nous allons maintenant chercher les sous problèmes et leurs chevauchements.
  1. Dans l'arbre précédent, entourer les deux plus grands sous arbres identiques
  2. Quelle est la taille de cet arbre ?
  3. De façon générale, si la planche a pour longueur n, quelle sera la taille de cet arbre ?

SOLUTION

Du haut vers le bas ou Top-Bottom

Nous allons maintenant tenter de construire un algorithme de résolution de ce problème grâce à la programmation dynamique, en se basant sur le code récursif précédent

Adapter le code récursif en vous inspirant de la fonction fibonacciProgDyna vue précédemment :
  • La case r[i] du tableau rcontiendra les revenus maximum pour une planche de longueur i
  • Le tableau r devra être transmis lors des appels récursifs
  • Il sera nécessaire de construire deux fonctions, dont une ne fera que construire le tableau r rempli initialement de 0, et appeler ensuite la fonction récursive elle même.
  • def planche_memo()->list: pass def coupe_dyna_TB(n: int, p: list, r: list)->int: pass p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]

    SOLUTION

Du bas vers le haut (pour une fois) ou Bottom-Up
La réflexion issue de la programmation dynamique consiste, comme vous avez pu le voir jusque là, à supprimer des calculs redondants.
Nous avons vu qu'une solution consiste à mémoïser (stocker dans une structure de données adaptée) les calculs réalisés dans une fonction récursive.
Mais la programmation dynamique ne s'appuie pas nécessairement sur la programmation récursive !
On peut aussi tout simplement raisonner plus classiquement : de façon itérative.
Dans le cas du problème de la planche, on peut :
  • L'approche Bottom Up (ou du bas vers le haut, ou approche ascendante) est typiquement itérative.
  • L'approche Top Down (ou du haut vers le bas, ou approche descendante) est typiquement récursive.
Dans les deux cas on évite des calculs redondants en stockant chaque résultat intermédiaire.
Vous devez maintenant coder la fonction coue_dyna_BU(n, p) permettant de résoudre le problème de façon dynamique en "Bottom-Up" (d'où le BU).
Pour cela vous pouvez vous aider de l'ébauche algorithmique suivante :

n est la longueur de la planche
p est le tableau contenant les prix de chaque planche (p[i] est le prix de la planche de longueur i)
r est un tableau de longueur n+1 contenant initialement des 0

boucle i pour calculer le revenu optimal d'une planche de longueur i (i de 1 à n)
	initialiser q
	boucle j pour tester toutes les découpes pour la planche de longueur i (j de 1 à i : on réalise une découpe de longueur j<i)
		q reçoit le maximum entre : une découpe précédente et ce que rapporte la découpe de longueur j
	stocker le revenu d'une découpe i dans le tableau r
renvoyer le résultat
			
def coupe_dyna_BU(n: int, p: list)->int: pass p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]

SOLUTION

Le rendu de monnaie

Vous avez déjà vu ce problème en première lors de l'étude des algorithmes gloutons.

Présentation du problème

Le rendu de monnaie est un problème du quotidien. la question est la suivante :
vous devez rendre 4,24€ avec un système de pièces et de billets donnés en minimisant le nombre de pièce ou billets rendus.
Rendu monnaie
Vous avez déjà résolu ce problème de nombreuses fois, en suivant un algorithme "instinctif".
nous allons essayer de le généraliser la résolution de ce problème à n'importe quelle somme (bien-sur) et à n'importe quel système de pièces et de billets.
le problème du rendu de monnaie c'est :
  • Rendre une somme donnée
  • A partir d'un système de monnaie donné monnaie=[pièce1, pièce2, ..., billet1, billet2, ...]
Le problème consiste aussi bien sur à optimiser le rendu de monnaie en utilisant le moins de pièces ou de billets.

Rappels : la résolution gloutonne

Lorsque que vous avez résolu ce problème en jouant au marchand dans votre enfance ou l'année dernière, vous utilisez classiquement l'algorithme glouton :
Un algorithme glouton est un algorithme qui fait un choix local optimal en espérant que cela conduise à une solution globale optimale.
Concrètement dans le cas du rendu de monnaie, on choisit toujours la pièce ou le billet le plus grand possible puis on soustrait cette valeur à la somme à rendre et on recommence (en espérant rendre ainsi le moins de pièce ou de billets possibles !).
Dans le cas où le jeu de monnaie est le jeu européen actuel (le choix des valeurs des pièces et des billets n’a pas été fait au hasard), la solution gloutonne est optimale.
Mais nous pouvons imaginer un système dans lequel le rendu de monnaie glouton n'est pas optimal :
par exemple si l'on veut rendre 6 dans un système composé de [1,3,4]. Pour fixer les idées nous allons nous poser un problème simple :
Nous allons imaginer que nous disposons d'un système de pièces limitées : 2 cts, 5 cts, 10 cts, 50 cts et 1 euro (100 cts)
  1. Appliquer la méthode gloutonne pour une somme de 1€77cts
  2. Appliquer la méthode gloutonne pour une somme de 11cts
  3. Trouver une solution pour cette somme de 11cts

SOLUTION


Nous voyons donc que la solution gloutonne n'est pas forcément optimale, il peut même arriver qu'elle ne trouve pas de solution.

Exploration exhaustive

Si l'on explore toutes les possibilités de l'exemple précédent ( rendu de 11cts ), on arrive à l'arbre ci-dessous :

arbre rendu de monnaie

Une des solutions serait donc d'explorer de manière exhaustive toutes les branches de cet arbre, puis de sélectionner la ou les branches optimales.
La solution exhaustive est qualifiée de "force brute" car on parcourt toutes les solutions possibles avant de choisir la bonne.

Voici une fonction qui résout de façon récursive le problème du rendu de monnaie. La fonction renvoie le nombre de pièce optimal et non le rendu de monnaie lui même.
Le principe est le suivant : si l'on choisit une pièce de valeur x dans le système monétaire, il faut ensuite rendre la monnaie sur somme-x et choisir le nombre de pièce(s) optimal dans chaque cas.

def rendu_rec(somme:float, systeme:list)->int: if somme == 0: return 0 else: mini = 1000 for i in range(len(systeme)): if systeme[i] <= somme: nb = 1 + rendu_rec(somme-systeme[i], systeme) if nb < mini: mini = nb return mini systeme = [100, 50, 10, 5, 2] print(rendu_rec(11, systeme))
  1. Tester la fonction récursive sur une somme de 11cts avec le système de monnaie [100,50,10,5,2]
  2. Tester la fonction récursive sur les sommes 177cts et 289 cts avec le système de monnaie [100,50,10,5,2]. Quel problème rencontrez vous ?

SOLUTION


Optimisation : la programmation dynamique

Nous allons maintenant tenter d'optimiser cette fonction récursive grâce à la programmation dynamique.

Dans l'arbre des appels récursifs de l'exemple ( voir ci-dessus ) :

  1. Identifier les branches de cet arbre qui n'aboutissent pas à une solution
  2. Identifier les branches de cet arbre qui aboutissent à une solution
  3. Identifier le nombre de pièces optimales dans cette situation

SOLUTION


Si l'on continue à analyser, on peut trouver dans cet arbre, des sous problèmes et des chevauchements :

arbre rendu de monnaie

On se retrouve 2 fois à calculer comment rendre 4cts dans cette situation. on retrouve donc les conditions de la programmation dynamique avec :
  1. Coder une fonction fonction en python rendu_dyna, qui prend en argument une somme à rendre et un tableau de systemede monnaie.
  2. Cette fonction doit créer un tableau memo contenant des 0, et de longueur somme+1
  3. Cette fonction doit ensuite renvoyer un appel à rendu_dyna_memo(somme, systeme, f)

Comme vous l'avez compris nous venons de créer le tableau de mémoïsation f.
Par suite, ce tableau contiendra, à un indice i, le nombre de pièces à rendre pour la somme i.
Il nous reste à coder la fonction rendu_dyna_memo(somme, systeme, f) en s'appuyant sur la fonction récursive pure précédente.

Reprenez le code de votre fonction récursive et modifiez là comme indiqué ci-dessous :

  1. Avant de commencer le remplissage récursif du tableau f, la fonction va vérifier si f[somme] n'existe pas déjà dans le tableau.
  2. Nous gardons ensuite le cas d'arrêt.
  3. Au lieu de stocker l'appel récursif +1 dans la variable nb, il sera stocké dans la case du tableau f[somme]
  4. La recherche de minimum se fait ensuite sur f[somme]
  5. Et la fonction renvoie f[somme]
def rendu_dyna(somme: float, systeme: list)->list: pass def rendu_dyna_memo(somme: float, systeme: list, f: list)->int: pass systeme = [100, 50, 10, 5, 2]
  1. Tester la nouvelle fonction sur une somme de 11cts avec le système de monnaie [100,50,10,5,2]
  2. Tester la nouvelle fonction sur les sommes 177cts et 289 cts avec le système de monnaie [100,50,10,5,2]. Rencontrez vous le même problème que précédemment ?

Si vous avez suivi cette démarche pas à pas, vous voyez maintenant l'amélioration algorithmique qu'il existe entre une fonction récursive "simple" et une fonction récursive avec mémoïsation (programmation dynamique de haut en bas).

Question subsidiaire : pouvez-vous améliorer votre code pour qu'il renvoie aussi la liste des pièces à rendre pour la solution optimale ?

SOLUTION

Promenade dans un tableau de tableaux : projet Euler

Présentation du problème

Ce problème classique de l'informatique est extrait du site https://projecteuler.net.
Le problème consiste à trouver le "chemin de poids le plus faible" dans une matrice, appelé "plus court chemin".
matrice euler

Les règles sont les suivantes : L'objectif est de trouver le chemin le plus court en respectant ces règles. Dans le tableau précédent, ce parcours est indiqué en rouge, et vaut : 131+201+96+342+746+422+121+37+331=2427. Un algorithme glouton donnerait-il la solution optimale ?

Résolution par programmation dynamique

Nous allons tenter de résoudre ce problème grâce à la programmation dynamique.
Nous testerons notre programme sur la matrice présentée ci-dessus puis nous tenterons de l'appliquer à une matrice de 80x80 ! (le fichier de données pèse 31ko! Cela peut sembler petit...mais parcourez les données à l'œil vous comprendrez...)
Vous trouverez ci-dessous un peu de code permettant de transférer les fichiers texte fournis dans une liste de listes ( penser à charger ces fichiers dans l'éditeur ) Vous allez maintenant devoir résoudre le problème donné. Nous sommes bien dans la situation de la programmation dynamique car on peut le décomposer en problèmes plus petits, et que ces problèmes se chevauchent.
De plus il sera possible de conserver des résultats précédents pour trouver la solution optimale.
vous devez résoudre le problème précédent. en vous aidant des indications ci-dessous.
  • Créer un tableau de tableaux de la même taille que la matrice de départ, que vous nommerez phi. Chaque case de ce tableau de tableaux contiendra le plus court chemin pour aller jusqu'à la case (i,j) dans la matrice de départ.
  • Créer une fonction plus_court_chemin prenant en paramètre le tableau phi, la matrice de départ et deux entiers i et j. Cette fonction renverra le plus court chemin dans la matrice de départ jusqu'à la case (i,j).
  • Utiliser la fonction plus_court_chemin pour remplir le tableau de tableaux phi
  • Afficher la dernière case du tableau phi.
Testez votre programme sur la matrice de test puis lancez le calcul sur la matrice complète. Évitez bien tout affichage dans ce cas.
Indication : pour la matrice 80x80 la réponse est 427337.
def plus_court_chemin(matrix: list, phi: list, i: int, j: int)->int: pass with open('matrix.txt','r') as f: matrix = [ligne.split(",") for ligne in f] matrix = [[int(matrix[i][j]) for j in range(len(matrix[0]))] for i in range(len(matrix))]

SOLUTION

Recherche du plus grand carré blanc

Comme nous l'avons constaté, la programmation dynamique peut permettre d'améliorer la résolution de nombreux problèmes. Cette technique est très adaptée en particulier aux problèmes d'optimisation, c'est à dire à la recherche d'un maximum ou d'un minimum au sens large.

Présentation du problème

On considère le problème suivant :
Une image blanche de dimensions nxn contient aléatoirement répartis des pixels noirs.
Nous allons chercher dans ce carré le plus grand carré blanc possible.
image
carré optimum
Ce problème simplifié peut trouver de nombreuses applications :

Identification du sous problème

L'idée de base va donc être de parcourir toute l'image et de chercher le plus grand carré blanc. Mais il faut réfléchir un peu...Et déterminer un sous problème plus simple:
Nous allons donc chercher pour chaque pixel de coordonnées (x,y) la taille du plus grand carré blanc dont le pixel (x,y) est le coin en bas à droite.
image
A priori, il faut donc partir de ce pixel et vérifier si tous les carrés de dimensions (i,i) (dont le coin en haut à gauche a pour coordonnées ((x+i,y+i))sont blancs, dès que l'on trouve un pixel noir, on arrête et on note les dimensions du carré précédent.
Dans ce cas, pour tester un carré de dimensions mxm il faut vérifier m2 pixels.
Cependant on peut encore simplifier le problème grâce à une observation importante :
Pour vérifier si un carré mxm est bien blanc, il suffit de vérifier si :
image

Codage de la recherche du plus grand carré blanc

Fonction récursive

Nous allons tout d'abord tenter de coder une version récursive simple de la fonction qui détermine la taille du plus grand carré blanc en partant d'un pixel de coordonnées (x,y) placé en bas à droite de ce carré : plusGrdCarreBlanc(x,y,img).
  1. Que doit renvoyer la fonction si le pixel de coordonnées (x,y) est noir ?
  2. Que doit renvoyer la fonction si x=0 ou si y=0 ?
  3. Quels sont les coordonnées des pixels "en bas à droite" des trois carrés présentés ci-dessus
  4. En déduire (ou appeler le professeur) la relation de récursivité permettant de trouver plusGrdCarreBlanc(x,y,img) grâce à trois appels récursifs et à une fonction de recherche de minimum.
  5. Coder et tester la fonction plusGrdCarreBlanc(x,y,img)
  6. Parcourir le tableau de tableaux pour chercher le plus grand carré blanc
Pour vous aider dans vos tests je vous propose une image de test de dimensions 10x10 :

test = [[1, 0, 0, 1, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
        [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 1, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 1]]						
			
Ce tableau de tableaux est représenté ci-dessous avec son plus grand carré blanc.
image
et également une fonction générant un tableau de tableaux aléatoire :

from random import randint

def creerIMG(n):
    img=[[0 for i in range (n)] for j in range(n)]
    for i in range(n):
        x=randint(0,n-1)
        y=randint(0,n-1)
        img[x][y]=1
    return img						
			

SOLUTION

Fonction récursive avec mémoïsation : programmation dynamique de haut en bas

Si votre fonction récursive fonctionne correctement sur une image de dimensions 10x10, cela ne sera peut être pas le cas sur une image "réelle de 3000x2000 pixels !
Nous allons donc faire appel à la programmation dynamique et la mémoïsation.
Adapter le code récursif précédent en vos inspirant du travail réalisé sur les fonctions de calcul de la suite de Fibonacci ou sur le découpage d'un planche.
  • Dans ces fonctions, le tableau memo contiendra dans la case [x][y] le plus grand carré blanc en partant du pixel en bas à droite.
  • Le tableau memo
  • devra être transmis lors des appels récursifs
  • Il sera nécessaire de construire deux fonctions, dont une ne fera que construire le tableau de tableaux memo rempli initialement de None, et appeler ensuite la fonction récursive elle même.
  • Une fois la fonction testé sur des cases choisies, parcourir le tableau de tableaux pour chercher le plus grand carré blanc

SOLUTION

Distance de Levenshtein ( d'après Mon Lycée Numérique )

Principe

La distance d'édition (ou distance de Levenstein ou distance d'Ulam) est un algorithme permettant de comparer deux chaînes de caractères ch1 et ch2.

Cette distance est définie comme le nombre d'opérations minimum qui permet de passer de la première chaîne à la seconde en ne considérant que trois seules opérations possibles pour passer d'une chaîne à l'autre :

Par exemple :

Cet algorithme est utilisé de manière intensive dans les situations où il faut évaluer les différences entre un mot ou un texte, et un autre :

Analyse

On suppose ici que toutes les modifications ont un coût ( c'est à dire qu'elles augmentent la distance ) de 1.

L'idée de l'algorithme dans sa version "naïve" est de déterminer les valeurs de distance entre toutes les sous-chaînes de ch1 et ch2.

On compare les sous-chaînes de ch1 et ch2 pour 0 ⩽ i < len(ch1) et 0 ⩽ j < len(ch2).
Pour chaque comparaison, d(i, j) sera alors le minimum entre les valeurs de distance après chaque modification possible pour passer de la sous-chaîne i à la sous-chaîne j.

Analysons ces cas possibles :

  1. Insertion ou ajout d'une lettre : dans ce cas, la sous-chaîne j a une lettre en plus que la sous-chaîne i, et la distance est augmentée de 1 par rapport à la distance calculée à l'appel précédent, c'est à dire la distance entre deux chaînes de longueur :i et j-1 :
    → la relation de récurrence est alors : d(i, j) = 1 + d(i, j-1)
  2. Suppression d'une lettre : dans ce cas, la sous-chaîne j a au contraire une lettre en moins que la sous-chaîne i :
    Établir la relation de récurrence qui lie alors d(i, j) à la valeur de d calculée à l'appel précédent.
  3. Remplacement d'une lettre ou pas de modification : dans ces deux cas, les deux sous-chaînes ont la même taille, mais :
    • la distance augmente de 1 dans le cas du remplacement de la lettre à l'indice i de ch1 par la lettre à l'indice j de ch2 :
    • la distance n'augmente pas si ces deux lettres sont identiques :
    Établir alors la relation de récurrence qui donne d(i, j) dans ces deux situations.
  4. le cas général sera le minimum entre les 3 valeurs calculées; quels sont alors les deux cas de base de la récursion ?

SOLUTION

Implémentation naïve

Compléter la fonction dist_lev_rec récursive ci-dessous qui prend en paramètre deux indices i et j, et qui renvoie la distance de Levenshtein entre deux chaînes ch1 ( paramètre i ) et ch2 ( paramètre j ).

Pour simplifier la signature de la fonction ( il faudrait sinon aussi les passer en paramètres...), les chaînes ch1 et ch2 sont stockées dans deux variables définies dans le programme principal, et donc accessibles en lecture seule depuis la fonction.

Vérifier la correction de la fonction avec les exemples suivants, et d'autres !

  • d('CHAT', 'CHUTE') = 2
  • d('CHIEN', 'CHAT') = 3 ( 2 substitutions + 1 ajout )
  • d('CHIEN', 'NICHE') = 4 ( 2 suppressions + 1 insertion + 1 ajout )
  • d('CHAT', 'ANTICONSTITUTIONNELLEMENT') = 23 ( compte laissé à la charge du lecteur 😇)
def dist_lev_rec(i: int, j: int)->int: if i == ... : return ... if j == ... : return ... if ch1[i] == ch2[j]: d_subst = ... else: d_subst = ... d_ajout = ... d_supp = ... return min(d_subst, d_ajout, d_supp) ch1 = 'chat' ch2 = 'chute' i = len(ch1)-1 j = len(ch2)-1 print(dist_lev_rec(i ,j))

SOLUTION

On montre que la complexité de l'algorithme naïf est O(3(m+n)) ( où m et n sont les tailles des deux chaînes ) ! Pour comparer deux mots, c'est encore envisageable, mais pour comparer deux textes, cela devient inutilisable...

Essayez par exemple ( ou pas.. ) avec les deux phrases suivantes :


ch1 = "les sanglots longs des violons de l'automne blessent mon coeur d'une langueur monotone."
ch2 = "les sangliers lents des violents de l'atome blasent mon corps d'une longueur manhattan."            
            

On doit trouver 21...

Approche top-bottom

L'idée sera donc bien sûr de faire appel à la mémoïsation pour stocker les résultats déjà calculés à un appel récursif précédent, et de les réutiliser au besoin.

Ici, il faudra stocker ces résultats dans un dictionnaire, dont les clés seront les tuples (i, j) et les valeurs, celles des d(i, j) correspondant.
Comment initialiser ce dictionnaire ? Comment savoir si un résultat avait déjà été calculé ?

Écrire une fonction dist_lev_TB qui utilise la mémoïsation pour améliorer la complexité de l'algorithme :

Tester cette nouvelle version, notamment avec l'exemple des deux phrases ci-dessus !

def dist_lev_TB(i: int, j: int)->int: pass ch1 = 'chat' ch2 = 'chute' i = len(ch1)-1 j = len(ch2)-1 print(dist_lev_TB(i ,j))

SOLUTION

Approche bottom-up

Principe

Soit m = len(ch1), et n = len(ch2).

Dans cette approche itérative, on remplit un tableau de tableaux, de taille (m+1)*(n+1), dont les lignes correspondent aux caractères de ch1, et les colonnes à ceux de ch2.
Chaque case (i, j) contient la valeur d(i, j) ( exemple de la distance entre 'CHAT' et 'CHUTE' ) :

La première ligne et la première colonne correspondent chacune à une sous-chaîne vide.

Le remplissage du tableau se fait de la manière suivante :

Pour chaque case (i, j), on calcule le coût minimal des insertions, suppressions et substitutions en "provenance" des cases voisines.

  1. D'après l'étude précédente, et en vous aidant de la relation de récurrence établie précédemment, établir la relation générale qui permet de calculer la distance d(i, j) en fonction des valeurs des cases voisines.
  2. en fin de remplissage du tableau, où se trouve la valeur de la distance minimale entre les deux chaînes ch1 et ch2 ?

SOLUTION

Implémentation

Implémenter, puis tester cette dernière approche.

On fera particulièrement attention à la manière d'adresser un caractère dans les chaînes ch1 et ch2...

def dist_lev_BU(ch1: str, ch2: str)->int: m = len(ch1) n = len(ch2) # Création du tableau pass # Remplissage de la première ligne et de la première colonne du tableau : pass # Parcours du tableau et calcul des valeurs de distance pour chaque case : pass # Renvoi de la distance minimale pass

SOLUTION