Marathon d'algorithmes

a marqué ce sujet comme résolu.

Bonjour,

je me suis inspiré de ce topic pour la création de celui-ci.

Marathon d’algorithmes

Le but de ce topic est de s’entraîner à la réalisation d’algorithmes.

Déroulement du topic

  • Je commence par énoncer un problème.
  • Si un membre parvient à fournir un algorithme qui répond à mon problème sous trois jours, c’est à son tour de poser un problème.
  • Sinon, alors je poste une solution à mon problème (en utilisant une balise secret) et je donne la main à un membre de mon choix.
Cas particulier
  • Si un membre ayant trouvé un algorithme valide n’a aucun problème à poser en retour, alors il est en mesure de donner la main à n’importe qui d’autre.

Règles générales

  • N’importe quel langage pourra être utilisé : n’oubliez ceci-dit pas de préciser le langage que vous utilisez !
  • Les problèmes ne doivent pas porter sur un langage spécifique, on doit pouvoir apporter une réponse avec n’importe quel langage (ou du moins une grande majorité)
  • Il peut exister plusieurs solutions à un problème avec différents niveaux de complexité. Libres à vous de proposer différentes solutions !

J’ouvre le bal avec un premier algorithme relativement simple :

Algo n°1

Soit un tableau arr formé d’entiers relatifs.

Un sous-tableau de arr est un tableau formé d’éléments de arr. Par exemple, si arr = [1, 2, 3] alors sont des sous-tableaux de arr :

  • [1, 2, 3]
  • [1, 3]
  • [2, 3]
  • [2]
  • etc…

Un sous-tableau de arr est dit contiguë si il est formé d’éléments de arr situés côte à côte. En reprenant notre précédent exemple :

  • [1, 2, 3] est contiguë
  • [1, 3] n’est pas contiguë
  • [2, 3] est contiguë
  • [2] est contiguë
  • etc…

Écrire une fonction get_max_sub_arr(arr) qui calcule le sous-tableau contiguë de arr dont la somme des éléments est maximale. Cette fonction retournera la somme en question.

Voici quelques exemples :

  • Pour [1, 2, 3] la fonction retourne 1 + 2 + 3 = 6
  • Pour [-2, -1, 1, 2] la fonction retourne 1 + 2 = 3
  • Pour [2, -1, 2, 3, -9] la fonction retourne 2 - 1 + 2 + 3 = 6
  • Pour [-1, 2, 3, -9, 11] la fonction retourne 11
  • etc…

Dans le cas où tous les éléments du tableau sont négatifs, la fonction renverra 0, par convention.

Bonus : trouver une solution O(n)\mathcal{O}(n)

+3 -0

Salut,

Je pense à :

const getMaxSubArray = (arr) => {
  let minPrefix = 0;
  let currentPrefix = 0;
  let maxValue = 0;
  for (let i = 0; i < arr.length; ++i) {
    currentPrefix += arr[i];
    if (currentPrefix < minPrefix) {
      minPrefix = currentPrefix;
    }
    maxValue = Math.max(maxValue, currentPrefix - minPrefix)
  }
  return maxValue;
}

Voici ma solution en O(n2)O(n^2) ^^:

int get_max_sub_arr(std::vector<int> arr) {
  int max {0}, test {0};
	for (int i = 0 ; i < arr.size() ; ++i) {
		for (int j = i ; j < arr.size(); ++j) {
			test += arr[j];
			if (max < test) max = test;
		}
		test = 0;
	}
	if (std::all_of(arr.begin(), arr.end(), [](int i) { return i < 0; })) {
		return 0;
	}
	return max;
}
+0 -0

@Jeph: Ta solution est très élégante.
J’essaye de comprend le principe.

Pourquoi la sommes cumulée des valeurs moins la plus petite sommes cumulée négative donnerait la somme maximal des sous tableaux ?

J’ai le sentiment qu’il y a quelque chose qui s’annule mais pas précisément quoi.

+0 -0

En notant SiS_i la somme des éléments jusqu’à l’indice ii, le somme des éléments dans l’intervalle ]i;j]]i; j] est SjSiS_j - S_i. Pour maximiser SjSiS_j - S_i, il faut maximiser SjS_j et minimiser SiS_i, tout en conservant i<ji < j.

Dans le code de @Jeph, currentPrefix correspond à SjS_j et minPrefix correspond à SiS_i.

Edit: ma solution en Haskell.

Ça fait bien longtemps que j’y ai pas touché, donc c’est pas forcément super idéomatique.

solve :: (Num a, Ord a) => [a] -> a
solve xs = impl_ 0 0 0 xs
  where impl_ _ _ best [] = best
        impl_ si sj best (x:xs) =
          impl_ (min (sj + x) si) (sj + x) (max (sj - si) best) xs
+0 -0

