LeetCode 600. Non-negative Integers without Consecutive Ones【数位DP,斐波那契,位运算】困难

本文介绍了如何使用动态规划解决LeetCode中关于统计无连续1的二进制数问题,从数位DP到斐波那契数列优化解法,详细解析了算法思路和代码实现,并给出了相关仓库链接便于查阅和学习。

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

给定一个正整数 n ,请你统计在 [0, n] 范围的非负整数中,有多少个整数的二进制表示中不存在 连续的 1

示例 1:

输入: n = 5
输出: 5
解释: 
下面列出范围在 [0, 5] 的非负整数与其对应的二进制表示:
0 : 0
1 : 1
2 : 10
3 : 11
4 : 100
5 : 101
其中,只有整数 3 违反规则(有两个连续的 1 ),其他 5 个满足规则。

示例 2:

输入: n = 1
输出: 2

示例 3:

输入: n = 2
输出: 3

提示:

  • 1 <= n <= 10^9

题意:给定一个正整数 n ,找出小于或等于 n 的非负整数中,其二进制表示不包含 连续的1 的个数。


解法2 数位DP

n n n 转换成二进制字符串 s s s,定义 f ( i , l e a d , i s N u m , i s L i m i t ) f(i, lead, isNum, isLimit) f(i,lead,isNum,isLimit) ​表示构造从左往右第 i i i 位及其之后数位的合法方案数,其余参数的含义为:​

  • l e a d lead lead 表示第 i − 1 i−1 i1 位是否为 1 1 1 ,如果为真则当前位不能填 1 1 1
  • isNum \textit{isNum} isNum 表示 i i i 前面的数位是否填了数字。若为假,则当前位可以跳过(不填数字),或者要填入的数字至少为 1 1 1 ;若为真,则要填入的数字可以从 0 0 0 开始。
  • isLimit \textit{isLimit} isLimit 表示当前是否受到了 n n n 的约束。若为真,则第 i i i 位填入的数字至多为 s [ i ] s[i] s[i] ,否则可以是 1 1 1如果在受到约束的情况下填了 s [ i ] s[i] s[i] ,那么后续填入的数字仍会受到 n n n 的约束后面两个参数可适用于其它数位DP题目
class Solution {
public:
    int findIntegers(int n) {
       int m = std::__lg(n), dp[m + 1][2][2][2]; // __lg返回最高位1在第几位
        memset(dp, -1, sizeof(dp)); // lead为true表示前一位填1,否则是0
        function<int(int, bool, bool, bool)> f = [&](int i, bool lead, bool isNum, bool isLimit) -> int {
            if (i < 0) return 1;
            if (dp[i][lead][isNum][isLimit] != -1) return dp[i][lead][isNum][isLimit];
            int ans = 0;
            if (!isNum) { // 没填数字,可填0或1
                ans += f(i - 1, false, false, false); // 不填
                if (!isLimit || (n >> i & 1)) ans += f(i - 1, true, true, isLimit); // 能填1
            } else {
                ans += f(i - 1, false, true, isLimit && !(n >> i & 1)); // 无论是否受到限制都可填0
                if (!lead && (!isLimit || (n >> i & 1))) ans += f(i - 1, true, true, isLimit);
            }
            return dp[i][lead][isNum][isLimit] = ans;
        };
        return f(m, false, false, true); // i从m往小枚举,方便位运算
    }
};

对于本题来说,由于前面没填(相当于都"填了"0)对答案无影响——没填时可以填0或填1,填了的话根据情况也可填0或填1,且0也是答案之一,所以 i s N u m isNum isNum 可以省略。

class Solution {
public:
    int findIntegers(int n) {
        int m = std::__lg(n), dp[m + 1][2][2]; // __lg返回最高位1在第几位
        memset(dp, -1, sizeof(dp)); // lead为true表示前一位填1,否则是0 
        function<int(int, bool, bool)> f = [&](int i, bool lead, bool isLimit) -> int {
            if (i < 0) return 1;
            if (dp[i][lead][isLimit] != -1) return dp[i][lead][isLimit];
            int up = isLimit ? (n >> i & 1) : 1;  
            // 前面没填数字就相当于填了0,这里也可填0
            int ans = f(i - 1, false, isLimit && up == 0); // 一开始的isLimit是true,n>>i&1是1
            if (!lead && up == 1) ans += f(i - 1, true, isLimit); // 还可填1
            return dp[i][lead][isLimit] = ans;
        };
        return f(m, false, true); // i从m往小枚举,方便位运算
    }
};

事实上,代码中只需要记忆化 ( i , l e a d ) (i,lead) (i,lead) 这个状态,因为:对于一个固定的 ( i , l e a d ) (i,lead) (i,lead)
,这个状态受到 i s L i m i t isLimit isLimit 的约束在整个递归过程中至多会出现一次,没必要记忆化。另外,如果只记忆化 ( i , l e a d ) (i,lead) (i,lead)​ , d p dp dp 数组的含义就变成在不受到约束时的合法方案数,所以要在 ! i s L i m i t !isLimit !isLimit 成立时才去记忆化。


解法2 动态规划+斐波那契数列(最优)

