From 20021deb0194380945c623ad3a98799d909e3cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20L=C3=A9vy?= Date: Thu, 18 Sep 2025 21:10:30 +0200 Subject: [PATCH 1/3] Initiated with weak tests. --- .../AIGreedyMatchingTest.class.st | 254 ++++++++++++++++++ .../AIGreedyMatching.class.st | 116 ++++++++ 2 files changed, 370 insertions(+) create mode 100644 src/AI-Algorithms-Graph-Tests/AIGreedyMatchingTest.class.st create mode 100644 src/AI-Algorithms-Graph/AIGreedyMatching.class.st diff --git a/src/AI-Algorithms-Graph-Tests/AIGreedyMatchingTest.class.st b/src/AI-Algorithms-Graph-Tests/AIGreedyMatchingTest.class.st new file mode 100644 index 0000000..c8d26f1 --- /dev/null +++ b/src/AI-Algorithms-Graph-Tests/AIGreedyMatchingTest.class.st @@ -0,0 +1,254 @@ +Class { + #name : 'AIGreedyMatchingTest', + #superclass : 'TestCase', + #category : 'AI-Algorithms-Graph-Tests-Tests', + #package : 'AI-Algorithms-Graph-Tests', + #tag : 'Tests' +} + +{ #category : 'fixtures' } +AIGreedyMatchingTest >> graphForProvingNotOptimalWeight [ + "Answer a weighted graph for proving that the greedy maximum weighted matching algorithm is not optimal. + This is the example used for the proof in https://www.cs.cornell.edu/courses/cs6820/2014fa/matchingNotes.pdf. + The optimal maximal weight sum is 2. The optimal minimal weight sum is 1+epsilon." + + | nodes edges | + nodes := $a to: $d. + edges := { + #( $a $b 1 ). + { + $b. + $c. + (1 + Number epsilon) }. + #( $c $d 1 ) }. + ^ { + nodes. + edges } +] + +{ #category : 'running' } +AIGreedyMatchingTest >> newMaximumCardinality [ + + ^ AIGreedyMatching newMaximumCardinality +] + +{ #category : 'running' } +AIGreedyMatchingTest >> newMaximumWeighted [ + + ^ AIGreedyMatching newMaximumWeighted +] + +{ #category : 'running' } +AIGreedyMatchingTest >> newMinimumWeighted [ + + ^ AIGreedyMatching newMinimumWeighted +] + +{ #category : 'fixtures' } +AIGreedyMatchingTest >> singleNodeGraphWithLoop [ + + ^ #( #( $a ) #( #( $a $a ) ) ) +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testCyclicNonWeightedComplex [ + + self testMaximumCardinalityIsAtMostPerfectForGraph: + AICyclicNonWeightedComplexFixture new complexCycleGraph. + self testMaximumCardinalityIsAtMostPerfectForGraph: + AICyclicNonWeightedComplexFixture new complexCycleGraph2. + self testMaximumCardinalityIsAtMostPerfectForGraph: + AICyclicNonWeightedComplexFixture new complexUndirectedGraph. + self testMaximumCardinalityIsAtMostPerfectForGraph: + AICyclicNonWeightedComplexFixture new stronglyConnectedGraph +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testCyclicNonWeightedSimple [ + + self testMaximumCardinalityIsAtMostPerfectForGraph: + AICyclicNonWeightedSimpleFixture new aseCircuitGraph. + self testMaximumCardinalityIsAtMostPerfectForGraph: + AICyclicNonWeightedSimpleFixture new aseSccGraph. + self testMaximumCardinalityIsAtMostPerfectForGraph: + AICyclicNonWeightedSimpleFixture new cycleGraph. + self testMaximumCardinalityIsAtMostPerfectForGraph: + AICyclicNonWeightedSimpleFixture new dependencyGraph. + self testMaximumCardinalityIsAtMostPerfectForGraph: + AICyclicNonWeightedSimpleFixture new moduleGraph. + self testMaximumCardinalityIsAtMostPerfectForGraph: + AICyclicNonWeightedSimpleFixture new nestedCycleGraph. + self testMaximumCardinalityIsAtMostPerfectForGraph: + AICyclicNonWeightedSimpleFixture new simpleGraphForHits +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testCyclicWeightedComplex [ + + self testMaximumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedComplexFixture new complexWeightedGraph. + self testMaximumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedComplexFixture new complexWeightedGraph2. + self testMaximumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedComplexFixture new complexWeightedGraph3. + self testMaximumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedComplexFixture new complexWeightedGraph4. + self testMinimumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedComplexFixture new complexWeightedGraph. + self testMinimumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedComplexFixture new complexWeightedGraph2. + self testMinimumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedComplexFixture new complexWeightedGraph3. + self testMinimumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedComplexFixture new complexWeightedGraph4 +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testCyclicWeightedSimple [ + + self testMaximumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedSimpleFixture new aseCircuitWeightedGraph. + self testMaximumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedSimpleFixture new aseWeightedCircuitGraph. + self testMaximumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedSimpleFixture new negativeUnconnectedWeightedGraph. + self testMaximumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedSimpleFixture new negativeWeightedGraph. + self testMaximumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedSimpleFixture new negativeWeightedGraph2. + self testMaximumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedSimpleFixture new simpleWeightedGraph. + self testMaximumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedSimpleFixture new simpleWeightedGraph2. + self testMinimumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedSimpleFixture new aseCircuitWeightedGraph. + self testMinimumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedSimpleFixture new aseWeightedCircuitGraph. + self testMinimumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedSimpleFixture new negativeUnconnectedWeightedGraph. + self testMinimumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedSimpleFixture new negativeWeightedGraph. + self testMinimumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedSimpleFixture new negativeWeightedGraph2. + self testMinimumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedSimpleFixture new simpleWeightedGraph. + self testMinimumWeightedIsAtMostPerfectForGraph: + AICyclicWeightedSimpleFixture new simpleWeightedGraph2 +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testIsAtMostPerfectWithAlgorithm: aAIGreedyMatching graph: graph [ + + aAIGreedyMatching nodes: graph nodes. + aAIGreedyMatching + edges: graph edges + from: #first + to: #second + weight: #third. + "Is at most a (near-)perfect matching." + self assert: aAIGreedyMatching run size <= (graph nodes size // 2) +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testLoop [ + + | algorithm | + algorithm := self newMaximumCardinality. + algorithm nodes: self singleNodeGraphWithLoop first. + algorithm + edges: self singleNodeGraphWithLoop second + from: #first + to: #second. + self assert: algorithm run isEmpty +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testMaximumCardinalityIsAtMostPerfectForGraph: graph [ + + | algorithm | + algorithm := self newMaximumCardinality. + algorithm nodes: graph nodes. + algorithm edges: graph edges from: #first to: #second. + "Is at most a (near-)perfect matching." + self assert: algorithm run size <= (graph nodes size // 2) +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testMaximumWeightedIsAtMostPerfectForGraph: graph [ + + self + testIsAtMostPerfectWithAlgorithm: self newMaximumWeighted + graph: graph +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testMinimumWeightedIsAtMostPerfectForGraph: graph [ + + self + testIsAtMostPerfectWithAlgorithm: self newMinimumWeighted + graph: graph +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testNewShouldnt [ + + self should: [ AIGreedyMatching new ] raise: ShouldNotImplement +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testNonWeightedDAG [ + + self testMaximumCardinalityIsAtMostPerfectForGraph: + AINonWeightedDAGFixture new simpleGraph. + self testMaximumCardinalityIsAtMostPerfectForGraph: + AINonWeightedDAGFixture new moduleGraph2. + self testMaximumCardinalityIsAtMostPerfectForGraph: + AINonWeightedDAGFixture new withoutCyclesComplexGraph. + self testMaximumCardinalityIsAtMostPerfectForGraph: + AINonWeightedDAGFixture new withoutCyclesMediumGraph +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testNotOptimalMaximumWeight [ + + | algorithm provingGraph | + algorithm := self newMaximumWeighted. + provingGraph := self graphForProvingNotOptimalWeight. + algorithm nodes: provingGraph first. + algorithm + edges: provingGraph second + from: #first + to: #second + weight: #third. + "The optimal maximum weight is 2." + self assert: (algorithm run sum: #weight) ~= 1 +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testNotOptimalMinimumWeight [ + + | algorithm provingGraph | + algorithm := self newMinimumWeighted. + provingGraph := self graphForProvingNotOptimalWeight. + algorithm nodes: provingGraph first. + algorithm + edges: provingGraph second + from: #first + to: #second + weight: #third. + "The optimal minimum weight is 1+epsilon." + self assert: (algorithm run sum: #weight) equals: 2 +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testWeightedDAG [ + + self testMaximumWeightedIsAtMostPerfectForGraph: + AIWeightedDAGFixture new weightedDAG. + self testMaximumWeightedIsAtMostPerfectForGraph: + AIWeightedDAGFixture new withoutCyclesComplexWeightedGraph. + self testMinimumWeightedIsAtMostPerfectForGraph: + AIWeightedDAGFixture new weightedDAG. + self testMinimumWeightedIsAtMostPerfectForGraph: + AIWeightedDAGFixture new withoutCyclesComplexWeightedGraph +] diff --git a/src/AI-Algorithms-Graph/AIGreedyMatching.class.st b/src/AI-Algorithms-Graph/AIGreedyMatching.class.st new file mode 100644 index 0000000..5753d8b --- /dev/null +++ b/src/AI-Algorithms-Graph/AIGreedyMatching.class.st @@ -0,0 +1,116 @@ +" +Concepts: +The maximum weight matching problem consists of finding in a weighted graph a matching for which the sum of weights is maximized. +Dually, for the minimum weight matching problem, the sum of weights is minimized. +A maximum matching (also known as maximum-cardinality matching) is a matching that contains the largest possible number of edges. +Maximum matching is not to be confused with maximal matching. +More on these concepts in https://en.wikipedia.org/wiki/Matching_(graph_theory). + +Greedy algorithm: +The greedy matching algorithm doesn't always find the optimal solution. +However, it is a 2 approximation (greedy result >= 1/2 optimal result). +These two claims are concisely proven for the maximum weight matching in the following lecture at page 1: +https://www.cs.cornell.edu/courses/cs6820/2014fa/matchingNotes.pdf +A greedy matching algorithm can be efficient as about O(|E| log(|V|)). +By the way: A greedy matching algorithm always finds a maximal matching. + +Usage: +Instantiate me with `newMaximumWeighted` for the greedy maximum weighted matching algorithm. +Instantiate me with `newMinimumWeighted` for the greedy minimum weighted matching algorithm. +Instantiate me with `newMaximumCardinality` for the greedy maximum cardinality matching algorithm. + +Examples: +See in my test class examples of how to use me. +" +Class { + #name : 'AIGreedyMatching', + #superclass : 'AIGraphAlgorithm', + #instVars : [ + 'edgeClass', + 'preprocessingSortBlock' + ], + #category : 'AI-Algorithms-Graph-Matching', + #package : 'AI-Algorithms-Graph', + #tag : 'Matching' +} + +{ #category : 'instance creation' } +AIGreedyMatching class >> new [ + + ^ self shouldNotImplement +] + +{ #category : 'instance creation' } +AIGreedyMatching class >> newMaximumCardinality [ + + ^ self basicNew initializeMaximumCardinality +] + +{ #category : 'instance creation' } +AIGreedyMatching class >> newMaximumWeighted [ + + ^ self basicNew initializeMaximumWeighted +] + +{ #category : 'instance creation' } +AIGreedyMatching class >> newMinimumWeighted [ + + ^ self basicNew initializeMinimumWeighted +] + +{ #category : 'configuration' } +AIGreedyMatching >> edgeClass [ + + ^ edgeClass +] + +{ #category : 'initialization' } +AIGreedyMatching >> initializeMaximumCardinality [ + + self initialize. + edgeClass := AIGraphEdge. + "No preprocessing sorting is used here. + Alternatively, the edges could be first sorted by the sum of degrees of their endpoints. + This can sometimes produce better results, albeit at the cost of some additional computational overhead. + Runtime complexity: O(m) when the edges are not sorted, O(m+mlogn) otherwise, where n is the number of vertices, and m the number of edges. + Information source: https://jgrapht.org/javadoc-1.5.1/org.jgrapht.core/org/jgrapht/alg/matching/GreedyMaximumCardinalityMatching.html." + preprocessingSortBlock := nil +] + +{ #category : 'initialization' } +AIGreedyMatching >> initializeMaximumWeighted [ + + self initialize. + edgeClass := AIWeightedEdge. + preprocessingSortBlock := [ :a :b | a weight >= b weight ] +] + +{ #category : 'initialization' } +AIGreedyMatching >> initializeMinimumWeighted [ + + self initialize. + edgeClass := AIWeightedEdge. + preprocessingSortBlock := [ :a :b | a weight <= b weight ] +] + +{ #category : 'running' } +AIGreedyMatching >> run [ + + | matchingEdges eligibleEdges | + matchingEdges := Set new. + "Loops are not eligible for matching." + eligibleEdges := self edges reject: [ :edge | edge from = edge to ]. + preprocessingSortBlock ifNotNil: [ :sortBlock | + eligibleEdges sort: preprocessingSortBlock ]. + [ eligibleEdges notEmpty ] whileTrue: [ + | eligibleEdge | + eligibleEdge := eligibleEdges first. + matchingEdges add: eligibleEdge. + eligibleEdges := eligibleEdges reject: [ :edge | + { + edge from. + edge to } includesAny: { + eligibleEdge from. + eligibleEdge to } ] ]. + ^ matchingEdges +] From 88deb6a63a3d2bf0dbe7f38d0d186e67e80debaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20L=C3=A9vy?= Date: Sat, 20 Sep 2025 10:40:53 +0200 Subject: [PATCH 2/3] Added Greedy Matching Algorithm Implementation --- .../AIGreedyMatchingTest.class.st | 173 +++++++++++++++++- .../AIGreedyMatching.class.st | 2 +- 2 files changed, 166 insertions(+), 9 deletions(-) diff --git a/src/AI-Algorithms-Graph-Tests/AIGreedyMatchingTest.class.st b/src/AI-Algorithms-Graph-Tests/AIGreedyMatchingTest.class.st index c8d26f1..212fdf0 100644 --- a/src/AI-Algorithms-Graph-Tests/AIGreedyMatchingTest.class.st +++ b/src/AI-Algorithms-Graph-Tests/AIGreedyMatchingTest.class.st @@ -1,3 +1,13 @@ +" +The tests can be grouped as follows: +- If the fixture's optimal matching is known, test that the greedy result approximates by the half at least. +- For (generated) complete graphs, test that the greedy result is (near-)perfect. +- If the fixture's optimal matching is not known, test that the greedy result is at best perfect. +- Miscellaneous + - Test that loops are not considered for matching. + - Test that the greedy method is not optimal. + - Test that the class AIGreedyMatching shouldn't implement the new method. +" Class { #name : 'AIGreedyMatchingTest', #superclass : 'TestCase', @@ -6,6 +16,53 @@ Class { #tag : 'Tests' } +{ #category : 'fixtures' } +AIGreedyMatchingTest >> bipartiteCompleteGraphWithVertices: numberOfVerticesA and: numberOfVerticesB [ + "Answer a complete bipartite graph of size numberOfVerticesA,numberOfVerticesB." + + | oddNodes evenNodes allNodes edges | + oddNodes := 1 to: numberOfVerticesA * 2 by: 2. + evenNodes := 2 to: numberOfVerticesB * 2 by: 2. + edges := oddNodes + inject: OrderedCollection new + into: [ :collection :from | + collection addAll: (evenNodes collect: [ :to | + { + from. + to } ]). + collection ]. + allNodes := oddNodes asOrderedCollection + addAll: evenNodes; + yourself. + ^ { + allNodes. + edges } +] + +{ #category : 'fixtures' } +AIGreedyMatchingTest >> completeGraphWithVertices: numberOfVertices [ + "Answer a complete graph of size numberOfVertices." + + | nodes edges nodesCopy | + nodes := 1 to: numberOfVertices. + "This to avoid the following (unjustified?) warning: 'Modifies collection while iterating over it'." + nodesCopy := nodes. + edges := nodes + inject: OrderedCollection new + into: [ :collection :from | + | adjacentEdges | + adjacentEdges := (nodesCopy copyWithout: from) collect: [ :to | + { + from. + to } ]. + collection + addAll: adjacentEdges; + yourself ]. + ^ { + nodes. + edges } +] + { #category : 'fixtures' } AIGreedyMatchingTest >> graphForProvingNotOptimalWeight [ "Answer a weighted graph for proving that the greedy maximum weighted matching algorithm is not optimal. @@ -26,6 +83,28 @@ AIGreedyMatchingTest >> graphForProvingNotOptimalWeight [ edges } ] +{ #category : 'fixtures' } +AIGreedyMatchingTest >> mwmfSampleWeigthedGraph [ + "Answer the sample graph (slightly modified to get different maximum and minimum matching weight sums) of the Maximum Weighted Matching Finder online app available at https://mwmatching.johnridesa.bike. +This is an app for finding optimal matches on weighted graphs. The user can enter any example graph with weights at will in order to calculate an optimal maximum weighted matching or the maximum cardinality. It is also possible to dually calculate an optimal minimum weighted matching by subtracting all edge weights in the graph from the maximum weight. +- The maximum weighted matching sums 155. +- The maximum cardinality is 5. +- The minimum weighted matching sums 145." + + | nodes edges | + nodes := #( #john #gabriel #james #joseph #mary #michael #paul #peter + #raphael #andrew ). + edges := #( #( #raphael #andrew 40 ) #( #andrew #gabriel 30 ) + #( #gabriel #raphael 50 ) #( #gabriel #joseph 55 ) + #( #james #paul 10 ) #( #john #peter 10 ) + #( #joseph #michael 60 ) #( #joseph #mary 40 ) + #( #mary #paul 15 ) #( #mary #michael 40 ) + #( #michael #raphael 55 ) #( #peter #raphael 30 ) ). + ^ { + nodes. + edges } +] + { #category : 'running' } AIGreedyMatchingTest >> newMaximumCardinality [ @@ -50,6 +129,40 @@ AIGreedyMatchingTest >> singleNodeGraphWithLoop [ ^ #( #( $a ) #( #( $a $a ) ) ) ] +{ #category : 'tests' } +AIGreedyMatchingTest >> testCompleteBipartiteRegularGraph [ + + | algorithm graph | + algorithm := self newMaximumCardinality. + "A bipartite graph is regular if it is balanced (both parts have the same cardinality)." + graph := self bipartiteCompleteGraphWithVertices: 10 and: 10. + algorithm nodes: graph first. + algorithm edges: graph second from: #first to: #second. + "For a regular bipartite graph, it exists a perfect matching." + self assert: algorithm run size equals: algorithm nodes size // 2 +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testCompleteGraph [ + + self testCompleteGraphVertices: 5. + self testCompleteGraphVertices: 6. + self testCompleteGraphVertices: 10. + self testCompleteGraphVertices: 11 +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testCompleteGraphVertices: aNumberOfVertices [ + + | algorithm graph | + algorithm := self newMaximumCardinality. + graph := self completeGraphWithVertices: aNumberOfVertices. + algorithm nodes: graph first. + algorithm edges: graph second from: #first to: #second. + "For a complete graph, the greedy algorithm finds always a (near-)perfect matching." + self assert: algorithm run size equals: aNumberOfVertices // 2 +] + { #category : 'tests' } AIGreedyMatchingTest >> testCyclicNonWeightedComplex [ @@ -137,16 +250,16 @@ AIGreedyMatchingTest >> testCyclicWeightedSimple [ ] { #category : 'tests' } -AIGreedyMatchingTest >> testIsAtMostPerfectWithAlgorithm: aAIGreedyMatching graph: graph [ +AIGreedyMatchingTest >> testIsAtMostPerfectWithAlgorithm: aAIGreedyMatching weightedGraph: weightedGraph [ - aAIGreedyMatching nodes: graph nodes. + aAIGreedyMatching nodes: weightedGraph nodes. aAIGreedyMatching - edges: graph edges + edges: weightedGraph edges from: #first to: #second weight: #third. "Is at most a (near-)perfect matching." - self assert: aAIGreedyMatching run size <= (graph nodes size // 2) + self assert: aAIGreedyMatching run size <= (weightedGraph nodes size // 2) ] { #category : 'tests' } @@ -174,19 +287,63 @@ AIGreedyMatchingTest >> testMaximumCardinalityIsAtMostPerfectForGraph: graph [ ] { #category : 'tests' } -AIGreedyMatchingTest >> testMaximumWeightedIsAtMostPerfectForGraph: graph [ +AIGreedyMatchingTest >> testMaximumWeightedIsAtMostPerfectForGraph: weightedGraph [ self testIsAtMostPerfectWithAlgorithm: self newMaximumWeighted - graph: graph + weightedGraph: weightedGraph ] { #category : 'tests' } -AIGreedyMatchingTest >> testMinimumWeightedIsAtMostPerfectForGraph: graph [ +AIGreedyMatchingTest >> testMinimumWeightedIsAtMostPerfectForGraph: weightedGraph [ self testIsAtMostPerfectWithAlgorithm: self newMinimumWeighted - graph: graph + weightedGraph: weightedGraph +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testMwmfSampleMaximumCardinality [ + + | algorithm sampleGraph | + algorithm := self newMaximumCardinality. + sampleGraph := self mwmfSampleWeigthedGraph. + algorithm nodes: sampleGraph first. + algorithm edges: sampleGraph second from: #first to: #second. + "The optimal maximum cardinality for this sample graph is 5." + self assert: algorithm run size >= (5 / 2) +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testMwmfSampleMaximumWeight [ + + | algorithm sampleGraph | + algorithm := self newMaximumWeighted. + sampleGraph := self mwmfSampleWeigthedGraph. + algorithm nodes: sampleGraph first. + algorithm + edges: sampleGraph second + from: #first + to: #second + weight: #third. + "The optimal maximum weight for this sample graph is 155." + self assert: (algorithm run sum: #weight) >= (155 / 2) +] + +{ #category : 'tests' } +AIGreedyMatchingTest >> testMwmfSampleMinimumWeight [ + + | algorithm sampleGraph | + algorithm := self newMinimumWeighted. + sampleGraph := self mwmfSampleWeigthedGraph. + algorithm nodes: sampleGraph first. + algorithm + edges: sampleGraph second + from: #first + to: #second + weight: #third. + "The optimal minimum weight for this sample graph is 145." + self assert: (algorithm run sum: #weight) <= (145 * 2) ] { #category : 'tests' } diff --git a/src/AI-Algorithms-Graph/AIGreedyMatching.class.st b/src/AI-Algorithms-Graph/AIGreedyMatching.class.st index 5753d8b..7175d22 100644 --- a/src/AI-Algorithms-Graph/AIGreedyMatching.class.st +++ b/src/AI-Algorithms-Graph/AIGreedyMatching.class.st @@ -20,7 +20,7 @@ Instantiate me with `newMinimumWeighted` for the greedy minimum weighted matchin Instantiate me with `newMaximumCardinality` for the greedy maximum cardinality matching algorithm. Examples: -See in my test class examples of how to use me. +See in my commented test class examples of how to use me. " Class { #name : 'AIGreedyMatching', From 88370dc06dbff74cd184b3aeb31f1d476239d006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20L=C3=A9vy?= Date: Wed, 24 Sep 2025 22:04:50 +0200 Subject: [PATCH 3/3] Added the AIGraphMatchingAlgorithm class implementation. --- ... => AIGraphMatchingAlgorithmTest.class.st} | 103 +++++++++++------- ...s.st => AIGraphMatchingAlgorithm.class.st} | 43 ++++---- 2 files changed, 85 insertions(+), 61 deletions(-) rename src/AI-Algorithms-Graph-Tests/{AIGreedyMatchingTest.class.st => AIGraphMatchingAlgorithmTest.class.st} (76%) rename src/AI-Algorithms-Graph/{AIGreedyMatching.class.st => AIGraphMatchingAlgorithm.class.st} (70%) diff --git a/src/AI-Algorithms-Graph-Tests/AIGreedyMatchingTest.class.st b/src/AI-Algorithms-Graph-Tests/AIGraphMatchingAlgorithmTest.class.st similarity index 76% rename from src/AI-Algorithms-Graph-Tests/AIGreedyMatchingTest.class.st rename to src/AI-Algorithms-Graph-Tests/AIGraphMatchingAlgorithmTest.class.st index 212fdf0..d640180 100644 --- a/src/AI-Algorithms-Graph-Tests/AIGreedyMatchingTest.class.st +++ b/src/AI-Algorithms-Graph-Tests/AIGraphMatchingAlgorithmTest.class.st @@ -6,10 +6,11 @@ The tests can be grouped as follows: - Miscellaneous - Test that loops are not considered for matching. - Test that the greedy method is not optimal. + - Test that all vertices of a matching are distinct. - Test that the class AIGreedyMatching shouldn't implement the new method. " Class { - #name : 'AIGreedyMatchingTest', + #name : 'AIGraphMatchingAlgorithmTest', #superclass : 'TestCase', #category : 'AI-Algorithms-Graph-Tests-Tests', #package : 'AI-Algorithms-Graph-Tests', @@ -17,7 +18,7 @@ Class { } { #category : 'fixtures' } -AIGreedyMatchingTest >> bipartiteCompleteGraphWithVertices: numberOfVerticesA and: numberOfVerticesB [ +AIGraphMatchingAlgorithmTest >> bipartiteCompleteGraphWithVerticesA: numberOfVerticesA andVerticesB: numberOfVerticesB [ "Answer a complete bipartite graph of size numberOfVerticesA,numberOfVerticesB." | oddNodes evenNodes allNodes edges | @@ -40,7 +41,7 @@ AIGreedyMatchingTest >> bipartiteCompleteGraphWithVertices: numberOfVerticesA an ] { #category : 'fixtures' } -AIGreedyMatchingTest >> completeGraphWithVertices: numberOfVertices [ +AIGraphMatchingAlgorithmTest >> completeGraphWithVertices: numberOfVertices [ "Answer a complete graph of size numberOfVertices." | nodes edges nodesCopy | @@ -64,7 +65,7 @@ AIGreedyMatchingTest >> completeGraphWithVertices: numberOfVertices [ ] { #category : 'fixtures' } -AIGreedyMatchingTest >> graphForProvingNotOptimalWeight [ +AIGraphMatchingAlgorithmTest >> graphForProvingNotOptimalWeight [ "Answer a weighted graph for proving that the greedy maximum weighted matching algorithm is not optimal. This is the example used for the proof in https://www.cs.cornell.edu/courses/cs6820/2014fa/matchingNotes.pdf. The optimal maximal weight sum is 2. The optimal minimal weight sum is 1+epsilon." @@ -84,9 +85,9 @@ AIGreedyMatchingTest >> graphForProvingNotOptimalWeight [ ] { #category : 'fixtures' } -AIGreedyMatchingTest >> mwmfSampleWeigthedGraph [ +AIGraphMatchingAlgorithmTest >> mwmfSampleWeigthedGraph [ "Answer the sample graph (slightly modified to get different maximum and minimum matching weight sums) of the Maximum Weighted Matching Finder online app available at https://mwmatching.johnridesa.bike. -This is an app for finding optimal matches on weighted graphs. The user can enter any example graph with weights at will in order to calculate an optimal maximum weighted matching or the maximum cardinality. It is also possible to dually calculate an optimal minimum weighted matching by subtracting all edge weights in the graph from the maximum weight. +This is an app for finding optimal matchings on weighted graphs. The user can enter any example graph with weights at will in order to calculate an optimal maximum weighted matching or the maximum cardinality. It is also possible to dually calculate an optimal minimum weighted matching by subtracting all edge weights in the graph from the maximum weight. - The maximum weighted matching sums 155. - The maximum cardinality is 5. - The minimum weighted matching sums 145." @@ -106,36 +107,36 @@ This is an app for finding optimal matches on weighted graphs. The user can ente ] { #category : 'running' } -AIGreedyMatchingTest >> newMaximumCardinality [ +AIGraphMatchingAlgorithmTest >> newMaximumCardinality [ - ^ AIGreedyMatching newMaximumCardinality + ^ AIGraphMatchingAlgorithm newMaximumCardinality ] { #category : 'running' } -AIGreedyMatchingTest >> newMaximumWeighted [ +AIGraphMatchingAlgorithmTest >> newMaximumWeighted [ - ^ AIGreedyMatching newMaximumWeighted + ^ AIGraphMatchingAlgorithm newMaximumWeighted ] { #category : 'running' } -AIGreedyMatchingTest >> newMinimumWeighted [ +AIGraphMatchingAlgorithmTest >> newMinimumWeighted [ - ^ AIGreedyMatching newMinimumWeighted + ^ AIGraphMatchingAlgorithm newMinimumWeighted ] { #category : 'fixtures' } -AIGreedyMatchingTest >> singleNodeGraphWithLoop [ +AIGraphMatchingAlgorithmTest >> singleNodeGraphWithLoop [ ^ #( #( $a ) #( #( $a $a ) ) ) ] { #category : 'tests' } -AIGreedyMatchingTest >> testCompleteBipartiteRegularGraph [ +AIGraphMatchingAlgorithmTest >> testCompleteBipartiteRegularGraph [ | algorithm graph | algorithm := self newMaximumCardinality. "A bipartite graph is regular if it is balanced (both parts have the same cardinality)." - graph := self bipartiteCompleteGraphWithVertices: 10 and: 10. + graph := self bipartiteCompleteGraphWithVerticesA: 10 andVerticesB: 10. algorithm nodes: graph first. algorithm edges: graph second from: #first to: #second. "For a regular bipartite graph, it exists a perfect matching." @@ -143,28 +144,28 @@ AIGreedyMatchingTest >> testCompleteBipartiteRegularGraph [ ] { #category : 'tests' } -AIGreedyMatchingTest >> testCompleteGraph [ +AIGraphMatchingAlgorithmTest >> testCompleteGraph [ - self testCompleteGraphVertices: 5. - self testCompleteGraphVertices: 6. - self testCompleteGraphVertices: 10. - self testCompleteGraphVertices: 11 + self testCompleteGraphWithVertices: 5. + self testCompleteGraphWithVertices: 6. + self testCompleteGraphWithVertices: 10. + self testCompleteGraphWithVertices: 11 ] { #category : 'tests' } -AIGreedyMatchingTest >> testCompleteGraphVertices: aNumberOfVertices [ +AIGraphMatchingAlgorithmTest >> testCompleteGraphWithVertices: numberOfVertices [ | algorithm graph | algorithm := self newMaximumCardinality. - graph := self completeGraphWithVertices: aNumberOfVertices. + graph := self completeGraphWithVertices: numberOfVertices. algorithm nodes: graph first. algorithm edges: graph second from: #first to: #second. "For a complete graph, the greedy algorithm finds always a (near-)perfect matching." - self assert: algorithm run size equals: aNumberOfVertices // 2 + self assert: algorithm run size equals: numberOfVertices // 2 ] { #category : 'tests' } -AIGreedyMatchingTest >> testCyclicNonWeightedComplex [ +AIGraphMatchingAlgorithmTest >> testCyclicNonWeightedComplex [ self testMaximumCardinalityIsAtMostPerfectForGraph: AICyclicNonWeightedComplexFixture new complexCycleGraph. @@ -177,7 +178,7 @@ AIGreedyMatchingTest >> testCyclicNonWeightedComplex [ ] { #category : 'tests' } -AIGreedyMatchingTest >> testCyclicNonWeightedSimple [ +AIGraphMatchingAlgorithmTest >> testCyclicNonWeightedSimple [ self testMaximumCardinalityIsAtMostPerfectForGraph: AICyclicNonWeightedSimpleFixture new aseCircuitGraph. @@ -196,7 +197,7 @@ AIGreedyMatchingTest >> testCyclicNonWeightedSimple [ ] { #category : 'tests' } -AIGreedyMatchingTest >> testCyclicWeightedComplex [ +AIGraphMatchingAlgorithmTest >> testCyclicWeightedComplex [ self testMaximumWeightedIsAtMostPerfectForGraph: AICyclicWeightedComplexFixture new complexWeightedGraph. @@ -217,7 +218,7 @@ AIGreedyMatchingTest >> testCyclicWeightedComplex [ ] { #category : 'tests' } -AIGreedyMatchingTest >> testCyclicWeightedSimple [ +AIGraphMatchingAlgorithmTest >> testCyclicWeightedSimple [ self testMaximumWeightedIsAtMostPerfectForGraph: AICyclicWeightedSimpleFixture new aseCircuitWeightedGraph. @@ -250,7 +251,27 @@ AIGreedyMatchingTest >> testCyclicWeightedSimple [ ] { #category : 'tests' } -AIGreedyMatchingTest >> testIsAtMostPerfectWithAlgorithm: aAIGreedyMatching weightedGraph: weightedGraph [ +AIGraphMatchingAlgorithmTest >> testDifferentVertices [ + + | algorithm graph distintVertices | + algorithm := self newMaximumCardinality. + graph := self completeGraphWithVertices: 8. + algorithm nodes: graph first. + algorithm edges: graph second from: #first to: #second. + + "A matching (aka independent edge set) has no common vertices." + distintVertices := algorithm run + inject: OrderedCollection new + into: [ :vertices :edge | + vertices + add: edge from; + add: edge to; + yourself ]. + self assert: distintVertices size equals: distintVertices asSet size +] + +{ #category : 'tests' } +AIGraphMatchingAlgorithmTest >> testIsAtMostPerfectWithAlgorithm: aAIGreedyMatching weightedGraph: weightedGraph [ aAIGreedyMatching nodes: weightedGraph nodes. aAIGreedyMatching @@ -263,7 +284,7 @@ AIGreedyMatchingTest >> testIsAtMostPerfectWithAlgorithm: aAIGreedyMatching weig ] { #category : 'tests' } -AIGreedyMatchingTest >> testLoop [ +AIGraphMatchingAlgorithmTest >> testLoop [ | algorithm | algorithm := self newMaximumCardinality. @@ -276,7 +297,7 @@ AIGreedyMatchingTest >> testLoop [ ] { #category : 'tests' } -AIGreedyMatchingTest >> testMaximumCardinalityIsAtMostPerfectForGraph: graph [ +AIGraphMatchingAlgorithmTest >> testMaximumCardinalityIsAtMostPerfectForGraph: graph [ | algorithm | algorithm := self newMaximumCardinality. @@ -287,7 +308,7 @@ AIGreedyMatchingTest >> testMaximumCardinalityIsAtMostPerfectForGraph: graph [ ] { #category : 'tests' } -AIGreedyMatchingTest >> testMaximumWeightedIsAtMostPerfectForGraph: weightedGraph [ +AIGraphMatchingAlgorithmTest >> testMaximumWeightedIsAtMostPerfectForGraph: weightedGraph [ self testIsAtMostPerfectWithAlgorithm: self newMaximumWeighted @@ -295,7 +316,7 @@ AIGreedyMatchingTest >> testMaximumWeightedIsAtMostPerfectForGraph: weightedGrap ] { #category : 'tests' } -AIGreedyMatchingTest >> testMinimumWeightedIsAtMostPerfectForGraph: weightedGraph [ +AIGraphMatchingAlgorithmTest >> testMinimumWeightedIsAtMostPerfectForGraph: weightedGraph [ self testIsAtMostPerfectWithAlgorithm: self newMinimumWeighted @@ -303,7 +324,7 @@ AIGreedyMatchingTest >> testMinimumWeightedIsAtMostPerfectForGraph: weightedGrap ] { #category : 'tests' } -AIGreedyMatchingTest >> testMwmfSampleMaximumCardinality [ +AIGraphMatchingAlgorithmTest >> testMwmfSampleMaximumCardinality [ | algorithm sampleGraph | algorithm := self newMaximumCardinality. @@ -315,7 +336,7 @@ AIGreedyMatchingTest >> testMwmfSampleMaximumCardinality [ ] { #category : 'tests' } -AIGreedyMatchingTest >> testMwmfSampleMaximumWeight [ +AIGraphMatchingAlgorithmTest >> testMwmfSampleMaximumWeight [ | algorithm sampleGraph | algorithm := self newMaximumWeighted. @@ -331,7 +352,7 @@ AIGreedyMatchingTest >> testMwmfSampleMaximumWeight [ ] { #category : 'tests' } -AIGreedyMatchingTest >> testMwmfSampleMinimumWeight [ +AIGraphMatchingAlgorithmTest >> testMwmfSampleMinimumWeight [ | algorithm sampleGraph | algorithm := self newMinimumWeighted. @@ -347,13 +368,13 @@ AIGreedyMatchingTest >> testMwmfSampleMinimumWeight [ ] { #category : 'tests' } -AIGreedyMatchingTest >> testNewShouldnt [ +AIGraphMatchingAlgorithmTest >> testNewShouldnt [ - self should: [ AIGreedyMatching new ] raise: ShouldNotImplement + self should: [ AIGraphMatchingAlgorithm new ] raise: ShouldNotImplement ] { #category : 'tests' } -AIGreedyMatchingTest >> testNonWeightedDAG [ +AIGraphMatchingAlgorithmTest >> testNonWeightedDAG [ self testMaximumCardinalityIsAtMostPerfectForGraph: AINonWeightedDAGFixture new simpleGraph. @@ -366,7 +387,7 @@ AIGreedyMatchingTest >> testNonWeightedDAG [ ] { #category : 'tests' } -AIGreedyMatchingTest >> testNotOptimalMaximumWeight [ +AIGraphMatchingAlgorithmTest >> testNotOptimalMaximumWeight [ | algorithm provingGraph | algorithm := self newMaximumWeighted. @@ -382,7 +403,7 @@ AIGreedyMatchingTest >> testNotOptimalMaximumWeight [ ] { #category : 'tests' } -AIGreedyMatchingTest >> testNotOptimalMinimumWeight [ +AIGraphMatchingAlgorithmTest >> testNotOptimalMinimumWeight [ | algorithm provingGraph | algorithm := self newMinimumWeighted. @@ -398,7 +419,7 @@ AIGreedyMatchingTest >> testNotOptimalMinimumWeight [ ] { #category : 'tests' } -AIGreedyMatchingTest >> testWeightedDAG [ +AIGraphMatchingAlgorithmTest >> testWeightedDAG [ self testMaximumWeightedIsAtMostPerfectForGraph: AIWeightedDAGFixture new weightedDAG. diff --git a/src/AI-Algorithms-Graph/AIGreedyMatching.class.st b/src/AI-Algorithms-Graph/AIGraphMatchingAlgorithm.class.st similarity index 70% rename from src/AI-Algorithms-Graph/AIGreedyMatching.class.st rename to src/AI-Algorithms-Graph/AIGraphMatchingAlgorithm.class.st index 7175d22..a49dde0 100644 --- a/src/AI-Algorithms-Graph/AIGreedyMatching.class.st +++ b/src/AI-Algorithms-Graph/AIGraphMatchingAlgorithm.class.st @@ -1,71 +1,74 @@ " -Concepts: +##Concepts +In graph theory, a *matching* (also known as an *independent edge set*) in an undirected graph is a set of edges without common vertices. The maximum weight matching problem consists of finding in a weighted graph a matching for which the sum of weights is maximized. Dually, for the minimum weight matching problem, the sum of weights is minimized. A maximum matching (also known as maximum-cardinality matching) is a matching that contains the largest possible number of edges. Maximum matching is not to be confused with maximal matching. More on these concepts in https://en.wikipedia.org/wiki/Matching_(graph_theory). +(Don't confuse with another meaning of *graph matching* that is computing the similarity of graphs). -Greedy algorithm: -The greedy matching algorithm doesn't always find the optimal solution. -However, it is a 2 approximation (greedy result >= 1/2 optimal result). -These two claims are concisely proven for the maximum weight matching in the following lecture at page 1: +##Greedy algorithm +For finding matchings in a graph, I provide a version of the well studied greedy algorithms for matching, which are considerably easy to implement. +However, in contrast to [more elaborated matching algorithms](https://brilliant.org/wiki/matching-algorithms), a greedy graph matching algorithm doesn't always find the optimal solution. +Nevertheless, it is a **2-approximation** (greedy result >= 1/2 optimal result). +These two claims are concisely proven for the maximum weight matching at the beginning of the following lecture: https://www.cs.cornell.edu/courses/cs6820/2014fa/matchingNotes.pdf A greedy matching algorithm can be efficient as about O(|E| log(|V|)). By the way: A greedy matching algorithm always finds a maximal matching. -Usage: +##Usage Instantiate me with `newMaximumWeighted` for the greedy maximum weighted matching algorithm. Instantiate me with `newMinimumWeighted` for the greedy minimum weighted matching algorithm. Instantiate me with `newMaximumCardinality` for the greedy maximum cardinality matching algorithm. -Examples: +##Examples See in my commented test class examples of how to use me. " Class { - #name : 'AIGreedyMatching', + #name : 'AIGraphMatchingAlgorithm', #superclass : 'AIGraphAlgorithm', #instVars : [ 'edgeClass', 'preprocessingSortBlock' ], - #category : 'AI-Algorithms-Graph-Matching', + #category : 'AI-Algorithms-Graph-Graph Matching', #package : 'AI-Algorithms-Graph', - #tag : 'Matching' + #tag : 'Graph Matching' } { #category : 'instance creation' } -AIGreedyMatching class >> new [ +AIGraphMatchingAlgorithm class >> new [ ^ self shouldNotImplement ] { #category : 'instance creation' } -AIGreedyMatching class >> newMaximumCardinality [ +AIGraphMatchingAlgorithm class >> newMaximumCardinality [ ^ self basicNew initializeMaximumCardinality ] { #category : 'instance creation' } -AIGreedyMatching class >> newMaximumWeighted [ +AIGraphMatchingAlgorithm class >> newMaximumWeighted [ ^ self basicNew initializeMaximumWeighted ] { #category : 'instance creation' } -AIGreedyMatching class >> newMinimumWeighted [ +AIGraphMatchingAlgorithm class >> newMinimumWeighted [ ^ self basicNew initializeMinimumWeighted ] { #category : 'configuration' } -AIGreedyMatching >> edgeClass [ +AIGraphMatchingAlgorithm >> edgeClass [ ^ edgeClass ] { #category : 'initialization' } -AIGreedyMatching >> initializeMaximumCardinality [ +AIGraphMatchingAlgorithm >> initializeMaximumCardinality [ self initialize. edgeClass := AIGraphEdge. @@ -78,7 +81,7 @@ AIGreedyMatching >> initializeMaximumCardinality [ ] { #category : 'initialization' } -AIGreedyMatching >> initializeMaximumWeighted [ +AIGraphMatchingAlgorithm >> initializeMaximumWeighted [ self initialize. edgeClass := AIWeightedEdge. @@ -86,7 +89,7 @@ AIGreedyMatching >> initializeMaximumWeighted [ ] { #category : 'initialization' } -AIGreedyMatching >> initializeMinimumWeighted [ +AIGraphMatchingAlgorithm >> initializeMinimumWeighted [ self initialize. edgeClass := AIWeightedEdge. @@ -94,11 +97,11 @@ AIGreedyMatching >> initializeMinimumWeighted [ ] { #category : 'running' } -AIGreedyMatching >> run [ +AIGraphMatchingAlgorithm >> run [ | matchingEdges eligibleEdges | matchingEdges := Set new. - "Loops are not eligible for matching." + "Loops are not eligible." eligibleEdges := self edges reject: [ :edge | edge from = edge to ]. preprocessingSortBlock ifNotNil: [ :sortBlock | eligibleEdges sort: preprocessingSortBlock ].