Marathon d'algorithmes

a marqué ce sujet comme résolu.

Bon un quick and dirty en C.

j’implémente directement une solution en base B quelconque (2≤B≤36). Dans la suite du texte je note Δ le plus grand chiffre de cette base (9 en base 10, F en base 16, 1 en base 2, …). L’idée de base commence par éliminer deux cas particuliers :

  • les nombres composés uniquement de chiffres Δ (comme 11111 en base 2, 9 en base 10 ou encore BBBBBB en base 12). Cela me permet d’éviter le cas où le palindrome suivant contient plus de chiffres que le nombre de départ. Pour un palindrome du type Δⁿ, nextpal(Δⁿ)=1Δⁿ⁻¹1 ;
  • puis les nombres composés d’un seul chiffre strictement plus petits que Δ, dans ce cas nextpal(n)=n+1 ;

Après ce filtre, on se retrouve donc avec un nombre d’au moins deux chiffres dont le palindrome suivant possédera le même nombre de chiffres.

Mon algo traverse ensuite plusieurs étapes :

  1. on part du milieu du nombre pour déterminer le plus grand palindrome interne, comme par exemple 22 dans 1223 ou 54345 dans 895434512 ;
  2. on copie la partie à gauche (si elle existe) du palindrome interne à sa droite ;
  3. si le nombre entier était un palindrome ou si le chiffre à gauche du palindrome interne était plus petit que celui à sa droite alors j’ajoute 1 à partir du milieu du nombre en propageant la retenue et en copiant en miroir à droite.

Par exemple : 808 → plus grand palindrome interne 808 → ajout de 1 à partir du milieu → 818 898 → plus grand palindrome interne 898 → ajout de 1 à partir du milieu → 908 → copie miroir → 909 3221 → plus grand palindrome interne 3221 → copie miroir hors palindrome interne → 3223 12328 → plus grand palindrome interne 12328, chiffre à gauche plus petit que celui à droite → copie miroir → 12321 → ajout de 1 à partir du milieu 12421 → copie miroir → 12421 3284 → plus grand palindrome interne 3284 → chiffre à plus petit que celui à doite → copie miroir → 3223 → ajout de 1 à partir du milieu → 3323 → copie miroir → 3333

