Skip to content

Commit b9224bb

Browse files
authored
Merge pull request #215 from cicirello/k-cycle-distance
Added implementation of k-cycle distance
2 parents f6ac699 + c93d84c commit b9224bb

File tree

5 files changed

+205
-0
lines changed

5 files changed

+205
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010
* CycleDistance: implementation of cycle distance described in https://doi.org/10.3390/app12115506
1111
* CycleEditDistance: implementation of cycle edit distance described in https://doi.org/10.3390/app12115506
12+
* KCycleDistance: implementation of k-cycle distance described in https://doi.org/10.3390/app12115506
1213

1314
### Changed
1415

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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>K-Cycle distance is the count of the number of non-singleton permutation cycles
28+
* of length at most K. Specifically, each non-singleton cycle contributes to the
29+
* total distance the number of cycles of length at most K necessary to transform
30+
* the cycle to all fixed points. K-cycle distance is a metric provided that K &le; 4.
31+
* However, if K &gt; 4, it is only a semi-metric because it fails to satisfy the
32+
* triangle inequality when K &ge; 5.</p>
33+
*
34+
* <p>K-Cycle distance was introduced in the following article:</p>
35+
*
36+
* <p>Vincent A. Cicirello. 2022. <a href="https://www.cicirello.org/publications/applsci-12-05506.pdf">Cycle
37+
* Mutation: Evolving Permutations via Cycle Induction</a>, <i>Applied Sciences</i>, 12(11), Article 5506 (June 2022).
38+
* doi:<a href="https://doi.org/10.3390/app12115506">10.3390/app12115506</a></p>
39+
*
40+
* <p>Runtime: O(n), where n is the permutation length.</p>
41+
*
42+
* @author <a href=https://www.cicirello.org/ target=_top>Vincent A. Cicirello</a>,
43+
* <a href=https://www.cicirello.org/ target=_top>https://www.cicirello.org/</a>
44+
*/
45+
public final class KCycleDistance implements NormalizedPermutationDistanceMeasurer {
46+
47+
private final int maxCycleLength;
48+
49+
private int lastLength;
50+
private int precomputedMax;
51+
52+
/**
53+
* Constructs the distance measurer as specified in the class documentation.
54+
*
55+
* @param k The maximum length cycle that is considered an atomic edit operation, such that k is greater than or
56+
* equal to 2.
57+
*
58+
* @throws IllegalArgumentException if k is less than 2
59+
*/
60+
public KCycleDistance(int k) {
61+
if (k < 2) {
62+
throw new IllegalArgumentException("k must be at least 2");
63+
}
64+
this.maxCycleLength = k;
65+
}
66+
67+
/**
68+
* {@inheritDoc}
69+
*
70+
* @throws IllegalArgumentException if p1.length() is not equal to p2.length().
71+
*/
72+
@Override
73+
public int distance(Permutation p1, Permutation p2) {
74+
if (p1.length() != p2.length()) {
75+
throw new IllegalArgumentException("Permutations must be the same length");
76+
}
77+
boolean[] used = new boolean[p1.length()];
78+
for (int k = 0; k < used.length; k++) {
79+
if (p1.get(k) == p2.get(k)) {
80+
used[p1.get(k)] = true;
81+
}
82+
}
83+
int i = 0;
84+
for (i = 0; i < used.length; i++) {
85+
if (!used[p1.get(i)]) {
86+
break;
87+
}
88+
}
89+
90+
int[] invP1 = p1.getInverse();
91+
int cycleCount = 0;
92+
int iLast = i;
93+
94+
while (i < used.length) {
95+
int j = p1.get(i);
96+
int cycleLength = 0;
97+
while (!used[j]) {
98+
used[j] = true;
99+
cycleLength++;
100+
j = p2.get(i);
101+
i = invP1[j];
102+
}
103+
104+
if (cycleLength > maxCycleLength) {
105+
cycleCount += (int)Math.ceil((cycleLength-1.0)/(maxCycleLength-1.0));
106+
} else {
107+
cycleCount++;
108+
}
109+
110+
for (i = iLast + 1; i < used.length; i++) {
111+
if (!used[p1.get(i)]) {
112+
break;
113+
}
114+
}
115+
iLast = i;
116+
}
117+
return cycleCount;
118+
}
119+
120+
@Override
121+
public int max(int length) {
122+
if (length != lastLength) {
123+
lastLength = length;
124+
precomputedMax = Math.max(
125+
length >> 1,
126+
(int)Math.ceil((length-1.0)/(maxCycleLength-1.0))
127+
);
128+
}
129+
return precomputedMax;
130+
}
131+
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,18 @@ public void testCycleEditDistance() {
236236
}
237237
}
238238

