Skip to content

Commit be55a71

Browse files
committed
Merge branch 'develop'
2 parents b2b3c69 + bdcb5ab commit be55a71

21 files changed

+1306
-321
lines changed

.github/workflows/CI.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Test # autorun of the project build and tests
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
paths:
7+
- '**.kt'
8+
push:
9+
paths:
10+
- '**.kt'
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Check out repository code
16+
uses: actions/checkout@v4
17+
- name: Set up JDK 21
18+
uses: actions/setup-java@v4
19+
with:
20+
java-version: '21'
21+
distribution: zulu
22+
- name: Build
23+
run: ./gradlew build -x test
24+
- name: Test
25+
run: ./gradlew test

gradlew

100644100755
File mode changed.

src/main/kotlin/Main.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,30 @@ import androidx.compose.runtime.remember
99
import androidx.compose.runtime.setValue
1010
import androidx.compose.ui.window.Window
1111
import androidx.compose.ui.window.application
12+
import model.graph.DirectedGraph
1213
import model.graph.Graph
1314
import model.graph.UndirectedGraph
1415
import view.MainScreen
1516
import viewmodel.MainScreenViewModel
1617
import viewmodel.graph.CircularPlacementStrategy
1718

18-
val sampleGraph = UndirectedGraph<String>().apply {
19+
val sampleGraph = DirectedGraph<String>().apply {
1920
addVertex(1, "A")
2021
addVertex(2, "B")
2122
addVertex(3, "C")
2223
addVertex(4, "D")
2324
addVertex(5, "E")
24-
25-
addEdge(Pair(1, 2), 1)
26-
addEdge(Pair(2, 3), 2)
25+
addVertex(6, "F")
26+
addVertex(7, "G")
27+
addVertex(8, "H")
28+
addVertex(9, "I")
29+
addEdge(Pair(1, 2), 4)
30+
addEdge(Pair(2, 3), 12)
2731
addEdge(Pair(3, 4), 3)
28-
addEdge(Pair(2, 4), 4)
29-
addEdge(Pair(1, 5), 5)
32+
addEdge(Pair(4, 3), 56)
33+
addEdge(Pair(7, 8), 0)
34+
addEdge(Pair(8, 9), 19)
35+
addEdge(Pair(9, 7), 2)
3036
}
3137

3238
@Composable
Lines changed: 140 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,162 @@
11
package model.algorithms
22

3-
import model.graph.Graph
43
import model.graph.Edge
4+
import model.graph.UndirectedGraph
5+
import model.graph.Vertex
6+
7+
/**
8+
* The class [CycleSearch] implements the algorithm for finding a cycle around
9+
* selected vertex of the undirected graph.
10+
*
11+
* At the same time, the algorithm has some minimization of the desired cycle
12+
* by iterating through possible pairs of neighbors of the selected vertex
13+
* through which the desired cycle will pass.
14+
* @param D input type
15+
* @property [graph] a undirected graph for whose vertex we will search for a cycle
16+
* @constructor Creates a graph, based on [graph],for which it will be possible to apply
17+
* a cycle search algorithm around the vertex selected in it.
18+
*/
19+
20+
class CycleSearch<D>(private val graph: UndirectedGraph<D>) {
21+
22+
/**
23+
* This auxiliary function for the function [findAnyCycle] aims to get from
24+
* some hashmap with extra elements the hashmap the elements of which
25+
* accurately describe found cycle path.
26+
*
27+
* @param cyclePath hashmap with extra elements
28+
* @param cycleVertexId the index of the vertex around which the cycle must be found
29+
* @return cycle path
30+
* @receiver private fun [findAnyCycle]
31+
*/
32+
private fun getCyclePath(cyclePath: HashMap<Int, Int>, cycleVertexId: Int): HashMap<Int, Int> {
33+
34+
var current: Int = cycleVertexId
35+
var next: Int
36+
val returnCyclePath = HashMap<Int, Int>()
37+
do {
38+
next = cyclePath.filter { it.key == current }.values.first()
39+
returnCyclePath[current] = next
40+
current = next
41+
} while (current != cycleVertexId)
42+
43+
return returnCyclePath
544

6-
class CycleSearch<D>(private val graph: Graph<D>) {
45+
}
746

8-
fun findCycle(cycleVertexId: Int): List<Edge<D>>? {
47+
/**Searches for a cycle by dfs algorithm and writes it into [cyclePath]
48+
*
49+
*
50+
* @param cycleVertexId the index of the vertex around which the cycle must be found
51+
* @param currentVertexId using for recording the [cyclePath]
52+
* @param visited stores information about which vertices of the graph
53+
* have already been processed by [dfs]
54+
* @param cyclePath hashmap consisting of elements that can be used to restore the found cycle path
55+
* by applying the auxiliary function [getCyclePath] to it in function [findAnyCycle].
56+
* @receiver private fun [findAnyCycle]
57+
*/
58+
private fun dfs(
59+
cycleVertexId: Int,
60+
currentVertexId: Int,
61+
visited: HashMap<Int, Boolean>,
62+
cyclePath: HashMap<Int, Int>
63+
) {
64+
65+
for (idAdjacency in graph.adjacency[currentVertexId]!!.keys) {
966

10-
if (cycleVertexId !in graph.vertices.keys) {
11-
throw IllegalArgumentException("Vertex with index = $cycleVertexId doesn't exist in the graph. The cycle can't be found")
67+
if (visited[idAdjacency] == false) {
68+
visited[idAdjacency] = true
69+
cyclePath[currentVertexId] = idAdjacency
70+
dfs(cycleVertexId, idAdjacency, visited, cyclePath)
71+
} else if (idAdjacency == cycleVertexId && cyclePath[cycleVertexId] != currentVertexId) {
72+
cyclePath[currentVertexId] = idAdjacency
73+
return
74+
}
1275
}
76+
}
1377

14-
val visited = HashMap<Int, Boolean>()
15-
for (vertexId in graph.vertices.keys) {
16-
visited[vertexId] = false
78+
/** Finds any existing cycle in the graph around a given vertex
79+
*
80+
* @param vertexId the index of the vertex around which the cycle must be found
81+
* @return found cycle path or null if the cycle doesn't exist around vertex with id = [vertexId]
82+
* @receiver fun [findCycle]
83+
*/
84+
private fun findAnyCycle(vertexId: Int): HashMap<Int, Int>? {
85+
86+
val devCyclePath = HashMap<Int, Int>()
87+
val visited = hashMapOf<Int, Boolean>()
88+
for (idVertex in graph.vertices.keys) {
89+
visited[idVertex] = false
1790
}
18-
val cycleWayFrom =
19-
HashMap<Int, Int>()
20-
val cycleIsFound = Array<Boolean>(1) { false }
21-
val returnList = mutableListOf<Edge<D>>()
22-
23-
dfs(cycleVertexId, visited, cycleWayFrom, cycleVertexId, cycleIsFound, returnList)
24-
if (cycleIsFound[0]) {
25-
return returnList
91+
visited[vertexId] = true
92+
dfs(vertexId, vertexId, visited, devCyclePath)
93+
94+
if (devCyclePath.filter { it.value == vertexId }.isEmpty()) {
95+
return null
2696
}
27-
return null
97+
return getCyclePath(devCyclePath, vertexId)
2898

2999
}
30100

31-
private fun dfs(
32-
vertexId: Int,
33-
visited: HashMap<Int, Boolean>,
34-
cycleWayFrom: HashMap<Int, Int>,
35-
cycleVertexId: Int,
36-
cycleIsFound: Array<Boolean>,
37-
returnList: MutableList<Edge<D>>
38-
) {
101+
/**
102+
* The function implements the algorithm for finding a cycle around
103+
* vertex with id = [vertex] of the undirected graph.
104+
*
105+
* The function searches for a cycle and performs some minimization of it.
106+
* During the cycle minimization we iterate through variants of cycle,
107+
* based on the choice of a pair of neighbors of [vertex] through which the cycle will pass.
108+
* Each of the found cycle variants is written to a variable 'currentCyclePath'
109+
* and its size compared with 'minCycleSize'.
110+
* @param vertex the vertex around which the cycle must be found
111+
* @return cycle path or null if it doesn't exist
112+
*/
113+
fun findCycle(vertex: Vertex<D>): UndirectedGraph<D>? {
114+
115+
var returnCyclePath = hashMapOf<Int, Int>()
116+
var currentCyclePath: HashMap<Int, Int>?
117+
var minCycleSize: Int = Int.MAX_VALUE
118+
119+
if (vertex.id !in graph.vertices.keys) {
120+
throw IllegalArgumentException("Vertex with id = ${vertex.id} doesn't exist in the graph")
121+
}
122+
val returnGraph = UndirectedGraph<D>()
123+
if (graph.adjacency[vertex.id]!!.size < 2) {
124+
return null
125+
} else { // we consider all possible cases of a cycle by choosing a pair of neighbors to minimize the found cycle
126+
val adjacencyOfVertex: MutableList<Int> = arrayListOf()
127+
graph.adjacency[vertex.id]!!.keys.forEach { adjacencyOfVertex.add(it) }
128+
val adjacencyWas: MutableList<Int> = arrayListOf()
129+
val removedEdges: MutableList<Edge<Int>> = arrayListOf()
130+
for (firstAdjacency in adjacencyOfVertex) {
131+
adjacencyWas.add(firstAdjacency)
132+
for (secondAdjacency in adjacencyOfVertex.filter { it !in adjacencyWas && it != vertex.id }) {
133+
134+
for (adjacency in graph.adjacency[vertex.id]!!.filter { it.key != firstAdjacency && it.key != secondAdjacency }) {
135+
removedEdges.add(Edge(vertex.id to adjacency.key, adjacency.value))
136+
graph.removeEdge(vertex.id to adjacency.key, adjacency.value)
137+
}
39138

40-
visited[vertexId] = true
139+
currentCyclePath = findAnyCycle(vertex.id)
140+
if (currentCyclePath != null && currentCyclePath.size < minCycleSize) {
141+
minCycleSize = currentCyclePath.size
142+
returnCyclePath = currentCyclePath
143+
}
41144

42-
if (!cycleIsFound[0]) {
43-
for (adjacencyVertexId in graph.adjacency[vertexId]!!.keys) {
44-
45-
if (visited[adjacencyVertexId] == false) {
46-
cycleWayFrom[adjacencyVertexId] = vertexId
47-
dfs(adjacencyVertexId, visited, cycleWayFrom, cycleVertexId, cycleIsFound, returnList)
48-
} else if (adjacencyVertexId == cycleVertexId && adjacencyVertexId != cycleWayFrom[vertexId]) {
49-
cycleWayFrom[cycleVertexId] = vertexId
50-
var id = cycleWayFrom.keys.last()
51-
while (id != cycleVertexId) {
52-
returnList.add(Edge<D>(cycleWayFrom[id]!! to id, graph.adjacency[cycleWayFrom[id]]!![id]))
53-
id = cycleWayFrom[id]!!
145+
for (edge in removedEdges) {
146+
graph.addEdge(edge.vertices, edge.weight)
54147
}
55-
returnList.add(Edge<D>(cycleWayFrom[id]!! to id, graph.adjacency[cycleWayFrom[id]]!![id]))
56-
cycleIsFound[0] = true
148+
removedEdges.clear()
57149
}
58-
59150
}
60151
}
61-
}
62-
63-
64-
}
65-
66152

153+
if (minCycleSize == Int.MAX_VALUE) {
154+
return null
155+
}
156+
returnCyclePath.forEach { returnGraph.addVertex(it.key, graph.vertices[it.key]!!.data) }
157+
returnCyclePath.forEach { returnGraph.addEdge(it.key to it.value, graph.adjacency[it.key]!![it.value]) }
158+
return returnGraph
67159

160+
}
68161

162+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package model.algorithms
2+
3+
import model.graph.DirectedGraph
4+
import model.graph.Graph
5+
import model.graph.UndirectedGraph
6+
import kotlin.math.roundToInt
7+
8+
/** The class [HarmonicCentrality] implements the normalized harmonic centrality algorithm for
9+
* a graph of any kind. For implementation using the Dijkstra algorithm.
10+
*
11+
* Harmonic centrality is a kind of closeness centrality, but the difference is that
12+
* this algorithm is relevant not only for connected graphs, but also for disconnected ones.
13+
* The harmonic centrality index for each vertex of the graph is calculated using the formula:
14+
* Index = sum(1/the length of the shortest path to i-th vertex),
15+
* for i in 1..n-1, where 'n' is amount of graph vertices.
16+
* So that the index value lies in the interval from 0 to 1, we use the normalization of the centrality index
17+
* by dividing the result by n, where 'n' is amount of graph vertices:
18+
* Index = sum(1/the length of the shortest path to i-th vertex) / n,
19+
* for i in 1..n-1, where 'n' is amount of graph vertices.
20+
* In this case the length of the path is the amount of edges of this path.
21+
*
22+
* @param D input type
23+
* @property [graph] a graph (of any kind) for the vertices of which it is necessary to calculate the centrality index
24+
* @constructor Creates a graph, based on [graph], for which the algorithm for calculating the
25+
* normalized harmonic centrality can be applied.
26+
*/
27+
28+
class HarmonicCentrality<D>(private val graph: Graph<D>) {
29+
30+
/**
31+
* This auxiliary function for the function [getIndex], that rounds
32+
* the value [number] to the 4th digit after the decimal point.
33+
*
34+
* @return result of rounding
35+
* @receiver [getIndex]
36+
*/
37+
private fun roundTo(number: Double): Double {
38+
return (number * 10000.0).roundToInt() / 10000.0
39+
}
40+
41+
/**
42+
* This function calculate the centrality index for vertex with id = [vertexId].
43+
*
44+
* @param graph graph for the vertex of which we calculate the centrality index.
45+
* But weight of each graph edge is 1, what is used for applying Dijkstra algorithm.
46+
* @param vertexId the index of the vertex for which calculating the centrality index.
47+
* @return centrality index of vertex with id = [vertexId]
48+
* @receiver [harmonicCentrality]
49+
*/
50+
private fun getIndex(graph: Graph<D>, vertexId: Int): Double {
51+
52+
var index = 0.00
53+
54+
graph.vertices.filter { it.key != vertexId }
55+
.forEach {
56+
if (Dijkstra(graph).findShortestPaths(vertexId, it.key).isNotEmpty()) {
57+
index += 1.0 / ((Dijkstra(graph).findShortestPaths(vertexId, it.key)).size - 1)
58+
}
59+
}
60+
61+
return roundTo(index / (graph.vertices.size - 1))
62+
63+
}
64+
65+
/**
66+
* This function calculates the centrality index for each graph vertex.
67+
*
68+
* @return hashmap, where key = vertex id; value = centrality index.
69+
*/
70+
fun harmonicCentrality(): HashMap<Int, Double> {
71+
72+
val centralityIndexes = HashMap<Int, Double>()
73+
74+
val graphForCentrality: Graph<D> = if (graph is UndirectedGraph) {
75+
UndirectedGraph()
76+
} else {
77+
DirectedGraph()
78+
}
79+
graph.vertices.forEach { graphForCentrality.addVertex(it.key, it.value.data) }
80+
graph.edges.forEach { graphForCentrality.addEdge(it.vertices, 1) }
81+
82+
for (vertexId in graphForCentrality.vertices.keys) {
83+
centralityIndexes[vertexId] = getIndex(graphForCentrality, vertexId)
84+
}
85+
86+
return centralityIndexes
87+
88+
}
89+
90+
}

0 commit comments

Comments
 (0)