BCM0505-15 - Processamento da Informação

Aula A03 – 01/05: Permutações em vetores

  • Permutações cíclicas, alternantes e adjacentes.
  • Enumeração de permutações em ordem lexicográfica.
  • Geração de uma permutação aleatória de forma uniforme.

Permutações

Uma permutação para um conjunto finito $A=\{a_0,a_1,\ldots,a_{n-1}\}$ é uma função bijetora $\sigma:A\to A$ que mapeia elementos distintos de $A$ a elementos distintos de $A$. O mapeamento estabelecido por $\sigma$ pode ser interpretado como substituições entre os elementos de $A$. Informalmente, permutações são rearranjos ou embaralhamentos dos elementos de $A$.

É conveniente indexar os elementos de $A$ — como fizemos — e listá-los como uma sequência, com $a_i$ na posição $i$, para $0\leq i<n$: $$\langle a_0,a_1,\ldots,a_{n-1}\rangle =: \mathsf{id}.$$

Definindo $[\![n]\!]:=\{0,1,\ldots,n-1\}$, podemos abusar da notação e reinterpretar $\sigma$ como uma função bijetora $\sigma:[\![n]\!]\to[\![n]\!]$ que mapeia índices a índices. A sequência $\mathsf{id}$ acima é denominada permutação identidade, pois $$\sigma(A) := \langle a_{\sigma(0)},a_{\sigma(1)},\ldots,a_{\sigma(n-1)}\rangle = \langle a_0,a_1,\ldots,a_{n-1}\rangle,$$ com $\sigma(i)=i$.

A título de ilustração:

  • uma permutação $\pi:[\![n]\!]\to[\![n]\!]$ para $A$ definida por $\pi(i)=n-i-1$ equivale a $$\pi(A) := \langle a_{\pi(0)},a_{\pi(1)},\ldots,a_{\pi(n-1)}\rangle = \langle a_{n-1},a_{n-2},\ldots,a_0\rangle,$$ que é a permutação reversa à $\mathsf{id}$;
  • uma permutação $\tau:[\![n]\!]\to[\![n]\!]$ para $A$ definida por $\tau(i)=(i+1)\!\!\mod n$ equivale a $$\tau(A) := \langle a_{\tau(0)},\ldots,a_{\tau(n-2)},a_{\tau(n-1)}\rangle = \langle a_1,\ldots,a_{n-1},a_0\rangle,$$ que é a permutação cíclica à esquerda de ordem $1$ à $\mathsf{id}$;
  • uma permutação $\rho:[\![n]\!]\to[\![n]\!]$ para $A$ definida por $\rho(i)=(i-2)\!\!\mod n$ equivale a $$\rho(A) := \langle a_{\rho(0)},a_{\rho(1)},a_{\rho(2)},\ldots,a_{\rho(n-1)}\rangle = \langle a_{n-2},a_{n-1},a_0,\ldots,a_{n-3}\rangle,$$ que é a permutação cíclica à direita de ordem $2$ à $\mathsf{id}$;
  • uma permutação $\theta:[\![n]\!]\to[\![n]\!]$ para $A$ definida por $$\theta(i)=\begin{cases} i/2 & \text{se}~i~\text{é par}, \\
    \lfloor n/2\rfloor + \lfloor i/2\rfloor& \text{se}~i~\text{é ímpar}, \end{cases}$$ equivale a $$\theta(A) := \langle a_{\theta(0)},a_{\theta(1)},a_{\theta(2)},\ldots,a_{\theta(n-1)}\rangle = \langle a_0,a_2,a_4,\ldots,a_1,a_3,a_5,\ldots\rangle,$$ uma permutação em que os elementos de índice par em $\mathsf{id}$ ocorrem antes dos elementos de índice ímpar, ambos os grupos de índices de forma crescente.

Não é difícil perceber que a composição de permutações é uma permutação, que a permutação identidade ($\mathsf{id}$) age como elemento neutro da composição, e que para cada permutação $\sigma$ existe uma única permutação inversa $\sigma^{-1}$ tal que $\sigma\circ\sigma^{-1}=\sigma^{-1}\circ\sigma=\mathsf{id}$.

Representação em Python

