Skip to content

Commit f6ac699

Browse files
authored
Merge pull request #214 from cicirello/cycle-edit-distance
Added implementation of cycle edit distance
2 parents 8b56a0b + 7105a19 commit f6ac699

File tree

5 files changed

+158
-3
lines changed

5 files changed

+158
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## [Unreleased] - 2022-05-31
7+
## [Unreleased] - 2022-06-01
88

99
### Added
1010
* CycleDistance: implementation of cycle distance described in https://doi.org/10.3390/app12115506
11+
* CycleEditDistance: implementation of cycle edit distance described in https://doi.org/10.3390/app12115506
1112

1213
### Changed
1314

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* JavaPermutationTools: A Java library for computation on permutations and sequences
3+
* Copyright 2005-2022 Vincent A. Cicirello, <https://www.cicirello.org/>.
4+
*
5+
* This file is part of JavaPermutationTools (https://jpt.cicirello.org/).
6+
*
7+
* JavaPermutationTools is free software: you can
8+
* redistribute it and/or modify it under the terms of the GNU
9+
* General Public License as published by the Free Software
10+
* Foundation, either version 3 of the License, or (at your
11+
* option) any later version.
12+
*
13+
* JavaPermutationTools is distributed in the hope
14+
* that it will be useful, but WITHOUT ANY WARRANTY; without even
15+
* the implied warranty of MERCHANTABILITY or FITNESS FOR A
16+
* PARTICULAR PURPOSE. See the GNU General Public License for more
17+
* details.
18+
*
19+
* You should have received a copy of the GNU General Public License
20+
* along with JavaPermutationTools. If not, see <http://www.gnu.org/licenses/>.
21+
*/
22+
package org.cicirello.permutations.distance;
23+
24+
import org.cicirello.permutations.Permutation;
25+
26+
/**
27+
* <p>Cycle edit distance is the minimum number of non-singleton permutation cycles
28+
* necessary to transform permutation p1 into p2. If p1 equals p2, then this distance
29+
* is 0. If p1 and p2 have a single permutation cycle, then this distance is clearly 1
30+
* since the inverse of that cycle will produce n fixed points. Otherwise, this distance
31+
* is equal to 2 since it can be shown that at most 2 permutation cycle operations are
32+
* necessary to transform any permutation of length n into any other. Cycle edit distance
33+
* satisfies all of the metric properties.</p>
34+
*
35+
* <p>Cycle edit distance was introduced in the following article:</p>
36+
*
37+
* <p>Vincent A. Cicirello. 2022. <a href="https://www.cicirello.org/publications/applsci-12-05506.pdf">Cycle
38+
* Mutation: Evolving Permutations via Cycle Induction</a>, <i>Applied Sciences</i>, 12(11), Article 5506 (June 2022).
39+
* doi:<a href="https://doi.org/10.3390/app12115506">10.3390/app12115506</a></p>
40+
*
41+
* <p>Runtime: O(n), where n is the permutation length.</p>
42+
*
43+
* @author <a href=https://www.cicirello.org/ target=_top>Vincent A. Cicirello</a>,
44+
* <a href=https://www.cicirello.org/ target=_top>https://www.cicirello.org/</a>
45+
*/
46+
public final class CycleEditDistance implements NormalizedPermutationDistanceMeasurer {
47+
48+
/**
49+
* Constructs the distance measurer as specified in the class documentation.
50+
*/
51+
public CycleEditDistance() {
52+
53+
}
54+
55+
/**
56+
* {@inheritDoc}
57+
*
58+
* @throws IllegalArgumentException if p1.length() is not equal to p2.length().
59+
*/
60+
@Override
61+
public int distance(Permutation p1, Permutation p2) {
62+
if (p1.length() != p2.length()) {
63+
throw new IllegalArgumentException("Permutations must be the same length");
64+
}
65+
boolean[] used = new boolean[p1.length()];
66+
for (int k = 0; k < used.length; k++) {
67+
if (p1.get(k) == p2.get(k)) {
68+
used[p1.get(k)] = true;
69+
}
70+
}
71+
int i = 0;
72+
for (i = 0; i < used.length; i++) {
73+
if (!used[p1.get(i)]) {
74+
break;
75+
}
76+
}
77+
78+
if (i >= used.length) {
79+
return 0;
80+
} else {
81+
int[] invP1 = p1.getInverse();
82+
int iLast = i;
83+
84+
int j = p1.get(i);
85+
while (!used[j]) {
86+
used[j] = true;
87+
j = p2.get(i);
88+
i = invP1[j];
89+
}
90+
for (i = iLast + 1; i < used.length; i++) {
91+
if (!used[p1.get(i)]) {
92+
return 2;
93+
}
94+
}
95+
return 1;
96+
}
97+
}
98+
99+
@Override
100+
public int max(int length) {
101+
return length >= 4 ? 2 : (length >= 2 ? 1 :0);
102+
}
103+
}

src/test/java/org/cicirello/permutations/distance/PermutationDistanceMaxTests.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,16 @@ public void testCycleDistance() {
226226
}
227227
}
228228

