Você está na página 1de 10

Em busca de uma sequência de tabuleiros vencedora

MAC 2166 – Introdução à Ciência da Computação — Turmas Python


Segundo Exercı́cio-Programa — Primeiro Semestre de 2023

Com este segundo exercı́cio programa, pretendemos resolver um conhecido quebra-cabeça


que desafia um jogador a reordenar um tabuleiro n × n, para um natural n > 1. Assim como
no EP1, o tabuleiro será comprimido num inteiro que o represente. No caso do jogo da velha,
somente três tipos de casas existem, de modo que a codificação de uma configuração do jogo é
feita através de um inteiro cuja representação por um numeral na base 3 é feita pela sequência
de nove dı́gitos ternários (0, 1 ou 2) representando as nove casas do tabuleiro. Em contraste,
temos n2 valores possı́veis em cada casa neste EP.
Neste exercı́cio, chamamos de tabuleiro válido aquele que
contém uma permutação dos inteiros de 0 a n2 − 1. Ademais,
dizemos que sua casa livre é aquela que contém o 0. De fato,
ela participa de qualquer movimento válido e o jogo se desen-
volve com uma sequência de tabuleiros tal que dois tabuleiros
consecutivos sempre formam um movimento válido. Dada uma
sequência de tabuleiros válidos, cada par de tabuleiros consecuti-
vos formam um movimento, que pode ser válido ou não, segundo
as regras de nosso jogo. Por definição, um movimento válido é
aquele envolvendo dois tabuleiros que:

• diferem em apenas duas casas; Figura 1: Um tabuleiro 3×3

• uma delas é a casa livre;

• a outra é uma casa vizinha à casa livre, na vertical ou na


horizontal.

Observe que ao listar as casas de um tabuleiro, podemos enumerá-las da esquerda para a direita
e de cima para baixo. A cada casa corresponde um ı́ndice na sequência, que vem a ser chamado
de ı́ndice da casa. No tabuleiro 3 × 3 abaixo cada casa do tabuleiro contém como número o seu
próprio ı́ndice:

0 1 2
3 4 5
6 7 8

Observe que a diferença entre ı́ndices de casas vizinhas é sempre 1 na horizontal e n = 3


na vertical. De fato, o que caracteriza que duas casas sejam vizinhas na vertical é que seus
respectivos ı́ndices possuem diferença n. Em contraste, nem todo par de casas cuja diferença
de ı́ndices é 1 é formado por casas vizinhas na horizontal. É o caso do par de casas de ı́ndices
2 e 3, com soma 2 + 3 = 5 = 2n − 1. Igualmente é o caso do par de casas de ı́ndices 5 e 6,
com soma 5 + 6 = 11 = 4n − 1. De uma maneira geral, a soma de ı́ndices (i − 1) + i = 2i − 1

1
é respectivamente 1, 3, . . . , 2n2 − 3, para i = 1, . . . , n2 − 1; sendo que os pares de ı́ndices de
diferença 1 cujas casas não são vizinhas são aqueles em que a soma dos ı́ndices mais 1 tem resto
2n − 1 ao ser dividida por 2n. Em suma,

• Duas casas de um tabuleiro são vizinhas na vertical se seus ı́ndices possuem diferença n;

• Duas casas de um tabuleiro são vizinhas na horizontal se seus ı́ndices possuem diferença
1 e sua soma não deixa resto 2n − 1 ao ser dividida por 2n.

Tomemos agora um exemplo mais complexo em que n = 3. Temos 9 valores possı́veis nas
9 casas de um tabuleiro válido. Usando o mesmo esquema de codificação como o do EP1, o
inteiro com representação decimal

6053444 = 8 × 90 + 7 × 91 + 6 × 92 + 5 × 93 + 4 × 94 + 3 × 95 + 2 × 96 + 1 × 97 + 0 × 98

é denotado 0123456789 na base 9 e representa o tabuleiro

8 7 6
5 4 3
2 1 0

Já o inteiro 44317196 é denotado 1023456789 e representa o tabuleiro

