203 lines
9.1 KiB
YAML
203 lines
9.1 KiB
YAML
title: Check If It Is a Good Array
|
|
slug: check-if-it-is-a-good-array
|
|
difficulty: hard
|
|
leetcode_id: 1250
|
|
leetcode_url: https://leetcode.com/problems/check-if-it-is-a-good-array/
|
|
categories:
|
|
- arrays
|
|
- math
|
|
patterns:
|
|
- slug: greedy
|
|
is_optimal: true
|
|
|
|
function_signature: "def is_good_array(nums: list[int]) -> bool:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { nums: [12, 5, 7, 23] }
|
|
expected: true
|
|
- input: { nums: [29, 6, 10] }
|
|
expected: true
|
|
- input: { nums: [3, 6] }
|
|
expected: false
|
|
hidden:
|
|
- input: { nums: [1] }
|
|
expected: true
|
|
- input: { nums: [2, 4, 6, 8] }
|
|
expected: false
|
|
- input: { nums: [6, 9, 15, 21] }
|
|
expected: false
|
|
- input: { nums: [5, 7] }
|
|
expected: true
|
|
- input: { nums: [100, 99] }
|
|
expected: true
|
|
- input: { nums: [2, 3] }
|
|
expected: true
|
|
|
|
description: |
|
|
Given an array `nums` of positive integers. Your task is to select some subset of `nums`, multiply each element by an integer and add all these numbers.
|
|
|
|
The array is said to be **good** if you can obtain a sum of `1` from the array by any possible subset and multiplicand.
|
|
|
|
Return `True` if the array is **good** otherwise return `False`.
|
|
|
|
constraints: |
|
|
- `1 <= nums.length <= 10^5`
|
|
- `1 <= nums[i] <= 10^9`
|
|
|
|
examples:
|
|
- input: "nums = [12,5,7,23]"
|
|
output: "true"
|
|
explanation: "Pick numbers 5 and 7. 5*3 + 7*(-2) = 1"
|
|
- input: "nums = [29,6,10]"
|
|
output: "true"
|
|
explanation: "Pick numbers 29, 6 and 10. 29*1 + 6*(-3) + 10*(-1) = 1"
|
|
- input: "nums = [3,6]"
|
|
output: "false"
|
|
explanation: "Both 3 and 6 are divisible by 3, so any linear combination will also be divisible by 3, never equal to 1."
|
|
|
|
explanation:
|
|
intuition: |
|
|
This problem might look intimidating at first — selecting subsets, multiplying by arbitrary integers, and summing to exactly `1`. But there's a beautiful mathematical theorem that makes this trivial once you recognise it.
|
|
|
|
**Bézout's Identity** states that for any two integers `a` and `b`, there exist integers `x` and `y` such that `ax + by = gcd(a, b)`. This extends to any number of integers: given `a₁, a₂, ..., aₙ`, we can find integers `x₁, x₂, ..., xₙ` such that `a₁x₁ + a₂x₂ + ... + aₙxₙ = gcd(a₁, a₂, ..., aₙ)`.
|
|
|
|
The key insight is that `1` can be expressed as such a linear combination **if and only if** the GCD of all numbers is `1`. Why? Because:
|
|
- Any linear combination of the numbers must be divisible by their GCD
|
|
- So if the GCD is greater than `1`, we can never reach `1`
|
|
- And if the GCD equals `1`, Bézout's identity guarantees we *can* reach `1`
|
|
|
|
Think of it like this: the GCD represents the "smallest building block" that all numbers share. If that building block is `1`, we can construct any integer (including `1`). If it's larger, we can only construct multiples of that larger value.
|
|
|
|
approach: |
|
|
With Bézout's Identity, the solution becomes remarkably simple:
|
|
|
|
**Step 1: Initialise the running GCD**
|
|
|
|
- `result`: Set to the first element of the array, or we can start with `0` (since `gcd(0, x) = x`)
|
|
|
|
|
|
|
|
**Step 2: Iterate through the array**
|
|
|
|
- For each number in `nums`, compute `gcd(result, nums[i])`
|
|
- Update `result` with this new GCD
|
|
- **Early termination**: If `result` becomes `1` at any point, we can immediately return `True` — the GCD can never increase, and `1` is the smallest positive integer
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- If the final GCD equals `1`, return `True`
|
|
- Otherwise, return `False`
|
|
|
|
|
|
|
|
The early termination optimisation is valuable because once we find two coprime numbers (GCD = 1), no further numbers can change this.
|
|
|
|
common_pitfalls:
|
|
- title: Overthinking the Subset Selection
|
|
description: |
|
|
The problem statement mentions "select some subset" and "multiply each element by an integer." This naturally leads to thinking about combinatorial approaches — trying different subsets, solving systems of equations, or using dynamic programming.
|
|
|
|
However, this is a red herring. The mathematical insight (Bézout's Identity) tells us that the *existence* of such integers is guaranteed when GCD = 1. We don't need to find the actual coefficients, just verify the GCD condition.
|
|
wrong_approach: "Dynamic programming or subset enumeration"
|
|
correct_approach: "Simply compute the GCD of all elements"
|
|
|
|
- title: Missing the GCD Connection
|
|
description: |
|
|
If you don't recognise this as a number theory problem, you might try brute force approaches that will time out. With `n` up to `10^5` elements and values up to `10^9`, any exponential or polynomial approach in terms of values won't work.
|
|
|
|
The GCD approach is O(n log(max_value)), which handles even the largest inputs efficiently.
|
|
wrong_approach: "Brute force subset enumeration"
|
|
correct_approach: "Recognise the Bézout's Identity pattern"
|
|
|
|
- title: Not Using Early Termination
|
|
description: |
|
|
While not strictly necessary for correctness, failing to check if the GCD becomes `1` early means unnecessary computation. Once two numbers are coprime, the overall GCD is locked at `1`.
|
|
|
|
For example, with `[1000000, 7, 3, 3, 3, ...]` (many 3s), we find GCD = 1 after just the first two elements. Without early termination, we'd wastefully compute GCDs with all remaining elements.
|
|
wrong_approach: "Always compute GCD with all elements"
|
|
correct_approach: "Return True immediately when GCD reaches 1"
|
|
|
|
key_takeaways:
|
|
- "**Bézout's Identity**: A linear combination of integers can equal `d` if and only if `d` is a multiple of their GCD — the key mathematical insight"
|
|
- "**Recognise number theory patterns**: Problems involving linear combinations and integer solutions often reduce to GCD checks"
|
|
- "**Early termination**: When computing running GCD, stop as soon as you reach `1` — it can never decrease"
|
|
- "**Don't overcomplicate**: What looks like a hard combinatorial problem becomes trivial with the right mathematical perspective"
|
|
|
|
time_complexity: "O(n log M). We iterate through `n` elements, and each GCD computation takes O(log M) where M is the maximum element value (using Euclidean algorithm)."
|
|
space_complexity: "O(1). We only track a single running GCD value regardless of input size."
|
|
|
|
solutions:
|
|
- approach_name: GCD with Early Termination
|
|
is_optimal: true
|
|
code: |
|
|
from math import gcd
|
|
from functools import reduce
|
|
|
|
def is_good_array(nums: list[int]) -> bool:
|
|
# Bézout's Identity: can reach 1 iff GCD of all elements is 1
|
|
# Use reduce to compute GCD across the entire array
|
|
return reduce(gcd, nums) == 1
|
|
explanation: |
|
|
**Time Complexity:** O(n log M) — n elements, each GCD takes O(log M) time.
|
|
|
|
**Space Complexity:** O(1) — Only stores the running GCD.
|
|
|
|
Python's `reduce` applies `gcd` cumulatively: `gcd(gcd(gcd(nums[0], nums[1]), nums[2]), ...)`. By Bézout's Identity, the array is "good" exactly when this overall GCD equals 1.
|
|
|
|
- approach_name: Iterative GCD with Explicit Early Exit
|
|
is_optimal: true
|
|
code: |
|
|
from math import gcd
|
|
|
|
def is_good_array(nums: list[int]) -> bool:
|
|
# Start with first element as our running GCD
|
|
result = nums[0]
|
|
|
|
for num in nums[1:]:
|
|
# Update running GCD
|
|
result = gcd(result, num)
|
|
|
|
# Early termination: GCD of 1 can never decrease
|
|
if result == 1:
|
|
return True
|
|
|
|
# Check if final GCD is 1
|
|
return result == 1
|
|
explanation: |
|
|
**Time Complexity:** O(n log M) — worst case iterates all elements, but often exits early.
|
|
|
|
**Space Complexity:** O(1) — Single variable tracks the running GCD.
|
|
|
|
This version explicitly shows the early termination optimisation. Once we find two coprime numbers (GCD = 1), no additional elements can change this fact. This is particularly effective when the array contains diverse numbers likely to be coprime.
|
|
|
|
- approach_name: Mathematical Proof Verification
|
|
is_optimal: false
|
|
code: |
|
|
from math import gcd
|
|
|
|
def is_good_array(nums: list[int]) -> bool:
|
|
# Compute GCD of all elements step by step
|
|
overall_gcd = 0 # gcd(0, x) = x, so this works as identity
|
|
|
|
for num in nums:
|
|
overall_gcd = gcd(overall_gcd, num)
|
|
|
|
# Optimisation: exit early if GCD reaches 1
|
|
if overall_gcd == 1:
|
|
return True
|
|
|
|
# By Bézout's Identity:
|
|
# ax + by = gcd(a,b) for some integers x, y
|
|
# Extended to n numbers: we can reach gcd(nums) via linear combination
|
|
# Therefore, we can reach 1 iff gcd(all nums) == 1
|
|
return overall_gcd == 1
|
|
explanation: |
|
|
**Time Complexity:** O(n log M) — Same as optimal, with more explicit comments.
|
|
|
|
**Space Complexity:** O(1) — Only the running GCD variable.
|
|
|
|
This version documents the mathematical reasoning inline. Starting with `0` works because `gcd(0, x) = x`, making it a valid identity element. The extended Euclidean algorithm guarantees that if GCD = 1, integer coefficients exist to form the linear combination.
|