code vite fait :

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static char digits[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
static uint8_t values[] = {
    ['0'] = 0,  ['1'] = 1,  ['2'] = 2,  ['3'] = 3,  ['4'] = 4,  ['5'] = 5,
    ['6'] = 6,  ['7'] = 7,  ['8'] = 8,  ['9'] = 9,  ['a'] = 10, ['b'] = 11,
    ['c'] = 12, ['d'] = 13, ['e'] = 14, ['f'] = 15, ['g'] = 16, ['h'] = 17,
    ['i'] = 18, ['j'] = 19, ['k'] = 20, ['l'] = 21, ['m'] = 22, ['n'] = 23,
    ['o'] = 24, ['p'] = 25, ['q'] = 26, ['r'] = 27, ['s'] = 28, ['t'] = 29,
    ['u'] = 30, ['v'] = 31, ['w'] = 32, ['x'] = 33, ['y'] = 34, ['z'] = 35,
    ['A'] = 10, ['B'] = 11, ['C'] = 12, ['D'] = 13, ['E'] = 14, ['F'] = 15,
    ['G'] = 16, ['H'] = 17, ['I'] = 18, ['J'] = 19, ['K'] = 20, ['L'] = 21,
    ['M'] = 22, ['N'] = 23, ['O'] = 24, ['P'] = 25, ['Q'] = 26, ['R'] = 27,
    ['S'] = 28, ['T'] = 29, ['U'] = 30, ['V'] = 31, ['W'] = 32, ['X'] = 33,
    ['Y'] = 34, ['Z'] = 35,
};
static bool valid[] = {
    ['0'] = true, ['1'] = true, ['2'] = true, ['3'] = true, ['4'] = true,
    ['5'] = true, ['6'] = true, ['7'] = true, ['8'] = true, ['9'] = true,
    ['a'] = true, ['b'] = true, ['c'] = true, ['d'] = true, ['e'] = true,
    ['f'] = true, ['g'] = true, ['h'] = true, ['i'] = true, ['j'] = true,
    ['k'] = true, ['l'] = true, ['m'] = true, ['n'] = true, ['o'] = true,
    ['p'] = true, ['q'] = true, ['r'] = true, ['s'] = true, ['t'] = true,
    ['u'] = true, ['v'] = true, ['w'] = true, ['x'] = true, ['y'] = true,
    ['z'] = true, ['A'] = true, ['B'] = true, ['C'] = true, ['D'] = true,
    ['E'] = true, ['F'] = true, ['G'] = true, ['H'] = true, ['I'] = true,
    ['J'] = true, ['K'] = true, ['L'] = true, ['M'] = true, ['N'] = true,
    ['O'] = true, ['P'] = true, ['Q'] = true, ['R'] = true, ['S'] = true,
    ['T'] = true, ['U'] = true, ['V'] = true, ['W'] = true, ['X'] = true,
    ['Y'] = true, ['Z'] = true,
};

typedef struct {
  char *input;
  int base;

  bool done;
  size_t number_len;
  uint8_t *number;
  size_t nextpal_len;
  uint8_t *nextpal;

} input_data_t;

input_data_t get_data(const char *input, int base);
void compute_nexpal(input_data_t *data);
void display(input_data_t data);

int main(int argc, char *argv[]) {
  input_data_t data = get_data(argv[1], argv[2] ? atoi(argv[2]) : 10);
  compute_nexpal(&data);
  display(data);
}

input_data_t get_data(const char *input, int base) {
  if (base < 2 || base > 36) {
    fprintf(stderr, "base invalide\n");
    exit(EXIT_FAILURE);
  }

  input_data_t data;
  data.input = strdup(input);
  data.base = base;
  data.number_len = strlen(input);
  if (data.number_len < 1) {
    fprintf(stderr, "nombre invalide\n");
    exit(EXIT_FAILURE);
  }

  data.number = malloc(data.number_len * sizeof *data.number);

  bool all_last_digit = true;
  for (size_t i = 0; i < data.number_len; i++) {
    if (!valid[(int)input[i]]) {
      fprintf(stderr, "caractère invalide\n");
      exit(EXIT_FAILURE);
    }
    uint8_t value = values[(int)input[i]];
    if (value != base - 1)
      all_last_digit = false;
    if (value >= base) {
      fprintf(stderr, "chiffre invalide\n");
      exit(EXIT_FAILURE);
    }
    data.number[i] = value;
  }

  if (all_last_digit) {
    data.done = true;
    data.nextpal_len = data.number_len + 1;
    data.nextpal = calloc(data.nextpal_len, sizeof *data.nextpal);
    data.nextpal[0] = data.nextpal[data.nextpal_len - 1] = 1;
  } else if (data.number_len == 1) {
    data.done = true;
    data.nextpal_len = data.number_len;
    data.nextpal = calloc(data.nextpal_len, sizeof *data.nextpal);
    data.nextpal[0] = values[(int)input[0]] + 1;
  } else {
    data.done = false;
    data.nextpal_len = data.number_len;
    data.nextpal = calloc(data.nextpal_len, sizeof *data.nextpal);
    memcpy(data.nextpal, data.number, data.number_len);
  }

  return data;
}

void compute_nexpal(input_data_t *data) {
  if (data->done)
    return;
  ssize_t middle = data->nextpal_len / 2;
  ssize_t left = middle - 1;
  ssize_t right = middle + (data->nextpal_len % 2);
  bool left_smaller = false;
  while (left >= 0 && data->nextpal[left] == data->nextpal[right]) {
    left--;
    right++;
  }
  left_smaller = (left < 0 || data->nextpal[left] < data->nextpal[right]);
  while (left >= 0) {
    data->nextpal[right] = data->nextpal[left];
    right++;
    left--;
  }

  if (left_smaller) {
    int carry = 1;
    left = middle - 1;
    if (data->nextpal_len % 2) {
      data->nextpal[middle] += carry;
      carry = data->nextpal[middle] / data->base;
      data->nextpal[middle] %= data->base;
      right = middle + 1;
    } else {
      right = middle;
    }
    while (left >= 0) {
      data->nextpal[left] += carry;
      carry = data->nextpal[left] / data->base;
      data->nextpal[left] %= data->base;
      data->nextpal[right++] = data->nextpal[left--];
    }
  }

  data->done = true;
}

void display(input_data_t data) {
  printf("le palindrome suivant %s en base %d est : ", data.input, data.base);
  for (size_t i = 0; i < data.nextpal_len; i++)
    putchar(digits[data.nextpal[i]]);
  putchar('\n');
}

Le code mériterait largement une simplification. J’ai pris le parti de ne saisir que des chaînes que je transforme en tableau d’entiers pour les manipulations.

Du coup en base 36, nextpal(256344510514743751332437247280545791⏨,36)=256344510514743751399753977111200702⏨

voilà voilà

Rigolo

Une façon partielle, en C++20 pour des ajouts dans la lib standard.

Mon approche a été

  1. je convertis le nombre dans la base choisie,
  2. je garde le début pour fabriquer par miroir le palindrome immédiat qui va me servir de référence. P.ex., en base 10, pour 201, je construis 2|0|2. Si le résultat est supérieur à mon nombre initial, exit, j’ai la réponse.
  3. sinon, j’ajoute 1 à la moitié gardée, pour mon 20, ça me donne 21, et je repasse en miroir, ce qui me donne 212, et c’est le résultat (donc pas ici pour un 201 de départ)

Maintenant, il y a des bidouilles au milieu pour distinguer le cas nombre de digits pairs ou impairs, et le + 1 qui change le nombre de digits, et le cas next_pal(9) que je n’ai pas réussi à intégrer dans ma généralisation… ^^'

Le code est en C++20 pour profiter de std::from_chars() et std::to_chars() qui savent faire les conversions chaine <-> nombre en base de 2 à 36. Et j’utilise doctest pour les tests.

Vu que je travaille sur des nombres et non des chaines dès le début, 256344510514743751332437247280545791 ne tenant pas sur 64 bits, le code ne le supporte pas. Le 3^39 est bien instantané, mais il n’est pas impossible que cela ait été résolu à la compilation :/

#include <charconv>
#include <string_view>
#include <numeric>
#include <iterator>
#include <cassert>

#define LET_S_TEST

#if defined(LET_S_TEST)
#   define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#   include <doctest/doctest.h>
#else
#   include <iostream>
#   define CAPTURE(v)
#   define INFO(i)
#endif

using int_t = std::uintmax_t;

template <std::size_t N>
std::string_view to_chars(char (&buffer)[N], int_t n, int base)
{
    assert(base <= 36);
    auto e = std::to_chars(std::begin(buffer), std::end(buffer), n, base);
    return std::string_view{&buffer[0], e.ptr};
}

int_t from_chars(char const* f, char const* l, int base)
{
    assert(f < l);
    assert(base <= 36);
    int_t res;
    std::from_chars(f, l, res, base);
    return res;
}

int_t from_chars(std::string_view sv, int base)
{
    return from_chars(std::begin(sv), std::end(sv), base);
}

template <std::size_t N>
int_t mirror(char (&buffer)[N], std::size_t offset, std::size_t p, int base)
{
    assert(p+offset <= N);
    auto reverse = [](char const* p) { return std::reverse_iterator{p}; };
    // - offset pour ignorer le midpoint central
    auto e = std::copy(reverse(buffer-offset+p), reverse(buffer), buffer+p);
    auto n = from_chars(buffer, e, base);
    // std::cout << "mirroring [0, "<< offset << "+" << p<<"[ : " << std::string_view{buffer, e} << "\n";
    return n;
}

int_t next_pal(int_t n, int base = 10)
{
    if (n == 9) {
        // Je n'arrive pas à le généraliser lui. Fatigué ^^'
        return 11;
    }
    
    char buffer[512]; // bien assez, non?

    auto const str_prev = to_chars(buffer, n, base);

    auto offset = size(str_prev)%2;
    auto const pos_middle
        = std::midpoint(begin(str_prev), end(str_prev))
        + offset;

    // Pour gérer le cas next_pal(21)
    auto const num_prev_mirror = mirror(buffer, offset, pos_middle-buffer, base);
    if (num_prev_mirror > n) {
        return num_prev_mirror;
    }

    // Si taille impaire, on prend 1 de plus
    auto const str_prev_head = std::string_view{buffer, pos_middle};
    auto const num_prev_head = from_chars(begin(str_prev_head), end(str_prev_head), base);
    
    auto str_next_head = to_chars(buffer, num_prev_head+1, base);

    // Le prochain prend plus de digits => mirrorer au bon endroit
    offset += (size(str_next_head) == size(str_prev_head) ? 0 : 1);

    auto l = std::copy(std::rbegin(str_next_head)+offset, std::rend(str_next_head), buffer+size(str_next_head));
    int_t next = from_chars(buffer, l, base);
    return next;
}


#if !defined(LET_S_TEST)
int main()
{
    int_t n;
    while ( std::cout << "nombre?" && std::cin >> n) {
        std::cout << next_pal(n) << std::endl;
    }
}
#else

TEST_CASE("1 digit, no overflow") {
    CHECK(next_pal(1) == 2);
    CHECK(next_pal(2) == 3);
    CHECK(next_pal(3) == 4);
    CHECK(next_pal(4) == 5);
    CHECK(next_pal(5) == 6);
    CHECK(next_pal(6) == 7);
    CHECK(next_pal(7) == 8);
    CHECK(next_pal(8) == 9);
}

TEST_CASE("overflows") {
    CHECK(next_pal(9) == 11);
    CHECK(next_pal(99) == 101);
    CHECK(next_pal(999) == 1001);
    CHECK(next_pal(9999) == 10001);
}

TEST_CASE("2 digits, no overflow") {
    CHECK(next_pal(10) == 11);
    CHECK(next_pal(11) == 22);
    CHECK(next_pal(19) == 22);
    CHECK(next_pal(20) == 22);
    CHECK(next_pal(21) == 22);
    CHECK(next_pal(22) == 33);
    CHECK(next_pal(32) == 33);
    CHECK(next_pal(33) == 44);
}

TEST_CASE("3 digits, no overflow") {
    CHECK(next_pal(101) == 111);
    CHECK(next_pal(111) == 121);
    CHECK(next_pal(191) == 202);
    CHECK(next_pal(201) == 202);
    CHECK(next_pal(202) == 212);
    CHECK(next_pal(292) == 303);
    CHECK(next_pal(300) == 303);
    CHECK(next_pal(302) == 303);
    CHECK(next_pal(303) == 313);
}

TEST_CASE("The originals...")
{
    CHECK(next_pal(808) == 818);
    CHECK(next_pal(999) == 1001);
    CHECK(next_pal(2133) == 2222);

    // 3^39 = 4 052 555 153 018 976 267
    // 405255515|3|018976267
    // => 405255515|3|515552504
    // => 4052555153515552504
    CHECK(next_pal(std::pow(3,39)) == 4052555153515552504);
}

TEST_CASE("en base 36")
{
    // char b[512];
    // std::cout << 808 << " -- > " << to_chars(b, 808, 36) << std::endl;
    // std::cout << "mm --> " << from_chars("mm", 36) << std::endl;
    // 808 [10]  == mg [36]
    // => mm [36] == 814 [10]
    CHECK(next_pal(808, 36) == 814);

    // std::cout << 999 << " -- > " << to_chars(b, 999, 36) << std::endl;
    // std::cout << "ss --> " << from_chars("ss", 36) << std::endl;
    // 999 [10]  == rr [36]
    // => ss [36] == 1036 [10]
    CHECK(next_pal(999, 36) == 1036);

    // std::cout << 2133 << " -- > " << to_chars(b, 2133, 36) << std::endl;
    // std::cout << "1o1 --> " << from_chars("1o1", 36) << std::endl;
    // 2133 [10]  == 1n9 [36]
    // => 1o1 [36] == 2161 [10]
    CHECK(next_pal(2133, 36) == 2161);

#if 0
    // 64 bits, ce n'est pas assez
    std::cout << 256344510514743751332437247280545791ULL << " -- > " << to_chars(b, 256344510514743751332437247280545791ULL, 36) << std::endl;
    std::cout << "1o1 --> " << from_chars("1o1", 36) << std::endl;
    // 256344510514743751332437247280545791 [10]  == 1n9 [36]
    // => 1o1 [36] == 2161 [10]
    CHECK(next_pal(256344510514743751332437247280545791ULL, 36) == 0);
#endif
}

#endif

// Vim: let $CXXFLAGS='-std=c++20 -Wextra -Wall'
// Vim: let $CXX='clang++'

@Jeph, @ludox.officiel, @lmghs: c’est sympa toutes ces solutions, dans des langages différents qui plus est :)

