Skip to content

Commit f0f6f2f

Browse files
feat(advanced-techniques): add MoAlgorithm and unit tests (right-exclusive ranges, validation)
1 parent 08d8c6b commit f0f6f2f

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed

Advanced-Techniques/MoAlgorithm.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Mo's Algorithm for answering range queries efficiently
3+
* @module MoAlgorithm
4+
*
5+
* Convention: queries are [left, right) — right is exclusive.
6+
*/
7+
8+
export class Query {
9+
/**
10+
* @param {number} left - inclusive
11+
* @param {number} right - exclusive
12+
* @param {number} index - original index of the query
13+
*/
14+
constructor(left, right, index) {
15+
this.left = left
16+
this.right = right
17+
this.index = index
18+
}
19+
}
20+
21+
class QueryComparator {
22+
constructor(blockSize) {
23+
this.blockSize = blockSize
24+
}
25+
26+
compare(a, b) {
27+
const blockA = Math.floor(a.left / this.blockSize)
28+
const blockB = Math.floor(b.left / this.blockSize)
29+
if (blockA !== blockB) {
30+
return blockA - blockB
31+
}
32+
return blockA % 2 === 0 ? a.right - b.right : b.right - a.right
33+
}
34+
}
35+
36+
export class MoAlgorithm {
37+
/**
38+
* @param {Array} arr
39+
* @param {Array<Query>} queries
40+
* @param {Function} addElement - called with arr[index]
41+
* @param {Function} removeElement - called with arr[index]
42+
* @param {Function} getResult - returns current result
43+
*/
44+
constructor(arr, queries, addElement, removeElement, getResult) {
45+
this.arr = arr
46+
this.queries = queries
47+
this.addElement = addElement
48+
this.removeElement = removeElement
49+
this.getResult = getResult
50+
this.blockSize = Math.max(1, Math.floor(Math.sqrt(arr.length)))
51+
this.comparator = new QueryComparator(this.blockSize)
52+
}
53+
54+
validateQueries() {
55+
for (const q of this.queries) {
56+
if (
57+
!Number.isInteger(q.left) ||
58+
!Number.isInteger(q.right) ||
59+
q.left < 0 ||
60+
q.right < 0 ||
61+
q.left > q.right ||
62+
q.right > this.arr.length
63+
) {
64+
throw new RangeError(
65+
`Invalid query bounds: [${q.left}, ${q.right}) for array length ${this.arr.length}`
66+
)
67+
}
68+
}
69+
}
70+
71+
processQueries() {
72+
// Validate input ranges first (right is allowed to be equal to arr.length)
73+
this.validateQueries()
74+
75+
const sorted = [...this.queries].sort((a, b) =>
76+
this.comparator.compare(a, b)
77+
)
78+
const results = new Array(this.queries.length)
79+
let currentLeft = 0
80+
let currentRight = -1 // current range is empty
81+
82+
for (const q of sorted) {
83+
const left = q.left
84+
const rightExclusive = q.right
85+
const targetRight = rightExclusive - 1 // inclusive target index
86+
87+
// expand right
88+
while (currentRight < targetRight) {
89+
currentRight++
90+
// safe: currentRight is guaranteed in [0, arr.length-1] by validation
91+
this.addElement(this.arr[currentRight])
92+
}
93+
94+
// move left leftwards (expand)
95+
while (currentLeft > left) {
96+
currentLeft--
97+
this.addElement(this.arr[currentLeft])
98+
}
99+
100+
// shrink right
101+
while (currentRight > targetRight) {
102+
this.removeElement(this.arr[currentRight])
103+
currentRight--
104+
}
105+
106+
// shrink left
107+
while (currentLeft < left) {
108+
this.removeElement(this.arr[currentLeft])
109+
currentLeft++
110+
}
111+
112+
results[q.index] = this.getResult()
113+
}
114+
115+
return results
116+
}
117+
}
118+
119+
export class UniqueElementsCounter {
120+
constructor() {
121+
this.frequency = new Map()
122+
this.uniqueCount = 0
123+
}
124+
125+
addElement(element) {
126+
const count = this.frequency.get(element) || 0
127+
this.frequency.set(element, count + 1)
128+
if (count === 0) {
129+
this.uniqueCount++
130+
}
131+
}
132+
133+
removeElement(element) {
134+
const count = this.frequency.get(element) || 0
135+
if (count <= 0) return
136+
if (count === 1) {
137+
this.frequency.delete(element)
138+
this.uniqueCount--
139+
} else {
140+
this.frequency.set(element, count - 1)
141+
}
142+
}
143+
144+
getResult() {
145+
return this.uniqueCount
146+
}
147+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { MoAlgorithm, Query, UniqueElementsCounter } from '../MoAlgorithm.js'
2+
import { describe, it, expect } from 'vitest'
3+
4+
describe('MoAlgorithm', () => {
5+
it('should process range queries correctly', () => {
6+
const arr = [1, 2, 3, 2, 1, 4, 5]
7+
// right is exclusive: to cover indices 0..2 use right=3, 1..4 use right=5, 2..5 use right=6
8+
const queries = [new Query(0, 3, 0), new Query(1, 5, 1), new Query(2, 6, 2)]
9+
10+
const counter = new UniqueElementsCounter()
11+
const processor = new MoAlgorithm(
12+
arr,
13+
queries,
14+
(element) => counter.addElement(element),
15+
(element) => counter.removeElement(element),
16+
() => counter.getResult()
17+
)
18+
19+
const results = processor.processQueries()
20+
expect(results).toEqual([3, 3, 4])
21+
})
22+
23+
it('should handle empty array', () => {
24+
const arr = []
25+
const queries = [new Query(0, 0, 0)]
26+
const counter = new UniqueElementsCounter()
27+
const processor = new MoAlgorithm(
28+
arr,
29+
queries,
30+
counter.addElement.bind(counter),
31+
counter.removeElement.bind(counter),
32+
counter.getResult.bind(counter)
33+
)
34+
35+
const results = processor.processQueries()
36+
expect(results).toEqual([0])
37+
})
38+
})

0 commit comments

Comments
 (0)