M2

Programmation par contraintes

Exemples d'applications

Les CSPs

Un CSP est défini par un tripler (V, D, C) :

On dit qu'une variable est assignée d'une valeur lorsqu'on lui attribue l'une des valeurs.

On appelle assignation partielle toute assignation où une partie des variables est assignée (pas toutes).

Quand les contraintes portent sur n variables, au maximum, on parle de CSP n-aires. Cas particulier des CSP binaires : ils ont une représentation grapique simple. Les variables sont représentés par des noeuds et les contraintes par des arcs.

Tout problème n-aire peut être ramené à un problème binaire.

Problèmes :

Un CSP qui n'a pas de solution est dit surcontraint, incohérent, inconsistent. On peut se demander quelles sont les causes de cette incohérence (recherche d'explications).

Dans un premier temps on s'intéresse aux problèmes dans lesquels toutes les contraintes doivent être satisfaites (contraintes "dures"). Mais dans la réalité, il est fréquent de traiter les problèmes pour lesquels certaines contraintes (de préférence soft constaints) peuvent être violées. Les contraintes peuvent être hiérarchisées en fonction d'un degré de préférence.

Exemple : les 4 reines

On représente un échiquier en 4x4. On base la réalisation des contraintes sur les colonnes (V1, V2, V3, V4) (implicitement, la contrainte qui fait que les reines ne doivent pas se trouver sur la même colonne est déjà résolue).

    V1  V2  V3  V4
  +---+---+---+---+
1 |   |   |   |   |
  +---+---+---+---+
2 |   |   |   |   |
  +---+---+---+---+
3 |   |   |   |   |
  +---+---+---+---+
4 |   |   |   |   |
  +---+---+---+---+

On peut représenter l'ensemble de ces contraintes comme suit :

Pour un problème avec 4 reines, l'arbe de possibilité a déjà 256 feuilles !

pour i1 de 1 à 4 faire
  v1 <- i1

  pour i2 de 1 à 4 faire
    v2 <- i2

    pour i3 de 1 à 4 faire
      v3 <- i3

      pour i4 de 1 à 4 faire
        v4 <- i4

        vérifier les contraintes

        (i3, i4)  C34
        (i2, i3)  C23
        ...
      fin
    fin
  fin
fin

Exemple : Le problème du zèbre

Enoncé : il y a 5 maisons de couleurs différentes 2 à 2. Elles sont habitées par des hommes de nationalité différentes. Chaque homme aime une marque de cigarettes et une boisson particulière et possède un animal de compagnie. Les couleurs des maisons sont : Rouge, Bleue, Jaune, Verte et Ivoire. Les nationalités sont : norvégienne, ukrainienne, anglais, espagnole et japonnaise. Les marques de cigarettes sont : Old-Gold, Parliament, Kools, Lucky et Chesterfield. Les animaux de compagnie sont : zèbre, chien, cheval, renard et escargots. Les boissons sont : café, thé, eau, lait et jus d'orange.

On sait que :

Traitement des CSPs

La recherche d'une solution ou de toutes les solutions se fait par énumération. On dit souvant par backtracking. On distingue :

Dans la communauté CSP on classe les algorithmes en 2 groupes :

Le backtracking

Le backtracking chronologique

Objectif :

procédure BT
début
  i <- 1
  Di* <- Di

  tant que 1 <= i <= n faire
    soit // x une valeur prise dans Di* telle que l'assignation
         // courante soit cohérente

    OK <- false

    tant que non OK et Di* !=  faire
      soit x  Di* : Di* <- Di* - {x}

      si // l'assignation courante est cohérente
        OK <- true
      fin
    fin

    si non OK // x n'existe pas
      backtracking
      i <- i - 1
    sinon
      i <- i + 1
      Di* <- Di
    fin
  fin

  si i = 0
    renvoyer(UNSAT)
  sinon
    renvoyer(assignation courante)
  fin
fin

Forces et faiblesses

Lorsqu'on fait un backtrack sur la variable Xi-1 on ne se préoccupe pas (à tord) de savoir s'il y a une contrainte entre Xi et Xi-1. Dans le backjumping, on prend en compte cette remarque.

Le backjumping