Elles fonctionnent bien avec les tests que j’ai pu faire, avec la performance attendue pour les grands nombres. Vous avez adopté le même algo a priori (au moins dans les grandes lignes), en traitant les "edge cases" au départ et en splittant le nombre en deux parties ensuite pour incrémenter la partie supérieure si nécessaire, et la recopier dans la partie inférieure.

@Jeph comme tu as répondu en premier, je te passe le relais pour proposer un autre challenge!

Mais mention spéciale également à @ludox.officiel et @lmghs pour avoir implémenté le bonus. @lmghs j’avais bien conscience que le nombre à traiter pour le bonus ne pouvait pas être géré sur 64 bits, c’est pour cela que j’ai limité le challenge "de base" à 3**39.

Pour info le résultat du bonus converti en base 36 doit donner ESOPERESTEICIETSEREPOSE, si je ne me suis pas trompé.

Merci à tous!

+0 -0

Bon, je me lance ; soyez indulgents je n’ai pas trop l’habitude :p Edit: mise en forme de l’énoncé …

Algo n°7

Maxnoï

Tout le monde connaît le classique «tours de Hanoï», 3 tours, n disques de taille croissante placés sur un tour de départ que l’on doit déplacer vers un tour d’arrivée en s’aidant d’une tour auxiliaire. On ne peut déplacer qu’un disque à la fois et on ne peut le déplacer que vers une tour vide ou une tour dont le disque couvert est plus grand. Au départ les disques sont empilés sur la tour de départ par taille croissante.

