diff --git a/src/main/java/com/williamfiset/algorithms/dp/KnapsackUnbounded.java b/src/main/java/com/williamfiset/algorithms/dp/KnapsackUnbounded.java
index 116d2ed1a..e985d6c6f 100644
--- a/src/main/java/com/williamfiset/algorithms/dp/KnapsackUnbounded.java
+++ b/src/main/java/com/williamfiset/algorithms/dp/KnapsackUnbounded.java
@@ -1,92 +1,118 @@
+package com.williamfiset.algorithms.dp;
+
/**
- * This file contains a dynamic programming solutions to the classic unbounded knapsack problem
- * where are you are trying to maximize the total profit of items selected without exceeding the
- * capacity of your knapsack.
+ * Unbounded Knapsack Problem — Bottom-Up Dynamic Programming
+ *
+ * Given n items, each with a weight and a value, determine the maximum total
+ * value that can be placed in a knapsack of a given capacity. Unlike the 0/1
+ * knapsack, each item may be selected unlimited times.
+ *
+ * Two implementations are provided:
*
- *
Version 1: Time Complexity: O(nW) Version 1 Space Complexity: O(nW)
+ * 1. unboundedKnapsack() — 2D DP table, O(n*W) time and space
+ * 2. unboundedKnapsackSpaceEfficient() — 1D DP array, O(n*W) time, O(W) space
*
- *
Version 2: Time Complexity: O(nW) Space Complexity: O(W)
+ * The key difference from 0/1 knapsack is in the recurrence: when including
+ * item i, we look at dp[i][sz - w] (same row) instead of dp[i-1][sz - w]
+ * (previous row), allowing the item to be selected again.
*
- *
Tested code against: https://www.hackerrank.com/challenges/unbounded-knapsack
+ * See also: Knapsack_01 for the variant where each item can be used at most once.
+ *
+ * Tested against: https://www.hackerrank.com/challenges/unbounded-knapsack
+ *
+ * Time: O(n*W) where n = number of items, W = capacity
+ * Space: O(n*W) or O(W) for the space-efficient version
*
* @author William Fiset, william.alexandre.fiset@gmail.com
*/
-package com.williamfiset.algorithms.dp;
-
public class KnapsackUnbounded {
+ // ==================== Implementation 1: 2D DP table ====================
+
/**
- * @param maxWeight - The maximum weight of the knapsack
- * @param W - The weights of the items
- * @param V - The values of the items
- * @return The maximum achievable profit of selecting a subset of the elements such that the
- * capacity of the knapsack is not exceeded
+ * Computes the maximum value achievable with unlimited item reuse.
+ *
+ * dp[i][sz] = max value using items 1..i with capacity sz.
+ * When including item i, we reference dp[i][sz-w] (not dp[i-1][sz-w])
+ * because the item can be selected again.
+ *
+ * @param maxWeight the maximum weight the knapsack can hold
+ * @param W array of item weights
+ * @param V array of item values
+ * @return the maximum total value
+ *
+ * Time: O(n*W)
+ * Space: O(n*W)
*/
public static int unboundedKnapsack(int maxWeight, int[] W, int[] V) {
-
if (W == null || V == null || W.length != V.length || maxWeight < 0)
throw new IllegalArgumentException("Invalid input");
- final int N = W.length;
+ final int n = W.length;
+ int[][] dp = new int[n + 1][maxWeight + 1];
- // Initialize a table where individual rows represent items
- // and columns represent the weight of the knapsack
- int[][] DP = new int[N + 1][maxWeight + 1];
-
- // Loop through items
- for (int i = 1; i <= N; i++) {
-
- // Get the value and weight of the item
+ for (int i = 1; i <= n; i++) {
int w = W[i - 1], v = V[i - 1];
-
- // Consider all possible knapsack sizes
for (int sz = 1; sz <= maxWeight; sz++) {
+ // Include item i (reuse allowed — look at same row dp[i])
+ if (sz >= w)
+ dp[i][sz] = dp[i][sz - w] + v;
- // Try including the current element
- if (sz >= w) DP[i][sz] = DP[i][sz - w] + v;
-
- // Check if not selecting this item at all is more profitable
- if (DP[i - 1][sz] > DP[i][sz]) DP[i][sz] = DP[i - 1][sz];
+ // Skip item i if that's more profitable
+ if (dp[i - 1][sz] > dp[i][sz])
+ dp[i][sz] = dp[i - 1][sz];
}
}
- // Return the best value achievable
- return DP[N][maxWeight];
+ return dp[n][maxWeight];
}
- public static int unboundedKnapsackSpaceEfficient(int maxWeight, int[] W, int[] V) {
+ // ==================== Implementation 2: Space-efficient 1D DP ====================
+ /**
+ * Space-efficient version using a single 1D array.
+ *
+ * dp[sz] = max value achievable with capacity sz using any items.
+ * For each capacity, try every item and keep the best.
+ *
+ * @param maxWeight the maximum weight the knapsack can hold
+ * @param W array of item weights
+ * @param V array of item values
+ * @return the maximum total value
+ *
+ * Time: O(n*W)
+ * Space: O(W)
+ */
+ public static int unboundedKnapsackSpaceEfficient(int maxWeight, int[] W, int[] V) {
if (W == null || V == null || W.length != V.length)
throw new IllegalArgumentException("Invalid input");
- final int N = W.length;
-
- // Initialize a table where we will only keep track of
- // the best possible value for each knapsack weight
- int[] DP = new int[maxWeight + 1];
+ final int n = W.length;
+ int[] dp = new int[maxWeight + 1];
- // Consider all possible knapsack sizes
for (int sz = 1; sz <= maxWeight; sz++) {
-
- // Loop through items
- for (int i = 0; i < N; i++) {
-
- // First check that we can include this item (we can't include it if
- // it's too heavy for our knapsack). Assumming it fits inside the
- // knapsack check if including this element would be profitable.
- if (sz - W[i] >= 0 && DP[sz - W[i]] + V[i] > DP[sz]) DP[sz] = DP[sz - W[i]] + V[i];
+ for (int i = 0; i < n; i++) {
+ // Include item i if it fits and improves the value
+ if (sz >= W[i] && dp[sz - W[i]] + V[i] > dp[sz])
+ dp[sz] = dp[sz - W[i]] + V[i];
}
}
- // Return the best value achievable
- return DP[maxWeight];
+ return dp[maxWeight];
}
public static void main(String[] args) {
-
int[] W = {3, 6, 2};
int[] V = {5, 20, 3};
- int knapsackValue = unboundedKnapsackSpaceEfficient(10, W, V);
- System.out.println("Maximum knapsack value: " + knapsackValue);
+
+ // Capacity 10: best is (w=6,v=20) + 2x(w=2,v=3) = weight 10, value 26
+ System.out.println("2D DP: " + unboundedKnapsack(10, W, V)); // 26
+
+ // Space-efficient: same result
+ System.out.println("Space-efficient: " + unboundedKnapsackSpaceEfficient(10, W, V)); // 26
+
+ // Capacity 12: two items of weight 6 and value 20 = 40
+ System.out.println("2D DP (cap=12): " + unboundedKnapsack(12, W, V)); // 40
+ System.out.println("Space (cap=12): " + unboundedKnapsackSpaceEfficient(12, W, V)); // 40
}
}
diff --git a/src/main/java/com/williamfiset/algorithms/dp/Knapsack_01.java b/src/main/java/com/williamfiset/algorithms/dp/Knapsack_01.java
index 8cea8b26a..42ee227f7 100644
--- a/src/main/java/com/williamfiset/algorithms/dp/Knapsack_01.java
+++ b/src/main/java/com/williamfiset/algorithms/dp/Knapsack_01.java
@@ -1,87 +1,136 @@
+package com.williamfiset.algorithms.dp;
+
+import java.util.LinkedList;
+import java.util.List;
+
/**
- * This file contains a dynamic programming solutions to the classic 0/1 knapsack problem where are
- * you are trying to maximize the total profit of items selected without exceeding the capacity of
- * your knapsack.
+ * 0/1 Knapsack Problem — Bottom-Up Dynamic Programming
+ *
+ * Given n items, each with a weight and a value, determine the maximum total
+ * value that can be placed in a knapsack of a given capacity. Each item may
+ * be selected at most once (hence "0/1").
*
- *
Time Complexity: O(nW) Space Complexity: O(nW)
+ * The DP table dp[i][sz] represents the maximum value achievable using the
+ * first i items with a knapsack capacity of sz. For each item we either:
+ * - Skip it: dp[i][sz] = dp[i-1][sz]
+ * - Include it: dp[i][sz] = dp[i-1][sz - w] + v (if it fits)
*
- *
Tested code against: https://open.kattis.com/problems/knapsack
+ * After filling the table, we backtrack to recover which items were selected:
+ * if dp[i][sz] != dp[i-1][sz], then item i was included.
+ *
+ * See also: KnapsackUnbounded for the variant where items can be reused.
+ *
+ * Tested against: https://open.kattis.com/problems/knapsack
+ *
+ * Time: O(n*W) where n = number of items, W = capacity
+ * Space: O(n*W)
*
* @author William Fiset, william.alexandre.fiset@gmail.com
*/
-package com.williamfiset.algorithms.dp;
-
-import java.util.ArrayList;
-import java.util.List;
-
public class Knapsack_01 {
/**
- * @param capacity - The maximum capacity of the knapsack
- * @param W - The weights of the items
- * @param V - The values of the items
- * @return The maximum achievable profit of selecting a subset of the elements such that the
- * capacity of the knapsack is not exceeded
+ * Computes the maximum value achievable without exceeding the knapsack capacity.
+ *
+ * @param capacity the maximum weight the knapsack can hold
+ * @param W array of item weights
+ * @param V array of item values
+ * @return the maximum total value
+ *
+ * Time: O(n*W)
+ * Space: O(n*W)
*/
public static int knapsack(int capacity, int[] W, int[] V) {
-
if (W == null || V == null || W.length != V.length || capacity < 0)
throw new IllegalArgumentException("Invalid input");
- final int N = W.length;
+ final int n = W.length;
- // Initialize a table where individual rows represent items
- // and columns represent the weight of the knapsack
- int[][] DP = new int[N + 1][capacity + 1];
+ // dp[i][sz] = max value using first i items with capacity sz
+ int[][] dp = new int[n + 1][capacity + 1];
- for (int i = 1; i <= N; i++) {
-
- // Get the value and weight of the item
+ for (int i = 1; i <= n; i++) {
int w = W[i - 1], v = V[i - 1];
-
for (int sz = 1; sz <= capacity; sz++) {
+ // Option 1: skip this item
+ dp[i][sz] = dp[i - 1][sz];
- // Consider not picking this element
- DP[i][sz] = DP[i - 1][sz];
-
- // Consider including the current element and
- // see if this would be more profitable
- if (sz >= w && DP[i - 1][sz - w] + v > DP[i][sz]) DP[i][sz] = DP[i - 1][sz - w] + v;
+ // Option 2: include this item if it fits and improves the value
+ if (sz >= w && dp[i - 1][sz - w] + v > dp[i][sz])
+ dp[i][sz] = dp[i - 1][sz - w] + v;
}
}
- int sz = capacity;
- List itemsSelected = new ArrayList<>();
+ return dp[n][capacity];
+ }
- // Using the information inside the table we can backtrack and determine
- // which items were selected during the dynamic programming phase. The idea
- // is that if DP[i][sz] != DP[i-1][sz] then the item was selected
- for (int i = N; i > 0; i--) {
- if (DP[i][sz] != DP[i - 1][sz]) {
- int itemIndex = i - 1;
- itemsSelected.add(itemIndex);
- sz -= W[itemIndex];
+ /**
+ * Returns the indices of items selected in the optimal solution.
+ *
+ * After filling the DP table, we recover the selected items by walking
+ * backwards from dp[n][capacity]. At each row i, we check:
+ *
+ * - If dp[i][sz] != dp[i-1][sz], then item i-1 contributed to the
+ * optimal value at this capacity, so it was selected. We add it
+ * to the result and reduce the remaining capacity by its weight.
+ *
+ * - If dp[i][sz] == dp[i-1][sz], then item i-1 was NOT selected
+ * (the optimal value came from the previous items alone), so we
+ * just move to row i-1.
+ *
+ * @param capacity the maximum weight the knapsack can hold
+ * @param W array of item weights
+ * @param V array of item values
+ * @return list of selected item indices (0-based, in ascending order)
+ *
+ * Time: O(n*W)
+ * Space: O(n*W)
+ */
+ public static List knapsackItems(int capacity, int[] W, int[] V) {
+ if (W == null || V == null || W.length != V.length || capacity < 0)
+ throw new IllegalArgumentException("Invalid input");
+
+ final int n = W.length;
+ int[][] dp = new int[n + 1][capacity + 1];
+
+ for (int i = 1; i <= n; i++) {
+ int w = W[i - 1], v = V[i - 1];
+ for (int sz = 1; sz <= capacity; sz++) {
+ dp[i][sz] = dp[i - 1][sz];
+ if (sz >= w && dp[i - 1][sz - w] + v > dp[i][sz])
+ dp[i][sz] = dp[i - 1][sz - w] + v;
}
}
- // Return the items that were selected
- // java.util.Collections.reverse(itemsSelected);
- // return itemsSelected;
+ // Backtrack through the table to find which items were selected.
+ // Starting at dp[n][capacity], walk backwards row by row:
+ // - dp[i][sz] != dp[i-1][sz] → item i-1 was included, reduce capacity
+ // - dp[i][sz] == dp[i-1][sz] → item i-1 was skipped, move on
+ // We walk backwards (high to low index), so inserting at the front
+ // of a LinkedList produces ascending order without a separate sort.
+ LinkedList items = new LinkedList<>();
+ int sz = capacity;
+ for (int i = n; i > 0; i--) {
+ if (dp[i][sz] != dp[i - 1][sz]) {
+ items.addFirst(i - 1);
+ sz -= W[i - 1];
+ }
+ }
- // Return the maximum profit
- return DP[N][capacity];
+ return items;
}
public static void main(String[] args) {
-
- int capacity = 10;
- int[] V = {1, 4, 8, 5};
+ // Example 1: capacity=10, items: (w=3,v=1), (w=3,v=4), (w=5,v=8), (w=6,v=5)
int[] W = {3, 3, 5, 6};
- System.out.println(knapsack(capacity, W, V));
+ int[] V = {1, 4, 8, 5};
+ System.out.println("Max value: " + knapsack(10, W, V)); // 12
+ System.out.println("Items: " + knapsackItems(10, W, V)); // [1, 2]
- capacity = 7;
- V = new int[] {2, 2, 4, 5, 3};
+ // Example 2: capacity=7, items: (w=3,v=2), (w=1,v=2), (w=3,v=4), (w=4,v=5), (w=2,v=3)
W = new int[] {3, 1, 3, 4, 2};
- System.out.println(knapsack(capacity, W, V));
+ V = new int[] {2, 2, 4, 5, 3};
+ System.out.println("Max value: " + knapsack(7, W, V)); // 10
+ System.out.println("Items: " + knapsackItems(7, W, V)); // [1, 3, 4]
}
}
diff --git a/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java b/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java
index febab0566..8d7ceae8e 100644
--- a/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java
+++ b/src/main/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequence.java
@@ -1,5 +1,13 @@
/**
- * Implementation of finding the longest paldindrome subsequence Time complexity: O(n^2)
+ * Longest Palindrome Subsequence (LPS)
+ *
+ * Given a string S, find the length of the longest subsequence in S that is also a palindrome.
+ *
+ *
Important: A subsequence is different from a substring. Subsequences do not need to be
+ * contiguous. For example, in the string "BBBAB", the longest palindrome subsequence is "BBBB" with
+ * length 4, whereas the longest palindrome substring is "BBB" with length 3.
+ *
+ *
Time Complexity: O(n^2)
*
* @author William Fiset, william.alexandre.fiset@gmail.com
*/
@@ -7,33 +15,73 @@
public class LongestPalindromeSubsequence {
- public static void main(String[] args) {
- System.out.println(lps("bbbab")); // Outputs 4 since "bbbb" is valid soln
- System.out.println(lps("bccd")); // Outputs 2 since "cc" is valid soln
- }
-
- // Returns the length of the longest paldindrome subsequence
- public static int lps(String s) {
+ /**
+ * Recursive implementation with memoization to find the length of
+ * the longest palindrome subsequence.
+ *
+ * Time Complexity: O(n^2)
+ * Space Complexity: O(n^2)
+ */
+ public static int lpsRecursive(String s) {
if (s == null || s.length() == 0) return 0;
Integer[][] dp = new Integer[s.length()][s.length()];
- return lps(s, dp, 0, s.length() - 1);
+ return lpsRecursive(s, dp, 0, s.length() - 1);
}
- // Private recursive method with memoization to count
- // the longest paldindrome subsequence.
- private static int lps(String s, Integer[][] dp, int i, int j) {
-
- // Base cases
+ private static int lpsRecursive(String s, Integer[][] dp, int i, int j) {
if (j < i) return 0;
if (i == j) return 1;
if (dp[i][j] != null) return dp[i][j];
- char c1 = s.charAt(i), c2 = s.charAt(j);
+ if (s.charAt(i) == s.charAt(j)) {
+ // If characters at both ends match, they form part of the palindrome.
+ // We add 2 to the result and shrink the window from both sides (i+1, j-1).
+ return dp[i][j] = lpsRecursive(s, dp, i + 1, j - 1) + 2;
+ }
+ // If characters don't match, we take the maximum by either:
+ // 1. Skipping the left character (i+1)
+ // 2. Skipping the right character (j-1)
+ return dp[i][j] = Math.max(lpsRecursive(s, dp, i + 1, j), lpsRecursive(s, dp, i, j - 1));
+ }
+
+ /**
+ * Iterative implementation (bottom-up) to find the length of
+ * the longest palindrome subsequence.
+ *
+ * Time Complexity: O(n^2)
+ * Space Complexity: O(n^2)
+ */
+ public static int lpsIterative(String s) {
+ if (s == null || s.isEmpty()) return 0;
+ int n = s.length();
+ int[][] dp = new int[n][n];
+
+ // Every single character is a palindrome of length 1
+ for (int i = 0; i < n; i++) dp[i][i] = 1;
- // Both end characters match
- if (c1 == c2) return dp[i][j] = lps(s, dp, i + 1, j - 1) + 2;
+ for (int len = 2; len <= n; len++) {
+ for (int i = 0; i <= n - len; i++) {
+ int j = i + len - 1;
+ if (s.charAt(i) == s.charAt(j)) {
+ // Characters match: use the result from the inner substring (i+1, j-1) and add 2.
+ dp[i][j] = dp[i + 1][j - 1] + 2;
+ } else {
+ // Characters don't match: take the best result from either skipping the
+ // left character (i+1) or the right character (j-1).
+ dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
+ }
+ }
+ }
+ return dp[0][n - 1];
+ }
+
+ public static void main(String[] args) {
+ String s1 = "bbbab";
+ System.out.println(lpsRecursive(s1)); // 4
+ System.out.println(lpsIterative(s1)); // 4
- // Consider both possible substrings and take the maximum
- return dp[i][j] = Math.max(lps(s, dp, i + 1, j), lps(s, dp, i, j - 1));
+ String s2 = "bccd";
+ System.out.println(lpsRecursive(s2)); // 2
+ System.out.println(lpsIterative(s2)); // 2
}
}
diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/TspDynamicProgrammingIterative.java b/src/main/java/com/williamfiset/algorithms/graphtheory/TspDynamicProgrammingIterative.java
index a75509b8f..9bd9557d6 100644
--- a/src/main/java/com/williamfiset/algorithms/graphtheory/TspDynamicProgrammingIterative.java
+++ b/src/main/java/com/williamfiset/algorithms/graphtheory/TspDynamicProgrammingIterative.java
@@ -1,17 +1,35 @@
-/**
- * An implementation of the traveling salesman problem in Java using dynamic programming to improve
- * the time complexity from O(n!) to O(n^2 * 2^n).
- *
- *
Time Complexity: O(n^2 * 2^n) Space Complexity: O(n * 2^n)
- *
- * @author William Fiset, william.alexandre.fiset@gmail.com
- */
package com.williamfiset.algorithms.graphtheory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+/**
+ * Traveling Salesman Problem — Iterative DP with Bitmask
+ *
+ * Given a complete weighted graph of n nodes, find the minimum-cost
+ * Hamiltonian cycle (a tour that visits every node exactly once and
+ * returns to the starting node).
+ *
+ * This iterative (bottom-up) approach builds solutions for increasing
+ * subset sizes. For each subset S of visited nodes and each endpoint
+ * node i in S, we compute the minimum cost to reach i having visited
+ * exactly the nodes in S. The recurrence is:
+ *
+ * memo[next][S | (1 << next)] = min over end in S of
+ * memo[end][S] + distance[end][next]
+ *
+ * After filling the table, we close the tour by connecting back to
+ * the start node and backtrack through the table to reconstruct
+ * the optimal path.
+ *
+ * See also: {@link TspDynamicProgrammingRecursive} for the top-down variant.
+ *
+ * Time: O(n^2 * 2^n)
+ * Space: O(n * 2^n)
+ *
+ * @author William Fiset, william.alexandre.fiset@gmail.com
+ */
public class TspDynamicProgrammingIterative {
private final int N, start;
@@ -39,67 +57,82 @@ public TspDynamicProgrammingIterative(int start, double[][] distance) {
this.distance = distance;
}
- // Returns the optimal tour for the traveling salesman problem.
+ /**
+ * Returns the optimal tour for the traveling salesman problem.
+ *
+ * @return ordered list of node indices forming the optimal tour
+ * (starts and ends with the start node)
+ */
public List getTour() {
if (!ranSolver) solve();
return tour;
}
- // Returns the minimal tour cost.
+ /**
+ * Returns the minimal tour cost.
+ *
+ * @return the total cost of the optimal Hamiltonian cycle
+ */
public double getTourCost() {
if (!ranSolver) solve();
return minTourCost;
}
- // Solves the traveling salesman problem and caches solution.
+ /**
+ * Solves the TSP and caches the result. Subsequent calls are no-ops.
+ *
+ * Phase 1: Fill the DP table bottom-up for subsets of size 2..N.
+ * Phase 2: Close the tour by connecting the last node back to start.
+ * Phase 3: Backtrack through the table to reconstruct the tour.
+ */
public void solve() {
-
if (ranSolver) return;
final int END_STATE = (1 << N) - 1;
Double[][] memo = new Double[N][1 << N];
- // Add all outgoing edges from the starting node to memo table.
+ // Phase 1a: Seed the memo table with direct edges from the start node.
+ // memo[end][{start, end}] = distance from start to end
for (int end = 0; end < N; end++) {
if (end == start) continue;
memo[end][(1 << start) | (1 << end)] = distance[start][end];
}
+ // Phase 1b: Build solutions for subsets of increasing size (3..N).
+ // For each subset, try extending the path to each node in the subset.
for (int r = 3; r <= N; r++) {
for (int subset : combinations(r, N)) {
if (notIn(start, subset)) continue;
for (int next = 0; next < N; next++) {
if (next == start || notIn(next, subset)) continue;
+ // Consider all possible previous endpoints
int subsetWithoutNext = subset ^ (1 << next);
double minDist = Double.POSITIVE_INFINITY;
for (int end = 0; end < N; end++) {
if (end == start || end == next || notIn(end, subset)) continue;
double newDistance = memo[end][subsetWithoutNext] + distance[end][next];
- if (newDistance < minDist) {
+ if (newDistance < minDist)
minDist = newDistance;
- }
}
memo[next][subset] = minDist;
}
}
}
- // Connect tour back to starting node and minimize cost.
+ // Phase 2: Close the tour — find the cheapest way to return to start.
for (int i = 0; i < N; i++) {
if (i == start) continue;
double tourCost = memo[i][END_STATE] + distance[i][start];
- if (tourCost < minTourCost) {
+ if (tourCost < minTourCost)
minTourCost = tourCost;
- }
}
+ // Phase 3: Reconstruct the tour by backtracking through the memo table.
int lastIndex = start;
int state = END_STATE;
tour.add(start);
- // Reconstruct TSP path from memo table.
for (int i = 1; i < N; i++) {
-
int bestIndex = -1;
double bestDist = Double.POSITIVE_INFINITY;
for (int j = 0; j < N; j++) {
@@ -122,48 +155,56 @@ public void solve() {
ranSolver = true;
}
+ /** Returns true if the given element's bit is not set in the subset bitmask. */
private static boolean notIn(int elem, int subset) {
return ((1 << elem) & subset) == 0;
}
- // This method generates all bit sets of size n where r bits
- // are set to one. The result is returned as a list of integer masks.
+ /**
+ * Generates all bitmasks of n bits where exactly r bits are set.
+ * Used to enumerate subsets of a given size.
+ *
+ * @param r - number of bits to set
+ * @param n - total number of bits
+ * @return list of integer bitmasks
+ */
public static List combinations(int r, int n) {
List subsets = new ArrayList<>();
combinations(0, 0, r, n, subsets);
return subsets;
}
- // To find all the combinations of size r we need to recurse until we have
- // selected r elements (aka r = 0), otherwise if r != 0 then we still need to select
- // an element which is found after the position of our last selected element
+ /**
+ * Recursively builds combinations by deciding whether to include
+ * each bit position. Backtracks when not enough positions remain.
+ */
private static void combinations(int set, int at, int r, int n, List subsets) {
-
- // Return early if there are more elements left to select than what is available.
+ // Not enough positions remaining to pick r more bits
int elementsLeftToPick = n - at;
if (elementsLeftToPick < r) return;
- // We selected 'r' elements so we found a valid subset!
if (r == 0) {
subsets.add(set);
} else {
for (int i = at; i < n; i++) {
// Try including this element
set ^= (1 << i);
-
combinations(set, i + 1, r - 1, n, subsets);
-
// Backtrack and try the instance where we did not include this element
set ^= (1 << i);
}
}
}
+ // ==================== Main ====================
+
public static void main(String[] args) {
- // Create adjacency matrix
+ // Create a 6-node directed graph with a known optimal tour
int n = 6;
double[][] distanceMatrix = new double[n][n];
- for (double[] row : distanceMatrix) java.util.Arrays.fill(row, 10000);
+ for (double[] row : distanceMatrix)
+ java.util.Arrays.fill(row, 10000);
+
distanceMatrix[5][0] = 10;
distanceMatrix[1][5] = 12;
distanceMatrix[4][1] = 2;
@@ -175,10 +216,10 @@ public static void main(String[] args) {
TspDynamicProgrammingIterative solver =
new TspDynamicProgrammingIterative(startNode, distanceMatrix);
- // Prints: [0, 3, 2, 4, 1, 5, 0]
+ // Tour: [0, 3, 2, 4, 1, 5, 0]
System.out.println("Tour: " + solver.getTour());
- // Print: 42.0
+ // Tour cost: 42.0
System.out.println("Tour cost: " + solver.getTourCost());
}
}
diff --git a/src/test/java/com/williamfiset/algorithms/dp/BUILD b/src/test/java/com/williamfiset/algorithms/dp/BUILD
index 8c6048b94..9890160fd 100644
--- a/src/test/java/com/williamfiset/algorithms/dp/BUILD
+++ b/src/test/java/com/williamfiset/algorithms/dp/BUILD
@@ -83,5 +83,27 @@ java_test(
deps = TEST_DEPS,
)
+# bazel test //src/test/java/com/williamfiset/algorithms/dp:KnapsackTest
+java_test(
+ name = "KnapsackTest",
+ srcs = ["KnapsackTest.java"],
+ main_class = "org.junit.platform.console.ConsoleLauncher",
+ use_testrunner = False,
+ args = ["--select-class=com.williamfiset.algorithms.dp.KnapsackTest"],
+ runtime_deps = JUNIT5_RUNTIME_DEPS,
+ deps = TEST_DEPS,
+)
+
+# bazel test //src/test/java/com/williamfiset/algorithms/dp:LongestPalindromeSubsequenceTest
+java_test(
+ name = "LongestPalindromeSubsequenceTest",
+ srcs = ["LongestPalindromeSubsequenceTest.java"],
+ main_class = "org.junit.platform.console.ConsoleLauncher",
+ use_testrunner = False,
+ args = ["--select-class=com.williamfiset.algorithms.dp.LongestPalindromeSubsequenceTest"],
+ runtime_deps = JUNIT5_RUNTIME_DEPS,
+ deps = TEST_DEPS,
+)
+
# Run all tests
# bazel test //src/test/java/com/williamfiset/algorithms/dp:all
diff --git a/src/test/java/com/williamfiset/algorithms/dp/KnapsackTest.java b/src/test/java/com/williamfiset/algorithms/dp/KnapsackTest.java
new file mode 100644
index 000000000..6e3819326
--- /dev/null
+++ b/src/test/java/com/williamfiset/algorithms/dp/KnapsackTest.java
@@ -0,0 +1,176 @@
+package com.williamfiset.algorithms.dp;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+public class KnapsackTest {
+
+ // ==================== 0/1 Knapsack ====================
+
+ @Test
+ public void testKnapsack01_nullWeights() {
+ assertThrows(
+ IllegalArgumentException.class, () -> Knapsack_01.knapsack(10, null, new int[] {1}));
+ }
+
+ @Test
+ public void testKnapsack01_nullValues() {
+ assertThrows(
+ IllegalArgumentException.class, () -> Knapsack_01.knapsack(10, new int[] {1}, null));
+ }
+
+ @Test
+ public void testKnapsack01_mismatchedArrays() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> Knapsack_01.knapsack(10, new int[] {1, 2}, new int[] {1}));
+ }
+
+ @Test
+ public void testKnapsack01_zeroCapacity() {
+ assertThat(Knapsack_01.knapsack(0, new int[] {1, 2}, new int[] {10, 20})).isEqualTo(0);
+ }
+
+ @Test
+ public void testKnapsack01_noItems() {
+ assertThat(Knapsack_01.knapsack(10, new int[] {}, new int[] {})).isEqualTo(0);
+ }
+
+ @Test
+ public void testKnapsack01_singleItemFits() {
+ assertThat(Knapsack_01.knapsack(5, new int[] {3}, new int[] {10})).isEqualTo(10);
+ }
+
+ @Test
+ public void testKnapsack01_singleItemTooHeavy() {
+ assertThat(Knapsack_01.knapsack(2, new int[] {3}, new int[] {10})).isEqualTo(0);
+ }
+
+ @Test
+ public void testKnapsack01_example1() {
+ // capacity=10, items: (w=3,v=1), (w=3,v=4), (w=5,v=8), (w=6,v=5)
+ // Optimal: items 1 and 2 (w=3+5=8, v=4+8=12)
+ int[] W = {3, 3, 5, 6};
+ int[] V = {1, 4, 8, 5};
+ assertThat(Knapsack_01.knapsack(10, W, V)).isEqualTo(12);
+ }
+
+ @Test
+ public void testKnapsack01_example2() {
+ // capacity=7, items: (w=3,v=2), (w=1,v=2), (w=3,v=4), (w=4,v=5), (w=2,v=3)
+ // Optimal: items 1,3 (w=1+4=5, v=2+5=7) or items 1,2,4 (w=1+3+2=6, v=2+4+3=9)
+ int[] W = {3, 1, 3, 4, 2};
+ int[] V = {2, 2, 4, 5, 3};
+ assertThat(Knapsack_01.knapsack(7, W, V)).isEqualTo(10);
+ }
+
+ /** Verify that selected items match the reported optimal value. */
+ @Test
+ public void testKnapsack01_itemsConsistentWithValue() {
+ int[] W = {3, 3, 5, 6};
+ int[] V = {1, 4, 8, 5};
+ int capacity = 10;
+
+ int maxValue = Knapsack_01.knapsack(capacity, W, V);
+ List items = Knapsack_01.knapsackItems(capacity, W, V);
+
+ int totalWeight = 0, totalValue = 0;
+ for (int idx : items) {
+ totalWeight += W[idx];
+ totalValue += V[idx];
+ }
+
+ assertThat(totalValue).isEqualTo(maxValue);
+ assertThat(totalWeight).isAtMost(capacity);
+ }
+
+ /** Each item should appear at most once in the solution. */
+ @Test
+ public void testKnapsack01_noDuplicateItems() {
+ int[] W = {2, 3, 4, 5};
+ int[] V = {3, 4, 5, 6};
+ List items = Knapsack_01.knapsackItems(10, W, V);
+ assertThat(items).containsNoDuplicates();
+ }
+
+ // ==================== Unbounded Knapsack ====================
+
+ @Test
+ public void testUnbounded_nullWeights() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> KnapsackUnbounded.unboundedKnapsack(10, null, new int[] {1}));
+ }
+
+ @Test
+ public void testUnbounded_zeroCapacity() {
+ assertThat(KnapsackUnbounded.unboundedKnapsack(0, new int[] {1}, new int[] {10})).isEqualTo(0);
+ }
+
+ @Test
+ public void testUnbounded_noItems() {
+ assertThat(KnapsackUnbounded.unboundedKnapsack(10, new int[] {}, new int[] {})).isEqualTo(0);
+ }
+
+ @Test
+ public void testUnbounded_singleItemReused() {
+ // Item (w=3, v=5) can be used 3 times in capacity 10 → value 15
+ assertThat(KnapsackUnbounded.unboundedKnapsack(10, new int[] {3}, new int[] {5})).isEqualTo(15);
+ }
+
+ @Test
+ public void testUnbounded_example1() {
+ // Items: (w=3,v=5), (w=6,v=20), (w=2,v=3)
+ // Capacity 10: best is one item of w=6,v=20 + one of w=3,v=5 = 25? No...
+ // Actually w=6 + w=3 = 9, leaving 1 unused. v=25
+ // Or two w=6 = 12 > 10, doesn't fit.
+ // w=6 + w=2 + w=2 = 10, v=20+3+3=26? No, w=6+2+2=10, v=26
+ // Actually let's just check: best for cap=10
+ int[] W = {3, 6, 2};
+ int[] V = {5, 20, 3};
+ int result = KnapsackUnbounded.unboundedKnapsack(10, W, V);
+ assertThat(result).isEqualTo(KnapsackUnbounded.unboundedKnapsackSpaceEfficient(10, W, V));
+ }
+
+ /** Both implementations should always agree. */
+ @Test
+ public void testUnbounded_bothImplementationsAgree() {
+ int[][] cases = {
+ {10, 3, 6, 2, 5, 20, 3}, // cap=10, W={3,6,2}, V={5,20,3}
+ {12, 3, 6, 2, 5, 20, 3}, // cap=12
+ {7, 1, 3, 4, 1, 4, 5}, // cap=7, W={1,3,4}, V={1,4,5}
+ {15, 5, 10, 3, 10, 30, 5}, // cap=15
+ };
+ for (int[] c : cases) {
+ int cap = c[0];
+ int n = (c.length - 1) / 2;
+ int[] W = new int[n], V = new int[n];
+ for (int i = 0; i < n; i++) {
+ W[i] = c[1 + i];
+ V[i] = c[1 + n + i];
+ }
+ assertThat(KnapsackUnbounded.unboundedKnapsack(cap, W, V))
+ .isEqualTo(KnapsackUnbounded.unboundedKnapsackSpaceEfficient(cap, W, V));
+ }
+ }
+
+ /** Unbounded should be >= 0/1 since it has more freedom (reuse allowed). */
+ @Test
+ public void testUnbounded_atLeastAsMuchAs01() {
+ int[] W = {2, 3, 5};
+ int[] V = {3, 4, 8};
+ int capacity = 10;
+ int bounded = Knapsack_01.knapsack(capacity, W, V);
+ int unbounded = KnapsackUnbounded.unboundedKnapsack(capacity, W, V);
+ assertThat(unbounded).isAtLeast(bounded);
+ }
+
+ @Test
+ public void testUnbounded_exactFit() {
+ // Capacity exactly fits 2 copies of the item
+ assertThat(KnapsackUnbounded.unboundedKnapsack(6, new int[] {3}, new int[] {7})).isEqualTo(14);
+ }
+}
diff --git a/src/test/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequenceTest.java b/src/test/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequenceTest.java
new file mode 100644
index 000000000..fd888777f
--- /dev/null
+++ b/src/test/java/com/williamfiset/algorithms/dp/LongestPalindromeSubsequenceTest.java
@@ -0,0 +1,38 @@
+package com.williamfiset.algorithms.dp;
+
+import static com.google.common.truth.Truth.assertThat;
+import org.junit.jupiter.api.Test;
+
+public class LongestPalindromeSubsequenceTest {
+
+ @Test
+ public void testLps() {
+ String s1 = "bbbab";
+ assertThat(LongestPalindromeSubsequence.lpsRecursive(s1)).isEqualTo(4);
+ assertThat(LongestPalindromeSubsequence.lpsIterative(s1)).isEqualTo(4);
+
+ String s2 = "bccd";
+ assertThat(LongestPalindromeSubsequence.lpsRecursive(s2)).isEqualTo(2);
+ assertThat(LongestPalindromeSubsequence.lpsIterative(s2)).isEqualTo(2);
+
+ String s3 = "abcde";
+ assertThat(LongestPalindromeSubsequence.lpsRecursive(s3)).isEqualTo(1);
+ assertThat(LongestPalindromeSubsequence.lpsIterative(s3)).isEqualTo(1);
+
+ String s4 = "aaaaa";
+ assertThat(LongestPalindromeSubsequence.lpsRecursive(s4)).isEqualTo(5);
+ assertThat(LongestPalindromeSubsequence.lpsIterative(s4)).isEqualTo(5);
+ }
+
+ @Test
+ public void testEmptyStrings() {
+ assertThat(LongestPalindromeSubsequence.lpsRecursive("")).isEqualTo(0);
+ assertThat(LongestPalindromeSubsequence.lpsIterative("")).isEqualTo(0);
+ }
+
+ @Test
+ public void testNullInputs() {
+ assertThat(LongestPalindromeSubsequence.lpsRecursive(null)).isEqualTo(0);
+ assertThat(LongestPalindromeSubsequence.lpsIterative(null)).isEqualTo(0);
+ }
+}