8 7 6
5 4 3
2 0 1

Observe que: os dois tabuleiros diferem apenas nas casas de ı́ndices 7 e 8, as duas últimas; que
elas são vizinhas, na horizontal; e que a diferença entre estes ı́ndices é 1 e sua soma é 15, que
deixa resto 3 ao ser dividida por 6.
Assim sendo, os dois tabuleiros codificados pelos inteiros 6053444 e 44317196 constituem um
movimento válido, o primeiro movimento da sequência de tabuleiros

6053444, 44317196, 63422828, 63265364, 20277692, 173332700, 181835756,


184489316, 184495140, 184495084, 184494652, 184435684, 184593148, 184632460,
184632388, 184455484, 184770412, 175217596, 196475236, 200720932, 200709268,
200394340, 28443652, 219762412, 248420860, 248432524, 248437620, 248437612,
248424508, 248214556, 33276196, 262858708, 296870932, 300585916, 300562588,
300300148, 42374116.

Esta é uma sequência de 37 tabuleiros válidos, cujos 36 movimentos são todos válidos, e que
permutam as casas do tabuleiro inicial 6053444 para o tabuleiro final 42374116, denotado
0876543219 e impresso

1 2 3
4 5 6
7 8 0

2
Uma sequência de tabuleiros cujos movimentos são todos válidos possui os tabuleiros todos
válidos e é dita uma sequência de tabuleiros coerente. Se além disso ela permuta as casas do
tabuleiro inicial para o tabuleiro final

1 2 ··· n − 1 n
n+1 n+2 · · · 2n − 1 2n
.. .. .. ..
. . . .
2 2 2
n − n + 1 n − n + 2 · · · n − 1 0,

ela é uma sequência de tabuleiros vencedora.


Se o objetivo de um jogador é o de encontrar uma sequência vencedora a partir de um
dado tabuleiro inicial, o objetivo deste EP é o de elaborar um conjunto de funções que possam
auxiliar neste processo.

***

Deverão ser completados os corpos das funções no código fornecido ao abrir o VPL. O nome
das funções e sua lista de parâmetros não devem ser alterados.
Pede-se que sejam entregues:

• A função imprime_tabuleiro que imprime um tabuleiro de n2 casas com números de 0


a n2 − 1 e definida a partir da linha de código:

def imprime_tabuleiro( tabuleiro, n ):

Para n igual a 3 e tabuleiro igual a 300585916, deve ser impresso (a menos da margem
em branco):

tabuleiro 300585916 :
1 2 3
0 4 5
7 8 6

Por exemplo, o tabuleiro 4 × 4 de código 931611432777499425 deve ser impresso:

tabuleiro 931611432777499425 :
1 2 3 4
5 6 7 8
9 10 15 11
13 14 12 0

e o tabuleiro 2 × 2 de código 0 (inválido) deve ser impresso:

tabuleiro 0 :
0 0
0 0

3
• A função tabuleiro_valido que verifica se o um inteiro tabuleiro codifica um tabuleiro
válido com n2 casas com inteiros de 0 a (n2 –1), cada qual aparecendo uma única vez, e
definida a partir da linha de código:

def tabuleiro_valido( tabuleiro, n ):

A função não lê nem imprime nada, apenas devolve o valor True ou False conforme seja
verdade que o tabuleiro seja válido ou não.

• A função numero definida a partir da linha de código:

def numero( sd, base ):

e que devolve o número cuja representação na base fornecida possui a sequência de


dı́gitos sd, ordenada do dı́gito menos significativo para o mais significativo. Um dı́gito
num sistema de numeração de uma certa base é qualquer inteiro entre 0 e base − 1,
inclusive, de modo que 15 é um dı́gito na base 16. Assim,
numero( [ 2, 3, 1, 0 ], 4 ) devolve 30, pois 30 = 2 × 1 + 3 × 4 + 1 × 16 + 0 × 64.

• A função sequencia_de_digitos definida a partir da linha de código:

def sequencia_de_digitos( num, base ):