La problème classique demande de placer tous les disques de la tour de départ sur la tour d’arrivée ne un nombre minimum de déplacements. Pour ce challenge, je vous demande de résoudre le problème en un nombre maximum de déplacements sans avoir deux configurations identiques lors de la résolution.

Entrée

Le nombre n de disques.

Sortie attendue

La liste des paires de tours (départ, arrivée) utilisée pour un coup, un coup par ligne.

Exemple

En notant les tours A B C, une solution pour le cas à 2 disques est :

A → B
B → C
A → B
C → B
B → A
B → C
A → B
B → C
+2 -0

Problème intéressant, voici ma proposition en Ruby:

En consultant l’article Wikipedia sur les tours de Hanoï, la représentation des chemins possible sur un triangle de Pascal a attiré mon attention. On voit bien sur le schema quel est le plus court chemin, en suivant le côté du triangle qui est en bas.

Mais on peut également tracer sur ce même triangle un chemin passant par toutes les configurations possibles du jeu, qui sera forcément le chemin le plus long possible. Mon programme se base sur cette observation: en choisissant systématiquement un déplacement entre 2 tours voisines qui ne soit pas le déplacement qu’on vient de faire en sens inverse, on arrive à reproduire le même parcours. Quand il n’y a plus de déplacement possible, c’est qu’on est arrivé à la position finale.

