diff --git a/TryAtSoftware.Extensions.Collections.Tests/SegmentTreeTests.cs b/TryAtSoftware.Extensions.Collections.Tests/SegmentTreeTests.cs new file mode 100644 index 0000000..2b60754 --- /dev/null +++ b/TryAtSoftware.Extensions.Collections.Tests/SegmentTreeTests.cs @@ -0,0 +1,73 @@ +namespace TryAtSoftware.Extensions.Collections.Tests; + +using TryAtSoftware.Extensions.Collections.Interfaces; +using TryAtSoftware.Randomizer.Core.Helpers; +using Xunit; + +public static class SegmentTreeTests +{ + [Fact] + public static void SegmentTreeShouldBeUpdatedSuccessfully() + { + var n = RandomizationHelper.RandomInteger(0, 100); + + var initializationEngine = new StandardSegmentTreeInitializationEngine(0); + var segmentTree = new RecursiveSegmentTree(n, initializationEngine); + + var numbers = new int[n]; + for (var i = 0; i < n; i++) + { + numbers[i] = RandomizationHelper.RandomInteger(0, 100); + segmentTree.Update(i, new StandardSegmentTreeChangeOperator(numbers[i])); + } + + ValidateQueries(numbers, segmentTree); + } + + [Fact] + public static void SegmentTreeShouldBeLazyUpdatedSuccessfully() + { + var n = RandomizationHelper.RandomInteger(0, 100); + + var initializationEngine = new StandardSegmentTreeInitializationEngine(0); + var segmentTree = new RecursiveSegmentTree(n, initializationEngine); + + var numbers = new int[n]; + for (var i = 0; i < n; i++) + { + numbers[i] = RandomizationHelper.RandomInteger(0, 100); + segmentTree.LazyUpdate(i, new StandardSegmentTreeChangeOperator(numbers[i])); + } + + ValidateQueries(numbers, segmentTree); + } + + private static void ValidateQueries(int[] numbers, RecursiveSegmentTree segmentTree) + { + var queryEngine = new SumSegmentTreeQueryOperator(); + for (var i = 0; i < numbers.Length; i++) Assert.Equal(numbers[i], segmentTree.Query(i, queryEngine)); + + var prefixSum = new int[numbers.Length + 1]; + for (var i = 0; i < numbers.Length; i++) prefixSum[i + 1] = prefixSum[i] + numbers[i]; + + for (var i = 0; i < numbers.Length; i++) + for (var j = i; j < numbers.Length; j++) Assert.Equal(prefixSum[j + 1] - prefixSum[i], segmentTree.Query(i, j, queryEngine)); + } +} + +public class StandardSegmentTreeInitializationEngine(TValue defaultValue) : ISegmentTreeInitializationEngine +{ + public TValue CreateInitialValue(int index) => defaultValue; +} + +public class StandardSegmentTreeChangeOperator(TValue newValue) : ISegmentTreeChangeOperator +{ + public TValue ApplyChange(TValue currentValue) => newValue; +} + +public class SumSegmentTreeQueryOperator : ISegmentTreeQueryOperator +{ + public int Merge(int left, int right) => left + right; + + public int ProduceResult(int value) => value; +} \ No newline at end of file diff --git a/TryAtSoftware.Extensions.Collections/Interfaces/ISegmentTree.cs b/TryAtSoftware.Extensions.Collections/Interfaces/ISegmentTree.cs new file mode 100644 index 0000000..e9936ef --- /dev/null +++ b/TryAtSoftware.Extensions.Collections/Interfaces/ISegmentTree.cs @@ -0,0 +1,13 @@ +namespace TryAtSoftware.Extensions.Collections.Interfaces; + +public interface ISegmentTree +{ + TOutput Query(int index, ISegmentTreeQueryOperator queryOperator); + TOutput Query(int start, int end, ISegmentTreeQueryOperator queryOperator); + + void Update(int index, ISegmentTreeChangeOperator changeOperator); + void Update(int start, int end, ISegmentTreeChangeOperator changeOperator); + + void LazyUpdate(int index, ISegmentTreeChangeOperator changeOperator); + void LazyUpdate(int start, int end, ISegmentTreeChangeOperator changeOperator); +} \ No newline at end of file diff --git a/TryAtSoftware.Extensions.Collections/Interfaces/ISegmentTreeChangeOperator.cs b/TryAtSoftware.Extensions.Collections/Interfaces/ISegmentTreeChangeOperator.cs new file mode 100644 index 0000000..d1f41a6 --- /dev/null +++ b/TryAtSoftware.Extensions.Collections/Interfaces/ISegmentTreeChangeOperator.cs @@ -0,0 +1,6 @@ +namespace TryAtSoftware.Extensions.Collections.Interfaces; + +public interface ISegmentTreeChangeOperator +{ + TValue ApplyChange(TValue currentValue); +} \ No newline at end of file diff --git a/TryAtSoftware.Extensions.Collections/Interfaces/ISegmentTreeInitializationEngine.cs b/TryAtSoftware.Extensions.Collections/Interfaces/ISegmentTreeInitializationEngine.cs new file mode 100644 index 0000000..a437783 --- /dev/null +++ b/TryAtSoftware.Extensions.Collections/Interfaces/ISegmentTreeInitializationEngine.cs @@ -0,0 +1,6 @@ +namespace TryAtSoftware.Extensions.Collections.Interfaces; + +public interface ISegmentTreeInitializationEngine +{ + TValue CreateInitialValue(int index); +} \ No newline at end of file diff --git a/TryAtSoftware.Extensions.Collections/Interfaces/ISegmentTreeQueryOperator.cs b/TryAtSoftware.Extensions.Collections/Interfaces/ISegmentTreeQueryOperator.cs new file mode 100644 index 0000000..8bbc656 --- /dev/null +++ b/TryAtSoftware.Extensions.Collections/Interfaces/ISegmentTreeQueryOperator.cs @@ -0,0 +1,7 @@ +namespace TryAtSoftware.Extensions.Collections.Interfaces; + +public interface ISegmentTreeQueryOperator +{ + TOutput Merge(TOutput left, TOutput right); + TOutput ProduceResult(TValue value); +} \ No newline at end of file diff --git a/TryAtSoftware.Extensions.Collections/RecursiveSegmentTree.cs b/TryAtSoftware.Extensions.Collections/RecursiveSegmentTree.cs new file mode 100644 index 0000000..f4b2ad1 --- /dev/null +++ b/TryAtSoftware.Extensions.Collections/RecursiveSegmentTree.cs @@ -0,0 +1,135 @@ +namespace TryAtSoftware.Extensions.Collections; + +using System; +using TryAtSoftware.Extensions.Collections.Interfaces; + +public class RecursiveSegmentTree : ISegmentTree +{ + private readonly RecursiveSegmentTreeNode _root; + private readonly int _n; + + public RecursiveSegmentTree(int n, ISegmentTreeInitializationEngine initializationEngine) + { + if (initializationEngine is null) throw new ArgumentNullException(nameof(initializationEngine)); + if (n < 0) throw new ArgumentException("The count of base elements for the segment tree must not be negative."); + + this._root = new RecursiveSegmentTreeNode(initializationEngine, 0, n - 1); + this._n = n; + } + + public TOutput Query(int index, ISegmentTreeQueryOperator queryOperator) => this.Query(index, index, queryOperator); + + public TOutput Query(int start, int end, ISegmentTreeQueryOperator queryOperator) + { + this.ValidateBounds(start, end); + return this._root.Query(start, end, queryOperator); + } + + public void Update(int index, ISegmentTreeChangeOperator changeOperator) => this.Update(index, index, changeOperator); + + public void Update(int start, int end, ISegmentTreeChangeOperator changeOperator) + { + this.ValidateBounds(start, end); + this._root.Update(start, end, changeOperator); + } + + public void LazyUpdate(int index, ISegmentTreeChangeOperator changeOperator) => this.LazyUpdate(index, index, changeOperator); + + public void LazyUpdate(int start, int end, ISegmentTreeChangeOperator changeOperator) + { + this.ValidateBounds(start, end); + this._root.LazyUpdate(start, end, changeOperator); + } + + private void ValidateBounds(int start, int end) + { + if (start < 0) throw new ArgumentException("The query start index must not be negative.", nameof(start)); + if (end >= this._n) throw new ArgumentException("The query end index must not exceed the original bounds.", nameof(end)); + if (start > end) throw new InvalidOperationException("The query start index must be less than or equal to the query end index"); + } +} + +internal class RecursiveSegmentTreeNode +{ + private readonly ISegmentTreeInitializationEngine _initializationEngine; + private readonly int _start, _end, _m1, _m2; + private readonly bool _isLeaf; + + private RecursiveSegmentTreeNode? _left, _right; + private TValue? _value; + private LazyUpdateDefinition? _lazyUpdateDefinition; + + private RecursiveSegmentTreeNode Left => this._left ??= new RecursiveSegmentTreeNode(this._initializationEngine, this._start, this._m1); + private RecursiveSegmentTreeNode Right => this._right ??= new RecursiveSegmentTreeNode(this._initializationEngine, this._m2, this._end); + private TValue Value => this._value!; + + public RecursiveSegmentTreeNode(ISegmentTreeInitializationEngine initializationEngine, int start, int end) + { + this._initializationEngine = initializationEngine; + this._start = start; + this._end = end; + this._isLeaf = start == end; + + if (end - start < 2) + { + this._m1 = start; + this._m2 = end; + } + else + { + this._m1 = start + (end - start) / 2; + this._m2 = this._m1 + 1; + } + + if (this._isLeaf) this._value = this._initializationEngine.CreateInitialValue(this._start); + } + + // For all the methods below is guaranteed that `this._start <= queryStart <= queryEnd <= this._end`. + public TOutput Query(int queryStart, int queryEnd, ISegmentTreeQueryOperator queryOperator) + { + if (this._isLeaf) return queryOperator.ProduceResult(this.Value); + + this.PopulateLazyUpdate(); + if (queryEnd <= this._m1) return this.Left.Query(queryStart, queryEnd, queryOperator); + if (queryStart >= this._m2) return this.Right.Query(queryStart, queryEnd, queryOperator); + + return queryOperator.Merge(this.Left.Query(queryStart, this._m1, queryOperator), this.Right.Query(this._m2, queryEnd, queryOperator)); + } + + public void Update(int queryStart, int queryEnd, ISegmentTreeChangeOperator changeOperator) + { + if (this._isLeaf) this._value = changeOperator.ApplyChange(this.Value); + else + { + this.PopulateLazyUpdate(); + if (queryStart <= this._m1) this.Left.Update(queryStart, Math.Min(this._m1, queryEnd), changeOperator); + if (queryEnd >= this._m2) this.Right.Update(Math.Max(this._m2, queryStart), queryEnd, changeOperator); + } + } + + public void LazyUpdate(int queryStart, int queryEnd, ISegmentTreeChangeOperator changeOperator) + { + if (this._isLeaf) this._value = changeOperator.ApplyChange(this.Value); + else + { + this.PopulateLazyUpdate(); + this._lazyUpdateDefinition = new LazyUpdateDefinition(queryStart, queryEnd, changeOperator); + } + } + + private void PopulateLazyUpdate() + { + if (this._lazyUpdateDefinition is null) return; + + if (this._lazyUpdateDefinition.Start <= this._m1) this.Left.LazyUpdate(this._lazyUpdateDefinition.Start, Math.Min(this._m1, this._lazyUpdateDefinition.End), this._lazyUpdateDefinition.ChangeOperator); + if (this._lazyUpdateDefinition.End >= this._m2) this.Right.LazyUpdate(Math.Max(this._m2, this._lazyUpdateDefinition.Start), this._lazyUpdateDefinition.End, this._lazyUpdateDefinition.ChangeOperator); + this._lazyUpdateDefinition = null; + } +} + +internal class LazyUpdateDefinition(int start, int end, ISegmentTreeChangeOperator changeOperator) +{ + public int Start { get; } = start; + public int End { get; } = end; + public ISegmentTreeChangeOperator ChangeOperator { get; } = changeOperator; +} \ No newline at end of file