239+
@Test
240+
public void testKCycleDistance() {
241+
for (int k = 2; k <= 5; k++) {
242+
KCycleDistance d = new KCycleDistance(k);
243+
for (int n = 0; n <= 7; n++) {
244+
int expected = bruteForceComputeMax(d,n);
245+
assertEquals(expected, d.max(n), "n,k=" + n + "," + k);
246+
assertEquals(1.0*expected, d.maxf(n), EPSILON, "Failed on length: " + n);
247+
}
248+
}
249+
}
250+
239251
//@Test // uncomment if we implement
240252
public void testEditDistance() {
241253
/* // Uncomment if we implement.

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,16 @@ public void testCycleEditDistance() {
175175
}
176176
}
177177

178+
@Test
179+
public void testKCycleDistance() {
180+
for (int k = 2; k <= 5; k++) {
181+
KCycleDistance d = new KCycleDistance(k);
182+
for (int n = 0; n <= 7; n++) {
183+
assertEquals(n<=1 ? 0.0 : 1.0, bruteForceComputeMax(d,n), EPSILON, "n,k=" + n + "," + k);
184+
}
185+
}
186+
}
187+
178188
//@Test // uncomment if we implement
179189
public void testEditDistance() {
180190
/* // Uncomment if we implement.

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,57 @@ public void testCycleEditDistance() {
859859
);
860860
}
861861

862+
@Test
863+
public void testKCycleDistance() {
864+
int[] p = { 1, 0, 4, 2, 3, 6, 7, 8, 5, 10, 11, 12, 13, 9, 19, 14, 15, 16, 17, 18 };
865+
int[] expectedDist = {0, 0, 15, 9, 7, 6};
866+
for (int k = 2; k <= 5; k++) {
867+
KCycleDistance d = new KCycleDistance(k);
868+
// All one cycle
869+
for (int n = 2; n <= 16; n *= 2) {
870+
int[] perm = new int[n];
871+
for (int i = 0; i < n; i++) {
872+
perm[i] = (i + 1) % n;
873+
}
874+
Permutation p1 = new Permutation(n, 0);
875+
Permutation p2 = new Permutation(perm);
876+
int expected = 1 + (n > k ? (int)Math.ceil((n-k)/(k-1.0)) : 0);
877+
assertEquals(expected, d.distance(p1, p2));
878+
assertEquals(expected, d.distance(p2, p1));
879+
}
880+
// maximal cycles
881+
for (int n = 2; n <= 16; n *= 2) {
882+
int[] perm = new int[n];
883+
for (int i = 0; i < n; i++) {
884+
if (i%2==1) {
885+
perm[i] = perm[i-1];
886+
perm[i-1] = i;
887+
} else {
888+
perm[i] = i;
889+
}
890+
}
891+
Permutation p1 = new Permutation(n, 0);
892+
Permutation p2 = new Permutation(perm);
893+
int expected = n / 2;
894+
assertEquals(expected, d.distance(p1, p2));
895+
assertEquals(expected, d.distance(p2, p1));
896+
}
897+
// Mix of cycle lengths
898+
Permutation p1 = new Permutation(p.length, 0);
899+
Permutation p2 = new Permutation(p);
900+
assertEquals(expectedDist[k], d.distance(p1, p2));
901+
assertEquals(expectedDist[k], d.distance(p2, p1));
902+
IllegalArgumentException thrown = assertThrows(
903+
IllegalArgumentException.class,
904+
() -> d.distance(new Permutation(1), new Permutation(2))
905+
);
906+
}
907+
IllegalArgumentException thrown = assertThrows(
908+
IllegalArgumentException.class,
909+
() -> new KCycleDistance(1)
910+
);
911+
}
912+
862913
private void identicalPermutations(PermutationDistanceMeasurer d) {
863914
for (int n = 0; n <= 10; n++) {
864915
Permutation p = new Permutation(n);

0 commit comments

Comments
 (0)