Skip to content

Commit

Permalink
Added a_star, hamiltonian_cycle, correct error in GraphNamedVertices
Browse files Browse the repository at this point in the history
  • Loading branch information
xtof-durr committed Oct 31, 2023
1 parent 3b04bce commit 3ad2d51
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 10 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@

# Changelog

## 1.5

- Corrected a bug in GraphNamedVertices. Now only the minimum weight edge is kept among multiple edges with same endpoints.
- Added algorithm a_star to compute shortest paths
- Added hamiltonian_cycle to compute a Hamiltonian cycle
- Started to add types to some function parameters

## 1.4

- Move to GitHub Actions and drop Python 2.7 support completely
Expand Down
2 changes: 2 additions & 0 deletions docs/content.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ shortest cycle undirected :math:`O(|V|\cdot|E|)` breath-first sear
minimum weight cycle directed :math:`O(|V|\cdot |E|)` `Bellman-Ford <https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm>`_ `bellman_ford <tryalgo/tryalgo.html#module-tryalgo_bellman_ford>`__
minimum mean cycle directed :math:`O(|V|\cdot |E|)` `Karp <http://www.sciencedirect.com/science/article/pii/0012365X78900110>`_ `min_mean_cycle <tryalgo/tryalgo.html#module-tryalgo_min_mean_cycle>`__
Eulerian cycle both :math:`O(|V|+|E|)` `Greedy <https://en.wikipedia.org/wiki/Eulerian_path>`_ `eulerian_tour <tryalgo/tryalgo.html#module-tryalgo_eulerian_tour>`__
Hamiltonian cycle complete :math:`O(n^2 2^n)` `Held-Karp https://en.wikipedia.org/wiki/Held%E2%80%93Karp_algorithm` `hamiltonian_cycle <tryalgo/tryalgo.html#module-tryalgo_hamiltonian_cycle>`__
Iterated function cycle outdeg=1 :math:`O(|V|)` `Floyd's tortoise and hare <https://en.wikipedia.org/wiki/Cycle_detection>`_ `tortoise_hare <tryalgo/tryalgo.html#module-tryalgo_tortoise_hare>`__
=========================== ========== ======================= ============================================================================== ===============

Expand All @@ -118,6 +119,7 @@ unweighted graph :math:`O(|E|)` `breadth-first search <htt
grid :math:`O(|E|)` breadth-first search adapted to the grid graph `dist_grid <tryalgo/tryalgo.html#module-tryalgo_dist_grid>`__
{0,1} weighted graph :math:`O(|E|)` `Dijkstra with a deque <http://goo.gl/w67Hs1>`_ `graph01 <tryalgo/tryalgo.html#module-tryalgo_graph01>`__
non negative weighted graph :math:`O(|E| \log |V|)` `Dijkstra <https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm>`_ `dijkstra <tryalgo/tryalgo.html#module-tryalgo_dijkstra>`__
with lower bound on distance :math:`O(|E| \log |V|)` `A* <https://en.wikipedia.org/wiki/A*_search_algorithm> `_ `a_star <tryalgo/tryalgo.html#module-tryalgo_a_star>`__
arbitrary weighted graph :math:`O(|E| \cdot |V|)` `Bellman-Ford`_ `bellman_ford <tryalgo/tryalgo.html#module-tryalgo_bellman_ford>`__
all source destination pairs :math:`O(|V|^3)` `Floyd-Warshall <https://en.wikipedia.org/wiki/Floyd-Warshall_algorithm>`_ `floyd_warshall <tryalgo/tryalgo.html#module-tryalgo_floyd_warshall>`__
============================ ======================== ============================================================================== ===============
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