e que devolve a sequência de dı́gitos da representação de num em um sistema de numeração


da base em questão. Assim, sequencia_de_digitos( 30, 4 ) devolve [2, 3, 1, 0
] e sequencia de digitos( 300585916, 9 ) devolve [1, 2, 3, 0, 4, 5, 7, 8, 6].
Observe que sequencia_de_digitos é muito semelhante a imprime tabuleiro e é uma
espécie de função inversa de numero.

• A função indice definida a partir da linha de código:

def indice( permut, y ):

e que devolve o ı́ndice x da permutação permut que contém y. A lista permut é uma per-
mutação de todos os inteiros de 0 a len(permut)-1 Ex: indice([2,3,1,0],3) devolve
1 e indice([2,3,1,0],2) devolve 0.

• A função casas vizinhas definida a partir da linha de código:

def casas_vizinhas(indice1, indice2, n):

e que devolve True se as casas de ı́ndices indice1 e indice2 em tabuleiros n por n forem
vizinhas, ou False caso contrário. Assim, as chamadas casas vizinhas( 5, 8, 3) e
casas vizinhas( 5, 4, 3) devolvem True e as chamadas casas vizinhas( 5, 6, 3)
e casas vizinhas( 5, 1, 3) devolvem False.

• A função movimento_valido definida a partir da linha de código:

def movimento_valido(tabuleiro1, tabuleiro2, n):

4
e que verifica se os dois tabuleiros fornecidos — tabuleiro1 e tabuleiro2, ambos n por
n — constituem um movimento válido ou não. Assim, a chamada movimento_valido(
6053444, 44317196, 3) devolve True e as chamadas movimento_valido( 6053444,
63422828, 3) e movimento valido( 57, 45, 2 ) devolvem False. O código fonte de
sua função deve chamar a função sequencia de digitos e casas vizinhas.

• A função sequencia_coerente verifica se a sequência seq de tabuleiros n por n é coerente


ou não. Observe que:

– o código fonte a seguir chama as funções tabuleiro valido e movimento valido;


– que a chamada sequencia coerente( [30,210,216,201,57], 2 ) devolve True;
– e que a chamada sequencia coerente( [30,216,201,57], 2 ) devolve False.

def sequencia_coerente( seq, n ):


ant = seq[0]
coerente = tabuleiro_valido( ant, n )
for i in range(1,len(seq)):
prox = seq[i]
coerente = ( coerente and tabuleiro_valido( prox, n )
and movimento_valido( ant, prox, n ) )
ant = prox
return coerente

• A função move numero definida a partir da linha de código:

def move_numero( tabuleiro, numero_a_mover, n ):

e que devolve o código do novo tabuleiro obtido ao mover, no tabuleiro válido n por
n, a casa especificada pelo numero a mover para a casa livre vizinha. Devolve -1 se
as duas casas envolvidas não são vizinhas. Assim, move numero( 6053444, 1, 3 ) de-
volve 44317196 e move numero( 6053444, 4, 3 ) devolve -1. O código fonte de sua
função será bastante simplificado se você chamar as funções sequencia de digitos,
casas vizinhas, numero e indice.

• A função jogo definida a partir da linha de código:

def jogo( inicial, final, n ):

e que propicia que o jogador possa percorrer uma sequência de tabuleiros vencedora.
Dados um tabuleiro válido inicial e um tabuleiro válido final, ambos n por n, a função
jogo( inicial, final, n) parte do tabuleiro inicial e permite que o jogador tente
percorrer uma sequência de tabuleiros coerente até o tabuleiro final. A cada rodada ela
pede que ele

"Digite o número da casa a trocar com a casa livre: "

