朴素解法 & 动态规划,完整 DP 分析思路 | Java 刷题打卡

简介: 朴素解法 & 动态规划,完整 DP 分析思路 | Java 刷题打卡

网络异常,图片无法展示
|


题目描述



这是 LeetCode 上的 338. 比特位计数


Tag : 「位运算」、「数学」、「线性 DP」


给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。


示例 1:


输入: 2
输出: [0,1,1]
复制代码


示例 2:


输入: 5
输出: [0,1,1,2,1,2]
复制代码


进阶:


  • 给出时间复杂度为 O(n*sizeof(integer))O(nsizeof(integer)) 的解答非常容易。但你可以在线性时间 O(n)O(n) 内用一趟扫描做到吗?
  • 要求算法的空间复杂度为 O(n)O(n)
  • 你能进一步完善解法吗?要求在C++或任何其他语言中不使用任何内置函数(如 C++ 中的 __builtin_popcount)来执行此操作。


朴素解法



这道题要对每个数进行统计,因此不会有比 O(n)O(n) 更低的做法。


而很容易想到的朴素做法是对每个数进行「位运算」计数,每个数都是 32 位的,因此是一个 O(32n)O(32n) 的做法。


32 作为常数不算大,OJ 要想做到卡掉 O(32n)O(32n),同时确保 O(n)O(n) 都能过,需要把数据出到 10^6106


根据在 LeetCode 的做题经验,通常 10^6106 是属于必须要进行说明的数据范围了,但是本题没有,所以我估摸着数据范围最多也就 10^4104 - 10^5105


PS. 规范的题目应该无论数据范围是多少,都应该进行说明。


class Solution {
    public int[] countBits(int n) {
        int[] ans = new int[n + 1];
        for (int i = 0; i <= n; i++) ans[i] = getCnt(i);
        return ans;
    }
    int getCnt(int u) {
        int ans = 0;
        for (int i = 0; i < 32; i++) ans += (u >> i) & 1;
        return ans;
    }
}
复制代码


  • 时间复杂度:O(n)O(n)
  • 空间复杂度:使用与输入同等规模的空间存储答案。复杂度为 O(n)O(n)


动态规划



事实上,这道题是有严格 O(n)O(n) 的解法的,要求 O(n)O(n) 复杂度又是输出方案的题,通常就是递推的 DP 题。


用已算出的值去凑出要算的值。


那么对于这类问题我们该如何考虑呢?一般是靠经验,如果实在没见过这类题型的话,我们就需要在纸上画一下,分析一下我们朴素做法的最后一步是怎么进行的。


不失一般性的,假设当前我要统计的数的 ii 对应的二进制表示是

00000...0010100101(共 32 位)


如果我们是使用「朴素解法」求解的话,无论是从高位进行统计,还是从低位进行统计,最后一位扫描的都是边缘的数(如果是 1 就计数,不是 1 就不计数)。


  • 从低位到高位,最后一步在扫描最高位之前,统计出 1 的个数应该等同于将 i 左移一位,并在最低位补 0,也就是等于 ans[i << 1],这时候就要求我们在计算 i 的时候 i << 1 已经被算出来(从大到小遍历)
  • 从高位到低位,最后一步在扫描最低位之前,统计出 1 的个数应该等同于将 i 右移一位,并在最高位补 0,也就是等于 ans[i >> 1],这时候就要求我们在计算 i 的时候 i >> 1 已经被算出来(从小到大遍历)


通过对「朴素做法」的最后一步分析,转移方程就出来了:


  • 从大到小遍历f(i) = f(i << 1) + ((i >>31 ) \& 1)f(i)=f(i<<1)+((i>>31)&1)
  • 从小到大遍历f(i) = f(i >> 1) + ( i \& 1 )f(i)=f(i>>1)+(i&1)


class Solution {
    // 从小到大遍历
    public int[] countBits(int n) {
        int[] ans = new int[n + 1];
        // ans[i] = 「i >> 1 所包含的 1 的个数」+「i 的最低位是否为 1」
        for (int i = 1; i <= n; i++) ans[i] = ans[i >> 1] + (i & 1);
        return ans;
    }
}
复制代码


class Solution {
    // 从大到小遍历
    public int[] countBits(int n) {
        int[] ans = new int[n + 1];
        for (int i = n; i >= 0; i--) {
            // 如果计算 i 所需要的 i << 1 超过 n,则不存储在 ans 内,需要额外计算
            int u = i << 1 <= n ? ans[i << 1] : getCnt(i << 1);
            // ans[i] =「i << 1 所包含的 1 的个数」 + 「i 的最高位是否为 1」
            ans[i] = u + ((i >> 31) & 1);
        } 
        return ans;
    }
    int getCnt(int u) {
        int ans = 0;
        for (int i = 0; i < 32; i++) ans += (u >> i) & 1;
        return ans;
    }
}
复制代码


  • 时间复杂度:O(n)O(n)
  • 空间复杂度:使用与输入同等规模的空间存储答案。复杂度为 O(n)O(n)


