446. Arithmetic Slices II - Subsequence

446. Arithmetic Slices II - Subsequence

Problem Solving - Day 57

ยท

7 min read

Hello, reader ๐Ÿ‘‹๐Ÿฝ ! Welcome to day 57 of the series on Problem Solving. Through this series, I aim to pick up at least one question everyday and share my approach for solving it.

Today, I will be picking up LeetCode's daily challenge problem: 446. Arithmetic Slices II - Subsequence.


๐Ÿค” Problem Statement

  • Given an integer array nums, return the number of all the arithmetic subsequences of nums.
  • A sequence of numbers is called arithmetic if it consists of at least three elements and if the difference between any two consecutive elements is the same.
  • nums = [2,4,6,8,10] => 7
  • nums = [7,7,7,7,7] => 16

๐Ÿ’ฌ Thought Process - Dynamic Programming

  • The naive way of solving this approach would be to generate all the subsequences and check if every subsequence forms an arithmetic sequence.
  • But this approach has a time complexity of O(2^n) and that is not very efficient.
  • To improve the time complexity, we can make use of the difference d between the i-1 and i indices in the subsequence.
  • We'll make use of an array of hash map of length n (n = number of elements). Every index i would store the count of subsequences with difference d that would end at index i.
  • We can start with subsequences of length two, and keep building it by adding new elements to it if the difference between elements remains the same.
  • Since subsequences need to be at least of size 3, if our input array has length less than 3, that means it can have no arithmetic sequences and we return 0.
  • To find subsequences, we'll use two loops - where the outer loop with index i runs from 1 to n and the inner loop with index j from 0 to i-1.
  • We find the difference diff between the diff = nums[j] - nums[i].
  • There would be four cases:

    1. There was already a previous subsequence that ended at index i where the elements had the same difference diff.
    2. There was no previous subsequence that ended at index i with the same difference diff.
    3. There was already a previous subsequence that ended at index j with the same different diff.
    4. There was no previous subsequence that ended at index j with the same difference diff.
  • If we had already seen a subsequence that ended at index i with difference diff, then:

    • We include the total count of subsequence ending at index j to this count as well as 1 to denote that we formed a new subsequence by adding the current element j.
    • Hence:
      int countSubsequencesEndingAtI = map[i].get(diff);
      int countSubsequencesEndingAtJ = map[j].get(diff);
      int newCount = countSubsequencesEndingAtI + 
                     countSubsequencesEndingAtJ + 1;
      // 1 denotes new subsequence formed by adding the current element
      map[i].put(diff, newCount);
      
  • But we would have to update our answer that keeps track of the overall count of subsequences.
  • We'll make use of the countSubsequencesEndingAtJ to update our answer. This is because if you notice, countSubsequencesEndingAtI holds the count of subsequences ending at index i and it could also hold subsequences of length 2, which we don't want to count.
  • Hence, we'll use countSubsequencesEndingAtJ, which will hold the count of arithmetic subsequences of length greater or equal to 3.

  • I know this is hard to grasp and it's not easy (at least for me) to get it in one go. So, let's understand with visual aid with the example of nums = [2,4,6,8,10].

  • Also note that we'll have an array of map with <diff, count> to denote the count of subsequences with difference diff, ending at index i, where 0 <= i < n.
  • For ease of explanation I will not use dp[0].get(-3) to denote subsequences of difference -3 ending at index 0. Rather I'll be using dp[0][-3].
  • The calculation for count of subsequences ending at index i with diff d would be:

    (count of subsequences with difference d ending at i) + (count of subsequences with difference d ending at j) + 1

  • Please note that i represents the control variable for outer loop and j for the inner.
  • We'll also hold a total count of subsequence numSlices that would keep track of total number of arithmetic subsequences which would be calculated by:

    count of subsequences with difference d ending at index j

  • At every loop, we would add dp[j][diff] to our numSlices total.

Detailed Explanation