5
e efetua a mudança de casas, obtendo um novo tabuleiro após a troca e exibindo-o com
a função imprime_tabuleiro. Novas rodadas são jogadas até que o tabuleiro final seja
atingido ou o jogador escolha algum movimento inválido através de uma casa que não
seja vizinha à casa livre. No primeiro caso, a mensagem "Tabuleiro final atingido!"
é impressa, bem como a sequência de tabuleiros vencedora (vide exemplo de execução a
seguir). No segundo caso, é exibida a mensagem "Número n~ ao está em casa vizinha à
casa livre!". A função devolve True caso o tabuleiro final seja atingido e False caso
contrário. A sequência vencedora de 37 tabuleiros vista anteriormente pode ser obtida
pelo jogador depois de executar jogo(6053444,42374116,3) ao digitar a sequência de
36 números 1, 4, 3, 1, 4, 2, 5, 8, 7, 6, 1, 3, 6, 1, 3, 6, 2, 5, 8, 2, 6, 4, 5, 6, 2, 7, 1, 2,
4, 5, 6, 8, 7, 4, 5, 6. Recomenda-se que o código fonte de sua função chame as funções
imprime tabuleiro, sequencia de digitos, casas vizinhas, numero e indice. Vemos
a seguir uma execução da chamada jogo(30,57,2) que a sua implementação deve ser
capaz de reproduzir:

Digite o código do tabuleiro inicial: 30


tabuleiro 30 :
2 3
1 0
Digite o número da casa a trocar com a casa livre: 3
tabuleiro 210 :
2 0
1 3
Digite o número da casa a trocar com a casa livre: 2
tabuleiro 216 :
0 2
1 3
Digite o número da casa a trocar com a casa livre: 1
tabuleiro 201 :
1 2
0 3
Digite o número da casa a trocar com a casa livre: 3
tabuleiro 57 :
1 2
3 0
Tabuleiro final atingido!
Sequ^
encia vencedora de 5 tabuleiros: 30 210 216 201 57

Convém notar que nem sempre existe uma sequência vencedora que leve do tabuleiro
inicial ao final, como se pode verificar no caso do tabuleiro 2x2 de código 27. A seguir
temos uma execução de jogo(27,57,2):

Digite o código do tabuleiro inicial: 27


tabuleiro 27 :
3 2
1 0
Digite o número da casa a trocar com a casa livre: 2

6
tabuleiro 147 :
3 0
1 2
Digite o número da casa a trocar com a casa livre: 3
tabuleiro 156 :
0 3
1 2
Digite o número da casa a trocar com a casa livre: 1
tabuleiro 141 :
1 3
0 2
Digite o número da casa a trocar com a casa livre: 2
tabuleiro 45 :
1 3
2 0
Digite o número da casa a trocar com a casa livre: 3
tabuleiro 225 :
1 0
2 3
Digite o número da casa a trocar com a casa livre: 1
tabuleiro 228 :
0 1
2 3
Digite o número da casa a trocar com a casa livre: 2
tabuleiro 198 :
2 1
0 3
Digite o número da casa a trocar com a casa livre: 3
tabuleiro 54 :
2 1
3 0
Digite o número da casa a trocar com a casa livre: 1
tabuleiro 114 :
2 0
3 1
Digite o número da casa a trocar com a casa livre: 2
tabuleiro 120 :
0 2
3 1
Digite o número da casa a trocar com a casa livre: 3
tabuleiro 75 :
3 2
0 1
Digite o número da casa a trocar com a casa livre: 1
tabuleiro 27 :
3 2
1 0

7
Digite o número da casa a trocar com a casa livre: 0
Número n~
ao está em casa vizinha à casa livre!
Sequ^encia coerente de 13 tabuleiros:
27 147 156 141 45 225 228 198 54 114 120 75 27

• A função main que executa o programa principal. Esta função oferece ao jogador um menu
de opções de jogo que direta ou indiretamente utiliza as funções acima. Observe que o
código fonte fornecido a seguir chama respectivamente as funções imprime sequencia,
numero, tabuleiro valido, movimento valido, sequencia coerente, move numero e
jogo nas opções de 1 a 7.

def main():
n = int( input( "Digite o valor de n: " ) )
base = n * n

m = []
for i in range(n*n):
m.append(i+1)
m[n*n-1] = 0
final = numero( m, base )