On n’a donc pas besoin de garder en mémoire les configurations déjà parcourues: la complexité en mémoire est O(n), n étant le nombre de disques. La complexité en temps est O(n^3), le nombre de configurations légales du jeu.

def valid_move?(towers, m0, m1)

	# Le déplacement est choisi s'il respecte la règle du jeu
	!towers[m0].empty? && (towers[m1].empty? || towers[m0][-1] < towers[m1][-1])
end

def next_move(towers, t0, t1)

	# Choix du prochain déplacement parmi ceux qui sont possibles
	return [t0, t1] if valid_move?(towers, t0, t1)
	return [t1, t0] if valid_move?(towers, t1, t0)

	# Aucun déplacement valide trouvé
	nil
end

def maxnoi(n, verbose)

	# Création des 3 tours
	tower_ids = "ABC"
	towers = Array.new
	3.times do
		towers.push(Array.new)
	end

	# Ajout des disques à la tour A
	disk = n
	while disk > 0
		towers[0].push(disk)
		disk -= 1
	end

	# Effectuer les déplacements jusqu'à la fin du jeu (plus de déplacement valide)
	moves = 0
	move = next_move(towers, 0, 1)
	while move != nil
		puts "#{tower_ids[move[0]]} -> #{tower_ids[move[1]]}" if verbose
		towers[move[1]].push(towers[move[0]].pop)

		# Si le déplacement courant implique la tour A le prochain doit impliquer la tour C et inversement
		moves += 1
		move = next_move(towers, 2-move[0], 2-move[1])
	end
	moves
end

puts maxnoi(0, true)
puts maxnoi(1, true)
puts maxnoi(2, true)
puts maxnoi(4, false)
puts maxnoi(8, false)
puts maxnoi(16, false)

EDIT Simplification / Optimisation du programme

+1 -0

@fromvega : bravo solution élégante et correcte.

Ta méthode permet aussi de trouver un algorithme simple pour qu’un humain puisse y jouer à la main sans trop réfléchir. On peut la trouver en déroulant un peu ta boucle de déplacements et en observant quel disque bouge à quel moment. Mais bon elle est déjà hyper simple :)

Je publierai une solution détaillée un peu plus tard dans la journée. Mais tu es d’ors et déjà déclaré vainqueur : relais à toi, @fromvega.

+0 -0

Ah oui ok, je vois maintenant comment on peut faire plus simple ;)

A priori le disque de taille n-1 fait un aller-retour-aller pendant que le disque de taille n fait un aller.

Mais tu es d’ors et déjà déclaré vainqueur : relais à toi, @fromvega.

ludox

Comme j’ai déjà proposé l’algo d’avant, je passe le relais soit à quelqu’un qui propose une solution entre temps au tien, ou sinon à toi @lmghs comme tu avais trouvé une solution au mien :)

+1 -0

Maxnoï

Le contexte

Le problème des tours de Hanoï (HANOI) est un problème classique d’algorithmique.

Le jeu

Il est composé de :

  • trois barres de longueur égale, les tours ;
  • plusieurs disques pouvant être empilés sur les tours.

Il y a :

  • une tour de départ sur laquelle tous les disques sont empilés au début du jeu ;
  • une tour d’arrivée sur laquelle tous les disques devront être empilés à la fin du jeu ;
  • une tour auxilliaire vide au début et à la fin du jeu mais pouvant être utilisée pendant son déroulement.

Les règles sont simples :

  • un disque ne peut être déplacé d’une tour vers une autre uniquement si cette dernière ne comporte aucun disque ou si le dernier disque qui y est empilé est d’un diamètre supérieur à celui que l’on désire empiler ;
  • la position de départ consiste en tous les disques empilés sur la tour de départ, un disque reposant soit sur le socle de la tour, soit étant d’un diamètre inférieur au disque sur lequel il est posé.

Le but du jeu est de déplacer en un minimum de coup tous les disques de la tour de départ vers la tour d’arrivée.

Le challenge

Le challenge consiste à trouver une liste de déplacements de longueur maximum. Afin d’éviter les longueurs arbitraires contenant des cycles, il est interdit de jouer une position déjà jouée.

La résolution

Préambule

Avant de nous lancer dans la recherche d’un algorithme quelconque, nous allons dans un premier temps montrer que résoudre ce problème revient à résoudre une autre variate des tours de Hanoï. Cette variante restreint le déplacement de disques qui n’est possible que d’une tour à une tour voisine (ADJANOI).
La tour auxiliaire est voisine des tours de départ et d’arrivée qui toutes deux sont également voisines de la tour auxiliaire. Les tours de départ et d’arrivée ne sont pas voisine l’une de l’autre.

Nombre de positions distinctes à HANOI

Proposition : il existe exactement 3ⁿ positions différentes au jeu HANOI avec n disques.

Démonstration :