En notant SiS_i la somme des éléments jusqu’à l’indice ii, le somme des éléments dans l’intervalle ]i;j]]i; j] est SjSiS_j - S_i. Pour maximiser SjSiS_j - S_i, il faut maximiser SjS_j et minimiser SiS_i, tout en conservant i<ji < j.

Berdes

Ça parait bien plus claire comme ça ! Effectivement SjSiS_j - S_i correspond bien à la somme du sous tableau de ii à jj.

Merci :)

+0 -0

Un truc amusant : en changeant les let par des var en en rajoutant un point-virgule à la ligne 10 de la solution de @Jeph, on obtient un corps de fonction qui fonctionne en JS et en JavaScript :

public int computeMaxSubListO1(int[] arr) {
    var minPrefix = 0;
    var currentPrefix = 0;
    var maxValue = 0;
    for (var i = 0; i < arr.length; ++i) {
        currentPrefix += arr[i];
        if (currentPrefix < minPrefix) {
            minPrefix = currentPrefix;
        }
        maxValue = Math.max(maxValue, currentPrefix - minPrefix);
    }
    return maxValue;
}

@Test
void computeMaxSubListO1() {
    Algo1 a = new Algo1();
    assertEquals(6, a.computeMaxSubListO1(new int[]{1, 2, 3}));
    assertEquals(3, a.computeMaxSubListO1(new int[]{-2, -1, 1, 2}));
    assertEquals(6, a.computeMaxSubListO1(new int[]{2, -1, 2, 3, -9}));
    assertEquals(11, a.computeMaxSubListO1(new int[]{-1, 2, 3, -9, 11}));
}

J’ai rajouté comme quoi il faudrait préciser le langage utilisé à chaque proposition d’algorithme.

Sinon, voici un équivalent python de la solution O(n2)\mathcal{O}(n^2) :

def get_max_sub_arr(*arr):
    max_sum = 0
    for index, i in enumerate(arr):
        sum_test = 0
	for j in arr[index:]:
	    sum_test += j
	    max_sum = max([sum_test, max_sum])
    return max_sum

Il me semble que la condition suivante dans le code de @imlambda soit inutile :

if (std::all_of(arr.begin(), arr.end(), [](int i) { return i < 0; })) {
    return 0;
}

En effet, si tu défini max à 0 avant d’entrer dans la première boucle, alors dans le cas où tous les éléments sont négatifs, jamais la condition sum > max ne sera vérifiée (car une somme d’entiers négatifs sera toujours inférieure à 0) et donc la fonction retournera max = 0.

Sinon, celle proposée par @Jeph est en effet très jolie !

Je lui donne donc la main. :)

Je lui donne donc la main. :)

Tchaïkovski

Allons-y dans ce cas, avec quelque chose de simple également (je n’avais pas trop d’idées d’algo…) :

Algo n°2

Soit un tableau arr formé d’entiers naturels

La position initiale du joueur est en 0.

Au début de chaque étape, le joueur est positionné sur une case i, il peut atteindre n’importe quelle case entre i et i + arr[i]

Question : le joueur peut-il atteindre la dernière case du tableau ?

Quelques examples :

  • Pour [1, 0, 1] la fonction retourne Faux

  • Pour [1, 1, 1] la fonction retourne Vrai

  • Pour [2, 0, 1] la fonction retourne Vrai

  • Pour [2, 4, 0, 0, 1] la fonction retourne Vrai

2 solutions en C++, O(n2)\mathcal{O}(n^2) et O(n)\mathcal{O}(n).

Edit: correction, c’est du O(n2)\mathcal{O}(n^2) dans le pire cas. Faut que je réfléchisse s’il y a moyen de faire mieux.

Edit2: Trouvé en O(n)\mathcal{O}(n).

C’est un problème de graphe où on veut trouver un chemin possible du début de l’array vers la fin. Première implémentation simple en O(n2)\mathcal{O}(n^2) avec un BFS plutôt qu’un DFS pour permettre de plus facilement faire évoluer l’algo pour trouver le chemin le plus court si nécessaire.

bool solve(const std::vector<int>& arr) {
  if (arr.empty()) return false;

  std::vector<bool> visited(arr.size(), false);
  std::deque<int> to_visit;
  to_visit.push_back(0);
  while (!to_visit.empty() && !visited.back()) {
    const int i = to_visit.front();
    to_visit.pop_front();
    if (visited[i]) continue;
    visited[i] = true;
    for (int j = i + 1; j <= i + arr[i] && j < arr.size(); ++j) {
      if (!visited[j]) to_visit.push_back(j);
    }
  }
  return visited.back();
}

Le fait que l’on ne puisse se déplacer que vers la droite veux dire que le graphe est acyclique et qu’il y a moyen de faire mieux que Dijkstra. Avec un peu de programmation dynamique, on se retrouve avec une solution en O(n)\mathcal{O}(n):