print( "Digite uma das opç~


oes abaixo:" )
print( " 0, para terminar o programa" )
print( " 1, para imprimir um tabuleiro" )
print( " 2, para ler uma lista de %d números" % (n*n),
"e imprimir o código do tabuleiro lido" )
print( " 3, para testar a validade de um tabuleiro" )
print( " 4, para testar a validade de um movimento" )
print( " 5, para testar a coer^encia de uma sequ^
encia de tabuleiros",
"terminada por 0" )
print( " 6, para mover um número que gere um movimento válido" )
print( " 7, para jogar (mover números) ", end="" )
opcao = int( input( ) )
while opcao != 0:
if opcao==1:
tab = int( input( "Digite o código do tabuleiro: " ) )
imprime_tabuleiro( tab, n )
elif opcao==2:
print( "Digite as", base, "casas do tabuleiro" )
t = []
for i in range(base):
t.append( int( input( "Digite a %da casa: " % (i+1) ) ) )
print( "O tabuleiro cujas casas foram digitadas possui código",
numero( t, base ) )
elif opcao==3:
tab = int( input( "Digite o código do tabuleiro: " ) )
print( "O tabuleiro", tab, end=" " )

8
if tabuleiro_valido( tab, n ):
print( "é válido" )
else:
print( "n~ao é válido" )
elif opcao==4:
tab1 = int( input( "Digite o código do primeiro tabuleiro: " ) )
tab2 = int( input( "Digite o código do segundo tabuleiro: " ) )
print( "O movimento do tabuleiro", tab1,
"para o tabuleiro", tab2, end=" " )
if movimento_valido(tab1, tab2, n):
print( "é válido" )
else:
print( "n~ao é válido" )
elif opcao==5:
print( "Digite uma sequ^ encia n~ ao vazia de tabuleiros",
"terminada por 0" )
t = int( input( "Digite o código do primeiro tabuleiro: " ) )
sequencia = []
while t != 0:
sequencia.append( t )
t = int( input( "Digite o código do tabuleiro seguinte" +
" ou 0 para terminar: " ) )
print( "A sequ^ encia digitada possui", len(sequencia),
"elementos e ", end="" )
if sequencia_coerente( sequencia, n ):
print( "é coerente" )
else:
print( "n~ao é coerente" )
elif opcao==6:
tab1 = int( input( "Digite o código do tabuleiro: " ) )
if tabuleiro_valido( tab1, n ):
numero_a_mover = int( input (
"Digite o número da casa a trocar com a casa livre: ") )
tab2 = move_numero( tab1, numero_a_mover, n )
if tab2 != -1:
print( "O tabuleiro após a troca tem código",
tab2, "e é impresso" )
imprime_tabuleiro( tab2, n)
else:
print( "Número n~ ao está em casa vizinha à casa livre!" )
else:
print( "O tabuleiro n~ ao é válido")
elif opcao==7:
tab = int( input( "Digite o código do tabuleiro inicial: " ) )
if tabuleiro_valido( tab, n ):
jogo( tab, final, n)
else:

9
print( "Tabuleiro inicial inválido")
print( "0: término; 1: impress~
ao; 2: leitura; 3: tabuleiro;" )
opcao = int( input(
"4: movimento; 5: sequ^encia; 6: número; 7: jogo. Opç~
ao: " ) )
print( "Término do programa" )

Para efeito da correção do EP2 no VPL, a função principal main() fornecida, bem como
a sua chamada (exibida abaixo), não devem ser alteradas. O propósito do comando
condicional abaixo será explicado em aula em um momento oportuno. Sem o seu uso, o
corretor do VPL não irá funcionar adequadamente.

if __name__ == "__main__":
main()

Neste EP, você deve usar os recursos da linguagem vistos até a semana 7 (listas). Em
particular, não é permitido o uso de:

• matrizes, mesmo que sejam abordadas em sala antes do prazo de entrega deste EP;

• manipulação de string além do que já tiver sido visto até a semana 7;

• métodos ou funções nativas associados à classe das listas que não foram abordados.

Bom trabalho a todos!

10

Você também pode gostar