Soit P(n) le nombre de positions distinctes au jeu avec n disques. Nous avons P(0)=1 car il n’existe qu’une seule position vide de tout disque. Un raisonnement par récurrence, en supposant la proposition vraie jusqu’à n, nous donne P(n+1)= 3 P(n).
En effet, au rang n+1 il existe 3 positions différentes pour le disque Δₙ₊₁ et pour chacune de ces positions il y a P(n) positions différentes que les disques Δ₁ à Δₙ peuvent prendre.
Il ne peut exister une position non prise en compte car cela signifierait qu’il existe une position distincte non comptabilisée par P(n) en enlevant le disque Δₙ₊₁ ce qui contredirait l’hypothèse de récurrence.

Corollaire : le nombre maximum de déplacements pour obtenir la position d’arrivée à partir de la position de départ est 3ⁿ-1

Résolution de ADJANOI

D’une manière similaire à la résolution de HANOI, on peut trouver un algorithme en remarquant que pour résoudre ADJANOI avec n disques (n>1) il faut dans un premier temps déplacer les disques Δ₁ à Δₙ₋₁ vers la tour d’arrivée pour dégager le disque Δₙ en gardant la tour auxiliaire libre. Puis il faut y déplacer le disque Δₙ avant de libérer la tour d’arrivée en déplaçant les disques Δ₁ à Δₙ₋₁ vers la tour de départ pour libérer la tour d’arrivée. Il devient possible d’y déplacer le disque Δₙ et pour finir, il faut déplacer les disques Δ₁ à Δₙ₋₁ de la tour de départ vers la tour d’arrivée.

Algorithme adjanoi résolvant ADJANOI(n)

En notant respectivement Tₛ, Tₑ, Tₐ les tours de départ, d’arrivée et auxiliaire, et move( from, to ) la procédure qui déplace le disque en haut de la tour from vers la tour to :

algo adjanoi( n : positive integer, from : tower, to : tower, using : tower)
begin
    if n = 1 then
        if using = Tₐ
            move( from, using )
            move( using, to )
        else
            move( from, to )
        endif
    else
        adjanoi( n-1, from,  to,    using )
        adjanoi( 1,   from,  using, to    )
        adjanoi( n-1, to,    from,  using )
        adjanoi( 1,   using, to,    from  )
        adjanoi( n-1, from,  to,    using )
    endif
end

La complexité C(n) en nombre de déplacements de cet algo en fonction du nombre de disques est définit par la relation de récurrence :

C(1)=2C(1) = 2

C(n)=3 C(n1)+2C(n) = 3\ C(n-1) + 2

C(1) indique les deux étapes de la résolution du cas où il n’y a qu’un seul disque (départ → auxiliaire → arrivée). C(n) indique les 3 appels de adjanoi(n-1) par adjanoi(n) en conservant la même tour auxiliaire ; le + 2 s’explique du fait qu’au premier appel de adjanoi using est la tour auxiliaire.

Il est facilement vérifiable que : C(n)=3n1C(n)=3^n-1

De plus, il est tout aussi facilement vérifiable en usant d’un argument similaire à celui de la démonstration du nombre de positions distinctes à HANOI que les positions atteintes par adjanoi sont toutes distinctes.

maxnoi = adjanoi

En effet, adjanoi permet de résoudre HANOI en exactement 3ⁿ-1 déplacements ce qui représente le nombre maximal de positions distinctes. Comme toutes les positions atteintes par adjanoi sont distinctes on peut en déduire que cet algorithme donne un chemin maximal parcourant toutes les positions possibles.
Il résout donc MAXNOI.

Implémentation de la solution

enum tower { START, AUX, END, MAX_TOWER };
static const char *tnames[MAX_TOWER] = {
    [START] = "A", [AUX] = "B", [END] = "C"};

void maxnoi(size_t n, enum tower from, enum tower to, enum tower using) {
  if (n == 0) {
    puts("Nothing to do");
  } else if (n == 1) {
    if (using == AUX) {
      printf("%s %s\n", tnames[from], tnames[using]);
      printf("%s %s\n", tnames[using], tnames[to]);
    } else {
      printf("%s %s\n", tnames[from], tnames[to]);
    }
  } else {
    maxnoi(n - 1, from, to, using);
    maxnoi(1, from, using, to);
    maxnoi(n - 1, to, from, using);
    maxnoi(1, using, to, from);
    maxnoi(n - 1, from, to, using);
  }
}

Solution manuelle

Tout comme il existe un moyen mnémotechnique pour résoudre HANOI, il en existe un pour MAXNOI :

  1. déplacer le plus petit disque vers la droite deux fois, aller en 6. si le jeu est terminé ;
  2. effectuer le seul autre déplacement valide n’utilisant pas le plus petit disque ;
  3. déplacer le plus petit disque vers la gauche deux fois ;
  4. effectuer le seul autre déplacement valide n’utilisant pas le plus petit disque ;
  5. recommencer en 1.
  6. Sabler le champagne !

Cette solution manuelle permet d’implémenter un algorithme non récursif optimal (laissé en exercice au lecteur).