位运算说明



  • a >> b & 1 代表检查 a 的第 b 位是否为 1,有两种可能性 0 或者 1
  • a += 1 << b 代表将 a 的第 b 位设置为 1 (当第 b 位为 0 的时候适用)


如不想写对第 b 位为 0 的前置判断,a += 1 << b 也可以改成 a |= 1 << b


PS. 1 的二进制就是最低位为 1,其他位为 0 哦


在之前的题解我就强调过,以上两个操作在位运算中使用频率超高,建议每位同学都加深理解


最后



这是我们「刷穿 LeetCode」系列文章的第 No.338 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先将所有不带锁的题目刷完。


在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。


为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…


在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

相关文章
|
2月前
|
存储 Java 大数据
Java 大视界 -- Java 大数据在智能家居能源消耗模式分析与节能策略制定中的应用(198)
简介:本文探讨Java大数据技术在智能家居能源消耗分析与节能策略中的应用。通过数据采集、存储与智能分析,构建能耗模型,挖掘用电模式,制定设备调度策略,实现节能目标。结合实际案例,展示Java大数据在智能家居节能中的关键作用。
|
3月前
|
数据采集 搜索推荐 算法
Java 大视界 -- Java 大数据在智能教育学习社区用户互动分析与社区活跃度提升中的应用(274)
本文系统阐述 Java 大数据技术在智能教育学习社区中的深度应用,涵盖数据采集架构、核心分析算法、活跃度提升策略及前沿技术探索,为教育数字化转型提供完整技术解决方案。
|
3月前
|
Java 数据库连接 API
互联网大厂校招 JAVA 工程师笔试题解析及常见考点分析
本文深入解析互联网大厂校招Java工程师笔试题,涵盖基础知识(数据类型、流程控制)、面向对象编程(类与对象、继承与多态)、数据结构与算法(数组、链表、排序算法)、异常处理、集合框架、Java 8+新特性(Lambda表达式、Stream API)、多线程与并发、IO与NIO、数据库操作(JDBC、ORM框架MyBatis)及Spring框架基础(IoC、DI、AOP)。通过技术方案讲解与实例演示,助你掌握核心考点,提升解题能力。
159 2
|
4月前
|
人工智能 Java
Java参数传递分析
本文详细探讨了Java中参数传递的机制,明确指出Java采用的是值传递而非引用传递。通过基本数据类型(如int)和引用类型(如Map、自定义对象People)的实例测试,证明方法内部对参数的修改不会影响原始变量。即使在涉及赋值返回的操作中,表面上看似引用传递,实际仍是值传递的结果。文中结合代码示例与执行结果,深入解析了值传递的本质及容易引起混淆的情形,帮助读者准确理解Java参数传递的核心概念。
|
传感器 分布式计算 安全
Java 大视界 -- Java 大数据在智能安防入侵检测系统中的多源数据融合与分析技术(171)
本文围绕 Java 大数据在智能安防入侵检测系统中的应用展开,剖析系统现状与挑战,阐释多源数据融合及分析技术,结合案例与代码给出实操方案,提升入侵检测效能。
|
4月前
|
缓存 安全 Java
【高薪程序员必看】万字长文拆解Java并发编程!(3-1):并发共享问题的解决与分析
活锁:多个线程相互影响对方退出同步代码块的条件而导致线程一直运行的情况。例如,线程1的退出条件是count=5,而线程2和线程3在其代码块中不断地是count进行自增自减的操作,导致线程1永远运行。内存一致性问题:由于JIT即时编译器对缓存的优化和指令重排等造成的内存可见性和有序性问题,可以通过synchronized,volatile,并发集合类等机制来解决。这里的线程安全是指,多个线程调用它们同一个实例的方法时,是线程安全的,但仅仅能保证当前调用的方法是线程安全的,不同方法之间是线程不安全的。
86 0
|
4月前
|
Java 程序员
【高薪程序员必看】万字长文拆解Java并发编程!(3-2):并发共享问题的解决与分析
wait方法和notify方法都是Object类的方法:让当前获取锁的线程进入waiting状态,并进入waitlist队列:让当前获取锁的线程进入waiting状态,并进入waitlist队列,等待n秒后自动唤醒:在waitlist队列中挑一个线程唤醒:唤醒所有在waitlist队列中的线程它们都是之间协作的手段,只有拥有对象锁的线程才能调用这些方法,否则会出现IllegalMonitorStateException异常park方法和unpark方法是LockSupport类中的方法。
88 0
|
2月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
124 0
|
2月前
|
Java API 调度
从阻塞到畅通:Java虚拟线程开启并发新纪元
从阻塞到畅通:Java虚拟线程开启并发新纪元
282 83

热门文章

最新文章