Quand on ne peut pas assigner de valeur à la variable X sans faire apparaître d'incohérence, le backjump se fait vers la variable la plus "récente" Y "connectée" à X.

Forward checking

Idée (simple et naturelle) : dès qu'une vafiable est instanciée, on propage vers les variables non encore instanciées.

On réduit le domaine des autres variables en fonction de la nouvelle instanciation.

Version 1 : dès qu'une variable est instanciée, on propage vers les prochaines variables non encore instanciées.

Version 2 : dès qu'une variable est instanciée, on propage vers toutes les variables non encore instanciées.

Lorsque le domaine d'une variable est vide, on revient sur le travail effectué auparavant.

Elements de complexité

Nombre de noeuds visités

                 +----+
                 | BT |
                 +----+
                   |
                 +----+
                 | BJ |
                 +----+
                   /\
                  /  \
                 /    \
            +----+    +-----+
            | FC |    | CBJ |
            +----+    +-----+

Le filtrage

Objectif :

Il y a plusieurs niveaux de consistances. Exemple : consistance locale, consistance de chemin. Plus la consistance est forte, plus l'espace de recherche est réduit mais plus il faut des traitements lourds et coûteux.

Problème : peut-on trouver les solutions uniquement par filtrage ?

Node consistency

Il s'agit d'un filtrage trivial. On ne traite que les contrainte d'arité 1 (qui ne portent que sur une seule variable) : on parle également de contrainte sur les domaines.

procédure NC(i : une variable du CSP)
début
  Di <- Di  { x / Ri(x) }
fin

procédure NC(CSP : un CSP)
début
  pour tout i dans CSP
    NC(i)
  fin
fin

Filtrage d'arc

Ces algorithmes sont également qualifiés de propagation par contraintes et on dit qu'ils font du filtra local. Ils sont généralement simples à mettre en oeuvre et fournissent un bon compromis entre réduction de l'espace de recherche et le temps de calcul.

Historiquement, l'idée est née dans le domaine de la vision (reconnaissance de forme).

AC1

AC1 est trivial : on traite de manière cyclique tous les arcs du réseau. Dès qu'il est possible de réduire un domaine grâce à un arc, on réitère le processus.

procédure AC1(G : un réseau)
début
  Q <- { (i, j) / arc(i, j) dans G  i != j }

  répéter
    CHANGE <- faux

    pour chaque (i, j) dans Q faire
      CHANGE <- (REVISE((i, j)  CHANGE)
    fin
  jusqu'à non CHANGE
fin

fonction REVISE((i, j) : un arc du réseau G) : booléen
début
  DELETE <- faux

  pour chaque vi dans Di faire
    si { vj appartient a Dj / R(vp vj)}
    fin
  fin

  retourner DELETE
fin

AC2

AC1 est trivial : on traite de manière cyclique tous les arcs du réseau. Dès qu'il est possible de réduire un domaine grâce à un arc on réitère le processus.

AC3

AC3 : on considère chaque arc (k, m). Si l'on modifie le domaine de la variable k, alors il y a des chances pour que l'on soit amené à modifier le domaine de ses noeuds prédécesseurs.

AC4

Notion de support : le support d'une valeur a pour une variable Vi par rapport à la contrainte Cij est l'ensemble des valeurs b des autres variables Vj telles que (a, b) ∈ Cij(Vi, Vj).

Idée : dès qu'une valeur n'a plus de support, elle peut être supprimée du domaine auquel elle appartient ce qui entraine une révision des supports des valeurs qu'elle supporte.

On construit l'ensemble des paires [(i, j), b] qui représentent les relations Rij(b, ?).

AC6

AC6 se base également sur la notion de support mais au lieu de gérer tous les supports de chaque valeur, ici on se limite à gérer l'existence d'au moins un support pour chaque valeur. Une valeur a de la variable i est viable si pour chaque contrainte Cij la variable j possède une valeur b qui supporte a. Seules les valeurs viables doivent être conservées, les autres peuvent êtres supprimées.

Éléments de compléxité

Éléments caractéristiques d'un CSP :

Consistances plus fortes

Peut-on filtrer plus fortement ?

Oui ! Au lieu de travailler uniquement avec des relations entre 2 variables on peut travailler avec des relations sur 3 variables ou plus.

Définitions :