nums = [2,4,6,8]

  • i = 1

    • j = 0
      • nums[0] = 2, nums[1] = 4, diff = 4 - 2 = 2
      • dp[1][2] = 0, since there has been no subsequences ending at index 1 with difference 2.
      • dp[0][2] = 0 as well, as we did not see any subsequence ending at index 0 with difference 2.
      • Hence dp[1][2] = 1.
      • <2,4> is a (invalid) subsequence.
      • numSlices = 0 i=1,j=0.png
  • i = 2

    • j = 0
      • nums[0] = 2, nums[2] = 6, diff = 6 - 2 = 4
      • dp[2][4] = 0, since there has been no subsequences ending at index 2 with difference 4.
      • dp[0][4] = 0 as well, since we did not see any subsequence ending at index 0 with difference 4.
      • Hence dp[2][4] = 1
      • numSlices = 0
    • j = 1
      • nums[1] = 4, nums[2] = 6, diff = 6 - 4 = 2
      • dp[2][2] = 0, since there has been no subsequences ending at index 2 with difference 2.
      • dp[1][2] = 1 , since we have seen a subsequence ending at index 1 with difference 4 before [2,4].
      • Including 6 to it makes a valid arithmetic subsequence: [2,4,6]. We also have another (invalid) subsequence: [4,6].
      • Hence dp[2][2] = 2
      • numSlices += dp[1][2] = 1 i=2, j=0. j=1.png
  • i = 3

    • j = 0
      • nums[0] = 2, nums[3] = 8, diff = 8 - 2 = 6
      • dp[3][8] = 0, since there has been no subsequences ending at index 3 with difference 8.
      • dp[0][8] = 0 as well, since we did not see any subsequence ending at index 0 with difference 8.
      • Hence dp[3][8] = 1
      • numSlices = 1
    • j = 1
      • nums[1] = 4, nums[3] = 8, diff = 8 - 4 = 4
      • dp[3][4] = 0, since there has been no subsequences ending at index 3 with difference 4.
      • dp[1][4] = 1 , since we did not see a subsequence ending at index 1 with difference 4.
      • Hence dp[3][4] = 1
      • numSlices = 1
    • j = 2
      • nums[2] = 6, nums[3] = 8, diff = 8 - 6 = 2
      • dp[3][2] = 0, since there has been no subsequences ending at index 3 with difference 2.
      • dp[2][2] = 2 , since we have seen subsequences ending at index 2 with difference 2.
      • We already saw 2 invalid and 1 valid subsequences: [2,4], [4,6], [2,4,6].
      • We cannot add 8 to [2,4]. But if we add 8 to the remaining two, we get 2 more valid subsequences:
        • [4,6,8], [2,4,6,8].
      • Hence dp[3][2] = 3. (we had previously seen [2,4,6].
      • numSlices += dp[2][2] = 3 i=3, j=0, j=1, j=2.png
  • Note: This solution can be extremely overwhelming. If you don't understand it right away, no problem. I didn't and I'm sure no one else would have too.

  • Take your time, try to simulate the solution with multiple examples. Trust me, you'll get it soon!

๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป Solution - Dynamic Programming

  • Below is the code for the dynamic programming approach.

    class Solution {
      private int n;
      Map<Integer, Integer>[] map;
      public int numberOfArithmeticSlices(int[] nums) {
          n = nums.length;
          if(n < 3) return 0;
    
          long numSlices = 0;
          map = new Map[n];
    
          for(int i = 0; i<n; i++) map[i] = new HashMap<>();
    
          for(int i = 1; i<n; i++) {
              for(int j = 0; j<i; j++) {
                  double difference = (double) nums[i] - (double) nums[j];
                  int diff = (int) difference;
    
                  int sum = 0, origin = 0;
                  if(map[j].containsKey(diff)) {
                      sum = map[j].get(diff);
                  }
                  if(map[i].containsKey(diff)) {
                      origin = map[i].get(diff);
                  }
    
                  map[i].put(diff, origin + sum + 1);
                  numSlices += sum;
              }
          }
    
          return (int) numSlices;
    
      }
    }
    
    Time Complexity: O(n^2)
    Space Complexity: O(n^2)
    


Conclusion

That's a wrap for today's problem. If you liked my explanation then please do drop a like/ comment. Also, please correct me if I've made any mistakes or if you want me to improve something!

Thank you for reading!