Eu estava curioso para saber como std:next_permutation
foi implementado, então extraí a gnu libstdc++ 4.7
versão e limpei os identificadores e a formatação para produzir a seguinte demonstração ...
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
template<typename It>
bool next_permutation(It begin, It end)
{
if (begin == end)
return false;
It i = begin;
++i;
if (i == end)
return false;
i = end;
--i;
while (true)
{
It j = i;
--i;
if (*i < *j)
{
It k = end;
while (!(*i < *--k))
/* pass */;
iter_swap(i, k);
reverse(j, end);
return true;
}
if (i == begin)
{
reverse(begin, end);
return false;
}
}
}
int main()
{
vector<int> v = { 1, 2, 3, 4 };
do
{
for (int i = 0; i < 4; i++)
{
cout << v[i] << " ";
}
cout << endl;
}
while (::next_permutation(v.begin(), v.end()));
}
A saída é a esperada: http://ideone.com/4nZdx
Minhas perguntas são: Como isso funciona? Qual é o significado de i
, j
e k
? Que valor eles têm nas diferentes partes da execução? O que é um esboço de uma prova de sua correção?
Claramente, antes de entrar no loop principal, ele apenas verifica os casos triviais de lista de 0 ou 1 elemento. Na entrada do loop principal, i está apontando para o último elemento (não um fim passado) e a lista tem pelo menos 2 elementos.
O que está acontecendo no corpo do loop principal?
c++
c++11
permutation
stl-algorithm
lexicographic
Andrew Tomazos
fonte
fonte
Respostas:
Vejamos algumas permutações:
Como vamos de uma permutação para a próxima? Em primeiro lugar, vamos ver as coisas de maneira um pouco diferente. Podemos ver os elementos como dígitos e as permutações como números . Vendo o problema desta forma , queremos ordenar as permutações / números em ordem "crescente" .
Quando pedimos números, queremos "aumentá-los pelo menor valor". Por exemplo, ao contar, não contamos 1, 2, 3, 10, ... porque ainda há 4, 5, ... entre e embora 10 seja maior que 3, faltam números que podem ser obtidos por aumentando 3 por um valor menor. No exemplo acima, vemos que
1
permanece como o primeiro número por um longo tempo, pois há muitas reordenações dos últimos 3 "dígitos" que "aumentam" a permutação em uma quantidade menor.Então, quando finalmente "usamos" o
1
? Quando não houver mais permutações dos últimos 3 dígitos.E quando não há mais permutações dos últimos 3 dígitos? Quando os últimos 3 dígitos estão em ordem decrescente.
Aha! Esta é a chave para entender o algoritmo.Nós só mudamos a posição de um "dígito" quando tudo à direita está em ordem decrescente, porque se não estiver em ordem decrescente, então ainda existem mais permutações para ir (ou seja, podemos "aumentar" a permutação em uma quantidade menor) .
Vamos agora voltar ao código:
Das 2 primeiras linhas do loop,
j
é um elemento ei
é o elemento anterior a ele.Então, se os elementos estiverem em ordem crescente, (
if (*i < *j)
) faça algo.Caso contrário, se tudo estiver em ordem decrescente, (
if (i == begin)
) então esta é a última permutação.Caso contrário, continuamos e vemos que j e i são essencialmente decrementados.
Agora entendemos o
if (i == begin)
parte, então tudo que precisamos entender é aif (*i < *j)
parte.Observe também: "Então se os elementos estão em ordem crescente ..." o que apóia nossa observação anterior de que só precisamos fazer algo para um dígito "quando tudo à direita está em ordem decrescente". A ordem crescente
if
declaração de é essencialmente encontrar o lugar mais à esquerda, onde "tudo à direita está em ordem decrescente".Vejamos novamente alguns exemplos:
Vemos que quando tudo à direita de um dígito está em ordem decrescente, encontramos o próximo dígito maior e o colocamos na frente e, em seguida, colocamos os dígitos restantes em ordem crescente .
Vejamos o código:
Bem, como as coisas à direita estão em ordem decrescente, para encontrar o "próximo dígito maior" nós apenas temos que iterar a partir do final, que vemos nas primeiras 3 linhas de código.
Em seguida, trocamos o "próximo dígito maior" para a frente com a
iter_swap()
instrução e, como sabemos que esse dígito foi o próximo maior, sabemos que os dígitos à direita ainda estão em ordem decrescente, portanto, para colocá-lo em ordem crescente, nós apenas temos que fazerreverse()
isso.fonte
Combinatorics
, mas este é o mais clássico.A implementação do gcc gera permutações em ordem lexicográfica. A Wikipedia explica isso da seguinte maneira:
fonte
Knuth aprofunda esse algoritmo e suas generalizações nas seções 7.2.1.2 e 7.2.1.3 de The Art of Computer Programming . Ele o chama de "Algoritmo L" - aparentemente, data do século XIII.
fonte
Aqui está uma implementação completa usando outros algoritmos de biblioteca padrão:
Demo
fonte
is_final_permutation
é mais informativo do quebegin == end - 1
. Chamaris_sorted_until
/upper_bound
separar a lógica de permutação dessas operações e torna isso muito mais compreensível. Além disso, upper_bound é uma pesquisa binária, enquantowhile (!(*i < *--k));
é linear, portanto, tem mais desempenho.Há uma implementação possível autoexplicativa no uso de cppreference
<algorithm>
.Altere o conteúdo para a próxima permutação lexicograficamente (no local) e retorne verdadeiro se existir, caso contrário, classifique e retorne falso se não existir.
fonte