携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
题目描述
给你一个二进制字符串 s 和一个正整数 k 。
请你返回 s 的 最长 子序列,且该子序列对应的 二进制 数字小于等于 k 。
注意:
- 子序列可以有 前导
0。 - 空字符串视为
0。 - 子序列 是指从一个字符串中删除零个或者多个字符后,不改变顺序得到的剩余字符序列。
提示:
s[i]要么是'0',要么是'1'。
示例 1:
输入:s = "1001010", k = 5
输出:5
解释:s 中小于等于 5 的最长子序列是 "00010" ,对应的十进制数字是 2 。
注意 "00100" 和 "00101" 也是可行的最长子序列,十进制分别对应 4 和 5 。
最长子序列的长度为 5 ,所以返回 5 。
示例 2:
输入:s = "00101001", k = 1
输出:6
解释:"000001" 是 s 中小于等于 1 的最长子序列,对应的十进制数字是 1 。
最长子序列的长度为 6 ,所以返回 6 。
整理题意
题目给定一个二进制字符串 s 和一个正整数 k,让我们在字符串 s 中选取子序列,要求选取的子序列要小于 k,问最长的子序列长度为多少。
题目规定子序列中可以存在前导零。
解题思路分析
由于题目规定可以存在前导零,我们考虑首先选取字符串 s 中所有的字符 '0',此时我们再考虑剩下的字符 '1' 该如何选择,我们可以 贪心 的选择二进制中低位的字符 '1',因为这样可以使得得到的数尽可能的小。
注意是子序列,不是子串,二者有区别,子序列不需要连续,子串要连续。
结论: 先选择所有的 0,再从低位到高位地贪心选择所有的 1,直到所构成的数超出 k。此时字符串长度即为答案。
考虑用一个未选取的 '1' 替换已选取的 '0' 或 '1':
- 无论是替换较高位或较低位的
'0',都会使得得到的数变大,故不可能。 - 由于是从低到高选取
'1',未选取的'1'一定在较高的位置,而在长度相同的情况下,地位的'1'会使得数更小,所以选取较低位的'1'更优。
考虑删除低位的 '0' 来使得数变小:
- 由于考虑的是子序列长度相同的情况,那么删除一个低位的
'0'必然会在高位或低位添加一个'1'('0'已经全部选取,不存在未选取的'0'),显然无论在高位还是低位添加一个'1'都会使得数变大,这样显然不是更优的选择。
优化
由于我们知道 k 的二进制长度,假设为 m,那么对于任何长度为 m - 1 的二进制数一定都小于 k(如 k = "1000" = 8 时 "111" = 7)
所以答案长度至少为 m - 1(当然,如果字符串 s 的长度小于 m - 1,直接返回 s 字符串长度即可)
那么考虑直接截取字符串 s 低位的 m - 1 位,此时考虑加上第 m 位是否大于 k,如果任然小于 k,就直接加上。
在字符串 s 中大于 m 位的 '1' 都是无法选取的,因为这都会导致最后得到的数大于 k,所以我们选取剩下的 '0' 即可,也就是前导零。
具体实现
贪心实现方法一
- 首先计算字符串
s中所有字符'0'的个数。 - 然后从低位到高位选取字符
'1' - 判断当前二进制数是否大于
k,如果小于k就继续选取,否则直接返回当前最长长度。
贪心实现方法二
- 计算正整数
k的二进制数长度m - 计算字符串
s较低的m位二进制数是否大于k,如果小于k更新答案至少为m,否则更新答案为m - 1。 - 计算字符串
s中剩下的字符'0'的个数,加在答案上即可。
复杂度分析
- 时间复杂度:,其中
n为字符串s的长度。 - 空间复杂度:,仅需常数存储空间。
代码实现
未优化
class Solution {
public:
int longestSubsequence(string s, int k) {
//ans记录最长子序列长度
int ans = 0;
//首先统计 0 的个数
int n = s.length();
for(int i = 0; i < n; i++) if(s[i] == '0') ans++;
//统计可以选择 1 的个数
int pos = n - 1;
//注意溢出问题
long long int v = 1, num = 0;
while(pos >= 0){
if(s[pos] == '1'){
//v记录当前位1所代表的10进制值,num记录总和
num += v;
if(num > k) return ans;
ans++;
}
v <<= 1;
//注意溢出问题
if(v > k) return ans;
pos--;
}
return ans;
}
};
优化
class Solution {
public:
int longestSubsequence(string s, int k) {
int ans = 0;
//计算k二进制长度
int m = 0;
int t = k;
while(t){
m++;
t >>= 1;
}
long long int num = 0;
int n = s.length();
//特判 n < m 的情况
if(n < m) return n;
int i = n - 1;
int v = 1;
//计算字符串 s 低 m 位的值
while(i >= n - m){
if(s[i] == '1'){
num += v;
}
i--;
v <<= 1;
}
//根据 num 值判断最小长度
if(num > k) ans = m - 1;
else ans = m;
//计算字符串剩下的 0
for(int i = 0; i < n - m; i++) if(s[i] == '0') ans++;
return ans;
}
};
总结
- 题目中
k的数据范围较大,需要注意溢出问题。 - 在贪心求解答案的时候,需要分类讨论在相同情况下的最优解是否为贪心结果。
- 由于字符串
s的长度较小,该题还可通过记忆化搜索和动态规划的方法解决。 - 测试结果:
结束语
愿你能永远保持对世界的好奇,把日子过得充实有趣,经历世事仍拥有一颗赤子之心。新的一天,加油!