aboutsummaryrefslogtreecommitdiff
path: root/src/dk.biks.bachelorizer/dk/biks/bachelorizer/Graph.java
blob: f8bafcd5702a7e43488ad23bc53b2a5521478243 (plain)
  1. // SPDX-FileCopyrightText: 2025 <Ian Valentin Christensen stud-ianc@ruc.dk>
  2. // SPDX-FileCopyrightText: 2025 Jonas Smedegaard <dr@jones.dk>
  3. // SPDX-License-Identifier: GPL-3.0-or-later
  4. package dk.biks.bachelorizer;
  5. import java.util.ArrayList;
  6. import java.util.Collection;
  7. import java.util.Collections;
  8. import java.util.HashSet;
  9. import java.util.HashMap;
  10. import java.util.Map;
  11. import java.util.List;
  12. import java.util.Random;
  13. import java.util.Set;
  14. import java.util.stream.Collectors;
  15. import com.example.portfolio3.AdjListGraph;
  16. import com.example.portfolio3.AbstractGraph;
  17. import com.example.portfolio3.GraphAlgorithms;
  18. import com.example.portfolio3.MatrixGraph;
  19. import com.example.portfolio3.Vertex;
  20. /// Bachelorizer - database model
  21. ///
  22. /// Slurps and parses data from upstream-provided comma-separated file.
  23. ///
  24. /// @version 0.0.1
  25. /// @see <https://moodle.ruc.dk/mod/assign/view.php?id=523186>
  26. public final class Graph extends Storage {
  27. /// amount of iterations in demo
  28. private static final int DEMO_ITERATIONS = 1000000;
  29. /// path to source file for graph
  30. private String path = "combi.txt";
  31. /// data about combinations as a Graph
  32. private AbstractGraph g;
  33. /// Default constructor
  34. // to silence javadoc
  35. private Graph() {}
  36. /// constructor with custom file source
  37. ///
  38. /// @param path path to source file for graph
  39. private Graph(String path) {
  40. this.path = path;
  41. }
  42. /// JVM entry point
  43. ///
  44. /// @param args command-line arguments
  45. public static void main(final String[] args) {
  46. Graph g;
  47. // first argument, if provided, is the data file path;
  48. // else use upstream named file in current directory.
  49. g = (args.length > 0)
  50. ? new Graph(args[0])
  51. : new Graph();
  52. g.initialize();
  53. g.demo();
  54. }
  55. /// load graph from file
  56. void initialize() {
  57. // read into temporary graph to resolve vertice count
  58. //
  59. // use Adjacency List Representation:
  60. // * cheap to bootstrap (done once)
  61. // * simple to count vertices (done once): O(1)
  62. AbstractGraph _g = new AdjListGraph();
  63. GraphAlgorithms.readGraph(_g, path);
  64. Integer _vertice_count = _g.vertices().size();
  65. // read into final graph
  66. //
  67. // use Adjacency Matrix Representation:
  68. // * expensive to bootstrap (done once)
  69. // * simple to add edges but needs vertice count
  70. // * simple to get edges (done repeatedly): O(1)
  71. //
  72. // TODO: avoid reading and parsing file twice
  73. g = new MatrixGraph(_vertice_count);
  74. GraphAlgorithms.readGraph(g, path);
  75. }
  76. /// check that a graph is connected
  77. ///
  78. /// If check fails, throw an unchecked exception,
  79. /// since it occurs at runtime and is unrecoverable.
  80. ///
  81. /// Time complexity of the operation is O(n²)
  82. /// where n is the amount of vertices,
  83. /// since visitDepthFirst() recurses out-edges of all vertices.
  84. ///
  85. /// @throws IllegalArgumentException
  86. /// https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html
  87. public void assertConnected() {
  88. // collect all vertices in the graph
  89. Collection<Vertex> c = g.vertices();
  90. // pick a random vertice in the graph
  91. Vertex v = g.vertices().iterator().next();
  92. // collect the set of visitable vertices
  93. Set<Vertex> visited = GraphAlgorithms.visitDepthFirst(
  94. g, v);
  95. // throw exception if not all vertices were visited
  96. if (visited.size() != c.size()) {
  97. throw new IllegalArgumentException(
  98. "graph is not connected");
  99. }
  100. }
  101. /// sets of disjoint choices
  102. ///
  103. /// @return list of disjoint sets
  104. public ArrayList<Set<Vertex>> disjoint() {
  105. // get all subject modules
  106. //
  107. // TODO: optionally seed this, to enable ranking control
  108. List<Vertex> modules = new ArrayList<>(g.vertices());
  109. long seed = System.currentTimeMillis();
  110. Random random = new Random(seed);
  111. System.out.println(
  112. "Seed used for shuffling modules: " + seed);
  113. Collections.shuffle(modules, random);
  114. return disjoint(modules);
  115. }
  116. /// groups of disjoint choices seeded by priority list of choices
  117. ///
  118. /// @param vip Ordered list of subject modules to prioritize
  119. /// @return List of sets of disjoint choices
  120. public ArrayList<Set<Vertex>> disjoint(final List<Vertex> vip) {
  121. ArrayList<Set<Vertex>> sets = new ArrayList<>();
  122. // track done subject modules as extendable set
  123. //
  124. // HashList used for efficient addition and lookup
  125. Set<Vertex> done = new HashSet<>();
  126. // convert module list to a set
  127. //
  128. // defined once outside loop as a slight optimization
  129. Set<Vertex> all_set = new HashSet<>(vip);
  130. // iterate over subject modules
  131. for (Vertex current : vip) {
  132. if (done.contains(current)) {
  133. continue;
  134. }
  135. // add set of current and any unconnected modules
  136. Set<Vertex> isolated = new HashSet<>();
  137. isolated.add(current);
  138. for (Vertex compared: vip) {
  139. // skip if already done
  140. if (compared.equals(current)
  141. || done.contains(compared)) {
  142. continue;
  143. }
  144. if (isolated.stream().allMatch(u ->
  145. g.getWeight(u,compared) == null)
  146. ) {
  147. isolated.add(compared);
  148. }
  149. }
  150. // add as set and omit from future iterations
  151. sets.add(isolated);
  152. done.addAll(isolated);
  153. }
  154. return sets;
  155. }
  156. /// sum of students' selections as a graph
  157. ///
  158. /// @param sets list of disjoint choices
  159. /// @return groups of disjoint choices as a graph
  160. private AbstractGraph moduleGroups(
  161. final ArrayList<Set<Vertex>> sets
  162. ) {
  163. AbstractGraph h = new AdjListGraph();
  164. Map<Set<Vertex>, String> groupLabel = new HashMap<>();
  165. for (Set<Vertex> groupSet : sets) {
  166. // stringify each group of module selections
  167. String name = groupSet.stream()
  168. .map(Vertex::toString)
  169. // avoid differently sorted duplicates
  170. .sorted()
  171. // formatting of groups as vertices
  172. .collect(Collectors.joining(
  173. ", ", "{", "}"));
  174. // map group to name of group
  175. groupLabel.put(groupSet, name);
  176. }
  177. for (Set<Vertex> s: sets) {
  178. for (Set<Vertex> t: sets) {
  179. if (t == s) {
  180. continue;
  181. }
  182. // process each pair only once
  183. if (s.hashCode() > t.hashCode()) {
  184. continue;
  185. }
  186. // accumulate student count across set
  187. int sum = 0;
  188. for (Vertex v: s) {
  189. // students with this choice
  190. for (Vertex w: t) {
  191. Integer weight =
  192. g.getWeight(v, w);
  193. if (weight != null) {
  194. sum += weight;
  195. }
  196. }
  197. }
  198. // ensure h is treated as undirected
  199. h.insertEdge(
  200. groupLabel.get(s),
  201. groupLabel.get(t),
  202. sum);
  203. h.insertEdge(
  204. groupLabel.get(t),
  205. groupLabel.get(s),
  206. sum);
  207. }
  208. }
  209. return h;
  210. }
  211. /// loop through random combinations of sets of modules
  212. ///
  213. /// @param g sets of disjoint choices as a graph
  214. /// @return best path among many random ones
  215. private static long[] solveManyTimes(final AbstractGraph g) {
  216. // number higher than total students
  217. long[] bestSolution = solve(g);
  218. for (int i = 1; i < DEMO_ITERATIONS; i++) {
  219. long[] solution = solve(g);
  220. if (solution[0] < bestSolution[0]) {
  221. // store shortest path
  222. bestSolution = solution;
  223. }
  224. }
  225. return bestSolution;
  226. }
  227. /// find total weight of random path through disjoint choice sets
  228. ///
  229. /// @param g sets of disjoint choices as a graph
  230. /// @return total weight of random path and its seed as array
  231. private static long[] solve(final AbstractGraph g) {
  232. /// parameters for solution
  233. long[] solution = new long[] {0, 0};
  234. List<Vertex> path = new ArrayList<>(g.vertices());
  235. solution[1] = System.currentTimeMillis();
  236. Random random = new Random(solution[1]);
  237. // order of list contents becomes path order
  238. Collections.shuffle(path, random);
  239. for (int i = 0; i < path.size() - 1; i++) {
  240. solution[0] += g.getWeight(
  241. path.get(i), path.get(i + 1));
  242. }
  243. return solution;
  244. }
  245. /// Demo function to solve assignment
  246. private void demo() {
  247. System.out.println("\n\nGraph of choices constructed:");
  248. GraphAlgorithms.printGraph(g);
  249. // ensure the graph is connected
  250. assertConnected();
  251. System.out.println(
  252. "\n\nGraph is connected"
  253. + " (otherwise an exception was thrown)");
  254. // collect disjoint choice sets
  255. ArrayList<Set<Vertex>> s = disjoint();
  256. System.out.printf(
  257. "\n\n%d disjoint choice sets collected:\n",
  258. s.size());
  259. for (Set<Vertex> verticeSet: s) {
  260. System.out.print(
  261. "set(" + verticeSet.size() + "):");
  262. for (Vertex v: GraphAlgorithms.sortVertex(
  263. verticeSet)
  264. ) {
  265. System.out.print(" " + v.toString());
  266. }
  267. System.out.println();
  268. }
  269. // construct graph of disjoint choices
  270. AbstractGraph h = moduleGroups(s);
  271. System.out.println(
  272. "\n\nGraph of disjoint choices constructed:");
  273. GraphAlgorithms.printGraph(h);
  274. // find path through disjoint choice graph
  275. long[] solution = solveManyTimes(h);
  276. System.out.printf(
  277. "\n\nSolution with disjoint choices found: "
  278. + "%d (disjoint shuffle seed: %d)%n",
  279. solution[0], solution[1]);
  280. }
  281. }