Podemos utilizar listas para representar permutações. Armazenamos o $i$-ésimo elemento da sequência descrevendo a permutação na $i$-ésima posição da lista. Por exemplo, a permutação identidade $\mathsf{id} = \langle a_0,a_1,\ldots,a_{n-1}\rangle$ pode ser representada por uma lista $$A[0\,..n-1]=[a_0,a_1,\ldots,a_{n-1}].$$ A permutação reversa $\pi(\mathsf{id})=\langle a_{n-1},a_{n-2},\ldots,a_0\rangle$ pode ser representada por outra lista $$B[0\,..n-1]=[a_{n-1},a_{n-2},\ldots,a_0].$$ Observe que a orderm dos elementos na lista corresponde à ordem dos elementos na sequência — como esperado — e não à indexação original dos elementos. Isto é, $B[0]$ é igual a $a_{n-1}$ e não a $a_0$.

Em certas circunstâncias, é conveniente representarmos uma permutação por uma lista $C[1\,..n]$ em vez de $C[0\,..n-1]$. Nestes casos, $C[1\,..n]$ deve ser interpretada como uma lista $C[0\,..n]$ com $n+1$ elementos em que ignoramos a posição $0$.

Ordem entre permutações

Quando os elementos do conjunto $A$ são comparáveis, podemos estabelecer ordens entre diferentes permutações para $A$.

Dadas duas permutações $B[0\,..n-1]$ e $C[0\,..n-1]$ para $A=\{0,1,\ldots,n-1\}$, dizemos que $B$ é lexicograficamente menor que $C$, indicado por $B<C$, se existe um índice $j\in\{0,1,\ldots,n-1\}$ tal que

  • $B[i] = C[i]$ para $0\leq i<j$, e
  • $B[j] < C[j]$.

Por exemplo, $[1,2,3,4] < [1,3,2,4]$; $[1,2,3,4] < [1,3,4,2]$; e $[1,3,2,4] < [1,3,4,2]$.

Problemas

Problema A3.1: Pontos fixos e desarranjos.
Seja $A[1\,..n]$ uma permutação para $\{1,2,\ldots,n\}$. Um ponto fixo de $A$ é um índice $j$ tal que $A[j]=j$.
Escreva uma função que recebe $A$ e devolve uma lista com os pontos fixos de $A$.

def fixed_pts (A: [int]) -> [int]:
  P = []
  for i in range (1, len (A)):
    if A[i] == i: P.append (i)
  return P

Versão utilizando enumerate:

def fixed_pts (A: [int]) -> [int]:
  P = []
  for i, a in enumerate (A[1:]):
    if a == i: P.append (i)
  return P

Nota: uma permutação que não possui pontos fixos é chamada desarranjo.

Problema A3.2: Verificação de permutações.
Escreva uma função que recebe uma lista de ints $A[1\,..n]$ e determina se $A$ é uma permutação para $\{1,2,\ldots,n\}$.

Vamos utilizar uma lista auxiliar $B$.

def is_permutation (A: [int]) -> bool:
  n = len (A)
  B = [False] * n
  for i in range (1, n):
    if B[A[i]]: return False
    B[A[i]] = True
  for i in range (n):
    if not B[i]: return False
  return True

Nota: Python disponibiliza a função all() que recebe um iterável e devolve True se todos os seus elementos forem True ou se o iterável é vazio, e False em caso contrário. O código acima pode ser então reescrito como segue.

def is_permutation (A: [int]) -> bool:
  n = len (A)
  B = [False] * n
  for i in range (1, n):
    if B[A[i]]: return False
    B[A[i]] = True
  return all (B)

Problema A3.3: Permutações cíclicas I.
Uma permutação $A[0\,..n-1]$ de $\langle 0,1,\ldots,n-1\rangle$ é cíclica se existe um índice $j$ tal que $$A[0\,..j-1] = \langle n-j,n-j+1,\ldots,n-1\rangle \quad\text{e}\quad A[j\,..n-1] = \langle 0,1,\ldots,n-j-1\rangle.$$ Por exemplo, $\langle 0,1,2,3,4\rangle$ e $\langle 3,4,0,1,2\rangle$ são cíclicas; $\langle 0,3,4,1,2\rangle$ e $\langle 4,3,2,1,0\rangle$ não são.

