title: Arithmetic Slices slug: arithmetic-slices difficulty: medium leetcode_id: 413 leetcode_url: https://leetcode.com/problems/arithmetic-slices/ categories: - arrays - dynamic-programming patterns: - dynamic-programming function_signature: "def number_of_arithmetic_slices(nums: list[int]) -> int:" test_cases: visible: - input: { nums: [1, 2, 3, 4] } expected: 3 - input: { nums: [1] } expected: 0 hidden: - input: { nums: [1, 2] } expected: 0 - input: { nums: [1, 2, 3] } expected: 1 - input: { nums: [1, 2, 3, 4, 5] } expected: 6 - input: { nums: [7, 7, 7, 7] } expected: 3 - input: { nums: [1, 2, 3, 5, 7] } expected: 1 description: | An integer array is called *arithmetic* if it consists of **at least three elements** and if the difference between any two consecutive elements is the same. For example, `[1,3,5,7,9]`, `[7,7,7,7]`, and `[3,-1,-5,-9]` are arithmetic sequences. Given an integer array `nums`, return *the number of arithmetic **subarrays** of* `nums`. A **subarray** is a contiguous subsequence of the array. constraints: | - `1 <= nums.length <= 5000` - `-1000 <= nums[i] <= 1000` examples: - input: "nums = [1,2,3,4]" output: "3" explanation: "We have 3 arithmetic slices in nums: [1, 2, 3], [2, 3, 4] and [1, 2, 3, 4] itself." - input: "nums = [1]" output: "0" explanation: "A single element cannot form an arithmetic slice (minimum 3 elements required)." explanation: intuition: | Imagine you're walking along a staircase where each step has a consistent height difference from the previous one. An arithmetic slice is like finding a section of this staircase where the step heights are uniform — you need at least 3 steps to confirm a pattern. The key insight is that **arithmetic slices can extend**. If `[a, b, c]` forms an arithmetic slice with common difference `d`, and the next element `e` also continues the pattern (i.e., `e - c = d`), then we don't just get one more slice `[b, c, e]` — we also get `[a, b, c, e]`. Think of it like a snowball effect: each time we extend an arithmetic sequence by one element, the number of *new* slices we gain equals the number of slices that ended at the previous position, plus one more (the new minimal 3-element slice). For example, with `[1, 2, 3, 4]`: - At index 2: `[1, 2, 3]` is our first slice → 1 new slice - At index 3: The pattern continues, so we get `[2, 3, 4]` (new 3-element slice) AND `[1, 2, 3, 4]` (extended slice) → 2 new slices This cumulative relationship is perfect for dynamic programming. approach: | We solve this using a **Dynamic Programming** approach that tracks how many arithmetic slices end at each position. **Step 1: Handle edge cases** - If the array has fewer than 3 elements, return `0` immediately (no arithmetic slice possible)   **Step 2: Initialise variables** - `dp`: Tracks how many arithmetic slices **end at the current index**. Starts at `0` - `total`: Accumulates the total count of all arithmetic slices found   **Step 3: Iterate from index 2 onwards** - For each index `i` (starting from 2), check if `nums[i] - nums[i-1] == nums[i-1] - nums[i-2]` - If the differences match, we can extend the arithmetic sequence: - `dp = dp + 1` — we gain one more slice than we had ending at the previous position, plus the new 3-element slice - Add `dp` to `total` - If the differences don't match, reset `dp = 0` — the arithmetic sequence breaks here   **Step 4: Return the total** - After processing all elements, `total` contains the count of all arithmetic slices   The key insight is that `dp` at each position tells us how many slices end exactly there. When we extend a sequence, each previous slice that ended at `i-1` now has a corresponding longer version ending at `i`, plus we get one new minimal slice. common_pitfalls: - title: Counting Only Minimal Slices description: | A common mistake is to only count 3-element slices and miss longer ones. For `[1, 2, 3, 4]`, some might count only `[1, 2, 3]` and `[2, 3, 4]` (2 slices) and forget that `[1, 2, 3, 4]` is also a valid arithmetic slice. The DP approach handles this naturally: when we extend from position 2 to position 3, we count both the new 3-element slice AND the extended 4-element slice. wrong_approach: "Counting only consecutive triplets" correct_approach: "Track cumulative count using DP to capture all lengths" - title: Restarting Count Incorrectly description: | When the arithmetic pattern breaks, you must reset your running count to `0`. For `[1, 2, 3, 5, 7]`, the sequence `[1, 2, 3]` has difference `1`, but `5 - 3 = 2` breaks the pattern. You need to reset and start fresh from `[3, 5, 7]` which has a new common difference of `2`. Forgetting to reset leads to overcounting by incorrectly extending slices across different common differences. wrong_approach: "Continuing count across different common differences" correct_approach: "Reset dp to 0 when consecutive differences don't match" - title: Off-by-One in Loop Start description: | The loop must start at index 2 (the third element) because we need at least 3 elements to form an arithmetic slice. Starting at index 0 or 1 would cause index-out-of-bounds errors when checking `nums[i-2]`. wrong_approach: "Starting loop at index 0 or 1" correct_approach: "Start at index 2 to have three elements available" key_takeaways: - "**Cumulative DP pattern**: When extending a valid sequence adds multiple new valid subsequences, track how many end at each position" - "**Snowball counting**: Each extension adds `(previous count + 1)` new items — this pattern appears in many counting problems" - "**O(1) space DP**: When you only need the previous state, you can optimise from an array to a single variable" - "**Foundation for harder problems**: This extends to *Arithmetic Slices II* where subsequences (non-contiguous) are counted, requiring a hash map approach" time_complexity: "O(n). We iterate through the array once, performing constant-time operations at each step." space_complexity: "O(1). We only use two variables (`dp` and `total`) regardless of input size." solutions: - approach_name: Dynamic Programming is_optimal: true code: | def number_of_arithmetic_slices(nums: list[int]) -> int: n = len(nums) # Need at least 3 elements for an arithmetic slice if n < 3: return 0 # dp tracks slices ending at current position dp = 0 # total accumulates all arithmetic slices total = 0 for i in range(2, n): # Check if current element continues the arithmetic pattern if nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]: # Extend: we get all previous slices + 1 new minimal slice dp += 1 total += dp else: # Pattern breaks, reset count dp = 0 return total explanation: | **Time Complexity:** O(n) — Single pass through the array. **Space Complexity:** O(1) — Only two integer variables used. The key insight is that `dp` represents how many arithmetic slices end at the current index. When we can extend the sequence, each slice ending at `i-1` spawns a longer version ending at `i`, plus we get one new 3-element slice. When the pattern breaks, we reset to 0. - approach_name: Brute Force is_optimal: false code: | def number_of_arithmetic_slices(nums: list[int]) -> int: n = len(nums) count = 0 # Try every possible starting point for i in range(n - 2): # Calculate the common difference for this starting point diff = nums[i + 1] - nums[i] # Extend as far as possible with this difference for j in range(i + 2, n): # Check if the pattern continues if nums[j] - nums[j - 1] == diff: # Valid arithmetic slice from i to j count += 1 else: # Pattern breaks, no point continuing break return count explanation: | **Time Complexity:** O(n^2) — For each starting position, we may scan to the end. **Space Complexity:** O(1) — Only a counter variable used. This approach tries every possible starting position and extends the slice as long as the common difference is maintained. While correct, it's less efficient than the DP solution. However, with the constraint `n <= 5000`, this O(n^2) solution would still pass (around 25 million operations at worst).