This commit is contained in:
parent
c7c36f7717
commit
307c46d86a
208
src/ex4_furniture.py
Normal file
208
src/ex4_furniture.py
Normal file
@ -0,0 +1,208 @@
|
||||
"""
|
||||
Question 4 - Le beau, la bête et les meubles mouvants
|
||||
|
||||
But : calculer le nombre minimum de déplacements (et lesquels) pour passer d'un
|
||||
agencement de meubles à un autre
|
||||
|
||||
Approche :
|
||||
|
||||
Nous pouvons représenter la situation par un graphe d'états, dans lequel chaque
|
||||
nœud correspond à un agencement de meuble, et chaque arête à un déplacement.
|
||||
Nous pouvons ensuite partir de la situation initiale et explore cet arbre
|
||||
(par exemple en BFS) jusqu'à trouver la disposition finale. Afin d'optimiser un
|
||||
peu le BFS naïf, nous pouvons trier les nœuds selon la somme des distances entre
|
||||
la position actuelle de chaque meuble et sa position finale.
|
||||
|
||||
Par la nature du BFS, la première solution que nous trouverons sera la moins
|
||||
profonde, c'est-à-dire celle comportant le moins de déplacements.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
|
||||
|
||||
OFFSETS = [
|
||||
(0, -1),
|
||||
(-1, 0),
|
||||
(0, 1),
|
||||
(1, 0),
|
||||
]
|
||||
|
||||
|
||||
class Furniture:
|
||||
def __init__(self, id: int, pos: tuple[int, int], tiles: list[tuple[int, int]]):
|
||||
self.id: int = id
|
||||
self.pos: tuple[int, int] = pos
|
||||
self.tiles: list[tuple[int, int]] = tiles
|
||||
|
||||
@staticmethod
|
||||
def from_list(id: int, tiles: list[tuple[int, int]]) -> Furniture:
|
||||
x = [tile[0] for tile in tiles]
|
||||
y = [tile[1] for tile in tiles]
|
||||
ox = min(x)
|
||||
oy = min(y)
|
||||
tiles2 = [(tile[0] - ox, tile[1] - oy) for tile in tiles]
|
||||
return Furniture(id, (ox, oy), tiles2)
|
||||
|
||||
def move(self, offset: tuple[int, int]) -> Furniture:
|
||||
return Furniture(self.id, (self.pos[0] + offset[0], self.pos[1] + offset[1]), self.tiles)
|
||||
|
||||
def copy(self) -> Furniture:
|
||||
return Furniture(self.id, self.pos, self.tiles)
|
||||
|
||||
def get_tiles(self) -> list[tuple[int, int]]:
|
||||
ox, oy = self.pos
|
||||
return [
|
||||
(ox + dx, oy + dy)
|
||||
for dx, dy in self.tiles
|
||||
]
|
||||
|
||||
def dist(self, f2: Furniture) -> int:
|
||||
return abs(self.pos[0] - f2.pos[0]) + abs(self.pos[1] - f2.pos[1])
|
||||
|
||||
class State:
|
||||
def __init__(
|
||||
self,
|
||||
#plan: list[list[int]],
|
||||
width: int,
|
||||
height: int,
|
||||
furniture: dict[id, Furniture],
|
||||
parent: Optional[State] = None
|
||||
):
|
||||
#self.plan: list[list[int]] = plan
|
||||
self.furniture: dict[id, Furniture] = furniture
|
||||
self.parent: Optional[State] = parent
|
||||
#self.width: int = len(plan[0])
|
||||
#self.height: int = len(plan)
|
||||
self.width: int = width
|
||||
self.height: int = height
|
||||
|
||||
@staticmethod
|
||||
def from_list(plan: list[list[int]]) -> State:
|
||||
furniture: dict[int, list[tuple[int, int]]] = {}
|
||||
width = len(plan[0])
|
||||
height = len(plan)
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
tile: int = plan[y][x]
|
||||
if tile != 0:
|
||||
if tile not in furniture:
|
||||
furniture[tile] = []
|
||||
furniture[tile].append((x, y))
|
||||
furniture2: dict[int, Furniture] = {
|
||||
i: Furniture.from_list(i, tiles)
|
||||
for i, tiles in furniture.items()
|
||||
}
|
||||
|
||||
#return State(plan, furniture2)
|
||||
return State(width, height, furniture2)
|
||||
|
||||
def to_list(self) -> list[list[int]]:
|
||||
plan: list[list[int]] = [
|
||||
[0 for _ in range(self.width)]
|
||||
for _ in range(self.height)
|
||||
]
|
||||
for furniture in self.furniture.values():
|
||||
for tx, ty in furniture.get_tiles():
|
||||
plan[ty][tx] = furniture.id
|
||||
|
||||
return plan
|
||||
|
||||
def get_depth(self) -> int:
|
||||
if self.parent is None:
|
||||
return 0
|
||||
return self.parent.get_depth() + 1
|
||||
|
||||
def apply_move(self, id: int, offset: tuple[int, int]) -> Optional[State]:
|
||||
#new_plan: list[list[int]] = [[0] * self.width for _ in range(self.height)]
|
||||
|
||||
"""
|
||||
for y, row in enumerate(self.plan):
|
||||
for x, tile in enumerate(row):
|
||||
if tile == id:
|
||||
x2, y2 = x + offset[0], y + offset[1]
|
||||
if x2 < 0 or x2 >= self.width or y2 < 0 or y2 >= self.height:
|
||||
return None
|
||||
if new_plan[y2][x2] not in (0, id):
|
||||
return None
|
||||
new_plan[y2][x2] = id
|
||||
new_plan[y][x] = 0
|
||||
else:
|
||||
new_plan[y][x] = tile
|
||||
"""
|
||||
plan: list[list[int]] = self.to_list()
|
||||
furn2: Furniture = self.furniture[id].move(offset)
|
||||
for tx, ty in furn2.get_tiles():
|
||||
if tx < 0 or tx >= self.width or ty < 0 or ty >= self.height:
|
||||
return None
|
||||
|
||||
if plan[ty][tx] not in (0, id):
|
||||
return None
|
||||
|
||||
new_furn: dict[int, Furniture] = {
|
||||
i: f.copy()
|
||||
for i, f in self.furniture.items()
|
||||
}
|
||||
new_furn[id] = furn2
|
||||
|
||||
return State(self.width, self.height, new_furn, self)
|
||||
|
||||
def get_children(self) -> list[State]:
|
||||
moves: list[tuple[int, tuple[int, int]]] = self.get_moves()
|
||||
children: list[State] = [
|
||||
self.apply_move(move[0], move[1])
|
||||
for move in moves
|
||||
]
|
||||
children = list(filter(lambda child: child is not None, children))
|
||||
|
||||
return children
|
||||
|
||||
def get_moves(self) -> list[tuple[int, tuple[int, int]]]:
|
||||
moves: list[tuple[int, tuple[int, int]]] = []
|
||||
for id in self.furniture.keys():
|
||||
for offset in OFFSETS:
|
||||
moves.append((id, offset))
|
||||
return moves
|
||||
|
||||
def matches(self, state2: State) -> bool:
|
||||
for id in self.furniture.keys():
|
||||
f1: Furniture = self.furniture[id]
|
||||
f2: Furniture = state2.furniture[id]
|
||||
if f1.pos != f2.pos:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_path_list(self) -> list[list[list[int]]]:
|
||||
path = []
|
||||
if self.parent is not None:
|
||||
path = self.parent.get_path_list()
|
||||
|
||||
return path + [self.to_list()]
|
||||
|
||||
def get_dist(self, state2: State) -> int:
|
||||
total_dist: int = 0
|
||||
for id in self.furniture.keys():
|
||||
f1: Furniture = self.furniture[id]
|
||||
f2: Furniture = state2.furniture[id]
|
||||
total_dist += f1.dist(f2)
|
||||
|
||||
return total_dist
|
||||
|
||||
|
||||
def minimumMoves(current_plan: list[list[int]], target_plan: list[list[int]]) -> list[list[list[int]]]:
|
||||
current_state = State.from_list(current_plan)
|
||||
target_state = State.from_list(target_plan)
|
||||
|
||||
|
||||
states: list[State] = [current_state]
|
||||
while len(states) != 0:
|
||||
new_states: list[State] = []
|
||||
for state in states:
|
||||
if state.matches(target_state):
|
||||
return state.get_path_list()
|
||||
|
||||
children = state.get_children()
|
||||
new_states += children
|
||||
states = sorted(new_states, key=lambda s: s.get_dist(target_state))
|
||||
|
||||
print("Could not find a way to rearrange the room")
|
||||
return []
|
51
tests/test_ex4.py
Normal file
51
tests/test_ex4.py
Normal file
@ -0,0 +1,51 @@
|
||||
import unittest
|
||||
|
||||
from src.ex4_furniture import minimumMoves
|
||||
|
||||
|
||||
class MyTestCase(unittest.TestCase):
|
||||
def test_simple1(self):
|
||||
current_plan = [
|
||||
[0, 0, 0, 0],
|
||||
[2, 0, 1, 0],
|
||||
[0, 0, 1, 0]
|
||||
]
|
||||
target_plan = [
|
||||
[0, 0, 2, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0 ,0]
|
||||
]
|
||||
self.assertEqual(
|
||||
minimumMoves(current_plan, target_plan),
|
||||
[
|
||||
[
|
||||
[0, 0, 0, 0],
|
||||
[2, 0, 1, 0],
|
||||
[0, 0, 1, 0]
|
||||
],
|
||||
[
|
||||
[2, 0, 0, 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 1, 0]
|
||||
],
|
||||
[
|
||||
[0, 2, 0, 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 1, 0]
|
||||
],
|
||||
[
|
||||
[0, 0, 2, 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 1, 0]
|
||||
],
|
||||
[
|
||||
[0, 0, 2, 0],
|
||||
[0, 1, 0, 0],
|
||||
[0, 1, 0, 0]
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user