bool solve(const std::vector<int>& arr) {
  if (arr.empty()) return false;

  int max_reachable = 0;
  for (int i = 0; i <= max_reachable && max_reachable < arr.size() - 1; ++i) {
    max_reachable = std::max(max_reachable, i + arr[i]);
  }
  return max_reachable >= arr.size() - 1;
}
+0 -0

En Ruby.

On note max l’indice maximum auquel on peut aller, et i nous permet de parcourir le tableau. Au début, i et max valent tous les deux 0, et à chaque étape, le nouveau maximum est mis à jour : soit on peut aller plus loin car i + a[i] > max, soit ce n’est pas le cas et le maximum reste le même. On a gagné si max est plus grand ou égal à a.size - 1.

def last_position(ary)
  max = 0
  i = 0
  while i <= max && i < ary.size
    max = [max, ary[i] + i].max
    i += 1 
  end
  return [max, ary.size - 1].min
end

L = [
  []
  [1, 0, 1],
  [1, 1, 1],
  [2, 0, 1],
  [2, 4, 0, 0, 1]
]

L.each { |ary| puts "#{ary} : #{last_position(ary) == ary.size - 1}" } 

J’aurais pu faire un return dans la boucle quand je vois que mon nouveau max permet d’atteindre la fin du tableau. De plus, ici je renvoie l’indice maximal qu’on peut atteindre et pas si on a atteint la fin du tableau.

@Berdes, ta première solution ne fonctionne pas sur un tableau vide, non ? EDIT : Ah non, c’est bon, tu as rajouté un test.

EDIT 2 : je crois que je suis fatigué… Pour moi, il faudrait renvoyer true sur le tableau vide ?

+0 -0

Humm, je crois que j’ai pas compris un truc.

#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>


static int max(int a, int b) { return a > b ? a : b; }

bool est_un_chemin(unsigned int* tab, size_t n) {
    int m = 0;

    if( n <= 1)
        return true;

    for(size_t i = 0 ; i <=m && i < n; ++i)
        m = max(tab[i] + i, m);

    return m >= (n - 1);
}

int main(void) {

    unsigned int tab_1[] = {1, 0, 1};
    unsigned int tab_2[] = {1, 1, 1};
    unsigned int tab_3[] = {2, 0, 1};
    unsigned int tab_4[] = {2, 4, 0, 0, 1};
    unsigned int tab_5[] = {3, 2, 1, 0, 0};

    unsigned int *tabs[] = {tab_1, tab_2, tab_3, tab_4, tab_5};
    unsigned int sizes[] = {3, 3, 3, 5, 5};

    for(size_t i = 0 ; i < 5 ; i++) {
        if( est_un_chemin(tabs[i], sizes[i]) )
            puts("True");
        else 
            puts("False");
    }

    return EXIT_SUCCESS;
}

Je considère du coup que pour le tableau [0], ben c’est vrai car la première est la dernière case. ¯_(ツ)_/¯

@Karnaj: J’ai regardé ta solution, on a le même algo je crois. Moi en C. Il te manque une virgule ligne 13.

+1 -0

Top ! Désolé pour le manque d’indication pour le cas du tableau vide.

Et pour un tableau qui contient un seul élément l’algo doit bien retourner True. Vos trois solutions sont similaires et fonctionnent toutes !

@ache Pour chipoter, ta condition ligne 11->12 n’est pas nécessaire puisque forcément vérifier par ta ligne 17

À ton tour de proposer un exercice @Berdes !

+1 -0

@Jeph, pour n == 1 oui, mais pour n == 0 (tableau vide) ça ne marche pas justement. Ça va faire un underflow. n - 1 == UINT_MAX or UINT_MAX > 0.

De plus, j’aime m’assurer que je ne déréférence pas un tableau vide. ^^

Après, j’aurais pu utiliser des types signés. D’ailleurs m aurait du être unsigned int.

@Berdes: Effectivement ton interprétation pour le tableau vide me semble … Évidente. Et pourtant ce n’est pas ce que j’ai implémenté.

+1 -0
Algo n°3

Planification d’exécution de tâches dans un groupe de machines

Chaque tâche possède:

  • un identifiant unique (chaîne de caractère ou entier, suivant les préférences)
  • un temps d’exécution (nombre entier)
  • la liste des tâches (identifiants) qui doivent être fini avant que celle-ci ne puisse commencer.

Étant donné une liste de tâches et un nombre de machine, planifiez l’exécution des tâches de manière à ce que la tâche qui se fini en dernier se finisse au plus vite. Chaque machine ne peut exécuter qu’une seul tâche à la fois.

Edit: pour ceux qui ne se sentent pas d’implémenter une solution optimale, une "bonne" solution est aussi possible. À vous de juger à quel point une solution est proche/loin d’une solution optimale.

