From f8577abe07e389e58a42d98dd38fa1e3950d32e8 Mon Sep 17 00:00:00 2001 From: Joel Abraham Date: Sun, 12 Oct 2025 17:25:54 -0400 Subject: [PATCH] Add advanced optimization and modeling algorithms --- Data Structures/DisjointSetUnion.java | 95 +++++++++++ Data Structures/README.md | 15 +- Data Structures/SegmentTree.java | 137 ++++++++++++++++ Optimization/LinearProgrammingSimplex.java | 147 ++++++++++++++++++ .../MarkowitzPortfolioOptimization.java | 127 +++++++++++++++ Optimization/README.md | 13 +- README.md | 3 + 7 files changed, 524 insertions(+), 13 deletions(-) create mode 100644 Data Structures/DisjointSetUnion.java create mode 100644 Data Structures/SegmentTree.java create mode 100644 Optimization/LinearProgrammingSimplex.java create mode 100644 Optimization/MarkowitzPortfolioOptimization.java diff --git a/Data Structures/DisjointSetUnion.java b/Data Structures/DisjointSetUnion.java new file mode 100644 index 0000000..b17c405 --- /dev/null +++ b/Data Structures/DisjointSetUnion.java @@ -0,0 +1,95 @@ +/** + * Disjoint Set Union (Union-Find) with path compression and union by rank. + * + *

This data structure maintains a collection of disjoint sets over elements labeled + * {@code 0..n-1}. It supports near-constant-time merging of sets and connectivity queries, which + * makes it a staple for Kruskal's minimum spanning tree algorithm, offline connectivity problems, + * and clustering applications.

+ */ +public class DisjointSetUnion { + + private final int[] parent; + private final int[] rank; + private final int[] size; + private int components; + + /** + * Creates {@code n} singleton sets {0}, {1}, ..., {n-1}. + */ + public DisjointSetUnion(int n) { + if (n <= 0) { + throw new IllegalArgumentException("Size must be positive"); + } + this.parent = new int[n]; + this.rank = new int[n]; + this.size = new int[n]; + this.components = n; + for (int i = 0; i < n; i++) { + parent[i] = i; + size[i] = 1; + } + } + + /** + * Finds the representative for {@code x} using path compression. + */ + public int find(int x) { + if (parent[x] != x) { + parent[x] = find(parent[x]); + } + return parent[x]; + } + + /** + * Merges the sets containing {@code a} and {@code b}. Returns {@code true} if the sets were + * previously disjoint. + */ + public boolean union(int a, int b) { + int rootA = find(a); + int rootB = find(b); + if (rootA == rootB) { + return false; + } + if (rank[rootA] < rank[rootB]) { + int tmp = rootA; + rootA = rootB; + rootB = tmp; + } + parent[rootB] = rootA; + size[rootA] += size[rootB]; + if (rank[rootA] == rank[rootB]) { + rank[rootA]++; + } + components--; + return true; + } + + /** + * Returns the size of the set containing {@code x}. + */ + public int componentSize(int x) { + return size[find(x)]; + } + + /** + * Returns how many disjoint components remain. + */ + public int components() { + return components; + } + + public static void main(String[] args) { + DisjointSetUnion dsu = new DisjointSetUnion(10); + dsu.union(1, 2); + dsu.union(2, 3); + dsu.union(4, 5); + dsu.union(6, 7); + dsu.union(7, 8); + System.out.println("Components: " + dsu.components()); + System.out.println("1 and 3 connected? " + (dsu.find(1) == dsu.find(3))); + System.out.println("1 and 8 connected? " + (dsu.find(1) == dsu.find(8))); + dsu.union(3, 7); + System.out.println("Components after union(3, 7): " + dsu.components()); + System.out.println("Size of component containing 7: " + dsu.componentSize(7)); + } +} diff --git a/Data Structures/README.md b/Data Structures/README.md index 290e9ce..f5d0dfa 100644 --- a/Data Structures/README.md +++ b/Data Structures/README.md @@ -1,8 +1,9 @@ # Data Structures -* [Binary Indexed Tree (Fenwick Tree)](https://github.com/jpa99/Algorithms/edit/master/Data%20Structures/Binary_Indexed_Tree.java) -* [Binary Tree](https://github.com/jpa99/Algorithms/edit/master/Data%20Structures/BinaryTree.java) -* [Tree](https://github.com/jpa99/Algorithms/edit/master/Data%20Structures/Tree.java) -* [Graph](https://github.com/jpa99/Algorithms/edit/master/Data%20Structures/Graphs.java) - + [Edge](https://github.com/jpa99/Algorithms/edit/master/Data%20Structures/Edge.java) - + [Vertex](https://github.com/jpa99/Algorithms/edit/master/Data%20Structures/Vertex.java) - +* [Binary Indexed Tree (Fenwick Tree)](Binary_Indexed_Tree.java) +* [Binary Tree](BinaryTree.java) +* [Tree](Tree.java) +* [Graph](Graphs.java) + + [Edge](Edge.java) + + [Vertex](Vertex.java) +* [Disjoint Set Union (Union-Find)](DisjointSetUnion.java) +* [Segment Tree](SegmentTree.java) diff --git a/Data Structures/SegmentTree.java b/Data Structures/SegmentTree.java new file mode 100644 index 0000000..a55861c --- /dev/null +++ b/Data Structures/SegmentTree.java @@ -0,0 +1,137 @@ +import java.util.Arrays; + +/** + * Segment tree for range sum and minimum queries with point updates. + * + *

