【C/C++】2311. 小于等于 K 的最长二进制子序列

155 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情


题目链接:2311. 小于等于 K 的最长二进制子序列

题目描述

给你一个二进制字符串 s 和一个正整数 k 。

请你返回 s 的 最长 子序列,且该子序列对应的 二进制 数字小于等于 k 。

注意:

  • 子序列可以有 前导 0 。
  • 空字符串视为 0 。
  • 子序列 是指从一个字符串中删除零个或者多个字符后,不改变顺序得到的剩余字符序列。

提示:

  • 1s.length10001 \leqslant s.length \leqslant 1000
  • s[i] 要么是 '0' ,要么是 '1' 。
  • 1k1091 \leqslant k \leqslant 10^9

示例 1:

输入:s = "1001010", k = 5
输出:5
解释:s 中小于等于 5 的最长子序列是 "00010" ,对应的十进制数字是 2 。
注意 "00100""00101" 也是可行的最长子序列,十进制分别对应 45 。
最长子序列的长度为 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' 即可,也就是前导零。

具体实现

贪心实现方法一

  1. 首先计算字符串 s 中所有字符 '0' 的个数。
  2. 然后从低位到高位选取字符 '1'
  3. 判断当前二进制数是否大于 k,如果小于 k 就继续选取,否则直接返回当前最长长度。

贪心实现方法二

  1. 计算正整数 k 的二进制数长度 m
  2. 计算字符串 s 较低的 m 位二进制数是否大于 k,如果小于 k 更新答案至少为 m,否则更新答案为 m - 1
  3. 计算字符串 s 中剩下的字符 '0' 的个数,加在答案上即可。

复杂度分析

  • 时间复杂度:O(n)O(n),其中 n 为字符串 s 的长度。
  • 空间复杂度:O(1)O(1),仅需常数存储空间。

代码实现

未优化

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 的长度较小,该题还可通过记忆化搜索和动态规划的方法解决。
  • 测试结果:

2311.png

2311-1.png

结束语

愿你能永远保持对世界的好奇,把日子过得充实有趣,经历世事仍拥有一颗赤子之心。新的一天,加油!