LeetCode 2311 - Longest Binary Subsequence Less Than or Equal to K
The problem asks us to find the length of the longest subsequence of a given binary string s such that the resulting binary number is less than or equal to a given integer k. A subsequence is any selection of characters from s in their original order, possibly skipping some.
Difficulty: 🟡 Medium
Topics: String, Dynamic Programming, Greedy, Memoization
Solution
Problem Understanding
The problem asks us to find the length of the longest subsequence of a given binary string s such that the resulting binary number is less than or equal to a given integer k. A subsequence is any selection of characters from s in their original order, possibly skipping some. Importantly, leading zeros are allowed, and an empty string counts as 0.
The input consists of a string s of length up to 1000, where each character is either '0' or '1', and an integer k that can be as large as 10^9. The output is an integer representing the maximum length of a valid subsequence.
Key points to note include the allowance of leading zeros, the fact that subsequences do not need to be contiguous, and that the binary number formed must not exceed k. Edge cases that might trip up a naive implementation include strings with all zeros, very large values of k, or strings with mostly ones where including all ones would exceed k.
Approaches
A brute-force approach would be to generate all possible subsequences of s, convert each to a decimal integer, check if it is less than or equal to k, and track the maximum length. While this is correct, it is extremely slow, because a string of length n has 2^n possible subsequences, and n can be up to 1000, making this computationally infeasible.
The optimal approach leverages a greedy strategy. Since zeros do not increase the decimal value, we can always include all zeros in the subsequence. For ones, we need to be careful because they increase the decimal value. We iterate from the least significant bit to the most significant bit (i.e., from right to left in the string). We attempt to add ones one by one, keeping track of the decimal value. If adding a one would exceed k, we skip it. This allows us to maximize the length while ensuring the resulting binary number remains valid.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n * n) | O(n) | Generates all subsequences, checks each against k |
| Optimal (Greedy) | O(n) | O(1) | Counts zeros and adds ones from LSB to MSB until reaching k |
Algorithm Walkthrough
- Initialize a counter
countfor zeros and ones that can safely be included. - Initialize a variable
valueto track the decimal value of the selected ones. - Iterate backwards through
sfrom rightmost to leftmost character. - If the character is
'0', incrementcountbecause zeros do not increase the number. - If the character is
'1', compute the potential new value if this one were included, considering its bit position relative to the LSB of the subsequence. - If including the one does not make
valueexceedk, incrementcountand updatevalue. - Otherwise, skip this one, since including it would exceed
k. - After iterating through the string,
countrepresents the length of the longest valid subsequence.
Why it works: The greedy strategy works because zeros never increase the binary value, so we can always include them. Processing ones from least significant to most significant ensures we prioritize including ones that contribute less to the decimal value first, allowing more characters in total without exceeding k.
Python Solution
class Solution:
def longestSubsequence(self, s: str, k: int) -> int:
n = len(s)
count = 0
value = 0
power = 1
for i in range(n - 1, -1, -1):
if s[i] == '0':
count += 1
else:
if value + power <= k:
value += power
count += 1
power <<= 1 # Multiply by 2 for next bit
return count
Explanation: We iterate from the least significant bit (rightmost character) to the most significant. For zeros, we increment count directly. For ones, we check if including the current power of two would exceed k. If not, we include it and update the total value. The power variable doubles each iteration to represent the next higher bit. This guarantees that the resulting subsequence stays within the limit k.
Go Solution
func longestSubsequence(s string, k int) int {
n := len(s)
count := 0
value := 0
power := 1
for i := n - 1; i >= 0; i-- {
if s[i] == '0' {
count++
} else {
if value + power <= k {
value += power
count++
}
if power <= (1<<30) { // prevent overflow
power <<= 1
}
}
}
return count
}
Explanation: The Go version is similar to Python. One difference is the explicit check to prevent integer overflow when shifting power. Go does not automatically handle big integers, so we ensure power stays within bounds while iterating from the rightmost bit to the leftmost.
Worked Examples
Example 1: s = "1001010", k = 5
Iterating right to left:
| i | s[i] | value | count | power |
|---|---|---|---|---|
| 6 | 0 | 0 | 1 | 1 |
| 5 | 1 | 1 | 2 | 2 |
| 4 | 0 | 1 | 3 | 4 |
| 3 | 1 | 1 | 3 | 8 (skip, 1+8>5) |
| 2 | 0 | 1 | 4 | 16 |
| 1 | 0 | 1 | 5 | 32 |
| 0 | 1 | 1 | 5 | 64 |
Result: count = 5
Example 2: s = "00101001", k = 1
Iterating right to left:
| i | s[i] | value | count | power |
|---|---|---|---|---|
| 7 | 1 | 1 | 1 | 1 |
| 6 | 0 | 1 | 2 | 2 |
| 5 | 0 | 1 | 3 | 4 |
| 4 | 1 | 1 | 3 | 8 (skip) |
| 3 | 0 | 1 | 4 | 16 |
| 2 | 1 | 1 | 4 | 32 (skip) |
| 1 | 0 | 1 | 5 | 64 |
| 0 | 0 | 1 | 6 | 128 |
Result: count = 6
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | We iterate through the string once, performing constant time operations per character |
| Space | O(1) | Only a few integer variables are used, independent of string length |
The algorithm is efficient for the given constraints because n is up to 1000, and all operations are linear and memory-light.
Test Cases
# Provided examples
assert Solution().longestSubsequence("1001010", 5) == 5 # mixture of zeros and ones
assert Solution().longestSubsequence("00101001", 1) == 6 # mostly zeros, k = 1
# Edge cases
assert Solution().longestSubsequence("0", 1) == 1 # single zero
assert Solution().longestSubsequence("1", 1) == 1 # single one, equal to k
assert Solution().longestSubsequence("1", 0) == 0 # single one, cannot include
assert Solution().longestSubsequence("1111111111", 1023) == 10 # all ones, sum within k
assert Solution().longestSubsequence("1111111111", 5) == 3 # all ones, can only include smallest bits
assert Solution().longestSubsequence("0000000", 1) == 7 # all zeros, include all
| Test | Why |
|---|---|
| "1001010", 5 | Typical mixture of zeros and ones |
| "00101001", 1 | Mostly zeros, k very small |
| "0", 1 | Minimal input, zero only |
| "1", 1 | Minimal input, one equal to k |
| "1", 0 | Minimal input, one greater than k |
| "1111111111", 1023 | Large all ones, fits within k |
| "1111111111", 5 | Large all ones, cannot include all |
| "0000000", 1 | Large all zeros, include all |
Edge Cases
One important edge case is when the string contains only zeros.