本题的一个简单版本是:求出所有 x x x 位长度的二进制字符串中,不含连续的 1 1 1 的字符串个数, d p [ 0 ] = 1 ,   d p [ 1 ] = 2 dp[0] = 1,\ dp[1] = 2 dp[0]=1, dp[1]=2 。类似于爬楼梯,设 d p [ i ] dp[i] dp[i] 为所有 i i i 位长二进制字符串中不含连续 1 1 1 的字符串个数,有 d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] dp[i] = dp[i - 1] + dp[i - 2] dp[i]=dp[i1]+dp[i2] ,即「 i i i 位为 0 0 0 的不含连续1的字符串个数」加上「 i i i 位为 1 1 1 的不含连续1的字符串个数」。

长度为 x x x 的二进制数字中「不含连续1的非负整数个数」满足斐波那契数列。下面从文法角度给出这个结论的证明方法。
本题所描述的非负整数,其二进制形式可以由以下文法给出:

S -> 10S | 0S | 0 | 1 | ε

可以看到,如果我们用字符串 S S S 的长度 L ( S ) L(S) L(S) 来构造动态规划状态表的话,可以有初始状态

dp[0] = 1 # 表示 S -> ε,长度为0的字符串有一个
dp[1] = 2 # 表示 S -> 0 | 1,长度为1的字符串有两个

根据两种长度的初始状态,以及递推式 S -> 10S | 0S ——该递推式表示长度为 x x x 的字符串,都可以通过以下两种方式获得:

  1. 在长度为 x − 2 x-2 x2 的字符串左侧加10;
  2. 在长度为 x − 1 x-1 x1 的字符串左侧加0。

可以生成所有满足条件的字符串,从而得到状态表的递推式

dp[x] = dp[x - 2] + dp[x - 1]

可以看到这个递推式就是斐波那契生成式,从而就有了开头的那条结论。

接着,要求小于等于 n n n 的非负整数二进制表示(长度为 x x x )中,不含连续 1 1 1 的个数(其实这里可以出得更困难一些,比如求非负整数范围 [ x , y ] [x, y] [x,y] 中不含连续 1 1 1 的二进制表示的个数)。在前面的 d p dp dp 数组基础上,以 n = ( 101010 ) 2 n = (101010)_2 n=(101010)2 为例解释解题步骤:

  • 答案设为 a n s = 0 ans = 0 ans=0 ,于是先求区间 ( 000000 ) 2 ∼ ( 011111 ) 2 (000000)_2 \sim (011111)_2 (000000)2(011111)2 内的答案个数,易知 ( 00000 ) 2 ∼ ( 11111 ) 2 (00000)_2 \sim (11111)_2 (00000)2(11111)2 的答案为 d p [ 5 ] dp[5] dp[5]
  • 再寻找区间 ( 100000 ) 2 ∼ ( 100111 ) 2 (100000)_2 \sim (100111)_2 (100000)2(100111)2 内的答案个数,易知 ( 000 ) 2 ∼ ( 111 ) 2 (000)_2 \sim (111)_2 (000)2(111)2 的答案为 d p [ 3 ] dp[3] dp[3]
  • 再寻找区间 ( 101000 ) 2 ∼ ( 101001 ) 2 (101000)_2 \sim (101001)_2 (101000)2(101001)2 内的答案个数,易知 ( 0 ) 2 ∼ ( 1 ) 2 (0)_2 \sim (1)_2 (0)2(1)2 的答案为 d p [ 1 ] dp[1] dp[1]
  • 最后,验证 ( 101010 ) 2 (101010)_2 (101010)2 这个数字本身是否合法,用 n   &   ( n > > 1 ) = = 0 n\ \&\ (n \gt\gt 1) == 0 n & (n>>1)==0 判断。
  • 而非「对 n n n 的长度为 x x x 的二进制表示」用 d p [ x ] dp[x] dp[x] 直接计算,从而将一些超出 n n n 大小的非负整数计算入内
  • 如果原来的数中就已经有连续的 1 1 1 ,如 n = ( 11 …   ) 2 n = (11\dots)_2 n=(11)2就只计算 ( 000 …   ) 2 ∼ ( 011 …   ) 2 (000\dots)_2 \sim (011\dots)_2 (000)2(011)2 ( 10 …   ) 2 ∼ ( 1011 …   ) 2 (10\dots)_2 \sim (1011\dots)_2 (10)2(1011)2 这两个区间的答案个数之和,再计算下去,计算的区间开头就是 11 11 11 ,就都不符合规则。

具体代码如下,需要仔细阅读:

class Solution {
public:
    int findIntegers(int n) {
        int dp[32] = {1, 2}; // dp[i]:长度为i的二进制字符串中不存在连续1的字符串个数
        // =当前位为0且长度为i-1的个数+当前位为1且长度为i-2的个数(根据情况参考已求出的答案)
        for (int i = 2; i < 32; ++i) dp[i] = dp[i - 1] + dp[i - 2];
        int ans = !(n & (n >> 1)); // 如果n不存在连续1,则n&(n>>1)=0,是答案之
        for (int i = std::__lg(n); i >= 0; --i) {
            if (n >> i & 1) {
                ans += dp[i];
                if (i && n >> (i - 1) & 1) { ans += dp[i - 1]; break; }
            }
        }
        return ans;
    }
};

运行效率如下:

执行用时:0 ms, 在所有 C++ 提交中击败了100.00% 的用户
内存消耗:5.9 MB, 在所有 C++ 提交中击败了51.50% 的用户

时间复杂度为 O ( 32 ) O(32) O(32) ,空间复杂度为 O ( 32 ) O(32) O(32)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

memcpy0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值