101 Logo
onenoughtone

Solution Approach

Solution Overview

There are 4 main approaches to solve this problem:

  1. Recursive Approach (Brute Force) - Complex approach
  2. Memoization (Top-Down DP) - Complex approach
  3. Tabulation (Bottom-Up DP) - Complex approach
  4. Space-Optimized DP - Complex approach

Approach 1: Recursive Approach (Brute Force)

Let's start by understanding the problem from first principles. We need to count the number of distinct subsequences of string S that equal string T.

Thinking Process: For each character in S, we have two choices:

  • Include it in our subsequence (if it matches the current character in T)
  • Skip it (always an option)

We can use recursion to explore all possible ways to form string T from string S. At each step, we consider the current characters of both strings:

  • If they match, we have two options: either use this character or skip it
  • If they don't match, we can only skip the current character of S

Intuition: This recursive approach naturally models the decision-making process of including or excluding each character of S to form T.

Algorithm:

  1. Define a recursive function count(i, j) that returns the number of ways to form T[j...] from S[i...].
  2. Base cases:
  3. a. If j reaches the end of T, we've found a valid subsequence, so return 1.
  4. b. If i reaches the end of S but j hasn't reached the end of T, we can't form T, so return 0.
  5. For each recursive call:
  6. a. If S[i] equals T[j], we have two choices:
  7. - Include S[i]: count(i+1, j+1)
  8. - Skip S[i]: count(i+1, j)
  9. b. If S[i] doesn't equal T[j], we can only skip S[i]: count(i+1, j)
  10. Return the sum of all valid choices.
  11. The final answer is count(0, 0).

Time Complexity:

O(2^n)

In the worst case, every character in S matches the current character in T, leading to two recursive calls for each character. This results in an exponential time complexity.

Space Complexity:

O(m+n)

The space complexity is O(m+n) due to the recursion stack, where m and n are the lengths of strings S and T respectively.

Approach 2: Memoization (Top-Down DP)

The recursive approach works but is inefficient due to overlapping subproblems. We can optimize it using memoization.

Thinking Process: We notice that we're calculating the same subproblems multiple times in the recursive approach. For example, count(i, j) might be calculated multiple times for the same values of i and j.

To avoid this redundancy, we can use memoization to store the results of subproblems we've already solved. This way, when we encounter the same subproblem again, we can simply look up the result instead of recalculating it.

Intuition: Memoization transforms our exponential solution into a polynomial one by eliminating redundant calculations.

Algorithm:

  1. Create a memo table to store results of subproblems.
  2. Define a recursive function count(i, j) that returns the number of ways to form T[j...] from S[i...].
  3. Before computing, check if the result for (i, j) is already in the memo. If yes, return it.
  4. Base cases:
  5. a. If j reaches the end of T, we've found a valid subsequence, so return 1.
  6. b. If i reaches the end of S but j hasn't reached the end of T, we can't form T, so return 0.
  7. For each recursive call:
  8. a. If S[i] equals T[j], we have two choices:
  9. - Include S[i]: count(i+1, j+1)
  10. - Skip S[i]: count(i+1, j)
  11. b. If S[i] doesn't equal T[j], we can only skip S[i]: count(i+1, j)
  12. Store the result in the memo before returning.
  13. The final answer is count(0, 0).

Time Complexity:

O(m*n)

With memoization, each subproblem (i, j) is computed at most once, and there are m*n possible subproblems, where m and n are the lengths of strings S and T respectively.

Space Complexity:

O(m*n)

We use a memo table of size m*n to store the results of subproblems, plus O(m+n) for the recursion stack.

Approach 3: Tabulation (Bottom-Up DP)

We can further optimize our solution by using a bottom-up dynamic programming approach, which eliminates the need for recursion.

Thinking Process: Instead of starting from the beginning and working our way to the end (top-down), we can start from the base cases and build our way up (bottom-up).

Let's define dp[i][j] as the number of distinct subsequences of S[0...i-1] that equal T[0...j-1]. The base case is dp[i][0] = 1 for all i, representing the empty string T which is a subsequence of any string S.

For dp[i][j], we consider two cases:

  • If S[i-1] equals T[j-1], we can either include S[i-1] or skip it
  • If S[i-1] doesn't equal T[j-1], we can only skip S[i-1]

Intuition: By building our solution from smaller subproblems to larger ones, we can avoid recursion and its associated overhead.

Algorithm:

  1. Create a 2D DP array of size (m+1) x (n+1), where m and n are the lengths of strings S and T respectively.
  2. Initialize dp[i][0] = 1 for all i from 0 to m (base case: empty string T is a subsequence of any string S).
  3. For each i from 1 to m and j from 1 to n:
  4. a. If S[i-1] equals T[j-1], we have two choices:
  5. - Include S[i-1]: dp[i-1][j-1]
  6. - Skip S[i-1]: dp[i-1][j]
  7. So, dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
  8. b. If S[i-1] doesn't equal T[j-1], we can only skip S[i-1]:
  9. dp[i][j] = dp[i-1][j]
  10. Return dp[m][n] as the final answer.

Time Complexity:

O(m*n)

We fill a 2D DP array of size m*n, performing constant-time operations for each cell.

Space Complexity:

O(m*n)

We use a 2D DP array of size (m+1) x (n+1).

Approach 4: Space-Optimized DP

We can optimize the space complexity of our bottom-up DP approach by observing that we only need the previous row of the DP table to calculate the current row.

Thinking Process: In the bottom-up approach, dp[i][j] depends only on dp[i-1][j] and dp[i-1][j-1]. This means we only need to keep track of the previous row to calculate the current row.

However, there's a catch: when updating dp[j], we need the original value of dp[j-1] from the previous row, but this value might have already been overwritten if we update the array from left to right. To avoid this issue, we can update the array from right to left.

Intuition: By updating the DP array from right to left, we ensure that we don't overwrite values that we still need, allowing us to use a 1D array instead of a 2D array.

Algorithm:

  1. Create a 1D DP array of size n+1, where n is the length of string T.
  2. Initialize dp[0] = 1 (base case: empty string T is a subsequence of any string S).
  3. For each character in S:
  4. a. Iterate from right to left (j = n to 1):
  5. - If the current character of S equals T[j-1]:
  6. dp[j] += dp[j-1]
  7. Return dp[n] as the final answer.

Time Complexity:

O(m*n)

We still need to process each character of S and T, resulting in O(m*n) time complexity.

Space Complexity:

O(n)

We use a 1D DP array of size n+1, where n is the length of string T.

Pseudocode

solution.pseudo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function numDistinct(s, t):
function count(i, j):
// Base cases
if j == t.length:
return 1 // Found a valid subsequence
if i == s.length:
return 0 // Can't form t
// If characters match, we have two choices
if s[i] == t[j]:
return count(i+1, j+1) + count(i+1, j)
else:
// If characters don't match, we can only skip s[i]
return count(i+1, j)
return count(0, 0)
ProblemSolutionCode
101 Logo
onenoughtone