1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* @package s9e\ShortestCommonSuperstring |
5
|
|
|
* @copyright Copyright (c) 2016 The s9e Authors |
6
|
|
|
* @license http://www.opensource.org/licenses/mit-license.php The MIT License |
7
|
|
|
*/ |
8
|
|
|
namespace s9e\ShortestCommonSuperstring; |
9
|
|
|
|
10
|
|
|
class ShortestCommonSuperstring |
11
|
|
|
{ |
12
|
|
|
/** |
13
|
|
|
* @var integer Affix length for current iteration |
14
|
|
|
*/ |
15
|
|
|
protected $len; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* @var string[] Prefixes of current length |
19
|
|
|
*/ |
20
|
|
|
protected $prefixes; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @var string[] Strings being merged |
24
|
|
|
*/ |
25
|
|
|
protected $strings; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var string[] Suffixes of current length |
29
|
|
|
*/ |
30
|
|
|
protected $suffixes; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Get the shortest string that contains all given strings |
34
|
|
|
* |
35
|
|
|
* @param string[] $strings |
36
|
|
|
* @return string |
37
|
|
|
*/ |
38
|
11 |
|
public function getShortest(array $strings) |
39
|
|
|
{ |
40
|
11 |
|
$this->strings = array_unique($strings); |
41
|
11 |
|
$this->sortStrings(); |
42
|
11 |
|
$this->removeEmptyStrings(); |
43
|
11 |
|
$this->removeFullyOverlappingStrings(); |
44
|
11 |
|
if (count($this->strings) > 1) |
45
|
|
|
{ |
46
|
9 |
|
$this->len = strlen($this->strings[0]); |
47
|
9 |
|
while (--$this->len > 0) |
48
|
|
|
{ |
49
|
9 |
|
$this->mergeStrings(); |
50
|
|
|
} |
51
|
|
|
} |
52
|
|
|
|
53
|
11 |
|
return implode('', $this->strings); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Compare strings |
58
|
|
|
* |
59
|
|
|
* @param string $a |
60
|
|
|
* @param string $b |
61
|
|
|
* @return integer |
62
|
|
|
*/ |
63
|
9 |
|
protected static function compareStrings($a, $b) |
64
|
|
|
{ |
65
|
9 |
|
$aLen = strlen($a); |
66
|
9 |
|
$bLen = strlen($b); |
67
|
9 |
|
if ($aLen !== $bLen) |
68
|
|
|
{ |
69
|
|
|
// Longest first |
70
|
3 |
|
return $bLen - $aLen; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
// Lexical order |
74
|
9 |
|
return ($a > $b) ? 1 : -1; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Return the list of keys pointing to strings whose prefix is identical to their suffix |
79
|
|
|
* |
80
|
|
|
* @return integer[] |
81
|
|
|
*/ |
82
|
9 |
|
protected function getIdenticalAffixKeys() |
83
|
|
|
{ |
84
|
9 |
|
$identicalAffixKeys = []; |
85
|
9 |
|
foreach ($this->prefixes as $k => $prefix) |
86
|
|
|
{ |
87
|
9 |
|
if ($this->suffixes[$k] === $prefix) |
88
|
|
|
{ |
89
|
9 |
|
$identicalAffixKeys[] = $k; |
90
|
|
|
} |
91
|
|
|
} |
92
|
|
|
|
93
|
9 |
|
return $identicalAffixKeys; |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Match and merge a string by key |
98
|
|
|
* |
99
|
|
|
* @param integer $leftKey Left string's key |
100
|
|
|
* @return bool Whether a match was found and strings merged |
101
|
|
|
*/ |
102
|
9 |
|
protected function mergeString($leftKey) |
103
|
|
|
{ |
104
|
9 |
|
$suffix = $this->suffixes[$leftKey]; |
105
|
9 |
|
foreach ($this->prefixes as $rightKey => $prefix) |
106
|
|
|
{ |
107
|
9 |
|
if ($prefix === $suffix && $leftKey !== $rightKey) |
108
|
|
|
{ |
109
|
7 |
|
$this->mergeStringPair($leftKey, $rightKey); |
110
|
|
|
|
111
|
9 |
|
return true; |
112
|
|
|
} |
113
|
|
|
} |
114
|
|
|
|
115
|
9 |
|
return false; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Merge two stored strings together at current affix length |
120
|
|
|
* |
121
|
|
|
* @param integer $leftKey Left string's key |
122
|
|
|
* @param integer $rightKey Right string's key |
123
|
|
|
* @return void |
124
|
|
|
*/ |
125
|
7 |
|
protected function mergeStringPair($leftKey, $rightKey) |
126
|
|
|
{ |
127
|
7 |
|
$this->strings[$leftKey] .= substr($this->strings[$rightKey], $this->len); |
128
|
7 |
|
$this->suffixes[$leftKey] = $this->suffixes[$rightKey]; |
129
|
7 |
|
unset($this->prefixes[$rightKey], $this->strings[$rightKey]); |
130
|
7 |
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Merge all stored strings using current affix length |
134
|
|
|
* |
135
|
|
|
* @return void |
136
|
|
|
*/ |
137
|
9 |
|
protected function mergeStrings() |
138
|
|
|
{ |
139
|
9 |
|
$this->storeAffixes(); |
140
|
|
|
|
141
|
|
|
// Merge strings whose prefix is identical to their suffix |
142
|
9 |
|
$keys = $this->getIdenticalAffixKeys(); |
143
|
9 |
|
$this->mergeStringsGroup($keys); |
144
|
|
|
|
145
|
|
|
// Merge the remaining strings that have a prefix stored |
146
|
9 |
|
$keys = array_diff(array_keys($this->prefixes), $keys); |
147
|
9 |
|
$this->mergeStringsGroup($keys); |
148
|
|
|
|
149
|
9 |
|
$this->resetKeys(); |
150
|
9 |
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Match and merge strings from given group |
154
|
|
|
* |
155
|
|
|
* @param integer[] $keys List of keys |
156
|
|
|
* @return void |
157
|
|
|
*/ |
158
|
9 |
|
protected function mergeStringsGroup(array $keys) |
159
|
|
|
{ |
160
|
9 |
|
foreach ($keys as $leftKey) |
161
|
|
|
{ |
162
|
9 |
|
if (isset($this->strings[$leftKey])) |
163
|
|
|
{ |
164
|
9 |
|
while ($this->mergeString($leftKey)) |
|
|
|
|
165
|
|
|
{ |
166
|
|
|
// Keep going |
167
|
|
|
} |
168
|
|
|
} |
169
|
|
|
} |
170
|
9 |
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Remove empty strings from the list |
174
|
|
|
* |
175
|
|
|
* @return void |
176
|
|
|
*/ |
177
|
11 |
|
protected function removeEmptyStrings() |
178
|
|
|
{ |
179
|
11 |
|
if (end($this->strings) === '') |
180
|
|
|
{ |
181
|
1 |
|
array_pop($this->strings); |
182
|
|
|
} |
183
|
11 |
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* Remove fully-overlapping strings from the list |
187
|
|
|
* |
188
|
|
|
* @return void |
189
|
|
|
*/ |
190
|
11 |
|
protected function removeFullyOverlappingStrings() |
191
|
|
|
{ |
192
|
11 |
|
$i = count($this->strings); |
193
|
11 |
|
while (--$i > 0) |
194
|
|
|
{ |
195
|
9 |
|
$j = $i; |
196
|
9 |
|
while (--$j >= 0) |
197
|
|
|
{ |
198
|
9 |
|
if (strpos($this->strings[$j], $this->strings[$i]) !== false) |
199
|
|
|
{ |
200
|
2 |
|
unset($this->strings[$i]); |
201
|
2 |
|
break; |
202
|
|
|
} |
203
|
|
|
} |
204
|
|
|
} |
205
|
|
|
|
206
|
11 |
|
$this->resetKeys(); |
207
|
11 |
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* Reset the keys in the string list to remove the gaps |
211
|
|
|
* |
212
|
|
|
* @return void |
213
|
|
|
*/ |
214
|
11 |
|
protected function resetKeys() |
215
|
|
|
{ |
216
|
11 |
|
end($this->strings); |
217
|
11 |
|
if (key($this->strings) !== count($this->strings) - 1) |
218
|
|
|
{ |
219
|
6 |
|
$this->strings = array_values($this->strings); |
|
|
|
|
220
|
|
|
} |
221
|
11 |
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* Sort the stored strings |
225
|
|
|
* |
226
|
|
|
* @return void |
227
|
|
|
*/ |
228
|
11 |
|
protected function sortStrings() |
229
|
|
|
{ |
230
|
11 |
|
usort($this->strings, [__CLASS__, 'compareStrings']); |
231
|
11 |
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Capture and stored affixes of current length |
235
|
|
|
* |
236
|
|
|
* Will only store affixes from strings that are longer than current affix length |
237
|
|
|
* |
238
|
|
|
* @return void |
239
|
|
|
*/ |
240
|
9 |
|
protected function storeAffixes() |
241
|
|
|
{ |
242
|
9 |
|
$this->prefixes = []; |
243
|
9 |
|
$this->suffixes = []; |
244
|
9 |
|
foreach ($this->strings as $str) |
245
|
|
|
{ |
246
|
9 |
|
if (strlen($str) <= $this->len) |
247
|
|
|
{ |
248
|
1 |
|
break; |
249
|
|
|
} |
250
|
9 |
|
$this->prefixes[] = substr($str, 0, $this->len); |
251
|
9 |
|
$this->suffixes[] = substr($str, -$this->len); |
252
|
|
|
} |
253
|
|
|
} |
254
|
|
|
} |
This check looks for
while
loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.Consider removing the loop.