algobattle-problems 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. algobattle-problems-1.0.0/LICENSE +21 -0
  2. algobattle-problems-1.0.0/PKG-INFO +12 -0
  3. algobattle-problems-1.0.0/README.md +34 -0
  4. algobattle-problems-1.0.0/algobattle_problems/__init__.py +0 -0
  5. algobattle-problems-1.0.0/algobattle_problems/biclique/__init__.py +0 -0
  6. algobattle-problems-1.0.0/algobattle_problems/biclique/generator/main.py +18 -0
  7. algobattle-problems-1.0.0/algobattle_problems/biclique/problem.py +35 -0
  8. algobattle-problems-1.0.0/algobattle_problems/biclique/solver/main.py +16 -0
  9. algobattle-problems-1.0.0/algobattle_problems/biclique/tests.py +36 -0
  10. algobattle-problems-1.0.0/algobattle_problems/c4subgraphiso/__init__.py +0 -0
  11. algobattle-problems-1.0.0/algobattle_problems/c4subgraphiso/generator/main.py +26 -0
  12. algobattle-problems-1.0.0/algobattle_problems/c4subgraphiso/problem.py +55 -0
  13. algobattle-problems-1.0.0/algobattle_problems/c4subgraphiso/solver/main.py +12 -0
  14. algobattle-problems-1.0.0/algobattle_problems/c4subgraphiso/tests.py +74 -0
  15. algobattle-problems-1.0.0/algobattle_problems/circlecover/__init__.py +0 -0
  16. algobattle-problems-1.0.0/algobattle_problems/circlecover/generator/main.py +11 -0
  17. algobattle-problems-1.0.0/algobattle_problems/circlecover/problem.py +47 -0
  18. algobattle-problems-1.0.0/algobattle_problems/circlecover/solver/main.py +5 -0
  19. algobattle-problems-1.0.0/algobattle_problems/clusterediting/__init__.py +0 -0
  20. algobattle-problems-1.0.0/algobattle_problems/clusterediting/generator/main.py +28 -0
  21. algobattle-problems-1.0.0/algobattle_problems/clusterediting/problem.py +52 -0
  22. algobattle-problems-1.0.0/algobattle_problems/clusterediting/solver/main.py +15 -0
  23. algobattle-problems-1.0.0/algobattle_problems/clusterediting/tests.py +66 -0
  24. algobattle-problems-1.0.0/algobattle_problems/domset/__init__.py +0 -0
  25. algobattle-problems-1.0.0/algobattle_problems/domset/generator/main.py +21 -0
  26. algobattle-problems-1.0.0/algobattle_problems/domset/problem.py +36 -0
  27. algobattle-problems-1.0.0/algobattle_problems/domset/solver/main.py +10 -0
  28. algobattle-problems-1.0.0/algobattle_problems/domset/tests.py +42 -0
  29. algobattle-problems-1.0.0/algobattle_problems/hikers/__init__.py +0 -0
  30. algobattle-problems-1.0.0/algobattle_problems/hikers/generator/main.py +29 -0
  31. algobattle-problems-1.0.0/algobattle_problems/hikers/problem.py +51 -0
  32. algobattle-problems-1.0.0/algobattle_problems/hikers/solver/main.py +15 -0
  33. algobattle-problems-1.0.0/algobattle_problems/hikers/tests.py +49 -0
  34. algobattle-problems-1.0.0/algobattle_problems/longestpathboundedfvs/__init__.py +0 -0
  35. algobattle-problems-1.0.0/algobattle_problems/longestpathboundedfvs/generator/main.py +24 -0
  36. algobattle-problems-1.0.0/algobattle_problems/longestpathboundedfvs/problem.py +74 -0
  37. algobattle-problems-1.0.0/algobattle_problems/longestpathboundedfvs/solver/main.py +10 -0
  38. algobattle-problems-1.0.0/algobattle_problems/oscm3/__init__.py +0 -0
  39. algobattle-problems-1.0.0/algobattle_problems/oscm3/generator/main.py +21 -0
  40. algobattle-problems-1.0.0/algobattle_problems/oscm3/problem.py +45 -0
  41. algobattle-problems-1.0.0/algobattle_problems/oscm3/solver/main.py +14 -0
  42. algobattle-problems-1.0.0/algobattle_problems/oscm3/tests.py +40 -0
  43. algobattle-problems-1.0.0/algobattle_problems/pairsum/__init__.py +0 -0
  44. algobattle-problems-1.0.0/algobattle_problems/pairsum/generator/main.py +21 -0
  45. algobattle-problems-1.0.0/algobattle_problems/pairsum/generator_random/main.py +29 -0
  46. algobattle-problems-1.0.0/algobattle_problems/pairsum/problem.py +40 -0
  47. algobattle-problems-1.0.0/algobattle_problems/pairsum/solver/main.py +10 -0
  48. algobattle-problems-1.0.0/algobattle_problems/pairsum/solver_random/main.py +31 -0
  49. algobattle-problems-1.0.0/algobattle_problems/pairsum/tests.py +35 -0
  50. algobattle-problems-1.0.0/algobattle_problems/pathpacking/__init__.py +0 -0
  51. algobattle-problems-1.0.0/algobattle_problems/pathpacking/generator/main.py +24 -0
  52. algobattle-problems-1.0.0/algobattle_problems/pathpacking/problem.py +43 -0
  53. algobattle-problems-1.0.0/algobattle_problems/pathpacking/solver/main.py +12 -0
  54. algobattle-problems-1.0.0/algobattle_problems/pathpacking/tests.py +40 -0
  55. algobattle-problems-1.0.0/algobattle_problems/scheduling/__init__.py +0 -0
  56. algobattle-problems-1.0.0/algobattle_problems/scheduling/generator/main.py +18 -0
  57. algobattle-problems-1.0.0/algobattle_problems/scheduling/problem.py +41 -0
  58. algobattle-problems-1.0.0/algobattle_problems/scheduling/solver/main.py +10 -0
  59. algobattle-problems-1.0.0/algobattle_problems/scheduling/tests.py +30 -0
  60. algobattle-problems-1.0.0/algobattle_problems/tsptimewindows/__init__.py +0 -0
  61. algobattle-problems-1.0.0/algobattle_problems/tsptimewindows/generator/main.py +49 -0
  62. algobattle-problems-1.0.0/algobattle_problems/tsptimewindows/problem.py +75 -0
  63. algobattle-problems-1.0.0/algobattle_problems/tsptimewindows/solver/main.py +10 -0
  64. algobattle-problems-1.0.0/algobattle_problems/tsptimewindows/tests.py +73 -0
  65. algobattle-problems-1.0.0/algobattle_problems.egg-info/PKG-INFO +12 -0
  66. algobattle-problems-1.0.0/algobattle_problems.egg-info/SOURCES.txt +70 -0
  67. algobattle-problems-1.0.0/algobattle_problems.egg-info/dependency_links.txt +1 -0
  68. algobattle-problems-1.0.0/algobattle_problems.egg-info/entry_points.txt +13 -0
  69. algobattle-problems-1.0.0/algobattle_problems.egg-info/requires.txt +2 -0
  70. algobattle-problems-1.0.0/algobattle_problems.egg-info/top_level.txt +1 -0
  71. algobattle-problems-1.0.0/pyproject.toml +35 -0
  72. algobattle-problems-1.0.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Henri Lotze
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.1
2
+ Name: algobattle-problems
3
+ Version: 1.0.0
4
+ Author: Imogen Hergeth, Jan Dreier, Henri Lotze
5
+ License: MIT
6
+ Project-URL: Homepage, https://algobattle.org
7
+ Project-URL: Documentation, https://algobattle.org/docs
8
+ Project-URL: Repository, https://github.com/Benezivas/algobattle-problems
9
+ Requires-Python: >=3.11
10
+ License-File: LICENSE
11
+ Requires-Dist: algobattle-base~=4.0.0
12
+ Requires-Dist: networkx
@@ -0,0 +1,34 @@
1
+ # Algorithmic Battle: Problems
2
+ Collection of example problems that are ready to be used in the
3
+ [algobattle](https://algobattle.org) project, with a strong bias towards
4
+ problems of theoretical computer science.
5
+
6
+ Each problem directory contains a `description.md` describing the problem
7
+ and the I/O that the generator and solver use. The `generator` and `solver`
8
+ directories contain dummy solvers that output instances which are technically
9
+ legal, but rather unexiting. Finally, the `problem.py` implements the logic
10
+ described in the `description.md`.
11
+
12
+ # Setup and Usage
13
+ You need the [algobattle](https://github.com/Benezivas/algobattle) tool to use
14
+ any of the problems of this repository. Consult the
15
+ [official documentation](https://algobattle.org/docs)
16
+ on how to install the algobattle tool and how to use it for a given problem.
17
+
18
+ If you are using the [algobattle-web framework](https://github.com/Benezivas/algobattle-web),
19
+ you can simply import these problems in the framework. Consult the documentation
20
+ of the `algobattle-web` framework for more details.
21
+
22
+ A good and simple problem to start is the `pairsum` problem, which is well
23
+ suited as a primer task at the start of a lab course, should you want to host
24
+ one yourself. For this task, a simple randomized generator and solver are
25
+ implemented which you may use to see how the everything behaves realistically.
26
+
27
+ # Compatibility
28
+ The head of the main branch of this repository is always compatible with
29
+ the head of the main branch of the [algobattle repository](https://github.com/Benezivas/algobattle).
30
+
31
+ If you are using an older version of `algobattle`, see the releases of this
32
+ repository in order to find versions of problems that are compatible with older
33
+ releases. We do not actively back-port problems to older versions of
34
+ `algobattle`.
@@ -0,0 +1,18 @@
1
+ """Simple dummy generator for the BiClique problem, outputting a static instance."""
2
+ import json
3
+
4
+ with open("output/instance.json", "w+") as output:
5
+ json.dump(
6
+ {
7
+ "num_vertices": 4,
8
+ "edges": [
9
+ [0, 1],
10
+ [0, 2],
11
+ [0, 3],
12
+ ],
13
+ },
14
+ output,
15
+ )
16
+
17
+ with open("output/solution.json", "w+") as output:
18
+ json.dump({"s_1": [0], "s_2": [1, 2, 3]}, output)
@@ -0,0 +1,35 @@
1
+ """The Biclique problem class."""
2
+ from algobattle.problem import Problem, SolutionModel, maximize
3
+ from algobattle.util import Role, ValidationError
4
+ from algobattle.types import UndirectedGraph, Vertex
5
+
6
+
7
+ class Solution(SolutionModel[UndirectedGraph]):
8
+ """A solution to a bipartite clique problem."""
9
+
10
+ s_1: set[Vertex]
11
+ s_2: set[Vertex]
12
+
13
+ def validate_solution(self, instance: UndirectedGraph, role: Role) -> None:
14
+ super().validate_solution(instance, role)
15
+ edge_set = set(instance.edges) | set(edge[::-1] for edge in instance.edges)
16
+ if len(self.s_1.intersection(self.s_2)) != 0:
17
+ raise ValidationError("Solution contains vertex sets that aren't disjoint.")
18
+ if any((u, v) not in edge_set for u in self.s_1 for v in self.s_2):
19
+ raise ValidationError("The instance graph is missing an edge between the solution vertex sets.")
20
+ if any((u, v) in edge_set for u in self.s_1 for v in self.s_1) or any(
21
+ (u, v) in edge_set for u in self.s_2 for v in self.s_2
22
+ ):
23
+ raise ValidationError("The solution is not a bipartite graph.")
24
+
25
+ @maximize
26
+ def score(self, instance: UndirectedGraph, role: Role) -> float:
27
+ return len(self.s_1) + len(self.s_2)
28
+
29
+
30
+ Biclique = Problem(
31
+ name="Bipartite Clique",
32
+ instance_cls=UndirectedGraph,
33
+ solution_cls=Solution,
34
+ min_size=5,
35
+ )
@@ -0,0 +1,16 @@
1
+ """Simple dummy solver for the BiClique problem, outputting a static solution."""
2
+ import json
3
+ from random import random
4
+
5
+ sol = [1, 2]
6
+ if random() <= 0.9:
7
+ sol.append(3)
8
+
9
+ with open("output/solution.json", "w") as output:
10
+ json.dump(
11
+ {
12
+ "s_1": [0],
13
+ "s_2": sol,
14
+ },
15
+ output,
16
+ )
@@ -0,0 +1,36 @@
1
+ """Tests for the biclique problem."""
2
+ import unittest
3
+
4
+ from pydantic import ValidationError as PydanticValidationError
5
+ from algobattle.util import Role
6
+
7
+ from algobattle_problems.biclique.problem import UndirectedGraph, Solution, ValidationError
8
+
9
+
10
+ class Tests(unittest.TestCase):
11
+ """Tests for the Biclique problem solution class."""
12
+
13
+ def test_vertices_exist(self):
14
+ """Tests that only valid vertex indices are allowed."""
15
+ graph = UndirectedGraph(num_vertices=10, edges=[(i, j) for i in range(10) for j in range(i)])
16
+ with self.assertRaises(PydanticValidationError):
17
+ sol = Solution.model_validate({"s_1": set(), "s_2": {20}}, context={"instance": graph})
18
+ sol.validate_solution(graph, Role.generator)
19
+
20
+ def test_edges_exist(self):
21
+ """Tests that solutions that arent complete bicliques are not allowed."""
22
+ graph = UndirectedGraph(num_vertices=10, edges=[])
23
+ with self.assertRaises(ValidationError):
24
+ sol = Solution.model_validate({"s_1": {1}, "s_2": {2}}, context={"instance": graph})
25
+ sol.validate_solution(graph, Role.generator)
26
+
27
+ def test_edges_missing(self):
28
+ """Asserts that solutions that aren't bipartite are not allowed."""
29
+ graph = UndirectedGraph(num_vertices=10, edges=[(i, j) for i in range(10) for j in range(i)])
30
+ with self.assertRaises(ValidationError):
31
+ sol = Solution.model_validate({"s_1": {1, 2}, "s_2": {3, 4}}, context={"instance": graph})
32
+ sol.validate_solution(graph, Role.generator)
33
+
34
+
35
+ if __name__ == "__main__":
36
+ unittest.main()
@@ -0,0 +1,26 @@
1
+ """Simple dummy generator for the C4subgraphiso problem, outputting a static instance."""
2
+ import json
3
+
4
+ with open("output/instance.json", "w+") as output:
5
+ json.dump(
6
+ {
7
+ "num_vertices": 4,
8
+ "edges": [
9
+ [3, 0],
10
+ [0, 1],
11
+ [1, 2],
12
+ [2, 3],
13
+ ],
14
+ },
15
+ output,
16
+ )
17
+
18
+ with open("output/solution.json", "w+") as output:
19
+ json.dump(
20
+ {
21
+ "squares": [
22
+ [0, 1, 2, 3],
23
+ ],
24
+ },
25
+ output,
26
+ )
@@ -0,0 +1,55 @@
1
+ """The C4subgraphiso problem class."""
2
+
3
+ from itertools import combinations, cycle, islice
4
+ from typing import Annotated, Iterator
5
+ from pydantic import field_validator
6
+
7
+ from algobattle.problem import Problem, SolutionModel, maximize
8
+ from algobattle.util import Role, ValidationError
9
+ from algobattle.types import UndirectedGraph, Vertex, UniqueItems
10
+
11
+ Square = Annotated[tuple[Vertex, Vertex, Vertex, Vertex], UniqueItems()]
12
+
13
+
14
+ def edges(square: Square) -> Iterator[tuple[Vertex, Vertex]]:
15
+ """Returns all edges of a square."""
16
+ return zip(square, islice(cycle(square), 1, None))
17
+
18
+
19
+ def diagonals(square: Square) -> Iterator[tuple[Vertex, Vertex]]:
20
+ """Returns the diagonals of a square."""
21
+ yield square[0], square[2]
22
+ yield square[1], square[3]
23
+
24
+
25
+ class Solution(SolutionModel[UndirectedGraph]):
26
+ """A solution to a Square Subgraph Isomorphism problem."""
27
+
28
+ squares: set[Square]
29
+
30
+ @field_validator("squares", mode="after")
31
+ @classmethod
32
+ def check_squares(cls, value: set[Square]) -> set[Square]:
33
+ if any(set(a) & set(b) for a, b in combinations(value, 2)):
34
+ raise ValueError("A square in the solution is not node-disjoint to at least one other square")
35
+ return value
36
+
37
+ def validate_solution(self, instance: UndirectedGraph, role: Role) -> None:
38
+ super().validate_solution(instance, role)
39
+ edge_set = set(instance.edges) | {(v, u) for u, v in instance.edges}
40
+ if any(edge not in edge_set for square in self.squares for edge in edges(square)):
41
+ raise ValidationError("A square is not part of the instance.")
42
+ if any(edge in edge_set for square in self.squares for edge in diagonals(square)):
43
+ raise ValidationError("A square in the solution is not induced in the instance.")
44
+
45
+ @maximize
46
+ def score(self, instance: UndirectedGraph, role: Role) -> float:
47
+ return len(self.squares)
48
+
49
+
50
+ C4subgraphiso = Problem(
51
+ name="Square Subgraph Isomorphism",
52
+ instance_cls=UndirectedGraph,
53
+ solution_cls=Solution,
54
+ min_size=4,
55
+ )
@@ -0,0 +1,12 @@
1
+ """Simple dummy solver for the C4subgraphiso problem, outputting a static solution."""
2
+ import json
3
+
4
+ with open("output/solution.json", "w") as output:
5
+ json.dump(
6
+ {
7
+ "squares": [
8
+ [0, 1, 2, 3],
9
+ ],
10
+ },
11
+ output,
12
+ )
@@ -0,0 +1,74 @@
1
+ """Tests for the c4subgraphiso problem."""
2
+ import unittest
3
+
4
+ from pydantic import ValidationError as PydanticValidationError
5
+
6
+ from algobattle_problems.c4subgraphiso.problem import UndirectedGraph, Solution, ValidationError, Role
7
+
8
+
9
+ class Tests(unittest.TestCase):
10
+ """Tests for the c4subgraphiso verifier."""
11
+
12
+ @classmethod
13
+ def setUpClass(cls) -> None:
14
+ super().setUpClass()
15
+ cls.instance = UndirectedGraph(
16
+ num_vertices=10,
17
+ edges=[
18
+ (0, 1),
19
+ (1, 2),
20
+ (2, 3),
21
+ (2, 4),
22
+ (4, 5),
23
+ (5, 6),
24
+ (6, 7),
25
+ (7, 8),
26
+ (8, 9),
27
+ (9, 0),
28
+ (1, 8),
29
+ (4, 8),
30
+ (4, 7),
31
+ (2, 8),
32
+ (0, 3),
33
+ ],
34
+ )
35
+
36
+ def test_no_duplicate_squares(self):
37
+ with self.assertRaises(PydanticValidationError):
38
+ UndirectedGraph.model_validate(
39
+ {
40
+ "squares": {
41
+ (0, 1, 2, 3),
42
+ (0, 1, 2, 3),
43
+ }
44
+ }
45
+ )
46
+
47
+ def test_vertex_too_big(self):
48
+ solution = Solution(squares={(0, 1, 2, 10)})
49
+ with self.assertRaises(ValidationError):
50
+ solution.validate_solution(self.instance, Role.generator)
51
+
52
+ def test_edge_missing(self):
53
+ solution = Solution(squares={(2, 3, 4, 5)})
54
+ with self.assertRaises(ValidationError):
55
+ solution.validate_solution(self.instance, Role.generator)
56
+
57
+ def test_additional_edge(self):
58
+ solution = Solution(squares={(1, 2, 4, 8)})
59
+ with self.assertRaises(ValidationError):
60
+ solution.validate_solution(self.instance, Role.generator)
61
+
62
+ def test_score(self):
63
+ solution = Solution(squares={(0, 1, 8, 9), (4, 5, 6, 7)})
64
+ solution.validate_solution(self.instance, Role.generator)
65
+ self.assertEqual(solution.score(self.instance, Role.solver), 2)
66
+
67
+ def test_squares_disjoin(self):
68
+ with self.assertRaises(PydanticValidationError):
69
+ solution = Solution(squares={(0, 1, 2, 3), (0, 1, 8, 9)})
70
+ solution.validate_solution(self.instance, Role.generator)
71
+
72
+
73
+ if __name__ == "__main__":
74
+ unittest.main()
@@ -0,0 +1,11 @@
1
+ """Simple dummy generator for the Circle Cover problem, outputting a static instance."""
2
+ import json
3
+ from pathlib import Path
4
+
5
+ Path("output/instance.json").write_text(
6
+ json.dumps(
7
+ {"points": [[0, 1], [50, 50], [100, 17]]},
8
+ )
9
+ )
10
+
11
+ Path("/output/solution.json").write_text(json.dumps({"circles": {"10": [5, 5], "20": [60, 65], "30": [80, 10]}}))
@@ -0,0 +1,47 @@
1
+ """The Circle Cover problem class."""
2
+ from math import sqrt
3
+ from typing import Annotated, Literal
4
+ from algobattle.problem import Problem, InstanceModel, SolutionModel, maximize
5
+ from algobattle.util import Role
6
+ from algobattle.types import LaxComp, Interval
7
+
8
+
9
+ Coordinate = Annotated[float, Interval(ge=0, le=100)]
10
+ Point = tuple[Coordinate, Coordinate]
11
+
12
+
13
+ def distance(a: Point, b: Point) -> float:
14
+ """Calculates the distance between two points."""
15
+ return sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2)
16
+
17
+
18
+ class Instance(InstanceModel):
19
+ """An Instance of the Circle Cover Problem."""
20
+
21
+ points: set[Point]
22
+
23
+ @property
24
+ def size(self) -> int:
25
+ return len(self.points)
26
+
27
+
28
+ class Solution(SolutionModel[Instance]):
29
+ """A solution of the Circle Cover problem."""
30
+
31
+ circles: dict[Literal[10, 20, 30], Point]
32
+
33
+ @maximize
34
+ def score(self, instance: Instance, role: Role) -> float:
35
+ covered = 0
36
+ for size, center in self.circles.items():
37
+ covered += sum(distance(center, point) <= LaxComp(size, role) for point in instance.points)
38
+ return covered
39
+
40
+
41
+ CircleCover = Problem(
42
+ name="Circle Cover",
43
+ instance_cls=Instance,
44
+ solution_cls=Solution,
45
+ min_size=1,
46
+ test_instance=Instance(points={(0, 1), (50, 50), (100, 17)}),
47
+ )
@@ -0,0 +1,5 @@
1
+ """Simple dummy solver for the CircleCover problem, outputting a static solution."""
2
+ import json
3
+ from pathlib import Path
4
+
5
+ Path("/output/solution.json").write_text(json.dumps({"circles": {"10": [5, 5], "20": [60, 65], "30": [80, 10]}}))
@@ -0,0 +1,28 @@
1
+ """Simple dummy generator for the ClusterEditing problem, outputting a static instance."""
2
+ import json
3
+
4
+ with open("output/instance.json", "w+") as output:
5
+ json.dump(
6
+ {
7
+ "num_vertices": 4,
8
+ "edges": [
9
+ [0, 1],
10
+ [1, 2],
11
+ [0, 3],
12
+ ],
13
+ },
14
+ output,
15
+ )
16
+
17
+ with open("output/solution.json", "w+") as output:
18
+ json.dump(
19
+ {
20
+ "add": [
21
+ [0, 2],
22
+ ],
23
+ "delete": [
24
+ [0, 3],
25
+ ],
26
+ },
27
+ output,
28
+ )
@@ -0,0 +1,52 @@
1
+ """The Clusterediting problem class."""
2
+ from collections import defaultdict
3
+ from itertools import combinations
4
+
5
+ from algobattle.problem import Problem, SolutionModel, minimize
6
+ from algobattle.util import Role, ValidationError
7
+ from algobattle.types import Vertex, UndirectedGraph
8
+
9
+
10
+ class Solution(SolutionModel[UndirectedGraph]):
11
+ """A solution to a Cluster Editing problem."""
12
+
13
+ add: set[tuple[Vertex, Vertex]]
14
+ delete: set[tuple[Vertex, Vertex]]
15
+
16
+ def validate_solution(self, instance: UndirectedGraph, role: Role) -> None:
17
+ super().validate_solution(instance, role)
18
+ edge_set = set(instance.edges)
19
+
20
+ # Apply modifications to graph
21
+ for edge in self.add:
22
+ if edge[::-1] not in edge_set:
23
+ edge_set.add(edge)
24
+ for edge in self.delete:
25
+ if edge in edge_set:
26
+ edge_set.remove(edge)
27
+ elif (edge[1], edge[0]) in edge_set:
28
+ edge_set.remove((edge[1], edge[0]))
29
+ else:
30
+ raise ValidationError("Solution contains edge not found in instance.")
31
+
32
+ neighbors: defaultdict[int, set[int]] = defaultdict(set)
33
+ for u, v in edge_set:
34
+ neighbors[u].add(v)
35
+ neighbors[v].add(u)
36
+
37
+ for u in range(instance.num_vertices):
38
+ for v, w in combinations(neighbors[u], 2):
39
+ if not (v, w) in edge_set and not (w, v) in edge_set:
40
+ raise ValidationError("The solution does not transform the graph into a cluster.")
41
+
42
+ @minimize
43
+ def score(self, instance: UndirectedGraph, role: Role) -> float:
44
+ return len(self.add) + len(self.delete)
45
+
46
+
47
+ Clusterediting = Problem(
48
+ name="Cluster Editing",
49
+ min_size=4,
50
+ instance_cls=UndirectedGraph,
51
+ solution_cls=Solution,
52
+ )
@@ -0,0 +1,15 @@
1
+ """Simple dummy solver for the ClusterEditing problem, outputting a static solution."""
2
+ import json
3
+
4
+ with open("output/solution.json", "w") as output:
5
+ json.dump(
6
+ {
7
+ "add": [
8
+ [1, 3],
9
+ ],
10
+ "delete": [
11
+ [1, 2],
12
+ ],
13
+ },
14
+ output,
15
+ )
@@ -0,0 +1,66 @@
1
+ """Tests for the clusterediting problem."""
2
+ import unittest
3
+
4
+ from algobattle_problems.clusterediting.problem import UndirectedGraph, Solution, ValidationError, Role
5
+
6
+
7
+ class Tests(unittest.TestCase):
8
+ """Tests for the clusterediting solutions."""
9
+
10
+ @classmethod
11
+ def setUpClass(cls) -> None:
12
+ cls.instance = UndirectedGraph(
13
+ num_vertices=10,
14
+ edges=[
15
+ (0, 2),
16
+ (0, 5),
17
+ (0, 8),
18
+ (1, 5),
19
+ (1, 7),
20
+ (1, 8),
21
+ (2, 3),
22
+ (2, 5),
23
+ (2, 6),
24
+ (3, 6),
25
+ (4, 8),
26
+ ],
27
+ )
28
+
29
+ def test_delete_nonexisting_edge(self):
30
+ solution = Solution(add=set(), delete={(0, 1)})
31
+ with self.assertRaises(ValidationError):
32
+ solution.validate_solution(self.instance, Role.generator)
33
+
34
+ def test_delete_extra_edge(self):
35
+ instance = UndirectedGraph(num_vertices=4, edges=[(0, 1), (1, 2), (2, 0), (0, 3)])
36
+ solution = Solution(add=set(), delete={(0, 3)})
37
+ solution.validate_solution(instance, Role.generator)
38
+
39
+ def test_delete_and_add_edge(self):
40
+ instance = UndirectedGraph(num_vertices=4, edges=[(1, 2), (2, 0), (0, 3)])
41
+ solution = Solution(add={(0, 1)}, delete={(0, 3)})
42
+ solution.validate_solution(instance, Role.generator)
43
+
44
+ def test_add_edge_reverse(self):
45
+ solution = Solution(add={(2, 0)}, delete=set())
46
+ with self.assertRaises(ValidationError):
47
+ solution.validate_solution(self.instance, Role.generator)
48
+
49
+ def test_delete_edge_reverse(self):
50
+ instance = UndirectedGraph(num_vertices=3, edges=[(0, 1), (1, 2)])
51
+ solution = Solution(add=set(), delete={(1, 0)})
52
+ solution.validate_solution(instance, Role.generator)
53
+
54
+ def test_score(self):
55
+ solution = Solution(add={(0, 1), (5, 8)}, delete={(4, 8), (7, 1), (0, 2), (2, 5)})
56
+ solution.validate_solution(self.instance, Role.generator)
57
+ self.assertEqual(solution.score(self.instance, Role.solver), 1 / 6)
58
+
59
+ def test_solution_doesnt_triangulate(self):
60
+ solution = Solution(add={(0, 1), (5, 8)}, delete={(7, 1), (0, 2), (2, 5)})
61
+ with self.assertRaises(ValidationError):
62
+ solution.validate_solution(self.instance, Role.generator)
63
+
64
+
65
+ if __name__ == "__main__":
66
+ unittest.main()
@@ -0,0 +1,21 @@
1
+ """Simple dummy generator for the DomSet problem, outputting a static instance."""
2
+ import json
3
+
4
+ with open("output/instance.json", "w+") as output:
5
+ json.dump(
6
+ {
7
+ "num_vertices": 2,
8
+ "edges": [
9
+ [0, 1],
10
+ ],
11
+ },
12
+ output,
13
+ )
14
+
15
+ with open("output/solution.json", "w+") as output:
16
+ json.dump(
17
+ {
18
+ "domset": [0],
19
+ },
20
+ output,
21
+ )
@@ -0,0 +1,36 @@
1
+ """The Clusterediting problem class."""
2
+ from algobattle.problem import Problem, SolutionModel, minimize
3
+ from algobattle.util import Role, ValidationError
4
+ from algobattle.types import Vertex, UndirectedGraph
5
+
6
+
7
+ class Solution(SolutionModel[UndirectedGraph]):
8
+ """A solution to a Dominating Set problem."""
9
+
10
+ domset: set[Vertex]
11
+
12
+ def validate_solution(self, instance: UndirectedGraph, role: Role) -> None:
13
+ super().validate_solution(instance, role)
14
+ dominated = set(self.domset)
15
+ for u, v in instance.edges:
16
+ if u in self.domset:
17
+ dominated.add(v)
18
+ elif v in self.domset:
19
+ dominated.add(u)
20
+ if len(dominated) != instance.num_vertices:
21
+ raise ValidationError(
22
+ "Not every vertex is dominated.",
23
+ detail=f"{instance.num_vertices - len(dominated)} vertices are not dominated",
24
+ )
25
+
26
+ @minimize
27
+ def score(self, instance: UndirectedGraph, role: Role) -> float:
28
+ return len(self.domset)
29
+
30
+
31
+ Domset = Problem(
32
+ name="Dominating Set",
33
+ min_size=2,
34
+ instance_cls=UndirectedGraph,
35
+ solution_cls=Solution,
36
+ )
@@ -0,0 +1,10 @@
1
+ """Simple dummy solver for the DomSet problem, outputting a static solution."""
2
+ import json
3
+
4
+ with open("output/solution.json", "w") as output:
5
+ json.dump(
6
+ {
7
+ "domset": [1],
8
+ },
9
+ output,
10
+ )