Qu'est-ce
qu'un C.S.P ?
Un
CSP (Constraints Satisfaction Problem) se décrit
par un ensemble de variables (au sens « inconnues »)
dont le domaine est souvent discret (entier, ensemble, booléen)
et un ensemble de contraintes sur ces variables.
La résolution consiste alors à trouver un
tuple de valeurs tel que toutes les contraintes soient satisfaites.
Un
C.S.P. peut donc se décrire ainsi :
- X
= { X1, X2, ..., Xn}
- X1
D1, X2
D2, ... Xn
Dn
- C
= {C1, C2, ..., Ck}
Les
variables : Dans un CSP, les domaines sont discrets
et finis. On a donc soit un intervalle de valeurs
(Par exemple : X ∈ [[ 0,17 ]] ), soit un ensemble
discontinus de valeurs (Par exemple : {1,4,8,13} ). Ceci
donne donc naissance à deux
types de variables : les Variables sur les Bornes
( on ne stocke que les bornes actuelles du domaine ) et
les Variables Enumérées : on stocke
une à une les valeurs.
Les
Contraintes
Les
contraintes sont des relations sur les variables. Par exemple
si x = [[ 1,2 ]] et y = [[1,2]], la contrainte x != y peut
être décrite par la relation { (1,2) , (2,1)
}. Pour
plusieurs raisons, les solveurs sont fournis avec des contraintes
par défaut. Par exemple, dans le solveur libre Choco,
on retrouve :
- Des
contraintes arithmétiques (=, <, >,
>=
) sur des combinaisons linéaires
- Des
contraintes sémantiques :
- allDifferent
: elle permet de choisir des valeurs toutes
différentes pour chaque variable
Ex : allDifferent( { x1, x2, x3, x4 } )
- occurrence
: elle permet de contraindre le nombre doccurrences
dune valeur
Ex : occurrence( occ, 7, { x1, x2 } )
- element
: elle permet de choisir une valeur dans un
tableau
Ex : element( val, idx, ( 1,3,4,2 ) ) => val = (1,3,4,2)
[idx]
- Des
contraintes
en extension
où lon fournit les tuples autorisés
- Des
meta-contraintes logiques : implication, disjonction
LArité
des contraintes
Une
contrainte peut avoir une arité quelconque
:
- Une
contrainte est dite unaire si elle porte sur une
seule variable (x=4).
- Une
contrainte binaire comprend deux variables
(x+y=9).
- Une
contrainte n-aire comprend n variables.
On
utilise couramment le qualificatif « n-aire »
pour une contrainte dont le nombre de variables nest
pas fixé à lavance (Comme par exemple
occurrence, allDifferent
).
Les
Solutions
Un
tuple ou une affectation est une liste
de valeurs choisies pour chacune de variables du problèmes
(ou un sous-ensemble dans le cas dune affectation
partielle lors dune recherche). Un tuple viole
une contrainte si ses valeurs nappartiennent pas
à la relation. Il est dit consistant sil
ne viole aucune contrainte. Une affectation est dite totale
si elle instancie toutes les variables du problème
et partielle si elle n'en instancie qu'une partie.
Une
solution à un C.S.P est un tuple sur
lensemble des variables qui est consistant.
Un problème peut être sous-contraint
(s'il y a plusieurs solutions possibles) ou sur-contraint
(s'il n'y a pas de solution)
La
programmation par contraintes résout un CSP de manière
déclarative (lutilisateur ne fournit
que le modèle). Dans lidéal, on a donc
la modélisation à la charge de lutilisateur
et la résolution à la charge du solveur.
Il est donc primordial de savoir modéliser
un problème.
La
Recherche
Afin
de trouver une solution à un CSP, on va devoir implémenter
une stratégie de recherche. Cette stratégie
se base sur 2 choix simultanés : le choix des
variables et le choix des valeurs.
- Le
choix des variables
Pour
faire des choix dans la recherche, il faut choisir
la variable que lon va essayer de modifier. Une
solution simple consiste à utiliser une liste
(ordonnée) de variables fournie par
le problème (ordre dajout des variables
par exemple) ou fournie par lutilisateur (en
fonction de limportance dune variable dans
le problème). Il sagit dune heuristique
statique (planifiée en dehors de la résolution)
Le
but est dexplorer au minimum. On peut soit
commencer par les variables avec le moins de valeurs
possibles (« MinDomain »),
soit commencer par les variables les plus contraintes
(« MaxConstrained »).
Lheuristique la plus utilisée : « Dom
over deg » sinspire des deux.
On prend la variable ayant le rapport « Taille
domaine / Nombre contraintes » le plus
petit
L'utilisateur,
qui connaît bien son problème, peut décider
de d'une heuristique personnalisée de
branchement qui prend en compte sa sémantique.
Par exemple, si on minimise, on souhaite prendre les
variables qui ont le plus dimpact sur lobjectif.
Si des variables ont des rôles différents,
on peut souhaiter sélectionner en priorité
aux variables intervenants dans un nombre important
de contraintes ou ayant un impact élevé
dans la fonction objectif.
Pour
aller plus loin (des recherches sont menées actuellement
dans ce domaine), on peut sinspirer du passé
de la recherche de solutions et prendre des choix
qui ont déjà amené une contradiction
dans les branches précédentes pour accélérer
la recherche. L'utilisation d'explications
d'échecs peut permettre de faire
remonter des erreurs à ne pas reproduire.
- Le
choix des valeurs
Si
on a aucune connaissance sur la sémantique des
valeurs, il est difficile de choisir. Il existe donc
heuristique par défaut comme le Choix incrémental
des valeurs ou le Choix inverse.
Pour les grands domaines, il est inconcevable dessayer
toutes les valeurs. Dans ce cas, on utilise une Recherche
dichotomique.
Comme
dans le choix des variables, lutilisateur en connaît
souvent plus sur le problème. Il peut donc proposer
son propre choix de valeurs. Pour cela
il doit implémenter une nouvelle stratégie
en utilisant une « interface »
bien précise.
L'algorithme
de recherche va donc consister à choisir une stratégie
puis à faire un parcours complet de l'arbre
(les seules parties ignorées sont prouvées
inconsistantes). L'algorithme est donc global car
la recherche se fait en sachant à tout moment ce
qui a été parcouru et ce quil reste
à parcourir :
Explore(
n ) {
//
Création d'une heuristique.
b = getHeuristique( );
// Affectation du type de choix de variables.
v = b.selectBranchingObject( );
// Affectation du type de choix de valeurs.
i = b.getFirstBranch( x );
do {
//
Sauvegarde du monde ( <=> l'état) courant.
pb.getEnvironment( ).worldPush( );
// Branchement sur une des valeurs.
b.goDownBranch( x, i );
// Appel récursive dans ce monde.
explore(n+1);
// Retour au monde sauvegardé.
pb.getEnvironment( ).worldPop( );
// Remontée dans l'arbre.
b.goUpBranch( x, i );
// Choix de la variable suivante.
i = getNextBranch( x, i );
}
while ( ! finishedBranch() );
}
Il
est bien sûr possible de proposer des algorithmes
non complets comme par exemple, la Limited Discrepancy
Search qui consiste à ne pas séloigner
de lheuristique (au sens premier choix pris) plus
de k fois dans la branche en cours dexploration.
.
Il ne sagit cependant pas dune utilisation classique
de la PPC. La section suivante présente des techniques
plus avancées de Recherche
et Propagation.
Voici
le plan de cette présentation sur la programmation par contraintes
- Les
notions de base
:
- Quelques
notions avancées :
- Quelques
exemples pour utiliser la P.P.C. :
Cette page
a été co-réalisée avec Guillaume
Rochart, docteur en mathématiques dans mon équipe de
recherche au Bouygues
e-lab.10/03/2009.