This implementation exposes both range sum and range minimum queries to demonstrate how a + * single tree can maintain multiple aggregate statistics. It is designed for educational purposes + * to showcase how a tree decomposes an array into power-of-two intervals for logarithmic query and + * update times.

+ */ +public class SegmentTree { + + private final int size; + private final long[] sumTree; + private final long[] minTree; + + /** + * Builds the tree using the provided input array. + * + * @param values the array whose values should be indexed. The array is copied so that updates do + * not mutate the caller's data. + */ + public SegmentTree(long[] values) { + this.size = values.length; + int treeSize = 1; + while (treeSize < size) { + treeSize <<= 1; + } + treeSize <<= 1; // allocate enough space for the full binary tree + this.sumTree = new long[treeSize]; + this.minTree = new long[treeSize]; + build(1, 0, size - 1, Arrays.copyOf(values, values.length)); + } + + private void build(int node, int left, int right, long[] values) { + if (left == right) { + sumTree[node] = values[left]; + minTree[node] = values[left]; + return; + } + int mid = left + (right - left) / 2; + build(node * 2, left, mid, values); + build(node * 2 + 1, mid + 1, right, values); + pull(node); + } + + /** + * Performs a range sum query on the inclusive interval [queryLeft, queryRight]. + */ + public long rangeSum(int queryLeft, int queryRight) { + validateRange(queryLeft, queryRight); + return rangeSum(1, 0, size - 1, queryLeft, queryRight); + } + + private long rangeSum(int node, int left, int right, int queryLeft, int queryRight) { + if (queryLeft > right || queryRight < left) { + return 0; + } + if (queryLeft <= left && right <= queryRight) { + return sumTree[node]; + } + int mid = left + (right - left) / 2; + return rangeSum(node * 2, left, mid, queryLeft, queryRight) + + rangeSum(node * 2 + 1, mid + 1, right, queryLeft, queryRight); + } + + /** + * Performs a range minimum query on the inclusive interval [queryLeft, queryRight]. + */ + public long rangeMin(int queryLeft, int queryRight) { + validateRange(queryLeft, queryRight); + return rangeMin(1, 0, size - 1, queryLeft, queryRight); + } + + private long rangeMin(int node, int left, int right, int queryLeft, int queryRight) { + if (queryLeft > right || queryRight < left) { + return Long.MAX_VALUE; + } + if (queryLeft <= left && right <= queryRight) { + return minTree[node]; + } + int mid = left + (right - left) / 2; + return Math.min( + rangeMin(node * 2, left, mid, queryLeft, queryRight), + rangeMin(node * 2 + 1, mid + 1, right, queryLeft, queryRight)); + } + + /** + * Updates the value at {@code index} to {@code newValue}. + */ + public void update(int index, long newValue) { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException("Index " + index + " is outside the tree"); + } + update(1, 0, size - 1, index, newValue); + } + + private void update(int node, int left, int right, int index, long newValue) { + if (left == right) { + sumTree[node] = newValue; + minTree[node] = newValue; + return; + } + int mid = left + (right - left) / 2; + if (index <= mid) { + update(node * 2, left, mid, index, newValue); + } else { + update(node * 2 + 1, mid + 1, right, index, newValue); + } + pull(node); + } + + private void pull(int node) { + sumTree[node] = sumTree[node * 2] + sumTree[node * 2 + 1]; + minTree[node] = Math.min(minTree[node * 2], minTree[node * 2 + 1]); + } + + private void validateRange(int left, int right) { + if (left < 0 || right >= size || left > right) { + throw new IllegalArgumentException( + "Invalid query range [" + left + ", " + right + "] for size " + size); + } + } + + public static void main(String[] args) { + long[] values = {5, 2, 9, -4, 7, 6, 3, 8}; + SegmentTree tree = new SegmentTree(values); + System.out.println("Initial sum [0, 7]: " + tree.rangeSum(0, 7)); + System.out.println("Initial min [2, 5]: " + tree.rangeMin(2, 5)); + + tree.update(3, 10); + System.out.println("After updating index 3 to 10"); + System.out.println("Sum [0, 7]: " + tree.rangeSum(0, 7)); + System.out.println("Min [2, 5]: " + tree.rangeMin(2, 5)); + System.out.println("Min [3, 3]: " + tree.rangeMin(3, 3)); + } +} diff --git a/Optimization/LinearProgrammingSimplex.java b/Optimization/LinearProgrammingSimplex.java new file mode 100644 index 0000000..7b5ae84 --- /dev/null +++ b/Optimization/LinearProgrammingSimplex.java @@ -0,0 +1,147 @@ +import java.util.Arrays; + +/** + * Solves linear programs of the form maximize c^T x subject to Ax <= b and x >= 0 using the simplex + * algorithm. + * + *

The implementation follows the textbook tableau method and automatically introduces slack + * variables to obtain an initial basic feasible solution. It is intended for classroom-sized + * problems, so the implementation favors clarity over advanced pivoting rules.

+ */ +public class LinearProgrammingSimplex { + + private static final double EPS = 1e-9; + + private final int constraints; + private final int variables; + private final double[][] tableau; + private final int[] basis; + + /** + * Constructs a simplex solver. + * + * @param A constraint matrix where each row corresponds to an inequality of the form + * A[i] * x <= b[i] + * @param b right-hand side vector + * @param c objective function coefficients for maximize c^T x + */ + public LinearProgrammingSimplex(double[][] A, double[] b, double[] c) { + this.constraints = b.length; + this.variables = c.length; + int width = variables + constraints + 1; + this.tableau = new double[constraints + 1][width]; + this.basis = new int[constraints]; + + for (int i = 0; i < constraints; i++) { + if (A[i].length != variables) { + throw new IllegalArgumentException("Row " + i + " has incorrect length"); + } + System.arraycopy(A[i], 0, tableau[i], 0, variables); + tableau[i][variables + i] = 1.0; // slack variable + if (b[i] < 0) { + throw new IllegalArgumentException("Right-hand side must be non-negative"); + } + tableau[i][width - 1] = b[i]; + basis[i] = variables + i; + } + + for (int j = 0; j < variables; j++) { + tableau[constraints][j] = -c[j]; + } + + solve(); + } + + private void solve() { + while (true) { + int entering = enteringColumn(); + if (entering == -1) { + break; // optimal reached + } + int leaving = leavingRow(entering); + if (leaving == -1) { + throw new IllegalStateException("Linear program is unbounded"); + } + pivot(leaving, entering); + basis[leaving] = entering; + } + } + + private int enteringColumn() { + int width = tableau[0].length; + for (int j = 0; j < width - 1; j++) { + if (tableau[constraints][j] > EPS) { + return j; + } + } + return -1; + } + + private int leavingRow(int entering) { + double bestRatio = Double.POSITIVE_INFINITY; + int row = -1; + for (int i = 0; i < constraints; i++) { + if (tableau[i][entering] > EPS) { + double ratio = tableau[i][tableau[i].length - 1] / tableau[i][entering]; + if (ratio < bestRatio - EPS) { + bestRatio = ratio; + row = i; + } + } + } + return row; + } + + private void pivot(int pivotRow, int pivotColumn) { + double pivot = tableau[pivotRow][pivotColumn]; + for (int j = 0; j < tableau[pivotRow].length; j++) { + tableau[pivotRow][j] /= pivot; + } + for (int i = 0; i < tableau.length; i++) { + if (i == pivotRow) { + continue; + } + double factor = tableau[i][pivotColumn]; + if (Math.abs(factor) < EPS) { + continue; + } + for (int j = 0; j < tableau[i].length; j++) { + tableau[i][j] -= factor * tableau[pivotRow][j]; + } + } + } + + /** + * Returns the optimal primal solution. + */ + public double[] primal() { + double[] solution = new double[variables]; + for (int i = 0; i < constraints; i++) { + if (basis[i] < variables) { + solution[basis[i]] = tableau[i][tableau[i].length - 1]; + } + } + return solution; + } + + /** + * Returns the optimal objective value. + */ + public double value() { + return tableau[constraints][tableau[constraints].length - 1]; + } + + public static void main(String[] args) { + double[][] A = { + {2, 1}, + {1, 2}, + {1, 0} + }; + double[] b = {14, 14, 6}; + double[] c = {3, 2}; + + LinearProgrammingSimplex simplex = new LinearProgrammingSimplex(A, b, c); + System.out.println("Optimal value: " + simplex.value()); + System.out.println("Solution: " + Arrays.toString(simplex.primal())); + } +} diff --git a/Optimization/MarkowitzPortfolioOptimization.java b/Optimization/MarkowitzPortfolioOptimization.java new file mode 100644 index 0000000..3a88f48 --- /dev/null +++ b/Optimization/MarkowitzPortfolioOptimization.java @@ -0,0 +1,127 @@ +import java.util.Arrays; + +/** + * Computes mean-variance optimal portfolios using the classical Markowitz formulation. + * + *

The optimizer solves for portfolio weights that minimize variance subject to achieving a target + * expected return and the weights summing to one. The solution follows the closed-form KKT system + * for the quadratic program:

+ * + *
+ *     minimize   (1/2) w^T Σ w
+ *     subject to μ^T w = r_target
+ *                1^T w = 1
+ * 
+ */ +public class MarkowitzPortfolioOptimization { + + /** + * Solves for the optimal weights. + * + * @param expectedReturns asset expected returns μ + * @param covariance covariance matrix Σ + * @param targetReturn desired portfolio return + * @return weights that satisfy the optimality conditions + */ + public static double[] optimizePortfolio( + double[] expectedReturns, double[][] covariance, double targetReturn) { + int n = expectedReturns.length; + if (covariance.length != n || covariance[0].length != n) { + throw new IllegalArgumentException("Covariance matrix must be square"); + } + + double[][] system = new double[n + 2][n + 2]; + double[] rhs = new double[n + 2]; + + // Stationarity conditions: Σ w - λ μ - γ 1 = 0 + for (int i = 0; i < n; i++) { + System.arraycopy(covariance[i], 0, system[i], 0, n); + system[i][n] = -expectedReturns[i]; + system[i][n + 1] = -1.0; + } + + // μ^T w = targetReturn + for (int j = 0; j < n; j++) { + system[n][j] = expectedReturns[j]; + } + rhs[n] = targetReturn; + + // 1^T w = 1 + Arrays.fill(system[n + 1], 0); + for (int j = 0; j < n; j++) { + system[n + 1][j] = 1.0; + } + rhs[n + 1] = 1.0; + + double[] solution = solveLinearSystem(system, rhs); + return Arrays.copyOf(solution, n); + } + + private static double[] solveLinearSystem(double[][] A, double[] b) { + int n = b.length; + double[][] augmented = new double[n][n + 1]; + for (int i = 0; i < n; i++) { + System.arraycopy(A[i], 0, augmented[i], 0, n); + augmented[i][n] = b[i]; + } + + for (int col = 0; col < n; col++) { + int pivot = col; + for (int row = col + 1; row < n; row++) { + if (Math.abs(augmented[row][col]) > Math.abs(augmented[pivot][col])) { + pivot = row; + } + } + if (Math.abs(augmented[pivot][col]) < 1e-9) { + throw new IllegalArgumentException("System is singular"); + } + double[] tmp = augmented[pivot]; + augmented[pivot] = augmented[col]; + augmented[col] = tmp; + + double pivotValue = augmented[col][col]; + for (int j = col; j <= n; j++) { + augmented[col][j] /= pivotValue; + } + for (int row = 0; row < n; row++) { + if (row == col) { + continue; + } + double factor = augmented[row][col]; + for (int j = col; j <= n; j++) { + augmented[row][j] -= factor * augmented[col][j]; + } + } + } + + double[] x = new double[n]; + for (int i = 0; i < n; i++) { + x[i] = augmented[i][n]; + } + return x; + } + + public static void main(String[] args) { + double[] expectedReturns = {0.12, 0.08, 0.05}; + double[][] covariance = { + {0.0100, 0.0018, 0.0011}, + {0.0018, 0.0109, 0.0026}, + {0.0011, 0.0026, 0.0199} + }; + double targetReturn = 0.08; + + double[] weights = optimizePortfolio(expectedReturns, covariance, targetReturn); + double realizedReturn = 0.0; + double variance = 0.0; + for (int i = 0; i < weights.length; i++) { + realizedReturn += weights[i] * expectedReturns[i]; + for (int j = 0; j < weights.length; j++) { + variance += weights[i] * weights[j] * covariance[i][j]; + } + } + + System.out.println("Weights: " + Arrays.toString(weights)); + System.out.println("Expected return: " + realizedReturn); + System.out.println("Variance: " + variance); + } +} diff --git a/Optimization/README.md b/Optimization/README.md index 94e444d..8a8d56b 100644 --- a/Optimization/README.md +++ b/Optimization/README.md @@ -1,8 +1,9 @@ # Optimization Algorithms -This folder contains optimization implementations for locational optimization problems. The general problem statement solveable by this flavor of optimization is as follows: Given a set S of n candidate 2-tuples (ordered pairs) and a function cost(P): R^2 --> R where P = {(x_1, y_1), (x_2, y_2), ..., (x_k, y_k)}, find a subset Q of k 2-tuples (ordered pairs) such that cost(Q) is minimized. This can be reformulated as selecting k optimal points from a set of n candidate points. +This folder contains implementations for classical optimization techniques that appear in +competitive programming, operations research, and quantitative finance. Algorithms include: -* [Naive Optimization](https://github.com/jpa99/Algorithms/blob/master/Optimization/LightConfigurationOptimization.java) -* Simplex (LP) -* [Simulated Annealing](https://github.com/jpa99/Algorithms/blob/master/Optimization/LightConfigurationOptimizationSA.java) -* Genetic Algorithms -* Combinatoric Algorithms +* [Naive Optimization](LightConfigurationOptimization.java) +* [Simulated Annealing](LightConfigurationOptimizationSA.java) +* [Iterative Locational Optimization](LocationalOptimizationIterative.java) +* [Linear Programming via Simplex](LinearProgrammingSimplex.java) +* [Markowitz Mean-Variance Portfolio Optimization](MarkowitzPortfolioOptimization.java) diff --git a/README.md b/README.md index 5ef3ff3..37c2b97 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ This respository is a collection of various useful algorithms and data structure ## Optimization * Naive Optimization * Simplex (LP) +* Markowitz Mean-Variance Portfolio Optimization * Simulated Annealing * Genetic Algorithms * Combinatoric Algorithms @@ -127,6 +128,8 @@ This respository is a collection of various useful algorithms and data structure * Tree * Binary Tree * Fenwick Tree (Binary Indexed Tree) +* Disjoint Set Union (Union-Find) +* Segment Tree * Red-Black Tree * AVL Tree * B-Tree