\documentclass[12pt,a4paper]{article} % -*- coding: utf-8 -*- \usepackage[a4paper]{geometry} \usepackage[francais]{babel} \usepackage[utf8]{inputenc} \usepackage[T1]{fontenc} \usepackage{times} \usepackage{amsmath} \usepackage{amsfonts} \usepackage{amssymb} \usepackage{amsthm} \usepackage{mathrsfs} %\usepackage{bm} \usepackage{stmaryrd} \usepackage{wasysym} \usepackage{url} \usepackage{graphicx} \usepackage[usenames,dvipsnames]{xcolor} %\usepackage{tikz} %\usetikzlibrary{matrix,arrows,decorations.markings} \usepackage{hyperref} % % % \theoremstyle{definition} \newtheorem*{defn}{Définition} \newtheorem*{prop}{Proposition} \newtheorem*{lem}{Lemme} \newtheorem*{thm}{Théorème} \newtheorem*{cor}{Corollaire} \renewcommand{\qedsymbol}{\smiley} % \mathchardef\emdash="07C\relax \mathchardef\hyphen="02D\relax \DeclareUnicodeCharacter{00A0}{~} % % % \begin{document} \pretolerance=8000 \tolerance=50000 \textbf{Discussion préalable.} On s'intéresse ici à la question de savoir ce qu'un \textbf{algorithme} peut ou ne peut pas faire. Pour procéder de façon rigoureuse, il faudrait formaliser la notion d'algorithme (par exemple à travers le concept de machine de Turing) : on a préféré rester informel sur cette définition — par exemple « un algorithme est une série d'instruction précises indiquant des calculs à effectuer étape par étape et qui ne manipulent, à tout moment, que des données finies » ou « un algorithme est quelque chose qu'on pourrait, en principe, implémenter sur un ordinateur » — étant entendu que cette notion est déjà bien connue et comprise, au moins dans la pratique. Les démonstrations du fait que tel ou tel problème est décidable par un algorithme ou que telle ou telle fonction est calculable par un algorithme deviennent beaucoup moins lisibles quand on les formalise avec une définition rigoureuse d'algorithme (notamment, programmer une machine de Turing est encore plus fastidieux que programmer en assembleur un ordinateur, donc s'il s'agit d'exhiber un algorithme, c'est probablement une mauvaise idée de l'écrire sous forme de machine de Turing). Néanmoins, il est essentiel de savoir que ces formalisations existent : on peut par exemple évoquer le paradigme du $\lambda$-calcul de Church (la première formalisation rigoureuse de la calculabilité), les fonctions générales récursives (=$\mu$-récursives) à la Herbrand-Gödel-Kleene, les machines de Turing (des machines à états finis capables de lire, d'écrire et de se déplacer sur un ruban infini contenant des symboles d'un alphabet fini dont à chaque instant tous sauf un nombre fini sont des blancs), les machines à registres, le langage « FlooP » de Hofstadter, etc. Toutes ces formalisations sont équivalentes (au sens où, par exemple, elles conduisent à la même notion de fonction calculable ou calculable partielle, définie ci-dessous). La \textbf{thèse de Church-Turing} affirme, au moins informellement, que tout ce qui est effectivement calculable par un algorithme\footnote{Voire, dans certaines variantes, physiquement calculable dans notre Univers.} est calculable par n'importe laquelle de ces notions formelles d'algorithmes, qu'on peut rassembler sous le nom commun de \textbf{calculabilité au sens de Church-Turing}, ou « calculabilité » tout court. Notamment, quasiment tous les langages de programmation informatique\footnote{C, C++, Java, Python, JavaScript, Lisp, OCaml, Haskell, Prolog, etc. Certains langages se sont même révélés Turing-complets alors que ce n'était peut-être pas voulu : par exemple, HTML+CSS.}, au moins si on ignore les limites des implémentations et qu'on les suppose capables de manipuler des entiers, chaînes de caractère, tableaux, etc., de taille arbitraire (mais toujours finie)\footnote{Autre condition : ne pas utiliser de générateur aléatoire matériel.}, sont « Turing-complets », c'est-à-dire équivalents dans leur pouvoir de calcul à la calculabilité de Church-Turing. Pour imaginer intuitivement la calculabilité, on peut donc choisir le langage qu'on préfère et imaginer qu'on programme dedans. Essentiellement, pour qu'un langage soit Turing-complet, il lui suffit d'être capable de manipuler des entiers de taille arbitraire, de les comparer et de calculer les opérations arithmétiques dessus, et d'effectuer des tests et des boucles. \medbreak Il faut souligner qu'on s'intéresse uniquement à la question de savoir ce qu'un algorithme peut ou ne peut pas faire (calculabilité), pas au temps ou aux autres ressources qu'il peut prendre pour le faire (complexité), et on ne cherche donc pas à rendre les algorithmes efficaces en quelque sens que ce soit. Par exemple, pour arguër qu'il existe un algorithme qui décide si un entier naturel $n$ est premier ou non, il suffit de dire qu'on peut calculer tous les produits $pq$ avec $2\leq p,q\leq n-1$ et tester si l'un d'eux est égal à $n$, peu importe que cet algorithme soit absurdement inefficace. De même, nos algorithmes sont capables de manipuler des entiers arbitrairement grands : ceci permet de dire, par exemple, que toute chaîne binaire peut être considérée comme un entier, peu importe le fait que cet entier ait peut-être des milliards de chiffres (dans les langages informatiques réels, on a rarement envie de considérer toute donnée comme un entier, mais en calculabilité on peut se permettre de le faire). Notamment, plutôt que de considérer des « mots » (éléments de $\Sigma^*$ avec $\Sigma$ un alphabet fini) et « langages » (parties de $\Sigma^*$), il sera plus pratique de remplacer l'ensemble $\Sigma^*$ des mots par l'ensemble des entiers naturels, quitte à choisir un codage (calculable !) des mots par des entiers. (À titre d'exemple, on obtient une bijection de l'ensemble $\{0,1\}^*$ des mots sur l'alphabet à deux lettres avec $\mathbb{N}$ de la façon suivante : ajouter un $1$ au début du mot, lire celui-ci comme un nombre binaire, et soustraire $1$. Plus généralement, une fois choisi un ordre total sur l'alphabet fini $\Sigma$, on peut trier les mots par ordre de taille, et, à taille donnée, par ordre lexicographique, et leur associer les entiers naturels dans le même ordre : il n'est pas difficile de montrer que cela donne bien une bijection calculable entre $\Sigma^*$ et $\mathbb{N}$.) \medbreak \textbf{Terminaison des algorithmes.} Un algorithme qui effectue un calcul utile doit certainement terminer en temps fini. Néanmoins, même si on voudrait ne s'intéresser qu'à ceux-ci, il n'est pas possible d'ignorer le « problème » des algorithmes qui ne terminent jamais (et ne fournissent donc aucun résultat). C'est le point central de la calculabilité (et du théorème de Turing ci-dessous) qu'on ne peut pas se débarrasser des algorithmes qui ne terminent pas : on ne peut pas, par exemple, formaliser une notion suffisante\footnote{Tout dépend, évidemment, de ce qu'on appelle « suffisant » : il existe bien des notions de calculabilité, plus faibles que celle de Church-Turing, où tout calcul termine, voir par exemple la notion de fonction « primitive récursive » ou le langage « BlooP » de Hofstadter ; mais de telles notions ne peuvent pas disposer d'une machine universelle comme expliqué plus loin (en raison d'un argument diagonal), donc elles sont nécessairement incomplètes en un certain sens.} de calculabilité dans laquelle tout algorithme termine toujours ; ni développer un langage de programmation suffisamment général dans lequel il est impossible qu'un programme « plante » indéfiniment ou parte en boucle infinie. (Cette subtilité est d'ailleurs sans doute en partie responsable de la difficulté historique à dégager la bonne notion d'« algorithme » : on a commencé par développer des notions d'algorithmes terminant forcément, comme les fonctions primitives récursives, et on se rendait bien compte que ces notions étaient forcément toujours incomplètes.) \bigbreak \begin{defn} On dit qu'une fonction $f\colon\mathbb{N}\to\mathbb{N}$ est \textbf{calculable} (ou « récursive ») lorsqu'il existe un algorithme qui prend en entrée $n\in\mathbb{N}$, termine toujours en temps fini, et calcule (renvoie) $f(n)$. On dit qu'un ensemble $A \subseteq \mathbb{N}$ (« langage ») est \textbf{décidable} (ou « calculable » ou « récursif ») lorsque sa fonction indicatrice $\mathbf{1}_A \colon \mathbb{N} \to \mathbb{N}$ (valant $1$ sur $A$ et $0$ sur son complémentaire) est calculable. Autrement dit : lorsqu'il existe un algorithme qui prend en entrée $n\in\mathbb{N}$, termine toujours en temps fini, et renvoie « oui » ($1$) si $n\in A$, « non » ($0$) si $n\not\in A$ (on dira que l'algorithme « décide » $A$). On dit qu'une fonction partielle $f\colon\mathbb{N}\dasharrow\mathbb{N}$ (c'est-à-dire une fonction définie sur une partie de $\mathbb{N}$, appelé ensemble de définition de $f$) est \textbf{calculable partielle} (ou « récursive partielle ») lorsqu'il existe un algorithme qui prend en entrée $n\in\mathbb{N}$, termine en temps fini ssi $f(n)$ est définie, et dans ce cas calcule (renvoie) $f(n)$. (Une fonction calculable est donc simplement une fonction calculable partielle qui est toujours définie : on dira parfois « calculable totale » pour souligner ce fait.) On dit qu'un ensemble $A \subseteq \mathbb{N}$ est \textbf{semi-décidable} (ou « semi-calculable » ou « semi-récursif ») lorsque la fonction partielle $\mathbb{N}\dasharrow\mathbb{N}$ définie exactement sur $A$ et y valant $1$, est calculable partielle. Autrement dit : lorsqu'il existe un algorithme qui prend en entrée $n\in\mathbb{N}$, termine en temps fini ssi $n \in A$, et renvoie « oui » ($1$) dans ce cas\footnote{En fait, la valeur renvoyée n'a pas d'importance ; on peut aussi définir un ensemble semi-décidable comme l'ensemble de définition d'une fonction calculable partielle.} (on dira que l'algorithme « semi-décide » $A$). \end{defn} On s'est limité ici à des fonctions d'une seule variable (entière), mais il n'y a pas de difficulté à étendre ces notions à plusieurs variables, et de parler de fonction calculable $\mathbb{N}^k \to \mathbb{N}$ (voire $\mathbb{N}^* \to \mathbb{N}$ avec $\mathbb{N}^*$ l'ensemble des suites finies d'entiers naturels) ou de fonction calculable partielle de même type : de toute manière, on peut « coder » un couple d'entiers naturels comme un seul entier naturel (par exemple par $(m,n) \mapsto 2^m(2n+1)$, qui définit une bijection calculable $\mathbb{N}^2 \to \mathbb{N}$), ou bien sûr un nombre fini quelconque (même variable), ce qui permet de faire « comme si » on avait toujours affaire à un seul paramètre entier. {\footnotesize \textbf{Complément :} Comme on n'a pas défini formellement la notion d'algorithme, il peut être utile de signaler explicitement les faits suivants (qui devraient être évidents sur toute notion raisonnable d'algorithme) : les fonctions constantes sont calculables ; les opérations arithmétiques usuelles sont calculables ; les projections $(n_1,\ldots,n_k) \mapsto n_i$ sont calculables, ainsi que la fonction qui à $(m,n,p,q)$ associe $p$ si $m=n$ et $q$ sinon ; toute composée de fonctions calculables (partielle ou totale) est calculable idem ; si $\underline{m} \mapsto g(\underline{m})$ est calculable (partielle ou totale) et que $(\underline{m}, n, v) \mapsto h(\underline{m}, n, v)$ l'est, alors la fonction $f$ définie par récurrence par $f(\underline{m},0) = g(\underline{m})$ et $f(\underline{m},n+1) = h(\underline{m}, n, f(\underline{m},n))$ est encore calculable idem (algorithmiquement, il s'agit juste de boucler $n$ fois) ; et enfin, si $(\underline{m}, n) \mapsto g(\underline{m},n)$ est calculable partielle, alors la fonction $f$ (aussi notée $\mu_n g$) définie par $f(\underline{m}) = \min\{n : g(\underline{m},n) = 0 \land \forall n'