Escreva uma função que recebe $A$ e determina se ela é uma permutação cíclica de $\langle 0,1\ldots,n-1\rangle$. Em caso afirmativo, devolva um tal índice $j$; em caso contrário, devolva $j=n$.

def cyclic_1 (A: [int]) -> int:
  j, n = 0, len (A)
  while j < n and A[j] != 0: j += 1
  if j == n: return n
  for i in range (1, n):
    if i != A[(i+j) % n]: return n
  return j

Problema A3.4: Permutações cíclicas II.
Dadas duas listas $A[0\,..n-1]$ e $B[0\,..n-1]$, dizemos que $B$ é uma permutação cíclica de $A$ se existe um índice $j$ tal que $$A[0\,..j-1] = B[n-j\,..n-1] \quad\text{e}\quad A[j\,..n-1] = B[0\,..n-j-1].$$ Se $B$ é uma permutação cíclica de $A$, claramente $A$ é uma permutação cíclica de $B$. Escreva uma função que recebe $A$ e $B$ e devolve um tal índice $j$ se $A$ e $B$ são permutações cíclicas, ou devolve $j=n$ em caso contrário.

Supomos que len (A) == len (B).

def cyclic_2 (A: [int], B: [int]) -> int:
  j, n = 0, len (A)
  while j < n and A[j] != B[0]: j += 1
  if j == n: return n
  for i in range (1, n):
    if B[i] != A[(i+j) % n]: return n
  return j

Problema A3.5: Permutações adjacentes.
Duas permutações $A[0\,..n-1]$ e $B[0\,..n-1]$ para $\{0,1,\ldots,n-1\}$ são adjacentes se existe um índice $j$ tal que $k=(j+1)\!\!\mod n$ e $$A[j]=B[k],\quad A[k]=B[j] \quad\text{e}\quad A[i]=B[i]~\text{para}~i\in\{1,2,\ldots,n\}\setminus\{j,k\}.$$ Em outras palavras, $A$ e $B$ diferem somente nas posições $j$ e $k$. Escreva uma função que recebe $A$ e $B$ e determina se elas são adjacentes.

Supomos que len (A) == len (B).

def are_adjacent (A: [int], B: [int]) -> bool:
  n, d = len (A), [] 
  for j in range (n):
    if A[j] != B[j]: d.append (j)

  if len (d) != 2 or (d[1] - d[0]) not in (1, n-1):
    return False

  return A[d[0]] == B[d[1]] and A[d[1]] == B[d[0]]

Problema A3.6: Permutações inversas I.
Duas permutações $A[1\,..n]$ e $B[1\,..n]$ para $\{1,2,\ldots,n\}$ são inversas se $A[B[i]]=B[A[i]]=i$ para todo índice $i$. Em outras palavras, se a composição de $A$ com $B$ ou de $B$ com $A$ resulta na permutação identidade. Escreva uma função que recebe $A$ e $B$ e determina se elas são inversas.

Supomos que len (A) == len (B).

def are_inverse (A: [int], B: [int]) -> bool:
  for i in range (1, len (A)):
    if A[B[i]] != i: return False
  return True  

Problema A3.7: Permutações inversas II.
Escreva uma função que recebe uma permutação $A[1\,..n]$ para $\{1,2,\ldots,n\}$ e devolve a permutação inversa a $A$.

def perm_invert (A: [int]) -> [int]:
  n = len (A)
  B = [0] * n
  for i in range (n):
    B[A[i]] = i
  return B

Problema A3.8: Permutações alternantes.
Uma permutação $A[0\,..n-1]$ é alternante se $$A[0] < A[1] > A[2] < A[3] > \cdots \lessgtr A[n-1] \qquad\text{ou}\qquad A[0] > A[1] < A[2] > A[3] < \cdots \gtrless A[n-1].$$ Escreva uma função que recebe $A$ e determina se ela é alternante.

Claramente, uma sequência vazia é não alternante, e uma sequência unitária é alternante. Esses dois casos particulares são tratados em separado. O caso à direita pode ser reduzido ao da esquerda multiplicando-se os valores por $-1$ (veja (*) no código).