Entrée:

  • une liste de tâches
  • un nombre de machine

Sortie:

  • une liste indiquant comment les tâches doivent être planifiés. Chaque élément contiendra: une identifiant de tâche, le numéro de machine sur laquelle exécuter la tâche et le moment où la tâche doit démarrer.

On peut supposer que l’entrée est valide (nombre de machine strictement positif, pas de dépendances circulaires, les identifiants sont effectivement unique, etc).

Exemple d’entrées/sorties

Tâches: [{id: 1, durée: 10, dépendances: []}]
Nombre de machines: 1
Sortie: [{id: 1, machine: 0, démarrage: 0}]

Tâches: [{id: 1, durée: 5, dépendances: []},
         {id: 2, durée: 10, dépendances: []}]
Nombre de machines: 2
Sortie: [{id: 1, machine: 0, démarrage: 0},
         {id: 2, machine: 1, démarrage: 0}]

Tâches: [{id: 1, durée: 5, dépendances: []},
         {id: 2, durée: 5, dépendances: [1]},
         {id: 3, durée: 10, dépendances: []}]
Nombre de machines: 2
Sortie: [{id: 1, machine: 0, démarrage: 0},
         {id: 2, machine: 0, démarrage: 5},
         {id: 3, machine: 1, démarrage: 0}]

Conseil de résolution

Je conseille fortement de commencer par écrire une fonction qui va vérifier qu’une sortie est correcte avant de commencer à résoudre le problème.

Pour aller plus loin

Pour ceux qui trouvent le problème trop simple ou qui veulent aller plus loin, on rajoute une contrainte: la sortie de chaque tâche met du temps à se propager aux autres machines. Une tâche qui a une dépendance exécuté sur la machine 0 pourra commencer à s’exécuter sur la machine 0 immédiatement après que sa dépendances ait fini, mais devra attendre en plus le délai de propagation pour s’exécuter sur une autre machine.

Voici quelques exemples d’entrées/sorties pour bien clarifier:

Tâches: [{id: 1, durée: 5, dépendances: []},
         {id: 2, durée: 4, dépendances: [1]},
         {id: 3, durée: 5, dépendances: [1]}]
Nombre de machines: 2
Délai de propagation: 2
Sortie: [{id: 1, machine: 0, démarrage: 0},
         {id: 2, machine: 1, démarrage: 7},  // 5 unités de temps pour que 1 ai fini + 2 pour le délai de propagation
         {id: 3, machine: 0, démarrage: 5}]  // Sur la même machine, il n'y a pas de délai de propagation

Tâches: [{id: 1, durée: 5, dépendances: []},
         {id: 2, durée: 4, dépendances: []},
         {id: 3, durée: 10, dépendances: [1, 2]}]
Nombre de machines: 2
Délai de propagation: 1
Sortie: [{id: 1, machine: 0, démarrage: 0},
         {id: 2, machine: 1, démarrage: 0},
         {id: 3, machine: 0, démarrage: 5}]
// Les données de la dépendance à 1 sont dispo sur la machine 0 immédiatement après que 1 ait fini.
// Les données de la dépendance à 2 sont dispo sur la machine 0 1 unité de temps après que 2 ait fini sur la machine 1.

Pour ceux qui veulent encore aller plus loin, on peut définir plusieurs délais différents, suivant que les machines sont au sein d’une même rack, au sein du même data center ou dans des data center différents. Ou plus généralement, on peut définir le délai de propagation à l’aide d’un graphe complet où chaque nœud correspond à une ou plusieurs machines et chaque arête indique le temps de propagation entre ces deux groupes de machines.

+4 -0

N’étant pas chez-moi hier, je n’ai pas pu me pencher sur le problème de @Jeph !

Du coup, il y a un truc que je ne comprends pas dans l’énoncé :

La position initiale du joueur est en 0.

Au début de chaque étape, le joueur est positionné sur une case i, il peut atteindre n’importe quelle case entre i et i + arr[i]

Quand tu parles de 0, tu parles de l’index 0, donc au début du tableau, c’est ça ?

Ensuite, quand tu dis que le joueur est positionné sur une case i, tu parles de l’index ou de la valeur de la case ?

Car ça change pas mal de choses selon le contexte. :D

@Tchaïkovski: oui, je pense que le 0 représente la première case du tableau et pas une case qui contient 0. La deuxième interprétation n’est pas très intéressante puisque cela voudrais dire que le joueur ne peut jamais bouger.

@fifo: Je ne connaissait pas cette notation, mais oui, le problème n’a pas de solution (connu) en temps polynomial. Je ne pense pas qu’implémenter une solution soit si difficile que ça, mais vu que ça reste quand même nettement plus difficile que les problèmes précédents je vais un changer le problème pour passer d’une solution optimale à une "bonne" solution.

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