setup(
name='tryalgo',
version='1.4.0',
version='1.5.0',
description=(
'Algorithms and data structures '
'for preparing programming competitions'
Expand Down
85 changes: 85 additions & 0 deletions tests/test_tryalgo.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def isclose(a, b, rel_tol, abs_tol):
from tryalgo.graph import tree_adj_to_prec, tree_prec_to_adj
from tryalgo.graph import matrix_to_listlist, listlist_and_matrix_to_listdict
from tryalgo.graph import listdict_to_listlist_and_matrix, dictdict_to_listdict
from tryalgo.a_star import a_star
from tryalgo.anagrams import anagrams
from tryalgo.arithm_expr_eval import arithm_expr_eval, arithm_expr_parse
from tryalgo.arithm_expr_target import arithm_expr_target
Expand Down Expand Up @@ -50,6 +51,7 @@ def isclose(a, b, rel_tol, abs_tol):
from tryalgo.gauss_jordan import gauss_jordan, GJ_ZERO_SOLUTIONS, GJ_SINGLE_SOLUTION, GJ_SEVERAL_SOLUTIONS
from tryalgo.graph import GraphNamedVertices
from tryalgo.graph01 import dist01
from tryalgo.hamiltonian_cycle import hamiltonian_cycle
from tryalgo.horn_sat import horn_sat
from tryalgo.huffman import huffman
from tryalgo.interval_tree import interval_tree, intervals_containing
Expand Down Expand Up @@ -109,6 +111,73 @@ class TestTryalgo(unittest.TestCase):

def unorder(self, L):
return sorted(sorted(group) for group in L)

def test_a_star(self):
"""tests A* to compute the minimum number of swaps in a 3*3 grid to
transform one configuration into another one.
Has been tested here : [Swap Game](https://cses.fi//problemset/task/1670)
"""
# tous les mouvements possibles
permutations = [
(1, 0, 2, # vertical adjacent
3, 4, 5,
6, 7, 8),
(0, 2, 1,
3, 4, 5,
6, 7, 8),
(0, 1, 2,
4, 3, 5,
6, 7, 8),
(0, 1, 2,
3, 5, 4,
6, 7, 8),
(0, 1, 2,
3, 4, 5,
7, 6, 8),
(0, 1, 2,
3, 4, 5,
6, 8, 7),
(3, 1, 2, # horizontal adjacent
0, 4, 5,
6, 7, 8),
(0, 4, 2,
3, 1, 5,
6, 7, 8),
(0, 1, 5,
3, 4, 2,
6, 7, 8),
(0, 1, 2,
6, 4, 5,
3, 7, 8),
(0, 1, 2,
3, 7, 5,
6, 4, 8),
(0, 1, 2,
3, 4, 8,
6, 7, 5)
]

# retourne toutes les grilles atteignables en une permutation
def swaps(start):
for p in permutations:
yield tuple(start[i] for i in p)

# borne inférieure sur le nombre d'échange nécessaire pour atteindre la cible
def distance_lb(M):
retval = 0
for i, j in enumerate(M):
retval += abs((i%3) - (j%3)) + abs((i//3) - (j//3))
return retval // 2

a = (0, 1, 2, 3, 4, 5, 6, 7, 8)
b = (1, 0, 2, 3, 4, 5, 6, 7, 8)
c = (7, 8, 4, 5, 1, 6, 0, 3, 2)
d = (7, 7, 7, 7, 7, 7, 7, 7, 7)

self.assertEqual(a_star(swaps, a, distance_lb), 0)
self.assertEqual(a_star(swaps, b, distance_lb), 1)
self.assertEqual(a_star(swaps, c, distance_lb), 11)
self.assertEqual(a_star(swaps, d, distance_lb), -1)

def test_anagrams(self):
L = [(set("le chien marche vers sa niche et trouve une "
Expand Down Expand Up @@ -815,6 +884,15 @@ def test_graph(self):
self.assertEqual(G.weight, [{1: 4, 2: 1}, {0: 4, 2: 0}, {1: -2}])
self.assertEqual(len(G), 3)
self.assertEqual(G[0], [1, 2])
G = GraphNamedVertices()
G.add_arc("a", "b", 2)
a = G.add_node("a")
b = G.add_node("b")
self.assertEqual(G.weight[a][b], 2)
G.add_arc("a", "b", 3)
self.assertEqual(G.weight[a][b], 2)
G.add_arc("a", "b", 1)
self.assertEqual(G.weight[a][b], 1)

def test_dist01(self):
_ = None
Expand Down Expand Up @@ -859,6 +937,13 @@ def test_dist01(self):
for i in range(len(path) - 1))
self.assertEqual(dist[target], val)

def test_hamiltonian_cycle(self):
# the code was already tested at UVA:10496 - Collecting Beepers
weight = [[0, 5, 8, 6, 3], [5, 0, 5, 1, 8], [8, 5, 0, 4, 11], [6, 1, 4, 0, 9], [3, 8, 11, 9, 0]]
self.assertEqual(hamiltonian_cycle(weight), 24)
weight = [[0, 2], [10, 0]]
self.assertEqual(hamiltonian_cycle(weight), 12)

def test_horn_sat(self):
F1 = [(1, []), (1, []), (None, [2])]
F2 = [(1, []), (2, []), (3, [1, 2]),
Expand Down
38 changes: 38 additions & 0 deletions tryalgo/a_star.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""\
Shortest Path algorithm A*.
jill-jênn vie et christoph dürr - 2023
"""

from heapq import heappop, heappush


def a_star(graph, start, lower_bound):
"""single source shortest path by A* on an unweighted graph
:param graph: iterator on adjacent vertices
:param source: source vertex
:param lower_bound: lb function on distance to target,
must return 0 on target and only there.
:returns: distance or -1 (target unreachable)
:complexity: `O(|V| + |E|log|V|)`
"""
closedset = set()
openset = set([start])
g = {start: 0 }
Q = [(lower_bound(start), start)]
while Q:
(val, x) = heappop(Q)
if lower_bound(x) == 0:
return g[x]
closedset.add(x)
for y in graph(x):
if not y in closedset and not y in openset:
g[y] = g[x] + 1
val = g[y] + lower_bound(y)
heappush(Q, (val, y))
openset.add(y)
return -1
21 changes: 13 additions & 8 deletions tryalgo/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,22 +369,27 @@ def __getitem__(self, v):
return self.neighbors[v]

def add_node(self, name):
assert name not in self.name2node
self.name2node[name] = len(self.name2node)
self.node2name.append(name)
self.neighbors.append([])
self.weight.append({})
if name not in self.name2node:
self.name2node[name] = len(self.name2node)
self.node2name.append(name)
self.neighbors.append([])
self.weight.append({})
return self.name2node[name]

def add_edge(self, name_u, name_v, weight_uv=None):
self.add_arc(name_u, name_v, weight_uv)
self.add_arc(name_v, name_u, weight_uv)

def add_arc(self, name_u, name_v, weight_uv=None):
u = self.name2node[name_u]
v = self.name2node[name_v]
"""Adds an arc between two given nodes, eventually with a given arc weight.
In case of multiple arcs between the same pairs of nodes, only the lightest one is kept.
Adds the given nodes, if they are not already present.
"""
u = self.add_node(name_u)
v = self.add_node(name_v)
self.neighbors[u].append(v)
self.weight[u][v] = weight_uv
if v not in self.weight[u] or weight_uv < self.weight[u][v]:
self.weight[u][v] = weight_uv
# snip}

Graph = Union[List[List[int]], List[Dict[int, Any]], GraphNamedVertices]
37 changes: 37 additions & 0 deletions tryalgo/hamiltonian_cycle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""\
Hamiltonian Cycle
jill-jenn vie et christoph durr - 2023
"""


# snip{
def hamiltonian_cycle(weight):
"""Hamiltonian Cycle
:param weight: matrix of edge weights of a complete graph
:returns: minimum weight of a Hamiltonian cycle
:complexity: O(n^2 2^n)
"""
n = len(weight)
# A[S][v] = minimum weight path from vertex n-1,
# then visiting all vertices in S exactly once,
# and finishing in v (which is not in S)
A = [[float('inf')] * n for _ in range(1 << (n - 1))]
for v in range(n): # base case
A[0][v] = weight[n - 1][v]
for S in range(1, 1 << (n - 1)):
for v in range(n):
if not ((1 << v) & S): # v not in S
for u in range(n - 1):
U = 1 << u
if U & S: # u in S
alt = A[S ^ U][u] + weight[u][v]
if alt < A[S][v]:
A[S][v] = alt
S = (1 << (n - 1)) - 1 # {0, 1, ..., n - 2}
return A[S][n - 1]
# snip}

2 changes: 1 addition & 1 deletion tryalgo/matrix_chain_mult.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def matrix_mult_opt_order(M):
:returns: matrices opt, arg, such that opt[i][j] is the optimal number of
operations to compute M[i] * ... * M[j] when done in the order
(M[i] * ... * M[k]) * (M[k + 1] * ... * M[j]) for k = arg[i][j]
:complexity: :math:`O(n^2)`
:complexity: :math:`O(n^3)`
"""
n = len(M)
r = [len(Mi) for Mi in M]
Expand Down

0 comments on commit 3ad2d51

Please sign in to comment.