From a1f9075fe9e35018249b00bc316b0e41df36cfcd Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Fri, 24 Oct 2025 00:51:22 +0530 Subject: [PATCH] =?UTF-8?q?Added=20FFT=20Cooley=E2=80=93Tukey=20polynomial?= =?UTF-8?q?=20convolution,=20segment=20tree=20with=20lazy=20propagation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Maths/FFT.js | 152 ++++++++++++++++++++++++++++ Maths/test/FFT.test.js | 59 +++++++++++ String/SuffixAutomaton.js | 102 +++++++++++++++++++ String/test/SuffixAutomaton.test.js | 24 +++++ Trees/SegmentTree.js | 73 +++++++++++++ Trees/test/SegmentTree.test.js | 41 ++++++++ 6 files changed, 451 insertions(+) create mode 100644 Maths/FFT.js create mode 100644 Maths/test/FFT.test.js create mode 100644 String/SuffixAutomaton.js create mode 100644 String/test/SuffixAutomaton.test.js create mode 100644 Trees/SegmentTree.js create mode 100644 Trees/test/SegmentTree.test.js diff --git a/Maths/FFT.js b/Maths/FFT.js new file mode 100644 index 0000000000..c91135259f --- /dev/null +++ b/Maths/FFT.js @@ -0,0 +1,152 @@ +// Cooley–Tukey FFT (radix-2, iterative) and polynomial/big-integer multiplication +// Exports: fft, ifft, multiplyPolynomials, convolveReal, multiplyBigIntegers + +function isPowerOfTwo(n) { + return n && (n & (n - 1)) === 0 +} + +function nextPowerOfTwo(n) { + let p = 1 + while (p < n) p <<= 1 + return p +} + +function bitReverse(n, bits) { + let rev = 0 + for (let i = 0; i < bits; i++) { + rev = (rev << 1) | (n & 1) + n >>= 1 + } + return rev +} + +function fft(re, im, invert = false) { + const n = re.length + if (!isPowerOfTwo(n)) { + throw new Error('fft input length must be a power of two') + } + if (im.length !== n) throw new Error('re and im lengths must match') + + // Bit-reverse permutation + const bits = Math.floor(Math.log2(n)) + for (let i = 0; i < n; i++) { + const j = bitReverse(i, bits) + if (i < j) { + ;[re[i], re[j]] = [re[j], re[i]] + ;[im[i], im[j]] = [im[j], im[i]] + } + } + + // Iterative FFT + for (let len = 2; len <= n; len <<= 1) { + const ang = 2 * Math.PI / len * (invert ? 1 : -1) + const wLenRe = Math.cos(ang) + const wLenIm = Math.sin(ang) + + for (let i = 0; i < n; i += len) { + let wRe = 1 + let wIm = 0 + for (let j = 0; j < len / 2; j++) { + const uRe = re[i + j] + const uIm = im[i + j] + const vRe = re[i + j + len / 2] + const vIm = im[i + j + len / 2] + + // v * w + const tRe = vRe * wRe - vIm * wIm + const tIm = vRe * wIm + vIm * wRe + + // butterfly + re[i + j] = uRe + tRe + im[i + j] = uIm + tIm + re[i + j + len / 2] = uRe - tRe + im[i + j + len / 2] = uIm - tIm + + // w *= wLen + const nwRe = wRe * wLenRe - wIm * wLenIm + const nwIm = wRe * wLenIm + wIm * wLenRe + wRe = nwRe + wIm = nwIm + } + } + } + + if (invert) { + for (let i = 0; i < n; i++) { + re[i] /= n + im[i] /= n + } + } +} + +function ifft(re, im) { + fft(re, im, true) +} + +function convolveReal(a, b) { + const need = a.length + b.length - 1 + const n = nextPowerOfTwo(need) + + const reA = new Array(n).fill(0) + const imA = new Array(n).fill(0) + const reB = new Array(n).fill(0) + const imB = new Array(n).fill(0) + + for (let i = 0; i < a.length; i++) reA[i] = a[i] + for (let i = 0; i < b.length; i++) reB[i] = b[i] + + fft(reA, imA) + fft(reB, imB) + + const re = new Array(n) + const im = new Array(n) + for (let i = 0; i < n; i++) { + // (reA + i imA) * (reB + i imB) + re[i] = reA[i] * reB[i] - imA[i] * imB[i] + im[i] = reA[i] * imB[i] + imA[i] * reB[i] + } + + ifft(re, im) + + const res = new Array(need) + for (let i = 0; i < need; i++) { + res[i] = Math.round(re[i]) // round to nearest integer to counter FP errors + } + return res +} + +function multiplyPolynomials(a, b) { + return convolveReal(a, b) +} + +function trimLSD(arr) { + // Remove trailing zeros in LSD-first arrays + let i = arr.length - 1 + while (i > 0 && arr[i] === 0) i-- + return arr.slice(0, i + 1) +} + +function multiplyBigIntegers(A, B, base = 10) { + // A, B are LSD-first arrays of digits in given base + if (!Array.isArray(A) || !Array.isArray(B)) { + throw new Error('Inputs must be digit arrays') + } + const conv = convolveReal(A, B) + + // Carry handling + const res = conv.slice() + let carry = 0 + for (let i = 0; i < res.length; i++) { + const total = res[i] + carry + res[i] = total % base + carry = Math.floor(total / base) + } + while (carry > 0) { + res.push(carry % base) + carry = Math.floor(carry / base) + } + const trimmed = trimLSD(res) + return trimmed.length === 0 ? [0] : trimmed +} + +export { fft, ifft, convolveReal, multiplyPolynomials, multiplyBigIntegers } \ No newline at end of file diff --git a/Maths/test/FFT.test.js b/Maths/test/FFT.test.js new file mode 100644 index 0000000000..55b4071e1a --- /dev/null +++ b/Maths/test/FFT.test.js @@ -0,0 +1,59 @@ +import { multiplyPolynomials, multiplyBigIntegers, convolveReal } from '../FFT' + +describe('FFT polynomial multiplication', () => { + it('multiplies small polynomials', () => { + const a = [1, 2, 3] // 1 + 2x + 3x^2 + const b = [4, 5] // 4 + 5x + expect(multiplyPolynomials(a, b)).toEqual([4, 13, 22, 15]) + }) + + it('convolution matches naive for random arrays', () => { + const a = [0, 1, 0, 2, 3] + const b = [1, 2, 3] + const conv = convolveReal(a, b) + const naive = [] + for (let i = 0; i < a.length + b.length - 1; i++) { + let sum = 0 + for (let j = 0; j < a.length; j++) { + const k = i - j + if (k >= 0 && k < b.length) sum += a[j] * b[k] + } + naive.push(sum) + } + expect(conv).toEqual(naive) + }) +}) + +describe('FFT big integer multiplication', () => { + function digitsToBigInt(digs, base = 10) { + // LSD-first digits to BigInt + let s = '' + for (let i = digs.length - 1; i >= 0; i--) s += digs[i].toString(base) + return BigInt(s) + } + + function bigIntToDigits(n, base = 10) { + if (n === 0n) return [0] + const digs = [] + const b = BigInt(base) + let x = n + while (x > 0n) { + digs.push(Number(x % b)) + x /= b + } + return digs + } + + it('multiplies large integer arrays (base 10)', () => { + const A = Array.from({ length: 50 }, () => Math.floor(Math.random() * 10)) + const B = Array.from({ length: 50 }, () => Math.floor(Math.random() * 10)) + const prodDigits = multiplyBigIntegers(A, B, 10) + const prodBigInt = digitsToBigInt(A) * digitsToBigInt(B) + expect(prodDigits).toEqual(bigIntToDigits(prodBigInt)) + }) + + it('handles leading zeros and zero cases', () => { + expect(multiplyBigIntegers([0], [0])).toEqual([0]) + expect(multiplyBigIntegers([0, 0, 1], [0, 2])).toEqual([0, 0, 0, 2]) + }) +}) \ No newline at end of file diff --git a/String/SuffixAutomaton.js b/String/SuffixAutomaton.js new file mode 100644 index 0000000000..2d59a61b40 --- /dev/null +++ b/String/SuffixAutomaton.js @@ -0,0 +1,102 @@ +// Suffix Automaton implementation for substring queries +// Provides: buildSuffixAutomaton, countDistinctSubstrings, longestCommonSubstring + +class SAMState { + constructor() { + this.next = Object.create(null) + this.link = -1 + this.len = 0 + } +} + +function buildSuffixAutomaton(s) { + const states = [new SAMState()] + let last = 0 + + for (const ch of s) { + const cur = states.length + states.push(new SAMState()) + states[cur].len = states[last].len + 1 + + let p = last + while (p !== -1 && states[p].next[ch] === undefined) { + states[p].next[ch] = cur + p = states[p].link + } + + if (p === -1) { + states[cur].link = 0 + } else { + const q = states[p].next[ch] + if (states[p].len + 1 === states[q].len) { + states[cur].link = q + } else { + const clone = states.length + states.push(new SAMState()) + states[clone].len = states[p].len + 1 + states[clone].next = { ...states[q].next } + states[clone].link = states[q].link + + while (p !== -1 && states[p].next[ch] === q) { + states[p].next[ch] = clone + p = states[p].link + } + states[q].link = states[cur].link = clone + } + } + + last = cur + } + + return { states, last } +} + +function countDistinctSubstrings(s) { + const { states } = buildSuffixAutomaton(s) + let count = 0 + // State 0 is the initial state; skip it in the sum + for (let v = 1; v < states.length; v++) { + const link = states[v].link + const add = states[v].len - (link === -1 ? 0 : states[link].len) + count += add + } + return count +} + +function longestCommonSubstring(a, b) { + // Build SAM of string a, then walk b to find LCS + const { states } = buildSuffixAutomaton(a) + let v = 0 + let l = 0 + let best = 0 + let bestEnd = -1 + + for (let i = 0; i < b.length; i++) { + const ch = b[i] + if (states[v].next[ch] !== undefined) { + v = states[v].next[ch] + l++ + } else { + while (v !== -1 && states[v].next[ch] === undefined) { + v = states[v].link + } + if (v === -1) { + v = 0 + l = 0 + continue + } else { + l = states[v].len + 1 + v = states[v].next[ch] + } + } + if (l > best) { + best = l + bestEnd = i + } + } + + if (best === 0) return '' + return b.slice(bestEnd - best + 1, bestEnd + 1) +} + +export { buildSuffixAutomaton, countDistinctSubstrings, longestCommonSubstring } \ No newline at end of file diff --git a/String/test/SuffixAutomaton.test.js b/String/test/SuffixAutomaton.test.js new file mode 100644 index 0000000000..4d1cc7c361 --- /dev/null +++ b/String/test/SuffixAutomaton.test.js @@ -0,0 +1,24 @@ +import { countDistinctSubstrings, longestCommonSubstring } from '../SuffixAutomaton' + +describe('Suffix Automaton - distinct substrings', () => { + it('handles empty string', () => { + expect(countDistinctSubstrings('')).toBe(0) + }) + + it('counts distinct substrings correctly', () => { + expect(countDistinctSubstrings('aaa')).toBe(3) + expect(countDistinctSubstrings('abc')).toBe(6) + expect(countDistinctSubstrings('ababa')).toBe(9) + }) +}) + +describe('Suffix Automaton - longest common substring', () => { + it('finds LCS of two strings', () => { + expect(longestCommonSubstring('xabcdxyz', 'xyzabcd')).toBe('abcd') + expect(longestCommonSubstring('hello', 'yellow')).toBe('ello') + }) + + it('returns empty when no common substring', () => { + expect(longestCommonSubstring('abc', 'def')).toBe('') + }) +}) \ No newline at end of file diff --git a/Trees/SegmentTree.js b/Trees/SegmentTree.js new file mode 100644 index 0000000000..8706bf69ed --- /dev/null +++ b/Trees/SegmentTree.js @@ -0,0 +1,73 @@ +// Segment Tree with range sum query and range add updates (lazy propagation) +// Usage: const st = new SegmentTree(array); st.rangeSum(l, r); st.updateRange(l, r, add); + +class SegmentTree { + constructor(arr) { + this.n = arr.length + this.tree = new Array(this.n * 4).fill(0) + this.lazy = new Array(this.n * 4).fill(0) + this._build(1, 0, this.n - 1, arr) + } + + _build(node, l, r, arr) { + if (l === r) { + this.tree[node] = arr[l] + return + } + const mid = (l + r) >> 1 + this._build(node << 1, l, mid, arr) + this._build((node << 1) | 1, mid + 1, r, arr) + this.tree[node] = this.tree[node << 1] + this.tree[(node << 1) | 1] + } + + _apply(node, l, r, add) { + this.tree[node] += add * (r - l + 1) + this.lazy[node] += add + } + + _push(node, l, r) { + const add = this.lazy[node] + if (add !== 0 && l !== r) { + const mid = (l + r) >> 1 + this._apply(node << 1, l, mid, add) + this._apply((node << 1) | 1, mid + 1, r, add) + this.lazy[node] = 0 + } else if (l === r) { + this.lazy[node] = 0 + } + } + + updateRange(ql, qr, add) { + this._updateRange(1, 0, this.n - 1, ql, qr, add) + } + + _updateRange(node, l, r, ql, qr, add) { + if (ql > r || qr < l) return + if (ql <= l && r <= qr) { + this._apply(node, l, r, add) + return + } + this._push(node, l, r) + const mid = (l + r) >> 1 + this._updateRange(node << 1, l, mid, ql, qr, add) + this._updateRange((node << 1) | 1, mid + 1, r, ql, qr, add) + this.tree[node] = this.tree[node << 1] + this.tree[(node << 1) | 1] + } + + rangeSum(ql, qr) { + return this._rangeSum(1, 0, this.n - 1, ql, qr) + } + + _rangeSum(node, l, r, ql, qr) { + if (ql > r || qr < l) return 0 + if (ql <= l && r <= qr) return this.tree[node] + this._push(node, l, r) + const mid = (l + r) >> 1 + return ( + this._rangeSum(node << 1, l, mid, ql, qr) + + this._rangeSum((node << 1) | 1, mid + 1, r, ql, qr) + ) + } +} + +export { SegmentTree } \ No newline at end of file diff --git a/Trees/test/SegmentTree.test.js b/Trees/test/SegmentTree.test.js new file mode 100644 index 0000000000..3e5e633244 --- /dev/null +++ b/Trees/test/SegmentTree.test.js @@ -0,0 +1,41 @@ +import { SegmentTree } from '../SegmentTree' + +describe('Segment Tree range sum with lazy propagation', () => { + function bruteRangeSum(arr, l, r) { + let s = 0 + for (let i = l; i <= r; i++) s += arr[i] + return s + } + + it('answers queries on static array', () => { + const arr = [1, 2, 3, 4, 5] + const st = new SegmentTree(arr) + expect(st.rangeSum(0, 4)).toBe(15) + expect(st.rangeSum(1, 3)).toBe(9) + expect(st.rangeSum(2, 2)).toBe(3) + }) + + it('supports range updates and matches brute force', () => { + const n = 100 + const arr = Array.from({ length: n }, () => Math.floor(Math.random() * 10)) + const st = new SegmentTree(arr.slice()) + + for (let step = 0; step < 50; step++) { + const l = Math.floor(Math.random() * n) + const r = Math.floor(Math.random() * n) + const ql = Math.min(l, r) + const qr = Math.max(l, r) + const add = Math.floor(Math.random() * 5) - 2 + + st.updateRange(ql, qr, add) + for (let i = ql; i <= qr; i++) arr[i] += add + + const L = Math.floor(Math.random() * n) + const R = Math.floor(Math.random() * n) + const qL = Math.min(L, R) + const qR = Math.max(L, R) + + expect(st.rangeSum(qL, qR)).toBe(bruteRangeSum(arr, qL, qR)) + } + }) +}) \ No newline at end of file