// Copyright (c) 2026 Timofey Solomko // Licensed under MIT License // // See LICENSE for license information /// Suffix Array by Induced Sorting enum SuffixArray { private enum SuffixType { case sType case lType } private struct State { let input: [Int] let typemap: [SuffixType] let bucketSizes: [Int] let bucketHeads: [Int] let bucketTails: [Int] init(bytes: [Int], alphabetSize: Int) { self.input = bytes var typemap = Array(repeating: SuffixType.lType, count: bytes.count + 1) // Extra one for empty suffix. var bucketSizes = Array(repeating: 0, count: alphabetSize) typemap[bytes.count] = .sType // The empty suffix is S type. if bytes.count != 0 { bucketSizes[bytes[bytes.count - 1]] += 1 // The suffix containing only the last character must be "larger" than the empty suffix. typemap[bytes.count - 1] = .lType } for i in stride(from: bytes.count - 2, through: 0, by: -1) { if bytes[i] > bytes[i + 1] { typemap[i] = .lType } else if bytes[i] == bytes[i + 1] && typemap[i + 1] == .lType { typemap[i] = .lType } else { typemap[i] = .sType } bucketSizes[bytes[i]] += 1 } self.typemap = typemap self.bucketSizes = bucketSizes var bucketHeads = [Int]() bucketHeads.reserveCapacity(alphabetSize) var bucketTails = [Int]() bucketTails.reserveCapacity(alphabetSize) var offset = 1 for size in bucketSizes { bucketHeads.append(offset) offset += size bucketTails.append(offset - 1) } self.bucketHeads = bucketHeads self.bucketTails = bucketTails } } private struct Summary { let bytes: [Int] let alphabetSize: Int let suffixOffsets: [Int] init(_ guessedSuffixArray: [Int], _ state: State) { var lmsNames = Array(repeating: -1, count: state.input.count + 1) var currentName = 0 lmsNames[guessedSuffixArray[0]] = currentName var lastLMSSuffixOffset = guessedSuffixArray[0] for i in 1.. [Int] { if self.alphabetSize == self.bytes.count { var summarySuffixArray = Array(repeating: -1, count: self.bytes.count + 1) summarySuffixArray[0] = self.bytes.count for i in 0.. [Int] { let state = State(bytes: bytes, alphabetSize: alphabetSize) var guessedSuffixArray = guessLMSSort(state) induceSort(&guessedSuffixArray, state) let summary = Summary(guessedSuffixArray, state) let summarySuffixArray = summary.makeSuffixArray() var result = accurateLMSSort(summarySuffixArray, summary, state) induceSort(&result, state) return result } private static func isLMSChar(at offset: Int, _ state: State) -> Bool { if offset == 0 { return false } return state.typemap[offset] == .sType && state.typemap[offset - 1] == .lType } private static func lmsSubstringsAreEqual(_ offsetA: Int, _ offsetB: Int, _ state: State) -> Bool { if offsetA == state.input.count || offsetB == state.input.count { return false } var i = 0 while true { let aIsLMS = isLMSChar(at: offsetA + i, state) let bIsLMS = isLMSChar(at: offsetB + i, state) if i > 0 && aIsLMS && bIsLMS { // We reached the ends of both LMS substrings. return true } if aIsLMS != bIsLMS { // We found the end of one LMS substring but not of the other. return false } if state.input[i + offsetA] != state.input[i + offsetB] { // We found different characters in substrings. return false } i += 1 } } private static func guessLMSSort(_ state: State) -> [Int] { var result = Array(repeating: -1, count: state.input.count + 1) var bucketTails = state.bucketTails for i in 0.. [Int] { var suffixOffsets = Array(repeating: -1, count: state.input.count + 1) var bucketTails = state.bucketTails for i in stride(from: summarySuffixArray.count - 1, to: 1, by: -1) { let bytesIndex = summary.suffixOffsets[summarySuffixArray[i]] let bucketIndex = state.input[bytesIndex] suffixOffsets[bucketTails[bucketIndex]] = bytesIndex bucketTails[bucketIndex] -= 1 } suffixOffsets[0] = state.input.count return suffixOffsets } private static func induceSort(_ guessedSuffixArray: inout [Int], _ state: State) { var bucketHeads = state.bucketHeads var bucketTails = state.bucketTails for i in 0..= 0 && state.typemap[j] == .lType else { continue } let bucketIndex = state.input[j] guessedSuffixArray[bucketHeads[bucketIndex]] = j bucketHeads[bucketIndex] += 1 } for i in stride(from: guessedSuffixArray.count - 1, through: 0, by: -1) { let j = guessedSuffixArray[i] - 1 guard j >= 0 && state.typemap[j] == .sType else { continue } let bucketIndex = state.input[j] guessedSuffixArray[bucketTails[bucketIndex]] = j bucketTails[bucketIndex] -= 1 } } }