2311. Longest Binary Subsequence Less Than or Equal to K
Problem Description
You have a binary string s
(containing only '0's and '1's) and a positive integer k
. Your task is to find the longest subsequence of s
that, when interpreted as a binary number, has a value less than or equal to k
.
A subsequence means you can pick characters from the original string while maintaining their relative order, but you don't have to pick consecutive characters. For example, if s = "1010"
, then "11"
, "00"
, "110"
, and "1010"
are all valid subsequences.
Key points to remember:
- The subsequence can have leading zeros (e.g.,
"001"
is valid and equals 1 in decimal) - An empty subsequence is treated as having a value of 0
- You want to maximize the length of the subsequence, not its value
For example, if s = "1001010"
and k = 5
(which is 101
in binary), you need to find the longest subsequence from "1001010"
that represents a binary number ≤ 5. One possible answer could be the subsequence "00010"
(length 5), which equals 2 in decimal.
The solution uses a greedy approach: it includes all '0's since they don't increase the value when added to the left side of a binary number. For '1's, it traverses from right to left and only includes a '1' if adding it at the current position keeps the total value ≤ k
. The check ans < 30
prevents overflow since we're working with bit positions.
Intuition
To maximize the length of our subsequence, we should think about which bits contribute to the value and which don't. The key insight is that 0
bits are "free" - we can include all of them without increasing the binary value. So any optimal solution must include all 0
s from the original string.
Now for the 1
bits, we need to be selective. Consider how binary numbers work: a 1
at position i
(counting from the right, starting at 0) contributes 2^i
to the total value. The leftmost bits have the highest contribution to the value.
This suggests a greedy strategy: process the string from right to left. Why? Because the rightmost 1
s contribute less to the final value (2^0 = 1
, 2^1 = 2
, etc.) compared to leftmost 1
s. By going right to left, we can greedily include 1
s that have smaller contributions first.
Here's the clever part: as we traverse from right to left, we maintain a counter ans
that represents both the length of our current subsequence AND the position where the next 1
would be placed if we include it. When we encounter a 1
, we check if placing it at position ans
(which would contribute 2^ans
to our value) keeps us under k
. If yes, we include it by setting that bit in our value v
using v |= 1 << ans
.
The condition ans < 30
is a practical safeguard - since we're dealing with typical integer constraints, we don't need to consider positions beyond 30 (as 2^30
is already over a billion).
This approach guarantees we get the maximum length because we're including all possible 0
s and as many 1
s as we can fit while staying under the limit k
.
Learn more about Greedy, Memoization and Dynamic Programming patterns.
Solution Approach
The implementation follows a greedy approach by traversing the string from right to left. Here's how the algorithm works:
-
Initialize variables:
ans = 0
: tracks the length of our subsequence and also represents the bit position for the next1
v = 0
: stores the actual binary value of our current subsequence
-
Traverse the string in reverse (
s[::-1]
):- When we encounter a
'0'
:- Simply increment
ans += 1
because zeros don't increase the binary value - We can include all zeros without any constraint
- Simply increment
- When we encounter a
'1'
:- First check if
ans < 30
to prevent overflow (since2^30
is already very large) - Then check if adding this
1
at positionans
keeps our value ≤k
- The check
(v | 1 << ans) <= k
tests what the new value would be if we set the bit at positionans
- If both conditions pass, we:
- Update our value:
v |= 1 << ans
(sets the bit at positionans
) - Increment the length:
ans += 1
- Update our value:
- First check if
- When we encounter a
-
Return the final length
ans
The bit manipulation 1 << ans
creates a number with only the bit at position ans
set to 1. The OR operation v | (1 << ans)
adds this bit to our current value.
Example walkthrough with s = "1001010"
and k = 5
:
- Reverse string:
"0101001"
- Position 0:
'0'
→ include it,ans = 1
- Position 1:
'1'
→ check(0 | 1 << 1) = 2 <= 5
✓, include it,v = 2
,ans = 2
- Position 2:
'0'
→ include it,ans = 3
- Position 3:
'1'
→ check(2 | 1 << 3) = 10 > 5
✗, skip it - Position 4:
'0'
→ include it,ans = 4
- Position 5:
'0'
→ include it,ans = 5
- Position 6:
'1'
→ check(2 | 1 << 5) = 34 > 5
✗, skip it - Result: length = 5
This greedy strategy ensures maximum length by including all zeros and as many ones as possible while respecting the constraint.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a simple example with s = "10101"
and k = 3
(which is 11
in binary).
Step 1: Reverse the string
- Original:
"10101"
- Reversed:
"10101"
(happens to be palindromic)
Step 2: Process each character from the reversed string
Initialize: ans = 0
, v = 0
-
Index 0, char = '1':
- Check:
ans < 30
? Yes (0 < 30) - Check:
(0 | 1 << 0) = 1 <= 3
? Yes - Include this '1':
v = 0 | (1 << 0) = 1
,ans = 1
- Current subsequence represents:
"1"
= 1
- Check:
-
Index 1, char = '0':
- It's a '0', always include:
ans = 2
- Current subsequence represents:
"01"
= 1
- It's a '0', always include:
-
Index 2, char = '1':
- Check:
ans < 30
? Yes (2 < 30) - Check:
(1 | 1 << 2) = 1 | 4 = 5 <= 3
? No (5 > 3) - Skip this '1'
- Current subsequence still:
"01"
= 1
- Check:
-
Index 3, char = '0':
- It's a '0', always include:
ans = 3
- Current subsequence represents:
"001"
= 1
- It's a '0', always include:
-
Index 4, char = '1':
- Check:
ans < 30
? Yes (3 < 30) - Check:
(1 | 1 << 3) = 1 | 8 = 9 <= 3
? No (9 > 3) - Skip this '1'
- Final subsequence:
"001"
= 1
- Check:
Result: Maximum length = 3
The subsequence we built is "001"
(reading from the original string: we took positions 2, 3, and 4). This has value 1, which is ≤ 3, and has the maximum possible length of 3 characters.
Solution Implementation
1class Solution:
2 def longestSubsequence(self, s: str, k: int) -> int:
3 # Initialize the length of subsequence and current value
4 subsequence_length = 0
5 current_value = 0
6
7 # Process string from right to left (least significant bit first)
8 for char in s[::-1]:
9 if char == "0":
10 # Always include '0' as it doesn't increase the value
11 subsequence_length += 1
12 elif subsequence_length < 30 and (current_value | (1 << subsequence_length)) <= k:
13 # Include '1' only if:
14 # 1. Position is less than 30 (to avoid overflow)
15 # 2. Adding this '1' at current position keeps value <= k
16
17 # Set the bit at position 'subsequence_length' to 1
18 current_value |= (1 << subsequence_length)
19 subsequence_length += 1
20
21 return subsequence_length
22
1class Solution {
2 public int longestSubsequence(String s, int k) {
3 int subsequenceLength = 0; // Length of the longest valid subsequence
4 int currentValue = 0; // Current binary value being constructed
5
6 // Traverse the string from right to left (from least significant bit)
7 for (int i = s.length() - 1; i >= 0; i--) {
8 if (s.charAt(i) == '0') {
9 // A '0' bit doesn't increase the value, so always include it
10 subsequenceLength++;
11 } else if (subsequenceLength < 30 && (currentValue | (1 << subsequenceLength)) <= k) {
12 // For '1' bit:
13 // - Check if subsequenceLength < 30 to avoid integer overflow (1 << 30 is within int range)
14 // - Check if adding this '1' at position 'subsequenceLength' keeps value <= k
15 // - The bit position is 'subsequenceLength' because we're building from right to left
16 currentValue |= (1 << subsequenceLength);
17 subsequenceLength++;
18 }
19 // If '1' would make the value exceed k, skip this bit
20 }
21
22 return subsequenceLength;
23 }
24}
25
1class Solution {
2public:
3 int longestSubsequence(string s, int k) {
4 int subsequenceLength = 0; // Length of the current subsequence
5 int currentValue = 0; // Decimal value of the subsequence
6
7 // Traverse the binary string from right to left (LSB to MSB)
8 for (int i = s.size() - 1; i >= 0; --i) {
9 if (s[i] == '0') {
10 // Always include '0' as it doesn't increase the decimal value
11 ++subsequenceLength;
12 } else if (subsequenceLength < 30 && (currentValue | (1 << subsequenceLength)) <= k) {
13 // For '1': check if adding it at position 'subsequenceLength' keeps value <= k
14 // Note: subsequenceLength < 30 prevents integer overflow (2^30 > 10^9)
15 currentValue |= (1 << subsequenceLength); // Add 2^subsequenceLength to current value
16 ++subsequenceLength;
17 }
18 // If '1' would make the value exceed k, skip it
19 }
20
21 return subsequenceLength;
22 }
23};
24
1/**
2 * Finds the length of the longest subsequence of binary string s
3 * whose decimal value is less than or equal to k
4 * @param s - Binary string consisting of '0' and '1'
5 * @param k - Maximum decimal value constraint
6 * @returns Length of the longest valid subsequence
7 */
8function longestSubsequence(s: string, k: number): number {
9 // Track the length of the subsequence we're building
10 let subsequenceLength = 0;
11
12 // Traverse the string from right to left (least significant bit to most significant)
13 // currentValue accumulates the decimal value of selected '1' bits
14 for (let index = s.length - 1, currentValue = 0; index >= 0; index--) {
15 if (s[index] === '0') {
16 // Always include '0' bits as they don't increase the decimal value
17 subsequenceLength++;
18 } else if (subsequenceLength < 30 && (currentValue | (1 << subsequenceLength)) <= k) {
19 // Include '1' bit only if:
20 // 1. The bit position is less than 30 (to prevent overflow)
21 // 2. Adding this '1' bit at position 'subsequenceLength' keeps value <= k
22
23 // Set the bit at position 'subsequenceLength' in currentValue
24 currentValue |= 1 << subsequenceLength;
25 subsequenceLength++;
26 }
27 // Skip '1' bits that would make the value exceed k
28 }
29
30 return subsequenceLength;
31}
32
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the string s
. The algorithm iterates through the string once in reverse order using s[::-1]
, performing constant-time operations for each character (bit manipulation operations like |
, <<
, and comparisons are O(1)
).
The space complexity is O(1)
. The algorithm only uses a fixed amount of extra space with variables ans
, v
, and c
, regardless of the input size. Even though s[::-1]
creates a reversed copy of the string, this is part of the input processing rather than auxiliary space used by the algorithm itself. The bit operations and integer storage do not scale with input size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Bit Position Calculation
A common mistake is misunderstanding how the bit position relates to the subsequence length. When traversing from right to left, subsequence_length
represents both the current length AND the bit position for the next '1' we might add.
Wrong approach:
# Incorrectly using separate variables or wrong position calculation bit_position = 0 for char in s[::-1]: if char == "1": if (current_value | (1 << bit_position)) <= k: # Wrong position! current_value |= (1 << bit_position) bit_position += 1 # Incrementing regardless of inclusion
Correct approach:
The subsequence_length
variable serves dual purpose - it tracks how many characters we've included AND determines the bit position for the next '1'.
2. Forgetting the Overflow Check
Without checking subsequence_length < 30
, the bit shift operation 1 << subsequence_length
can cause integer overflow or unexpected behavior when the position gets large.
Wrong approach:
elif char == "1" and (current_value | (1 << subsequence_length)) <= k: # Missing overflow check - can cause issues with large strings
Correct approach: Always include the overflow check:
elif subsequence_length < 30 and (current_value | (1 << subsequence_length)) <= k:
3. Processing String in Wrong Direction
Processing the string from left to right (most significant bit first) makes it much harder to track the binary value correctly.
Wrong approach:
for char in s: # Processing left to right # Complex logic needed to track positions and values
Correct approach:
Process from right to left using s[::-1]
to handle least significant bits first, making the bit position calculation straightforward.
4. Incorrectly Handling the Value Update
Using addition instead of bitwise OR can lead to incorrect results if you accidentally add the same bit position multiple times.
Wrong approach:
current_value += (1 << subsequence_length) # Using addition
Correct approach:
current_value |= (1 << subsequence_length) # Using bitwise OR
5. Not Including All Zeros
Some might try to optimize by limiting zeros, but the key insight is that ALL zeros should be included since they don't increase the binary value when added to the left.
Wrong approach:
if char == "0" and some_condition: # Unnecessarily limiting zeros subsequence_length += 1
Correct approach:
if char == "0": subsequence_length += 1 # Always include zeros
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!