La solution manuelle proposée par @fromvega tient en encore moins de lignes :

  1. tant qu’il existe un déplacement valide entre deux tours adjacentes qui ne défasse pas le déplacement précédent ;
  2. faire ce mouvement ;
  3. sabler le champagne !

bon en attendant je peux en proposer un pas trop trop difficile :)

Algo n°8

Frogger fête ses 30 ans !

Frogger la pétillante grenouille que nous avions aidée lors de notre jeunesse à compléter son périple routier passe désormais une retraite paisible et méritée à Frogtown.

Une grande fête est organisée pour ses 30 ans. À cette occasion chaque frogtowner lui apporte un cadeau. Pour célébrer ses talents de sauteur, les cadeaux sont placés sur la Grande Ligne Infinie de Nénufars Espacés Unitairement. Mais son ennemi de toujours, Balthromaw, y a sournoisement placé un engin thermonucléaire !

Heureusement Frogger trouve le plan de l’emplacement nonchalamment abandonné par Balthromaw ; les méchants sont immanquablement certain de leur succès. Il sait sur quel nénufar se trouve la bombe. Nous allons devoir l’aider à planifier ses sauts pour qu’il y accède le plus rapidement possible !

Frogger a développé une technique redoutable pour sauter vite et loin :

  • s’il est à l’arrêt, il peut uniquement sauter sur le nénuphar suivant (distance 1) ;
  • s’il vient d’effectuer un saut d’une distance d alors il peut au choix continuer avec un saut d’une distance d-1, ou d ou d+1 ;
  • il ne peut s’arrêter que si le saut précédent est d’une distance de 1 ;
  • il ne peut aller que de l’avant, il ne se retourne jamais, ne saute jamais en arrière.

Il faut à tout prix l’aider en lui fournissant une liste de distances à sauter pour rejoindre la bombe en un minimum de sauts valides. Tous les espaces entre les nénufars valent exactement 1, les nénufars sont numérotés à partir de 0 et il y en a une infinité. Frogger part du nénufar 0 et doit se rendre au nénufar N pour désamorcer la bombe et sauver Frogtown ainsi que tous ses frogbitants.

Par exemple pour N=5 on pourra fournir à Frogger la liste (1,1,2,1) ou la liste (1,2,1,1). La liste (1,1,1,1,1) ne convient pas car les deux listes précédentes sont plus courtes ; la liste (2,2,1) ne convient pas car elle ne commence pas par 1, la liste (1,2,2) ne convient pas car elle ne finit pas par 1 ; la liste (1,3,1) ne convient pas car 3–1=2 ∉ {0,1}.

Je ne connaissais pas ce petit problème, très amusant ! Je n’ai pas cherché très loin, voilà ma solution en Python :

def _sum(n):
	""" Sum of 1, 2, ..., n. """
	return n * (n + 1) / 2

def frogger(N):
	L = [1]
	c = 1
	while c < N:
		# First, check if we can increase the speed
		n = L[-1] + 1
		if c + _sum(n) > N:
			# Next, check if we can keep current speed
			n = L[-1]
			if c + _sum(n) > N:
				# Otherwise, decrease the speed
				n = L[-1] - 1
		L.append(n)
		c += n
	return L

# Test
for i in range(1, 15):
	print(i, frogger(i))

Quelques explications. On remarque qu’à chaque étape kk, lorsque la grenouille se trouve sur un nénufar nn, trois choix se présentent : soit on augmente l’amplitude ak1a_{k-1} du saut précédent, soit on garde la même amplitude, soit on la réduit. Il reste exactement NnN-n nénufars à parcourir. Or, la grenouille doit pouvoir décélerer progressivement sur ces NnN-n nénufars restants. On doit donc avoir i=1akiNn,\sum_{i=1}^{a_k}i\le N-n, avec au choix ak=ak1+1a_k=a_{k-1}+1, ak=ak1a_k=a_{k-1} ou ak=ak11a_k=a_{k-1}-1.

+2 -0

Bravo @fifo. Tu as la main :)

Pour résoudre ce problème il faut remarquer qu’il s’agit de construire une partition d’un entier. Celle qui convient ici est une partition commençant et finissant par 1 puis qui « monte vers un maximum et redescend ensuite», partition où il va falloir doubler certains éléments.

Les nombres de la formes k2 (dont une partition solution est (1,2, … , k-1, k, k-1, … , 2, 1) )et k2+k y jouent un rôle particulier. C’est à partir de leur partition que l’on construit les autres en doublant/triplant un ou plusieurs de leurs éléments.

Je posterai une preuve un peu plus complète de cela pendant le WE.

Alors allons-y avec quelque chose d’un poil plus difficile !

Algo n°9

Le pont et la torche

Un groupe de nn aventuriers imprudents se fait surprendre par l’orage en pleine nuit. Pour rejoindre leur abri, ils n’ont pas d’autre choix que de passer par un fragile pont de cordes enjambant la rivière déchaînée. Le pont ne peut pas résister au poids de plus de deux personnes à la fois.