229+
@Test
230+
public void testCycleEditDistance() {
231+
CycleEditDistance d = new CycleEditDistance();
232+
for (int n = 0; n <= 7; n++) {
233+
int expected = bruteForceComputeMax(d,n);
234+
assertEquals(expected, d.max(n), "Failed on length: " + n);
235+
assertEquals(1.0*expected, d.maxf(n), EPSILON, "Failed on length: " + n);
236+
}
237+
}
238+
229239
//@Test // uncomment if we implement
230240
public void testEditDistance() {
231241
/* // Uncomment if we implement.

src/test/java/org/cicirello/permutations/distance/PermutationDistanceNormTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,14 @@ public void testCycleDistance() {
167167
}
168168
}
169169

170+
@Test
171+
public void testCycleEditDistance() {
172+
CycleEditDistance d = new CycleEditDistance();
173+
for (int n = 0; n <= 7; n++) {
174+
assertEquals(n<=1 ? 0.0 : 1.0, bruteForceComputeMax(d,n), EPSILON, "Failed on length: " + n);
175+
}
176+
}
177+
170178
//@Test // uncomment if we implement
171179
public void testEditDistance() {
172180
/* // Uncomment if we implement.

src/test/java/org/cicirello/permutations/distance/PermutationDistanceTests.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -811,9 +811,42 @@ public void testCycleDistance() {
811811
{1, 2, 3, 4, 5, 6, 7, 0},
812812
{7, 1, 2, 3, 4, 5, 6, 0},
813813
{7, 1, 5, 3, 4, 2, 6, 0},
814-
{7, 6, 5, 4, 3, 2, 1, 0}
814+
{7, 6, 5, 4, 3, 2, 1, 0},
815+
{2, 3, 4, 5, 0, 1, 7, 6},
816+
{15, 3, 4, 5, 6, 1, 7, 2, 9, 10, 11, 8, 13, 14, 12, 0}
815817
};
816-
int[] expected = {1, 1, 1, 2, 1, 1, 2, 4};
818+
int[] expected = {1, 1, 1, 2, 1, 1, 2, 4, 3, 5};
819+
for (int i = 0; i < expected.length; i++) {
820+
Permutation p1 = new Permutation(cases[i].length, 0);
821+
Permutation p2 = new Permutation(cases[i]);
822+
assertEquals(expected[i], d.distance(p1, p2));
823+
assertEquals(expected[i], d.distance(p2, p1));
824+
}
825+
IllegalArgumentException thrown = assertThrows(
826+
IllegalArgumentException.class,
827+
() -> d.distance(new Permutation(1), new Permutation(2))
828+
);
829+
}
830+
831+
@Test
832+
public void testCycleEditDistance() {
833+
CycleEditDistance d = new CycleEditDistance();
834+
identicalPermutations(d);
835+
int[][] cases = {
836+
{1, 0},
837+
{1, 2, 0},
838+
{1, 2, 3, 0},
839+
{1, 0, 3, 2},
840+
{1, 2, 3, 4, 5, 6, 7, 0},
841+
{7, 1, 2, 3, 4, 5, 6, 0},
842+
{7, 1, 5, 3, 4, 2, 6, 0},
843+
{7, 6, 5, 4, 3, 2, 1, 0},
844+
{2, 3, 4, 5, 0, 1, 7, 6},
845+
{15, 3, 4, 5, 6, 1, 7, 2, 9, 10, 11, 8, 13, 14, 12, 0},
846+
{0, 1},
847+
{0}
848+
};
849+
int[] expected = {1, 1, 1, 2, 1, 1, 2, 2, 2, 2, 0, 0};
817850
for (int i = 0; i < expected.length; i++) {
818851
Permutation p1 = new Permutation(cases[i].length, 0);
819852
Permutation p2 = new Permutation(cases[i]);

0 commit comments

Comments
 (0)