def alternating (A: [int]) -> bool:
  n = len (A)
  if n <= 1: return [False, True][n]

  sgn = 1 if A[0] < A[1] else -1 # (*)
  for i in range (2, n):
    if sgn * (A[i] - A[i-1]) >= 0:
      return False
    sgn *= -1
  return True

Problema A3.9: Inversões em permutações.
Seja $A[1\,..n]$ uma permutação para $\{1,2,\ldots,n\}$. Dois índices $i<j$ são uma inversão em $A$ se $A[i]>A[j]$.
Escreva uma função que recebe $A$ e devolve o número de inversões em $A$.

Um algoritmo consistem em: (i) gere todos os pares $(i,j)$ tais que $0\leq i<j<n$; (ii) para cada par gerado, verifique se $A[i]>A[j]$; (iii) em caso afirmativo, o par $(i,j)$ é uma inversão; (iv) em caso negativo, ele pode ser desconsiderado.

# recebe uma lista A[1..n] e devolve uma lista contendo as inversões em A.
def invertions (A: [int]) -> [int]:
  Inv, n = [], len(A)
  for i in range (1, n-1):
    for j in range (i+1, n):
      if A[i] > A[j]:
        # o par (i,j), uma inversão, é incluido no final da lista Inv.
        Inv.append ((i,j))
  return Inv

Nota: O código acima determina todas as inversões em tempo $\Theta(n^2)$. Utilizando as ideias introduzidas na ordenação por intercalação (merge-sort), é possível realizar a tarefa em $\Theta(n\log n)$.

Problema A3.10: Permutações aleatórias uniformes.
Escreva uma função que devolve uma permutação aleatória de $\{0,1,\ldots,n-1\}$.

Utilizamos as funções random.seed() e random.randrange() do módulo random.

import random

def shuffle (A: [int]):
  random.seed()
  for i in range (len(A) - 1, 0, -1):
    j = random.randrange (i + 1)
    A[i], A[j] = A[j], A[i]

Nota: Compare a solução acima com o exercício 8 da L1 .

Problema A3.11: Enumeração I.
Escreva uma função que devolve em uma lista todas as $4!$ permutações para $\{1,2,3,4\}$, em ordem lexicográfica. Isto é, sua função deve devolver: $$\big[[1,2,3,4],[1,2,4,3],[1,3,2,4],[1,3,4,2],\ldots,[4,3,2,1]\big].$$

def permute_4():
  P = []
  for i in range (1,5):
    for j in range (1,5):
      if i != j:
        for k in range (1,5):
          if k not in (i, j):
            for l in range (1,5):
              if l not in (i, j, k):
                P.append ([i,j,k,l])
  return P

Problema A3.12: Enumeração II.
Escreva uma função que devolve em uma lista todas as $5!$ permutações para $\{1,2,3,4,5\}$.

def permute_5():
  P = []
  for i in range (1,6):
    for j in range (1,6):
      if i != j:
        for k in range (1,6):
          if k not in (i, j):
            for l in range (1,6):
              if l not in (i, j, k):
                for m in range (1,6):
                  if m not in (i, j, k, l):
                    P.append ([i,j,k,l,m])
  return P

Observe que as soluções dos exercícios (01) e (02), baseadas em laços aninhados, podem ser estendidas para qualquer $n\geq 1$ fixo — via incremento de código. Tais soluções são não uniformes, já que são específicas para cada valor de $n$. Abaixo, desenvolvemos um algoritmo uniforme, que funciona para qualquer valor inteiro de $n\geq 1$.

Problema A3.13: Enumeração III.
Escreva uma função que recebe um inteiro $n\geq 1$ e imprime todas as $n!$ permutações de $\{1,2,\ldots,n\}$ em ordem lexicográfica.

Considere o seguinte algoritmo:
Comece com uma lista $p[0\,..n]=[0,1,2,\ldots,n]$; a entrada $p[0]=0$ funciona como sentinela.

  • (1) Imprima a permutação em $p[1\,..n]$.
  • (2) Faça $i=n-1$. Se $p[i]\geq p[i+1]$, faça $i=i-1$ até que $p[i]< p[i+1]$. Se $i=0$, o algoritmo termina.
  • (3) Faça $j=n$. Se $p[i]\geq p[j]$, faça $j=j-1$ até que $p[i]< p[j]$. Troque $p[i]$ com $p[j]$.
  • (4) Faça $j=i+1$ e $k=n$. Enquanto $j<k$, troque $p[j]$ com $p[k]$ e faça $j=j+1$ e $k=k-1$. Volte ao passo (1).
def permute_n (n: int):
  p = [i for i in range (0,n+1)]
  i = n
  while i > 0:
    print (p)

    i = n-1
    while i > 0 and p[i] >= p[i+1]: i -= 1
    if i == 0: break

    j = n
    while p[i] >= p[j]: j -= 1
    p[i], p[j] = p[j], p[i]

    j, k = i+1, n
    while j < k:
      p[j], p[k] = p[k], p[j]
      j, k = j+1, k-1

Observe que, ao final do passo (2), $i$ é o menor índice tal que todas as permutações com prefixo $p[1],p[2],\ldots,p[i]$ já foram geradas. Logo, a próxima permutação em ordem lexicográfica aumenta o valor de $p[i]$. Ao final do passo (3), temos que $p[i+1]\geq\cdots \geq p[n]$; $p[j]$ é o menor elemento maior que $p[i]$ que pode suceder $p[1],p[2],\ldots,p[i-1]$ em uma permutação. Após a troca, temos que $$p[i+1]\geq \cdots\geq p[j-1]\geq p[i]> p[j]\geq p[j+1]\geq \cdots\geq p[n].$$

Python possui a palavra reservada yield que possibilita uma construção fácil de geradores (iteráveis). Para tal, basta substituirmos a linha print (p) no código acima por yield p. Para conveniência, copiamos o código com a modificação.

def permute (n: int) -> [int]:
  p = [i for i in range (0,n+1)]
  i = n
  while i > 0:
    yield p

    i = n-1
    while i > 0 and p[i] >= p[i+1]: i -= 1
    if i == 0: break

    j = n
    while p[i] >= p[j]: j -= 1
    p[i], p[j] = p[j], p[i]

    j, k = i+1, n
    while j < k:
      p[j], p[k] = p[k], p[j]
      j, k = j+1, k-1

yield p devolve p e guarda o estado da função permute() no momento do yield. Em uma próxima chamada a permute(), a execução começa do ponto onde ocorreu a parada anterior — e não do início da função. O exemplo

for q in permute (4):
  print (q)

imprime todas as permutações de $\{1,2,3,4\}$ em ordem lexicográfica, prefixadas por um $0$.

Problema A3.14: Caminho do cavalo.
Suponha dado um tabuleiro de xadrez $n\times n$, com $n\geq 1$, e cujas casas são numeradas por $$(1,1), (1,2),\ldots,(1,n),(2,1),(2,2),\ldots,(2,n),(3,1),(3,2),\ldots,(n,n). \qquad{(*)}$$ Determine se é possível a um cavalo de jogo de xadrez partir da posição $(1,1)$ e visitar todas as demais casas do tabuleiro uma única vez, em $n^2-1$ passos válidos. Por exemplo, para um tabuleiro $5\times 5$ uma solução é $$\begin{array}{r|rrrrr} & 1& 2& 3& 4& 5\\
\hline 1& 1& 6& 15& 10& 21\\
2& 14& 9& 20& 5& 16\\
3& 19& 2& 7& 22& 11\\
4& 8& 13& 24& 17& 4\\
5& 25& 18& 3& 12& 23 \end{array}$$ em que um número $k$ na casa $(i,j)$ representa que ela é a $k$-ésima casa visitada ao longo do caminho.
Sugestão: represente o tabuleiro em uma lista, como sugerido por $(*)$; para cada permutação de $\{1,2,\ldots,n^2\}$, verifique se ela representa um caminho válido.

Vamos seguir a sugestão apresentada e utilizar o algoritmo do item anterior para gerar, em ordem lexicográfica, as $n^2!$ permutações de $L:=\{1,2,\ldots,n^2\}$. Cada permutação representa uma alocação dos números em $L$ nas casas de um tabuleiro $n\times n$.

Dada uma permutação, a função auxiliar is_horse_path() verifica se ela descreve um caminho válido para o cavalo. A função find_horse_path() coordena a enumeração e devolve o primeiro caminho válido encontrado, ou uma lista vazia caso um tal caminho não exista.

def is_horse_path (n: int, m: int, p: [int]) -> bool:
  def kth_horse_step (n: int, p: [int], k: int, ci: int, cj: int) -> (bool, int, int):
    for (di, dj) in ((-1,+2),(-2,+1),(-2,-1),(-1,-2),(+1,-2),(+2,-1),(+2,+1),(+1,+2)):
      ni, nj = ci+di, cj+dj
      if 1 <= ni <= n and 1 <= nj <= n and p[(ni-1)*n + nj] == k:      
        return (True, ni, nj)
    return (False, 0, 0)

  ci, cj = 1, 1
  for k in range (2, m+1):
    good_step, ci, cj = kth_horse_step (n, p, k, ci, cj)
    if not good_step:
      return False
  return True            

def find_horse_path (n: int = 8) -> [int]:
  m = n*n
  for q in permute (m):
    if is_horse_path (n, m, q):
      return q
  return []    

Nota: O algoritmo acima é extremamente lento para valores modestos de $n$, e impraticável para valores maiores. A razão: no pior caso, o tempo consumido pelo algoritmo é $O(n\cdot n^2!)$.

Por exemplo: se $n=5$, temos que $n\cdot n^2! = 7.75560502\times 10^{25}$; se $n=8$, temos que $n\cdot n^2! = 1.01509546\times 10^{90}$.

Em Novembro/2019, o supercomputador mais rápido em operação é o Summit - IBM Power System AC922, em operação no Oak Ridge National Laboratory, United States, com $148.6$ petaflops (em que um petaflop é igual a $10^{15}$ operações com pontos flutuantes por segundo).

As operações no algoritmo acima são sobre inteiros e não pontos flutuantes. Suponha que temos um supercomputador que realiza operações com inteiros $100$ vezes mais rápido que com pontos flutuantes (totalmente hipotético) e que nosso supercomputador é $1$ milhão de vezes mais rápido que o Summit. Isto é, nosso supercomputador hipotético realiza $148.6\times 10^{23}$ operações inteiras por segundo (não existe, no momento, um supercomputador com esse poder; e levará muitos anos até atingirmos esta marca).

O tempo necessário para processar $n\cdot n^2!$ (vamos desconsiderar as constantes envolvidas) operações, com $n=8$, seria: $$T = \frac{1.01509546\times 10^{90}}{148.6\times 10^{23}} = 6.83105962\times 10^{64} ~ \text{segundos}.$$ Um ano possui aproximadamente $60\cdot 60\cdot 24\cdot 365.25 = 31557600 < 3.2\times 10^7$ segundos. Logo, $$T > 2.13470613\times 10^{57} ~ \text{anos.}$$ Compare o valor acima com a idade estimada do universo: $1.38\times 10^{10}$ anos.

Uma observação positiva. A estrutura do grafo subjacente ao problema do caminho do cavalo é especial e altamente simétrica. Explorando estas características, é possível desenvolvermos um algoritmo linear em $n^2$.

E uma negativa. Inúmeros problemas de interesse teórico e prático possuem a mesma estrutura combinatória, mas carecem de subestruturas especiais. Para estas variantes, o melhor que sabemos fazer é um algoritmo de tempo $O(m\cdot 2^m)$, às custas de um uso de memória da ordem de $O(2^m)$ — note que nosso algoritmo acima requer $O(m)$ de memória, para $m=n^2$. Até o momento, não sabemos se é possível resolvê-los exatamente em tempo sub-exponencial em $m$. Uma resposta (afirmativa ou negativa) a esta questão conferirá US$ 1 milhão a seu autor, além de reconhecimento e fama.

Avatar
Aritanan Gruber
Assistant Professor

“See, if y’all haven’t the same feeling for this, I really don’t give a damn. If you ain’t feeling it, then dammit this ain’t for you!“
(desconheço a autoria; agradeço a indicação)