De plus, les aventuriers n’ont prévu qu’une seule torche, et chaque traversée du pont nécessite d’y voir clair. Bien évidemment, la rivière est trop large pour simplement s’échanger la torche en la lançant sur la rive opposée; l’un des aventuriers doit se dévouer à chaque étape pour porter la torche à ses camarades.

Pour couronner le tout, le voyage a plus ou moins épuisé nos pauvres aventuriers, et ils ne sont pas tous capables de traverser le pont instable au même rythme. Chaque personne ii a donc un temps de traversée tit_i.

Initialement, tous les aventuriers se trouvent sur la rive gauche. Le but est de trouver comment faire passer les nn aventuriers sur la rive droite en un minimum de temps.

Entrée

Un vecteur t=(t1,t2,,tn)t=(t_1,t_2,\ldots,t_n) de taille nn donnant les temps de traversée.

Sortie

Le temps total de traversée TT, ainsi qu’une suite d’états x=(x1,x2,x3,)x=(x_1,x_2,x_3,\ldots)xjx_j donne les indices des aventuriers traversant le pont à l’étape jj (on notera qu’un jj impair représente une traversée de la gauche vers la droite, et inversement pour un jj pair).

Exemple

Pour t=(10,20,30)t=(10,20,30), on a T=60T=60 et x=((1,2),(1),(1,3))x=((1,2),(1),(1,3)):

  1. 11 et 22 traversent le pont en premier, pour un temps de max(t1,t2)=20\max(t_1,t_2)=20.
  2. 11 revient porter la torche à 33, pour un temps de t1=10t_1=10.
  3. Finalement, 11 et 33 traversent le pont pour un temps de max(t1,t3)=30\max(t_1,t_3)=30.
Pour aller plus loin

Il est possible de généraliser le problème en ajoutant un paramètre CC représentant la capacité du pont (ce qui signifie que CC personnes peuvent traverser en même temps, et non plus seulement 2).

Je ne sais pas s’il y a une subtilité que je n’ai pas saisie (du genre un nombre maximum de traversées par personne) mais en l’état j’ai l’impression que l’algorithme consiste juste à sélectionner la personne la plus rapide et à lui faire faire tous les trajets.

Je ne sais pas s’il y a une subtilité que je n’ai pas saisie (du genre un nombre maximum de traversées par personne) mais en l’état j’ai l’impression que l’algorithme consiste juste à sélectionner la personne la plus rapide et à lui faire faire tous les trajets.

entwanne

C’est l’idée intuitive que la majorité des gens essaye en premier. Mais elle n’est pas optimale : soit t=(1,2,5,10)t=(1,2,5,10). Si on laisse 11 effectuer tous les trajets on a par exemple x=((1,2),(1),(1,3),(1),(1,4))x=((1,2),(1),(1,3),(1),(1,4)), pour T=19T=19. Pour obtenir la solution optimale, il faut que 11 n’effectue pas tous les trajets : x=((1,2),(1),(3,4),(2),(1,2))x=((1,2),(1),(3,4),(2),(1,2)), pour T=17T=17.

Attention car c’est un problème beaucoup plus compliqué qu’il n’y paraît. Pour le problème généralisé, il existe une solution dont la complexité en temps est O(n2)O(n^2) dans le pire cas, avec nn le nombre d’aventuriers, mais elle n’a été trouvée qu’en 2008.

Je conseille d’essayer de le résoudre de manière "naïve", c’est-à-dire en effectuant une recherche sur l’espace d’états.

Effectivement, c’est bien moins trivial que ce que je pensais. Et grâce à la littérature (que j’ai finie par réussir à trouver), voici un autre test qui montre à quel point c’est plus complexe que ce que je pensais:

    CHECK(solve({1, 2, 5,10}) == 17); // 1,2 -> | 1 <- | 5,10 -> | 2 <- | 1,2 ->
    CHECK(solve({1, 3, 4, 6}) == 15); // 1,3 -> | 1 <- | 1,4  -> | 1 <- | 1,6 ->

Effectivement, c’est bien moins trivial que ce que je pensais. Et grâce à la littérature (que j’ai finie par réussir à trouver), voici un autre test qui montre à quel point c’est plus complexe que ce que je pensais:

    CHECK(solve({1, 2, 5,10}) == 17); // 1,2 -> | 1 <- | 5,10 -> | 2 <- | 1,2 ->
    CHECK(solve({1, 3, 4, 6}) == 15); // 1,3 -> | 1 <- | 1,4  -> | 1 <- | 1,6 ->

lmghs

Pour le dernier cas on peut aussi imaginer un ( (1, 6), 1, (1, 4), 1, (1, 3) ). Dans tous les cas que j’ai essayé on peut toujours aboutir à une solution optimale du type :

  • on fait partir les deux plus rapides, un des deux revient ;
  • on fait partir les deux plus lents, le plus rapides de l’autre côté revient ;
  • on recommence jusqu’à un certain point où l’on fait partir le plus rapide avec le plus lent qui reste et l’autre revient.

Le tout est de déterminer le seuil pour « le certain point » .

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte