Partition
Une utilisation rigolote de try_for_each
…
On prend en entrée une tranche de i32
.
On cherche à savoir s’il est possible de la décomposer
en trois sous-tranches S1, S2, S3 telles que:
- les sommes des elements de chaque tranche sont égales
- toutes les tranches sont disjointes
- l’union des tranches forme S
Attention il s’agit de tranches, le problème n’est donc pas NP-dur.
Voici un petit jeu de tests:
fn main() {
let v = vec![1, 5, -1, 2, 3, 2, 3, -2, 2];
assert!(partition3(&v));
let w = vec![1, 3, 2, 4, 2, 3, 5, 3];
assert!(!partition3(&w));
let x = vec![5, 6, 5, -1];
assert!(!partition3(&x));
}
Le but du jeu est d’écrire un code fonctionnel de coût linéaire.
Solution
Commençons par l’idée principale. On commence par calculer la somme de toute la tranche, de manière à calculer la quantité à mettre dans chaque sous tranche:
fn partition3(tranche: &[i32]) -> bool {
let somme = tranche.iter().sum();
On va ensuite itérer sur toutes les sommes partielles de s
à l’aide de scan.
Les sommes partielles commencent à 0 et terminent à somme
.
Si il est possible de partitionner alors,
- à la fin de S1, la somme partielle est
somme/3
- à la fin de S2, la somme partielle est
2*somme/3
Cette propriété est valable même si plusieurs solutions sont possibles.
somme % 3 == 0
&& sous_sequence(
vec![somme / 3, 2 * somme / 3, somme],
tranche.iter().scan(0, |s, e| {
*s += e;
Some(*s)
}),
)
}
Il nous reste juste à vérifier que la séquence des tiers de somme est une sous-séquence de toutes les sommes partielles.
Est-ce que vous voyez comment faire (j’utilise try_for_each)?
Sous Séquence
Voici ma solution:
fn sous_sequence<T, I, J>(i: I, j: J) -> bool
where
T: Eq,
I: IntoIterator<Item = T>,
J: IntoIterator<Item = T>,
{
let mut iter_j = j.into_iter();
i.into_iter().all(|ei| {
iter_j
.try_for_each(|ej| if ei == ej { Err(()) } else { Ok(()) })
.is_err()
})
}
Le all
me permet de vérifier que tous les éléments i sont bien dans j.
Notons que l’on peut s’arrêter avant la fin de j mais que c’est une ‘feature’, la
somme des éléments restants valant alors 0.
Le truc délicat c’est qu’il faut à chaque fois repartir du dernier endroit où l’on s’était
arrêté lors du parcours de j.
Toutes les opérations en try_....
sur les itérateurs permettent exactement ça.
Si on regarde le prototype:
fn try_for_each<F, R>(&mut self, f: F) -> R where
F: FnMut(Self::Item) -> R,
R: Try<Ok = ()>,
on se rend compte que l’itérateur n’est pas consommé mais emprunté en écriture.
Le try_fold
en particulier est important en rust
et il est conseillé de l’implémenter
manuellement lorsque l’on développe un itérateur s’il est possible d’avoir une implémentation
meilleure que celle par défaut.