From 2fbb4a730378f51e186929486c316f4db4902338 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Fri, 10 May 2024 22:54:49 +0800 Subject: [PATCH] Perf: new timsort --- Build/lib/stable-sort-domain.ts | 8 +- Build/lib/timsort.ts | 956 ++++++++++++++++++++++++++++++++ bun.lockb | Bin 112066 -> 111370 bytes package.json | 2 - 4 files changed, 960 insertions(+), 6 deletions(-) create mode 100644 Build/lib/timsort.ts diff --git a/Build/lib/stable-sort-domain.ts b/Build/lib/stable-sort-domain.ts index 22b35fc64..2f24c317d 100644 --- a/Build/lib/stable-sort-domain.ts +++ b/Build/lib/stable-sort-domain.ts @@ -1,5 +1,5 @@ import type { PublicSuffixList } from '@gorhill/publicsuffixlist'; -import { sort } from 'timsort'; +import { sort } from './timsort'; const compare = (a: string, b: string) => { if (a === b) return 0; @@ -42,10 +42,10 @@ export const sortDomains = (inputs: string[], gorhill: PublicSuffixList) => { const $a = domains.get(a); const $b = domains.get(b); - if ($a && $b) { - return compare($a, $b) || compare(a, b); + if ($a == null || $b == null) { + return compare(a, b); } - return compare(a, b); + return compare($a, $b) || compare(a, b); }; sort(inputs, sorter); diff --git a/Build/lib/timsort.ts b/Build/lib/timsort.ts new file mode 100644 index 000000000..9d3a22515 --- /dev/null +++ b/Build/lib/timsort.ts @@ -0,0 +1,956 @@ +type Comparator = (a: T, b: T) => number; + +/** + * Default minimum size of a run. + */ +const DEFAULT_MIN_MERGE = 32; + +/** + * Minimum ordered subsequece required to do galloping. + */ +const DEFAULT_MIN_GALLOPING = 7; + +/** + * Default tmp storage length. Can increase depending on the size of the + * smallest run to merge. + */ +const DEFAULT_TMP_STORAGE_LENGTH = 256; + +/** + * Pre-computed powers of 10 for efficient lexicographic comparison of + * small integers. + */ +const POWERS_OF_TEN = [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9]; + +/** + * Estimate the logarithm base 10 of a small integer. + * + * @param x - The integer to estimate the logarithm of. + * @return {number} - The estimated logarithm of the integer. + */ +function log10(x: number): number { + if (x < 1e5) { + if (x < 1e2) { + return x < 1e1 ? 0 : 1; + } + + if (x < 1e4) { + return x < 1e3 ? 2 : 3; + } + + return 4; + } + + if (x < 1e7) { + return x < 1e6 ? 5 : 6; + } + + if (x < 1e9) { + return x < 1e8 ? 7 : 8; + } + + return 9; +} + +/** + * Default alphabetical comparison of items. + * + * @param a - First element to compare. + * @param b - Second element to compare. + * @return - A positive number if a.toString() > b.toString(), a + * negative number if .toString() < b.toString(), 0 otherwise. + */ +function alphabeticalCompare(a: any, b: any): number { + if (a === b) { + return 0; + } + + if (~~a === a && ~~b === b) { + if (a === 0 || b === 0) { + return a < b ? -1 : 1; + } + + if (a < 0 || b < 0) { + if (b >= 0) { + return -1; + } + + if (a >= 0) { + return 1; + } + + a = -a; + b = -b; + } + + const al = log10(a); + const bl = log10(b); + + let t = 0; + + if (al < bl) { + a *= POWERS_OF_TEN[bl - al - 1]; + b /= 10; + t = -1; + } else if (al > bl) { + b *= POWERS_OF_TEN[al - bl - 1]; + a /= 10; + t = 1; + } + + if (a === b) { + return t; + } + + return a < b ? -1 : 1; + } + + const aStr = String(a); + const bStr = String(b); + + if (aStr === bStr) { + return 0; + } + + return aStr < bStr ? -1 : 1; +} + +/** + * Compute minimum run length for TimSort + * + * @param n - The size of the array to sort. + */ +function minRunLength(n: number) { + let r = 0; + + while (n >= DEFAULT_MIN_MERGE) { + r |= (n & 1); + n >>= 1; + } + + return n + r; +} + +/** + * Counts the length of a monotonically ascending or strictly monotonically + * descending sequence (run) starting at array[lo] in the range [lo, hi). If + * the run is descending it is made ascending. + * + * @param array - The array to reverse. + * @param lo - First element in the range (inclusive). + * @param hi - Last element in the range. + * @param compare - Item comparison function. + * @return - The length of the run. + */ +function makeAscendingRun(array: T[], lo: number, hi: number, compare: Comparator): number { + let runHi = lo + 1; + + if (runHi === hi) { + return 1; + } + + // Descending + if (compare(array[runHi++], array[lo]) < 0) { + while (runHi < hi && compare(array[runHi], array[runHi - 1]) < 0) { + runHi++; + } + + reverseRun(array, lo, runHi); + // Ascending + } else { + while (runHi < hi && compare(array[runHi], array[runHi - 1]) >= 0) { + runHi++; + } + } + + return runHi - lo; +} + +/** + * Reverse an array in the range [lo, hi). + * + * @param array - The array to reverse. + * @param lo - First element in the range (inclusive). + * @param hi - Last element in the range. + */ +function reverseRun(array: T[], lo: number, hi: number) { + hi--; + + while (lo < hi) { + const t = array[lo]; + array[lo++] = array[hi]; + array[hi--] = t; + } +} + +/** + * Perform the binary sort of the array in the range [lo, hi) where start is + * the first element possibly out of order. + * + * @param array - The array to sort. + * @param lo - First element in the range (inclusive). + * @param hi - Last element in the range. + * @param start - First element possibly out of order. + * @param compare - Item comparison function. + */ +function binaryInsertionSort(array: T[], lo: number, hi: number, start: number, compare: Comparator) { + if (start === lo) { + start++; + } + + for (; start < hi; start++) { + const pivot = array[start]; + + // Ranges of the array where pivot belongs + let left = lo; + let right = start; + + /* + * pivot >= array[i] for i in [lo, left) + * pivot < array[i] for i in in [right, start) + */ + while (left < right) { + const mid = (left + right) >>> 1; + + if (compare(pivot, array[mid]) < 0) { + right = mid; + } else { + left = mid + 1; + } + } + + /* + * Move elements right to make room for the pivot. If there are elements + * equal to pivot, left points to the first slot after them: this is also + * a reason for which TimSort is stable + */ + let n = start - left; + // Switch is just an optimization for small arrays + switch (n) { + case 3: + array[left + 3] = array[left + 2]; + /* falls through */ + case 2: + array[left + 2] = array[left + 1]; + /* falls through */ + case 1: + array[left + 1] = array[left]; + break; + default: + while (n > 0) { + array[left + n] = array[left + n - 1]; + n--; + } + } + + array[left] = pivot; + } +} + +/** + * Find the position at which to insert a value in a sorted range. If the range + * contains elements equal to the value the leftmost element index is returned + * (for stability). + * + * @param value - Value to insert. + * @param array - The array in which to insert value. + * @param start - First element in the range. + * @param length - Length of the range. + * @param hint - The index at which to begin the search. + * @param compare - Item comparison function. + * @return - The index where to insert value. + */ +function gallopLeft(value: T, array: T[], start: number, length: number, hint: number, compare: Comparator): number { + let lastOffset = 0; + let maxOffset = 0; + let offset = 1; + + if (compare(value, array[start + hint]) > 0) { + maxOffset = length - hint; + + while (offset < maxOffset && compare(value, array[start + hint + offset]) > 0) { + lastOffset = offset; + offset = (offset << 1) + 1; + + if (offset <= 0) { + offset = maxOffset; + } + } + + if (offset > maxOffset) { + offset = maxOffset; + } + + // Make offsets relative to start + lastOffset += hint; + offset += hint; + + // value <= array[start + hint] + } else { + maxOffset = hint + 1; + while (offset < maxOffset && compare(value, array[start + hint - offset]) <= 0) { + lastOffset = offset; + offset = (offset << 1) + 1; + + if (offset <= 0) { + offset = maxOffset; + } + } + if (offset > maxOffset) { + offset = maxOffset; + } + + // Make offsets relative to start + const tmp = lastOffset; + lastOffset = hint - offset; + offset = hint - tmp; + } + + /* + * Now array[start+lastOffset] < value <= array[start+offset], so value + * belongs somewhere in the range (start + lastOffset, start + offset]. Do a + * binary search, with invariant array[start + lastOffset - 1] < value <= + * array[start + offset]. + */ + lastOffset++; + while (lastOffset < offset) { + const m = lastOffset + ((offset - lastOffset) >>> 1); + + if (compare(value, array[start + m]) > 0) { + lastOffset = m + 1; + } else { + offset = m; + } + } + return offset; +} + +/** + * Find the position at which to insert a value in a sorted range. If the range + * contains elements equal to the value the rightmost element index is returned + * (for stability). + * + * @param value - Value to insert. + * @param array - The array in which to insert value. + * @param start - First element in the range. + * @param length - Length of the range. + * @param hint - The index at which to begin the search. + * @param compare - Item comparison function. + * @return - The index where to insert value. + */ +function gallopRight(value: T, array: T[], start: number, length: number, hint: number, compare: Comparator): number { + let lastOffset = 0; + let maxOffset = 0; + let offset = 1; + + if (compare(value, array[start + hint]) < 0) { + maxOffset = hint + 1; + + while (offset < maxOffset && compare(value, array[start + hint - offset]) < 0) { + lastOffset = offset; + offset = (offset << 1) + 1; + + if (offset <= 0) { + offset = maxOffset; + } + } + + if (offset > maxOffset) { + offset = maxOffset; + } + + // Make offsets relative to start + const tmp = lastOffset; + lastOffset = hint - offset; + offset = hint - tmp; + + // value >= array[start + hint] + } else { + maxOffset = length - hint; + + while (offset < maxOffset && compare(value, array[start + hint + offset]) >= 0) { + lastOffset = offset; + offset = (offset << 1) + 1; + + if (offset <= 0) { + offset = maxOffset; + } + } + + if (offset > maxOffset) { + offset = maxOffset; + } + + // Make offsets relative to start + lastOffset += hint; + offset += hint; + } + + /* + * Now array[start+lastOffset] < value <= array[start+offset], so value + * belongs somewhere in the range (start + lastOffset, start + offset]. Do a + * binary search, with invariant array[start + lastOffset - 1] < value <= + * array[start + offset]. + */ + lastOffset++; + + while (lastOffset < offset) { + const m = lastOffset + ((offset - lastOffset) >>> 1); + + if (compare(value, array[start + m]) < 0) { + offset = m; + } else { + lastOffset = m + 1; + } + } + + return offset; +} + +class TimSort { + tmp: T[]; + minGallop = DEFAULT_MIN_GALLOPING; + length = 0; + tmpStorageLength = DEFAULT_TMP_STORAGE_LENGTH; + stackLength = 0; + runStart: number[]; + runLength: number[]; + stackSize = 0; + + constructor(public array: T[], public compare: Comparator) { + this.length = array.length; + + if (this.length < 2 * DEFAULT_TMP_STORAGE_LENGTH) { + this.tmpStorageLength = this.length >>> 1; + } + + this.tmp = new Array(this.tmpStorageLength); + + this.stackLength = ( + this.length < 120 + ? 5 + : this.length < 1542 + ? 10 + : this.length < 119151 + ? 19 + : 40 + ); + + this.runStart = new Array(this.stackLength); + this.runLength = new Array(this.stackLength); + } + + /** + * Push a new run on TimSort's stack. + * + * @param runStart - Start index of the run in the original array. + * @param runLength - Length of the run; + */ + pushRun(runStart: number, runLength: number) { + this.runStart[this.stackSize] = runStart; + this.runLength[this.stackSize] = runLength; + this.stackSize += 1; + } + + /** + * Merge runs on TimSort's stack so that the following holds for all i: + * 1) runLength[i - 3] > runLength[i - 2] + runLength[i - 1] + * 2) runLength[i - 2] > runLength[i - 1] + */ + mergeRuns() { + while (this.stackSize > 1) { + let n = this.stackSize - 2; + + if ((n >= 1 + && this.runLength[n - 1] <= this.runLength[n] + this.runLength[n + 1]) + || (n >= 2 + && this.runLength[n - 2] <= this.runLength[n] + this.runLength[n - 1])) { + if (this.runLength[n - 1] < this.runLength[n + 1]) { + n--; + } + } else if (this.runLength[n] > this.runLength[n + 1]) { + break; + } + this.mergeAt(n); + } + } + + /** + * Merge all runs on TimSort's stack until only one remains. + */ + forceMergeRuns() { + while (this.stackSize > 1) { + let n = this.stackSize - 2; + + if (n > 0 && this.runLength[n - 1] < this.runLength[n + 1]) { + n--; + } + + this.mergeAt(n); + } + } + + /** + * Merge the runs on the stack at positions i and i+1. Must be always be called + * with i=stackSize-2 or i=stackSize-3 (that is, we merge on top of the stack). + * + * @param i - Index of the run to merge in TimSort's stack. + */ + mergeAt(i: number) { + const compare = this.compare; + const array = this.array; + + let start1 = this.runStart[i]; + let length1 = this.runLength[i]; + const start2 = this.runStart[i + 1]; + let length2 = this.runLength[i + 1]; + + this.runLength[i] = length1 + length2; + + if (i === this.stackSize - 3) { + this.runStart[i + 1] = this.runStart[i + 2]; + this.runLength[i + 1] = this.runLength[i + 2]; + } + + this.stackSize--; + + /* + * Find where the first element in the second run goes in run1. Previous + * elements in run1 are already in place + */ + const k = gallopRight(array[start2], array, start1, length1, 0, compare); + start1 += k; + length1 -= k; + + if (length1 === 0) { + return; + } + + /* + * Find where the last element in the first run goes in run2. Next elements + * in run2 are already in place + */ + length2 = gallopLeft(array[start1 + length1 - 1], array, start2, length2, length2 - 1, compare); + + if (length2 === 0) { + return; + } + + /* + * Merge remaining runs. A tmp array with length = min(length1, length2) is + * used + */ + if (length1 <= length2) { + this.mergeLow(start1, length1, start2, length2); + } else { + this.mergeHigh(start1, length1, start2, length2); + } + } + + /** + * Merge two adjacent runs in a stable way. The runs must be such that the + * first element of run1 is bigger than the first element in run2 and the + * last element of run1 is greater than all the elements in run2. + * The method should be called when run1.length <= run2.length as it uses + * TimSort temporary array to store run1. Use mergeHigh if run1.length > + * run2.length. + * + * @param start1 - First element in run1. + * @param length1 - Length of run1. + * @param start2 - First element in run2. + * @param length2 - Length of run2. + */ + mergeLow(start1: number, length1: number, start2: number, length2: number) { + const compare = this.compare; + const array = this.array; + const tmp = this.tmp; + let i = 0; + + for (i = 0; i < length1; i++) { + tmp[i] = array[start1 + i]; + } + + let cursor1 = 0; + let cursor2 = start2; + let dest = start1; + + array[dest++] = array[cursor2++]; + + if (--length2 === 0) { + for (i = 0; i < length1; i++) { + array[dest + i] = tmp[cursor1 + i]; + } + return; + } + + if (length1 === 1) { + for (i = 0; i < length2; i++) { + array[dest + i] = array[cursor2 + i]; + } + array[dest + length2] = tmp[cursor1]; + return; + } + + let minGallop = this.minGallop; + + while (true) { + let count1 = 0; + let count2 = 0; + let exit = false; + + do { + if (compare(array[cursor2], tmp[cursor1]) < 0) { + array[dest++] = array[cursor2++]; + count2++; + count1 = 0; + + if (--length2 === 0) { + exit = true; + break; + } + } else { + array[dest++] = tmp[cursor1++]; + count1++; + count2 = 0; + if (--length1 === 1) { + exit = true; + break; + } + } + } while ((count1 | count2) < minGallop); + + if (exit) { + break; + } + + do { + count1 = gallopRight(array[cursor2], tmp, cursor1, length1, 0, compare); + + if (count1 !== 0) { + for (i = 0; i < count1; i++) { + array[dest + i] = tmp[cursor1 + i]; + } + + dest += count1; + cursor1 += count1; + length1 -= count1; + if (length1 <= 1) { + exit = true; + break; + } + } + + array[dest++] = array[cursor2++]; + + if (--length2 === 0) { + exit = true; + break; + } + + count2 = gallopLeft(tmp[cursor1], array, cursor2, length2, 0, compare); + + if (count2 !== 0) { + for (i = 0; i < count2; i++) { + array[dest + i] = array[cursor2 + i]; + } + + dest += count2; + cursor2 += count2; + length2 -= count2; + + if (length2 === 0) { + exit = true; + break; + } + } + array[dest++] = tmp[cursor1++]; + + if (--length1 === 1) { + exit = true; + break; + } + + minGallop--; + } while (count1 >= DEFAULT_MIN_GALLOPING || count2 >= DEFAULT_MIN_GALLOPING); + + if (exit) { + break; + } + + if (minGallop < 0) { + minGallop = 0; + } + + minGallop += 2; + } + + this.minGallop = minGallop; + + if (minGallop < 1) { + this.minGallop = 1; + } + + if (length1 === 1) { + for (i = 0; i < length2; i++) { + array[dest + i] = array[cursor2 + i]; + } + array[dest + length2] = tmp[cursor1]; + } else if (length1 === 0) { + // do nothing + } else { + for (i = 0; i < length1; i++) { + array[dest + i] = tmp[cursor1 + i]; + } + } + } + + /** + * Merge two adjacent runs in a stable way. The runs must be such that the + * first element of run1 is bigger than the first element in run2 and the + * last element of run1 is greater than all the elements in run2. + * The method should be called when run1.length > run2.length as it uses + * TimSort temporary array to store run2. Use mergeLow if run1.length <= + * run2.length. + * + * @param start1 - First element in run1. + * @param length1 - Length of run1. + * @param start2 - First element in run2. + * @param length2 - Length of run2. + */ + mergeHigh(start1: number, length1: number, start2: number, length2: number) { + const compare = this.compare; + const array = this.array; + const tmp = this.tmp; + let i = 0; + + for (i = 0; i < length2; i++) { + tmp[i] = array[start2 + i]; + } + + let cursor1 = start1 + length1 - 1; + let cursor2 = length2 - 1; + let dest = start2 + length2 - 1; + let customCursor = 0; + let customDest = 0; + + array[dest--] = array[cursor1--]; + + if (--length1 === 0) { + customCursor = dest - (length2 - 1); + + for (i = 0; i < length2; i++) { + array[customCursor + i] = tmp[i]; + } + + return; + } + + if (length2 === 1) { + dest -= length1; + cursor1 -= length1; + customDest = dest + 1; + customCursor = cursor1 + 1; + + for (i = length1 - 1; i >= 0; i--) { + array[customDest + i] = array[customCursor + i]; + } + + array[dest] = tmp[cursor2]; + return; + } + + let minGallop = this.minGallop; + + while (true) { + let count1 = 0; + let count2 = 0; + let exit = false; + + do { + if (compare(tmp[cursor2], array[cursor1]) < 0) { + array[dest--] = array[cursor1--]; + count1++; + count2 = 0; + if (--length1 === 0) { + exit = true; + break; + } + } else { + array[dest--] = tmp[cursor2--]; + count2++; + count1 = 0; + if (--length2 === 1) { + exit = true; + break; + } + } + } while ((count1 | count2) < minGallop); + + if (exit) { + break; + } + + do { + count1 = length1 - gallopRight(tmp[cursor2], array, start1, length1, length1 - 1, compare); + + if (count1 !== 0) { + dest -= count1; + cursor1 -= count1; + length1 -= count1; + customDest = dest + 1; + customCursor = cursor1 + 1; + + for (i = count1 - 1; i >= 0; i--) { + array[customDest + i] = array[customCursor + i]; + } + + if (length1 === 0) { + exit = true; + break; + } + } + + array[dest--] = tmp[cursor2--]; + + if (--length2 === 1) { + exit = true; + break; + } + + count2 = length2 - gallopLeft(array[cursor1], tmp, 0, length2, length2 - 1, compare); + + if (count2 !== 0) { + dest -= count2; + cursor2 -= count2; + length2 -= count2; + customDest = dest + 1; + customCursor = cursor2 + 1; + + for (i = 0; i < count2; i++) { + array[customDest + i] = tmp[customCursor + i]; + } + + if (length2 <= 1) { + exit = true; + break; + } + } + + array[dest--] = array[cursor1--]; + + if (--length1 === 0) { + exit = true; + break; + } + + minGallop--; + } while (count1 >= DEFAULT_MIN_GALLOPING || count2 >= DEFAULT_MIN_GALLOPING); + + if (exit) { + break; + } + + if (minGallop < 0) { + minGallop = 0; + } + + minGallop += 2; + } + + this.minGallop = minGallop; + + if (minGallop < 1) { + this.minGallop = 1; + } + + if (length2 === 1) { + dest -= length1; + cursor1 -= length1; + customDest = dest + 1; + customCursor = cursor1 + 1; + + for (i = length1 - 1; i >= 0; i--) { + array[customDest + i] = array[customCursor + i]; + } + + array[dest] = tmp[cursor2]; + } else if (length2 === 0) { + // do nothing + } else { + customCursor = dest - (length2 - 1); + for (i = 0; i < length2; i++) { + array[customCursor + i] = tmp[i]; + } + } + } +} + +/** + * Sort an array in the range [lo, hi) using TimSort. + * + * @param array - The array to sort. + * @param compare - Item comparison function. Default is + * alphabetical + * @param lo - First element in the range (inclusive). + * @param hi - Last element in the range. + * comparator. + */ +export function sort(array: T[], compare: Comparator | undefined, lo = 0, hi: number = array.length) { + // if (!Array.isArray(array)) { + // throw new TypeError('Can only sort arrays'); + // } + + /* + * Handle the case where a comparison function is not provided. We do + * lexicographic sorting + */ + if (!compare) { + compare = alphabeticalCompare; + } else if (typeof compare !== 'function') { + hi = lo; + lo = compare; + compare = alphabeticalCompare; + } + + let remaining = hi - lo; + + // The array is already sorted + if (remaining < 2) { + return; + } + + let runLength = 0; + // On small arrays binary sort can be used directly + if (remaining < DEFAULT_MIN_MERGE) { + runLength = makeAscendingRun(array, lo, hi, compare); + binaryInsertionSort(array, lo, hi, lo + runLength, compare); + return; + } + + const ts = new TimSort(array, compare); + + const minRun = minRunLength(remaining); + + do { + runLength = makeAscendingRun(array, lo, hi, compare); + if (runLength < minRun) { + let force = remaining; + if (force > minRun) { + force = minRun; + } + + binaryInsertionSort(array, lo, lo + force, lo + runLength, compare); + runLength = force; + } + // Push new run and merge if necessary + ts.pushRun(lo, runLength); + ts.mergeRuns(); + + // Go find next run + remaining -= runLength; + lo += runLength; + } while (remaining !== 0); + + // Force merging of remaining runs + ts.forceMergeRuns(); +} diff --git a/bun.lockb b/bun.lockb index 5605f47b5496d36c94d181d0103c125544ca92f9..360abbcb294db26dd3e4d0e3896f33e024ac2845 100755 GIT binary patch delta 20272 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 P%B{F%tM!-d>b3s{>;k;! delta 20691 zcmeHvd0bW1+W%fhkFr%%6a)lCaEg>!JRo`wIO}#oQ^cH5QAuzB98l?C=2bJNR=OqA zvPqnBAO|!PH7jqUl@(29gK6o_x^-{Z;QRgVLCx&G?|VP@kN5Zcv_E{$v%b$7pJzR5 zt$p_4ocqqzI`(y~Rnd{F>MhEcqwia&z5Jv%uWtAB(sPfj?04q0`on=0xtX6InH#@X zD7*qzXM}Z2Yh)5d34*GDqLdWoPADuudJQt_kmuwTOv^7+23WkHFh4shZ;rCUVdf85 zy*n^n)-XwxB^N{1L*=__DoP;a7|6Pi#a8}ErQNd%Or#yxi=7)o`UXZ09pA(fcb>cLo zJ~&OR0m*Xt*@gKbT(ABtLjkaaD& z*OD8p{7j4jelF<-*@WfLDC@04K8ltULDF5rAX_S4rNoT{D;&cBIMTeF!mPq9McEF{ z21+66p-UJQ2Udj_da4pi*2^x)&6!%LD48hC`U5TLgI;J*ZuYFHXhQkb(tlvdFCc@x zC@>*AdwNt(fszUZUFd?OYklAr@?1zZJQ|V~v_n3|Tv7|NHRN3dQAJsKxk^_4r0H4t z1=&9anB{T`q9)}|8>egxG3#G~o>P!BY3j86Y(@DTJc=E$R90?*@=~bTag|kYV$ReF zQ8TA2%iw=jERk6?0wYVBo9rY?(LTR0v-$j-y!@<*iemH$dtZ>7YP9t`gk090l3g^5 zUKlH@YJ??g5oVq2g6a9$7|K4hMIWDlo+HW{KP{h5+X_8Rz1Y&!ua$8%!^AR~UNg`; z1h&&XZQ7U< zhzXWG6SE2mXP|z5_RNBuiABoA+sv^Yjx@)j-fr~kE!hDD9ncdM;ggbfkZ^TLw|0sG zPnLv(bKd-{hU40s4Yh`(QD-dqCL|*?FUmC1o*#Q!enhlk>PwnIvL7?x5IKL;VL^7@ ztn7S6vDa!*R&H)iL80;h8fb+E>c*J^xH-_+;kY72Dag$#m<$i(W#>=Ij)!*u&PpXD z4XTv@&d42?pEW+aKv9aJV8(Gsx;&tRS-}pZ2|4*jD86Rt>!BeA#l=`fL41_V#lX0f zMnJZJ><>wgq(C-;EN^V`>W-$&0G|ky*p_w&A-C7!&9QlNoZTE>f8i3ji)o$)h?`ig98=BS29Ea^c+wfIrgAwEHZOTGOPWTUy z41Js5gK``P*Bdu=_RaCx_3d&)p~q0XC13S58^CxCL(glb18m2<2p*0Gg8Q5Kg;~?H z#^q)!`QRMD7UZ)dduHsJTLC={o&iY*Czu_Uyg1O@WK0Kl!Xg}GhNK-5c1JCKnS1Z; zA5NXv^Xc`oljgr$JGw^ek*&+x&3iuOp<%~%FM6&#R;{e<;|kh(`o)qjrK7y06i~PkeMg0&dM0klJj|R9LjT~}rBe%$q6^%S%ovg;|+cLSa$5FqAoZHy# z=v+e{Z0uGy)KC=6R!J=>8oSibEaoJ0gw>RD{oU#U+!+i*ks2~Bz@@$m<_7bT3(}H= zQ+5h)tCO9Ik_w%JB}AF52=J(XhYn!`9h>shlAW5k#XMQm#3PQ%iY6XMo7(aUR3l_^ zphr9)ivm6BC$$v?4k{6B#}VrzuLQc)Sw7|n4W-y4i-J53e^nj~a*KYlI>;jy%j95> z`YHm3<8sKOK`uw#I`Uw!TcpeCV2^VdVxpt$5|DzPi<)}GFS4Sk$B|N3UTNx9r(#~Q z9N@LIK$p4=EFG){M=gGq6(Jt+rK}F|I6GiRfuBQC)EP*PLYX;oL7+>#CM!Zcj;Q+b zN~l}Rkjc$F;ssgM%;TujKpt%7cFw>dGf*CGmg1~LN@Ge5YN#m5XhBGU7Sd%!bC2_W z==#gU%~PCJNKKZ9LsFb+h_%5?iAQ8bm`8jdtHV5whK*#W7H;PhMD$SErA3PKDpEa+ z)Lq!c`p7PWJImy7k2o%i!aeGEtZ>e-B1NzZ4ONGG96^DyQ-oU`i-RlH%)&TU$%+V% z^A+eia~uwzAla#JJgM z2dul1M`!&*R=4svJL25Tm%lVh5&L9OYma&n99yInDy>4~mDY$UtS5F`Q;Ie&wG51n zI^=?;F7jMYnQqVjO{4~I|+vE$y!JuIF~qH#8I$I z%>d&FQ4c*Y1Ebe#Gtc=BSZ_K*ZQjDv8zWZ-fU%A+>f8fnx(Ly&9<&&iM1)KI4vgcl zR!z9<)ZVR5!8S&v8OU2?MSG8W209KOOR|m25fC9eMY+|X5#~sp+%?2%SrO&ov_^T< z(3YkrVLR4Bkt~Y#i0!f>+T;9zIr4BoiZiAaR;=MSFH#(#u}g^kvLeQ#UVsjFC+x75 zOATmk4heR*aXIIM#mnNr6!mGQj8gRYCtwUPV=qw?a2Lj1#Awwy6D&yF9|ad$>vVKRbTVzNx`;89OPmui<*gKoE1!UBJn7%i(ic$^!c8)`)Mcd{ta z;~dit+X;}n_HhP%}-pkNc|zGYHP_-24%&I3qMjoFGZ)TuPU*)9s!8?iA9DMqy! zdAq>Oi07PrE~{N0M`*O{l;l>kqp@Y6U`=Cvz9}n`JdWBi@(Qws$C!JYxg&3hkq499 z;%ixv>`}uIk8Hk{u^2MJjQ!HN5~)$LOL&SJ7-!p#F4YV$mRIQ=bv0N|Fl@b%Nj@Nn zAaxA~x4iW`I48~Fj zA?z)uz&P#3X{9ztF*b)FzzBzfG5(Q<09gxW1}8>XlLd{K^}|3}$Os1KK(HY+*6~=P z>=f>HehCG3SSUJolm|U-F!Wx^iQ2?3@;OmyM>RzHi22j!>o_M=)pQP(%A?H#Q-Cm$I6PX9#w)fRD6sR z>Mfbv&7-!2-RaO73tKG&yWUe7m>F8!d(MMla3%GPe#2k@gB(i(i(x1j=L)TWEe4}S z*khwy&eLFN#t}0M12v8v^<|{SqJ$BXVvMZr;c@u6Wv8BQM^8gI=fSzy+j^upk0W)5 zkqY!6Sd7#tq((EP9zu#^gfFqz{{%*F82gUuLIws6zj4!=kt(nBa64aw(rCotnzG1$<1R=&4|a#aV1<|^tNVD=7rL6hH}}i>-Q?W9Zm~#K^!2D;bi+FX z6gTfRG?}b<)Wry~9?-!pAujb5Fgn-Vo_vvk);RE1E-_Fh_w%R=pc`Vg5$;mo2IKGz zH>!bHCS<0eqrm8JZ1FhMR)S%a)-xic0+D0^BNzKJv;yx1(zx*=dklo!`g4 z9Xi_7<*WefCV#;?{23_*r7>1VTwi%*klQh(uk4iW7Ej8ebPvwZ;)oBO{41jAg%B3CxGou?5 z=TOsSr(vDEp^EW-Oc9W{3ox!5WesRr01hAm;B_nMgdT^E(avowgo~2(vHu&_jgtCk zBNWVpQ4yQ9ArTVBb)&2a4W?KTvVTUP9GT%`LpbDyj6g39z$Ri`{}ajb2ngf4mGnVQ z8o+X0jZ(jrbO_dkaY1^Gf(U%$qGSQgh;dP}Aq1gu{VU0G2t4DuQL;S*nnF)wCJoOb zE{uMLD&rM}BOV2`0Tu$hC~E;rNpMlJp=AI&x*y>6uO!PY2jJ%{m+X+)(90UD`GV_K zk~JPMa!pAt0rtGc(o<5u){?p<*Fo~4WPT~YV0qZ$8|Z7A^f;v} z8zqN(0APLvz>BgbaF_(wEu`0Q>=9(LVkN+<5@3VJ0P2qeyly2q&=-u{-%1s_*DXCI z+dE}(O1AqZ=EJz&GzAo9##2&hs{p0>VoJug50uoamaJpR zx*d?Hg$zID-XytNnn2G1@IgB-N+Fj|2s0{#l0n;yFS6OlF!|vGGsO9Tp7mNX$C8|i zw%}CVh8GTu5B4J<2V43fkgPWpl7AlBzc?JtYTr+LG^4FZ)k4m(KfEDM}_kzzc_Y7LvVw2Fcpzm}yDQ z!&er+QAR*tkCiORdJQa2$)q1%ShbNuk=kfJmQ|F-md>9Aa8a^MfF+wiGAqd9!Io@F zB`!)%MK~n;Yz0ZrL_)Hi+ad9%wCBrjrPo;BvB;p&9U(b)-5_aXFGx1n*OL7q>EdCK zY&Zjwed(Y zCU|$omz4CVo4@Jfh|*U#w|Kb4_ia-i{C>*uH_8KF=-aDL_^c!D_h06+QmNK&-L;4i@%EMr- zrCJF4!14+;(N9Za6FVw^kSp~M_Uf6f9CWgwT_rktKun#OlCNF}0U>g=`B2!)fTlW{( z_ZLmvDNFwX`xe7Kuu-zlV%P_^W3k5PreI}DVBZo=WXUZ{VBb>Mw^S43W#&@YcOUEn z%a-bWun#QnK21!L6=0K>)tAke^~FaWIm=+*{jhJDCZ@>X`(Yng@%@^}lPAIEEQfu| zH8D*VEr)$8U?12F8My-Xfh}L5i2_*#wqzyjTd9eea_LIgw+i-w&6dflU?13qRT@4G zx&XFrHSAlh;bYy>)v)gY*aucD`#b>qz;-;K39qaME0eHKYGQ%hB4OWyulK z!oD@I4{VWC*T6ooyfqp=)vW-VycYAfRufBQ&RW=~!#=QOGFXRwV8yy7mdlf1bJoGW zb(&Zyi`K!uQrHK!T1J+_KCtDbnvk*zY{^5g?;%aBkxL(fed}Q#m@bpo!#=PL>orj- zFMzFk81_A^iS@GdVc53;_JM7XeKx>8upJvTexM3gwh{Ji)WjyaWh3l+1ol0miN|H; zBe3sL*a!BcR3C+XV0n*fVzaCOo4g73ZPLURnX?J@JqG*0w#nefU>{iVV;Vk|Ite!C zaoG2`CU(lA$6?M#1UByR<;H9ZPD-n`j#!QZ!7HE zs^OE3%&o9*8|(u+A=PcL4=it+CSH^kV3W7QzU`VgDRZ{Nz8$a+>}46e1NMOx@6g1n z@+8=tr(xgIns{9nJq`PI!alH5GIA&E16#gR6K}~XuqC@--!4s@mP>cRzTL17>|L3> z8}@;1*sX~wc>!$Q9@w`>!{_FudthHV>;wBq_9=&bU^~h+aYj~yl|2Ldp3%fvx#bzy zw-@&9)x>8qb1&@M2m8Rjkm^3z2bQ-_!+!--fKA>H`}S+%g3Q?u`wqZ9u&-tC0oVst zd_WVID6W_?93fOlL_JMsXBM-tpu;m9e@poATw&W1(JEVypHhhg7gP5dNF55vCaeEYrs;MEeJZMVm6eBcKW}sB@s_Wj`po;m-FLc`z9)X#J|zNg zPKEMogI^(kk>8@xAD{D;L!Z;|spzKX;HD$K(s@L42q6a_877BU`pUgWG=YyMD&eQ2 zzB0R#=jd**17HD1d5&fsg`bZ3$`fF!^g9MW9ru+pkMSHm3ic9M>*G8}XB>y0p7)if z!5YZ$=i#RlzH-s?JV#G~y$66K34%yeLrj-(uj}f#*4Do-m7jWDI`DBx_{-hoWxty8^4mU+k7LT4S9NsU zz~#nwni_QEn?5Xjdc4t&L?3f*)W7VNq3@n|&h3W(5yC)fi|d&fM1ru!RVIBJ-)5yb z&i_AA=@(UJiS-}LBb(^y4Mm#w*Q@5mH*xO)d;-F!BD`$Qr!IWt!>bCg%LEx3MS0)S z@!<#`|M2<%U^zY~u%A7BXzAEih*{37d}Jy4P%6}_{4vrT1fTVVS-P`Gvq3&nvLAMR zYU%JlMAsg&eP-$S@Ef038Q14{WnDhONdNZ7k1nfNC$Y? z557Z?9${$Ahu6#;5Y3Ur0^lq3{j^1#Z<-AG@5Ei7e zmQ^qu=`%>vMfOwl2&6l41i0)c>MfD>Ku62#qCEciU$9Cf611|OrNiE$+-~W7A=ykD zpoTD>sp842RgljPIWk(_(9-ewAqPesAH?HNi3AQJLCd+IsKdXPD7%oL<&7;JpEW*X z>HIBSd+1n~-U!IDlu;n8+yasx1K`hiFv-f)1zI{joa9q=S{Y>NVv(j*yn-#=5;lVC za#MY(zqpV8)nypK<*5Pv04_@|L$11J09RTIfRBv%Sh=A?j}8#`_|-wWF3=Bca77K& zj|PYkuLsFgAPH~*i9knylfvQA>GTi%L0cI)1JKMsU=WZFa6Juyo@=NF(tpLM8U768 zD1c9?@$_362is{Df*hup8I|lmoi}o;QUzQ9pGKy6m-V`M%GinT3{mdTu)p-j4Un> zE{7^WM?OD_@&@3AQO3|@C^Gb#0^FSW2}T48au4NR$i43s=kmAD(6Lz+7PSA0vL70?>s!sbF| zIW9o1H?FUJz|Fi2Wtv0JvRrTETLFIOMK$!8@TZX=*!hiVR^krOA_OnE*0tgyY~!UH z>3#vQ%rC&tz(0Y%1D$~spc9Y`aF<~`F%Y?$xT?6iB7krp4B)Eb&J+l!Kuw?qfG!mH z!Z<=}Lpr%)nBWl<0Pv(~0MrBO06Z`21HKmbgKTJV?#+z>?jqb*gMcOgJ8cednYRGA zr`p>Aw*qbnoK`N3_FOUCBe^Fs*ci>RKmx!O)B$h-w2&52UIJbR+&~xL2!OS3@I#Ou z;9-E*aV~k%wG>k^lyCHzY20Cumo5P+zTuK<^e^(9AGvu6UYZ<0Mme} z06mZe+zE^Th65SEFkmP!1Q-kq0tN!zfdN2Yz}p8eT>-i~4d@B<26_S9R9LthK)t!i znEy%9AG{xs4sfT;1lZ9iU?eaa7z2z2#sPW26kt3s0k{ju;rvfVf+Lv-WCN3|G-WO@ z9Vi3}09wxUEP#4~9+(T10CxlPfntE3WxU)2EClGKzW|E>#>p~ZDNEgl#0ubkU^%cF zpvyU^d#yCx{SZ(JtO4js=06DNz*=A(Kre8R8-QzbjP#?xM&Nf-%u)V3bIieTjya(J zZjNuB;zv-&HqiSR$m76%V4syy2Fba63Sh)O2_yrX0nYngpd8o+Z~$9@Edag90Nw*o zw;gyI*a7SWb^*Hqw*3r%KD~y!?9QKs@*u!DrW@(@=Kv0p?tBfPj+S%D(Xv;7O2Bsa zUy(iuya?Ft<}}{8nr4H)06fq6KaRvSU@E{MISSZIi5(3CZglZL@b16>fH82hdyhe9 zNBaq+8Mw~_vj7I{OMvbEdpZ9k9K_22JG(i+UWJa+Pzlhoi@;gn6wn2518-RJ6UaA# zRA4>uA#eux2zUozUo?u-#({h3-YVcU@D^aZQ6l{|aLcHL{y!A7-ap)#Zgwjzc^7yO zur1+$sbj2A+Il;HKY-3Q@O`8iz;-zrdXV$~F%ma6&I)Xp9no!c89hXu$KrD!KL76Q2Boo?KejAPpJDNPz_wM!1Psshd=YEcOcE1 z7Tm^pjY#HuCpR*9CsY$~0(3QXPXM;txe=uTwUN&|9{yJo?+1Bb$TG}30PF`?Zy&(v zO9j>g%(L4z^SsxON(S5}fSs_xR7lPdV}MJp60kYby!qq+x%4<4{@{Gy#rI?OQvLvX z##1H0xUi>UR*dACPhnoEjE+2=Hg_l)&x0WIyq!h`< zDA1s1#p;xX!{+;lw$RjrX1l(c1&)E%0ez%(N&9w-vd8&|(TUL=q7$)W>StPt;Es&> zU=;9MKW6B~ADcYx5DC$-(Q(i+nArIzM;DJ=^lfRJLyV4%PGD9Ip+~n8L889itCdI) zLHfK_A~=NaBO0K*+I3OM>*WVG8Rg^IRjj^~1(H#qE($DaI5Ic)Uhyyr#6`!&M#n4N zb-&w1u>Nx^(LoH*<64WRf!3GP?QgPKoyY3ETZ>S!M$c}IjtA(arg*ir2o@{!FUh9r zA#Ko^{e|?jF`iyEx2^vM4RnBEvAD6)2cm$G`YD#Pzt}!PJp4h=e?BrD3x~KHUWo316|d)^r%?N=!c|M!{OZ1QiHfRmX4{Qx>yNb+{lvHWH*G~b z^@@*iRFxdlTi%ALw!c!|@V;IZF((4=H|oW^q7#&1`i$FPLXo}sMj_>HQzZU;#?qgr|I@kOhtHv`g_O$LB ziD+7bDQ39YUqnQKjxa7RL9xF%pK##B*>fBBPjHCa5nsGQ>+2&UMNp{yo%-$W^w=vK zt}ee`qFV!f6-p%9-?ERo`#`}<-nc|#+BG}Q)Uyp4kf7cA`HTSlb?J>_`1*A2jC|pnup$1UbS3^Z`&i+6Cwh z`-(tEho1U^_NX*L&vKiM73!n(8YyF`3QTIYPYBka8#fW%EyC!;S494JUqK|b! zrZ&-Aq+zH7_2n^W%v@L^Q$I@ff&M`Zn)pI*5DWIJ9vO?4d;;~+v8Z8WQoEX&CJV8D zfgtn9-<%JBeCal0F5{wIcsHT1?t*f;dNmpm8}+C-u$T0+F61*Od z|Im`==6(~i*V(YRt`p0Sj$Ifv=L#)%SfNcYCW)i7jLM-iZZ7B8BK7j(2eDP(N8 zSvfDV`M9kQjvIJ=U)1!430Q6?B8<1SCF>qceg5qO!-LRzBDTkPoHz7xJ9St~b9r9W zi+f;DnjY5Sn$QPhqZ32)O&!pEn>PC84hR+dH!}vr9D6MIugCq-B0`wsJ)^fw#Hh>- zLaftgB%-CO`Z6jn>PHeW(K~eCj#z;9?*%Nb9l5dp<9jw+&P&EvvEH~nr-bz$J@aOEhY1WYC&`RIf4xDH2&}U|+ITaK9~k(9IKMmTTAT-o z|KnlqfDh|+zUIRqY|)vWF*(+fzNpU_DH8NY2Z#>za>FhtSvO9vKMHH$m_7OOk-DqWSsQr5h?$M5C-~Pr2L$!DM6o1{3ie1O3>96-m z70IFY&qZ8*zIgh>@4oUDy6I@fj2Re?MoIbsRx);_;Luxc!?8R*&26aPZ#T|R`*$$1 z@4hfQKl3aX1Y;78!&dq%wk>Nb^>@bOr2I{o>0w=k%iKs4tc^6p{%w%;rLCUpFE@XW z=917H4w`-Xk*;FW^&K^>8>Yk9QG@?rM-BeBc2oy8uTQ%pB>vR~grNjuXSiYGVT8Vr zaV(tUf2}uU!!o> zNUfEYe0IL!Ut`mAbTsikhCkwhg{B0NczTyZok&@y0g(``fE{^Y$wE zR)f7hN^9(u`piMNE5VMub&$CInhnOCoWK6hi5ZL=692)tAu-b-_Aj6;+?DqJ@w{yU;yd*`XOS0MdSZBo86bk!m>8IEdA0Qm_YLm z@;|oc>GuzV{Wsn~{+rv~Z}+XB5c}6zR>zE3-ezx?WW0aETadW;=p?TmH5_*#zb{~) zKR@VMC?HJnFUqabciy@oqo=s&1hXfrp+x)VVKRqon*VdweUq;rc=ivw%$oRdXwy5d zOmhg_$8xDFD|9Urr@pm|iY>Yq)OzC)V*j4Z?$2Vr9`nhPRSt1bGd}((Ij_Hpav{Ip z_VA{{*h9>BA67%=TM51;892g>Z{Bg}504N{L+ziCnf%Q&-wf_h80iq$SB)m}`|Ib% zU}f6BQuER)uYJ{Y(CBD~a9M*tuZP`<(HhML>t&-vqqQT250={R-p23{GH!kTb|<32 z+Q9LaKNcIfQFyQ3W#qM{titBj99`SEUolU0V}JJ7&$JWHYd22k^|qt1R*WUX!F)6d z4mFP_Yav;06jYuo{k(N!V>pkd*E@t;Mt+uo^~|&7CF4( z$GwT+sU}X8!R^nwF`}Qtnw2__j>B7QTrI})zhwONj$=i%K6$K&#rv+(vBJ|-`k4=a z?oCR3Ma-%H>!9&wg~y)JzZ)yc>ZcYKP0ucfv3~t;|3g_~vS?&JZHbAp)lsqft`(xC zp1)Ou?r%0;4D03p`y$Z^`nL~>2z}>PQENX+F0NhY6Mkxm>-;l%jXk1OqorNuh8$V( S=!&4drapeJ-f53`