diff --git a/src/CoinChange.java b/src/CoinChange.java new file mode 100644 index 00000000..a3661263 --- /dev/null +++ b/src/CoinChange.java @@ -0,0 +1,133 @@ +/* +COIN CHANGE I (LeetCode 322) + +Problem: +Given coin denominations and a target amount, return the minimum number of coins +required to make that amount. If it is not possible, return -1. + +This class demonstrates FOUR different approaches to solve the problem, +starting from brute force recursion and gradually optimizing to Dynamic Programming. + +Approaches implemented: +1. Pure Recursive (Brute Force) -> Exponential Time +2. Top-Down DP (Recursion + Memoization) -> O(coins * amount) +3. Bottom-Up DP (2D Table) -> O(coins * amount) +4. Bottom-Up DP (1D Optimized) -> O(coins * amount), Space O(amount) +*/ + +public class CoinChange { + + // Used for memoization in Top-Down DP + int[][] memo; + + /*1. PURE RECURSIVE SOLUTION (BRUTE FORCE) + Try all possibilities: + - Either skip the current coin + - Or take the current coin and reduce the amount + Time Complexity: Exponential (very slow) + Space Complexity: Recursion stack + */ + public int recursiveCoinChange(int[] coins, int amount) { + int result = recursiveHelper(coins, amount, 0); + return (result >= Integer.MAX_VALUE - 10) ? -1 : result; + } + + private int recursiveHelper(int[] coins, int amount, int idx) { + // Base cases + if (amount == 0) return 0; + if (idx == coins.length || amount < 0) return Integer.MAX_VALUE - 10; + + // Choice 1: Do not take current coin + int notChoose = recursiveHelper(coins, amount, idx + 1); + + // Choice 2: Take current coin (unbounded) + int choose = recursiveHelper(coins, amount - coins[idx], idx); + + return Math.min(notChoose, choose); + } + + /* 2. TOP-DOWN DP (MEMOIZATION) + Same recursive logic, but we store results for each state + (idx, amount) to avoid recomputation. + Time Complexity: O(coins * amount) + Space Complexity: O(coins * amount) + */ + public int coinChangeTopDown(int[] coins, int amount) { + memo = new int[coins.length][amount + 1]; + int result = topDownHelper(coins, amount, 0); + return (result >= Integer.MAX_VALUE - 10) ? -1 : result; + } + + private int topDownHelper(int[] coins, int amount, int idx) { + if (amount == 0) return 0; + if (idx == coins.length || amount < 0) return Integer.MAX_VALUE - 10; + + if (memo[idx][amount] != 0) { + return memo[idx][amount]; + } + + int notChoose = topDownHelper(coins, amount, idx + 1); + int choose = 1 + topDownHelper(coins, amount - coins[idx], idx); + + memo[idx][amount] = Math.min(notChoose, choose); + return memo[idx][amount]; + } + + /* 3. BOTTOM-UP DP (2D TABLE) + dp[i][j] = minimum coins needed to make amount j + using first i coin types + Time Complexity: O(coins * amount) + Space Complexity: O(coins * amount) + */ + public int coinChange2D(int[] coins, int amount) { + int m = coins.length; + int INF = 99999; + + int[][] dp = new int[m + 1][amount + 1]; + + // Base case: no coins, positive amount impossible + for (int j = 1; j <= amount; j++) { + dp[0][j] = INF; + } + + // Fill DP table + for (int i = 1; i <= m; i++) { + for (int j = 0; j <= amount; j++) { + if (j < coins[i - 1]) { + dp[i][j] = dp[i - 1][j]; // cannot take coin + } else { + dp[i][j] = Math.min( + dp[i - 1][j], // skip coin + 1 + dp[i][j - coins[i - 1]] // take coin + ); + } + } + } + + return dp[m][amount] == INF ? -1 : dp[m][amount]; + } + + /* 4. BOTTOM-UP DP (1D OPTIMIZED) + dp[j] = minimum coins needed to make amount j + Optimizes space by reusing previous results. + Time Complexity: O(coins * amount) + Space Complexity: O(amount) + */ + public int coinChange1D(int[] coins, int amount) { + int INF = 99999; + int[] dp = new int[amount + 1]; + + dp[0] = 0; + for (int j = 1; j <= amount; j++) { + dp[j] = INF; + } + + for (int coin : coins) { + for (int j = coin; j <= amount; j++) { + dp[j] = Math.min(dp[j], 1 + dp[j - coin]); + } + } + + return dp[amount] == INF ? -1 : dp[amount]; + } +} diff --git a/src/HouseRobber.java b/src/HouseRobber.java new file mode 100644 index 00000000..a94310c2 --- /dev/null +++ b/src/HouseRobber.java @@ -0,0 +1,112 @@ +/* +HOUSE ROBBER (LeetCode 198) + +Problem: +Given an array where each element represents money in a house, +find the maximum amount you can rob without robbing two adjacent houses. + +This class demonstrates MULTIPLE approaches, starting from brute force +and gradually optimizing to Dynamic Programming. + +Approaches implemented: +1. Pure Recursive (Exhaustive Decision Tree) +2. Top-Down DP (Recursion + Memoization) +3. Bottom-Up DP (1D Array) +4. Bottom-Up DP (O(1) Space Optimized) +*/ + +public class HouseRobber { + + // Used for memoization in Top-Down DP + private int[] memo; + + /* 1. PURE RECURSIVE SOLUTION (BRUTE FORCE) + At each house: + - Either rob it and skip next + - Or skip it and move to next + + Time Complexity: O(2^n) + Space Complexity: O(n) recursion stack + */ + public int robRecursive(int[] nums) { + return robHelper(nums, 0); + } + + private int robHelper(int[] nums, int index) { + // Base case: out of bounds + if (index >= nums.length) return 0; + + // Choice 1: Do not rob current house + int skip = robHelper(nums, index + 1); + + // Choice 2: Rob current house + int rob = nums[index] + robHelper(nums, index + 2); + + return Math.max(skip, rob); + } + + /*2. TOP-DOWN DP (MEMOIZATION) + Same recursion, but store results to avoid recomputation. + + Time Complexity: O(n) + Space Complexity: O(n) + */ + public int robTopDown(int[] nums) { + memo = new int[nums.length]; + return robHelperMemo(nums, 0); + } + + private int robHelperMemo(int[] nums, int index) { + if (index >= nums.length) return 0; + + if (memo[index] != 0) return memo[index]; + + int skip = robHelperMemo(nums, index + 1); + int rob = nums[index] + robHelperMemo(nums, index + 2); + + memo[index] = Math.max(skip, rob); + return memo[index]; + } + + /* 3. BOTTOM-UP DP (1D ARRAY) + dp[i] = maximum money that can be robbed up to house i + Transition: + dp[i] = max(dp[i-1], nums[i] + dp[i-2]) + + Time Complexity: O(n) + Space Complexity: O(n) + */ + public int robBottomUp(int[] nums) { + int n = nums.length; + if (n == 0) return 0; + if (n == 1) return nums[0]; + + int[] dp = new int[n]; + dp[0] = nums[0]; + dp[1] = Math.max(nums[0], nums[1]); + + for (int i = 2; i < n; i++) { + dp[i] = Math.max(dp[i - 1], nums[i] + dp[i - 2]); + } + + return dp[n - 1]; + } + + /* 4. BOTTOM-UP DP (SPACE OPTIMIZED) + Uses only two variables instead of dp array. + Time Complexity: O(n) + Space Complexity: O(1) + */ + public int robOptimized(int[] nums) { + int prev2 = 0; // dp[i-2] + int prev1 = 0; // dp[i-1] + + for (int money : nums) { + int curr = Math.max(prev1, prev2 + money); + prev2 = prev1; + prev1 = curr; + } + + return prev1; + } +} diff --git a/src/TestDP1.java b/src/TestDP1.java new file mode 100644 index 00000000..8258c0ef --- /dev/null +++ b/src/TestDP1.java @@ -0,0 +1,58 @@ +import java.util.Arrays; + +public class TestDP1 { + public static void main(String[] args) { + + CoinChange cc = new CoinChange(); + + int[][] coinsList = { + {1, 2, 5}, + {2}, + {1}, + {1, 3, 4} + }; + int[] amounts = {11, 3, 0, 6}; + int[] expectedCC = {3, -1, 0, 2}; + + for (int i = 0; i < coinsList.length; i++) { + int r1 = cc.recursiveCoinChange(coinsList[i], amounts[i]); + int r2 = cc.coinChangeTopDown(coinsList[i], amounts[i]); + int r3 = cc.coinChange2D(coinsList[i], amounts[i]); + int r4 = cc.coinChange1D(coinsList[i], amounts[i]); + + System.out.println( + r1 == expectedCC[i] && + r2 == expectedCC[i] && + r3 == expectedCC[i] && + r4 == expectedCC[i] + ); + } + System.out.println("Testing House Robber"); + + HouseRobber hr = new HouseRobber(); + + int[][] robberTests = { + {2, 7, 9, 3, 1}, + {1, 2, 3, 1}, + {2, 1, 1, 2}, + {1}, + {} + }; + + int[] expectedHR = {12, 4, 4, 1, 0}; + + for (int i = 0; i < robberTests.length; i++) { + int a = hr.robRecursive(robberTests[i]); + int b = hr.robTopDown(robberTests[i]); + int c = hr.robBottomUp(robberTests[i]); + int d = hr.robOptimized(robberTests[i]); + + System.out.println( + a == expectedHR[i] && + b == expectedHR[i] && + c == expectedHR[i] && + d == expectedHR[i] + ); + } + } +}