diff --git a/src/main/java/com/thealgorithms/recursion/CombinationSum.java b/src/main/java/com/thealgorithms/recursion/CombinationSum.java new file mode 100644 index 000000000000..caeb3cf70d25 --- /dev/null +++ b/src/main/java/com/thealgorithms/recursion/CombinationSum.java @@ -0,0 +1,60 @@ +package com.thealgorithms.recursion; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class implements the Combination Sum algorithm using recursion and backtracking. + * Given an array of distinct integers candidates and a target integer target, + * return a list of all unique combinations of candidates where the chosen numbers sum to target. + * The same number may be chosen from candidates an unlimited number of times. + * + * @see Subset Sum Problem (Wikipedia) + * @see Combination Sum (LeetCode) + * @author Tejas Rahane + */ +public final class CombinationSum { + private CombinationSum() { + } + + /** + * Finds all unique combinations that sum to target. + * + * @param candidates Array of distinct integers + * @param target Target sum + * @return List of all unique combinations that sum to target + */ + public static List> combinationSum(int[] candidates, int target) { + List> result = new ArrayList<>(); + if (candidates == null || candidates.length == 0) { + return result; + } + backtrack(candidates, target, 0, new ArrayList<>(), result); + return result; + } + + /** + * Backtracking helper method to find all combinations. + * + * @param candidates Array of distinct integers + * @param target Remaining target sum + * @param start Starting index for candidates + * @param current Current combination being built + * @param result List to store all valid combinations + */ + private static void backtrack(int[] candidates, int target, int start, List current, List> result) { + if (target == 0) { + result.add(new ArrayList<>(current)); + return; + } + if (target < 0) { + return; + } + + for (int i = start; i < candidates.length; i++) { + current.add(candidates[i]); + backtrack(candidates, target - candidates[i], i, current, result); + current.remove(current.size() - 1); + } + } +} diff --git a/src/test/java/com/thealgorithms/recursion/CombinationSumTest.java b/src/test/java/com/thealgorithms/recursion/CombinationSumTest.java new file mode 100644 index 000000000000..337c6431a6bf --- /dev/null +++ b/src/test/java/com/thealgorithms/recursion/CombinationSumTest.java @@ -0,0 +1,127 @@ +package com.thealgorithms.recursion; +import static org.junit.jupiter.api.Assertions.*; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; +/** + * Comprehensive test class for CombinationSum algorithm + * Tests various scenarios including edge cases + */ +class CombinationSumTest { + @Test + void testBasicCase() { + int[] candidates = {2, 3, 6, 7}; + int target = 7; + List> result = CombinationSum.combinationSum(candidates, target); + + assertTrue(result.contains(Arrays.asList(2, 2, 3))); + assertTrue(result.contains(Arrays.asList(7))); + assertEquals(2, result.size()); + } + @Test + void testMultipleCombinations() { + int[] candidates = {2, 3, 5}; + int target = 8; + List> result = CombinationSum.combinationSum(candidates, target); + + assertTrue(result.contains(Arrays.asList(2, 2, 2, 2))); + assertTrue(result.contains(Arrays.asList(2, 3, 3))); + assertTrue(result.contains(Arrays.asList(3, 5))); + assertEquals(3, result.size()); + } + @Test + void testNoSolution() { + int[] candidates = {2}; + int target = 1; + List> result = CombinationSum.combinationSum(candidates, target); + + assertTrue(result.isEmpty()); + } + @Test + void testSingleElement() { + + int[] candidates = {1}; + int target = 1; + List> result = CombinationSum.combinationSum(candidates, target); + + assertEquals(1, result.size()); + assertTrue(result.contains(Arrays.asList(1))); + } + @Test + void testSingleElementRepeated() { + + int[] candidates = {2}; + int target = 8; + List> result = CombinationSum.combinationSum(candidates, target); + + assertEquals(1, result.size()); + assertTrue(result.contains(Arrays.asList(2, 2, 2, 2))); + } + @Test + void testLargerNumbers() { + + int[] candidates = {10, 1, 2, 7, 6, 1, 5}; + int target = 8; + List> result = CombinationSum.combinationSum(candidates, target); + + assertFalse(result.isEmpty()); + // Verify all combinations sum to target + for (List combination : result) { + int sum = combination.stream().mapToInt(Integer::intValue).sum(); + assertEquals(target, sum); + } + } + @Test + void testTargetZero() { + + int[] candidates = {1, 2, 3}; + int target = 0; + List> result = CombinationSum.combinationSum(candidates, target); + + // Should return empty list in the combination + assertEquals(1, result.size()); + assertTrue(result.get(0).isEmpty()); + } + @Test + void testEmptyCandidates() { + + int[] candidates = {}; + int target = 5; + List> result = CombinationSum.combinationSum(candidates, target); + + assertTrue(result.isEmpty()); + } + @Test + void testLargeTarget() { + int[] candidates = {3, 5, 8}; + int target = 11; + List> result = CombinationSum.combinationSum(candidates, target); + + assertTrue(result.contains(Arrays.asList(3, 3, 5))); + assertTrue(result.contains(Arrays.asList(3, 8))); + + // Verify all combinations sum to target + for (List combination : result) { + int sum = combination.stream().mapToInt(Integer::intValue).sum(); + assertEquals(target, sum); + } + } + @Test + void testAllCombinationsValid() { + int[] candidates = {2, 3, 6, 7}; + int target = 7; + List> result = CombinationSum.combinationSum(candidates, target); + + // Verify each combination sums to target + for (List combination : result) { + int sum = 0; + for (int num : combination) { + sum += num; + } + assertEquals(target, sum, "Each combination should sum to target"); + } + + // Verify no duplicates in result + assertEquals(result.size(), result.stream().distinct().count()); + } +}