朴素解法 & 动态规划,完整 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二维数组存储和处理多个学生的各科成绩,包括成绩的输入、存储及格式化输出,适合初学者实践Java基础知识。
94 1
|
1天前
|
缓存 算法 搜索推荐
Java中的算法优化与复杂度分析
在Java开发中,理解和优化算法的时间复杂度和空间复杂度是提升程序性能的关键。通过合理选择数据结构、避免重复计算、应用分治法等策略,可以显著提高算法效率。在实际开发中,应该根据具体需求和场景,选择合适的优化方法,从而编写出高效、可靠的代码。
15 6
|
24天前
|
监控 算法 Java
jvm-48-java 变更导致压测应用性能下降,如何分析定位原因?
【11月更文挑战第17天】当JVM相关变更导致压测应用性能下降时,可通过检查变更内容(如JVM参数、Java版本、代码变更)、收集性能监控数据(使用JVM监控工具、应用性能监控工具、系统资源监控)、分析垃圾回收情况(GC日志分析、内存泄漏检查)、分析线程和锁(线程状态分析、锁竞争分析)及分析代码执行路径(使用代码性能分析工具、代码审查)等步骤来定位和解决问题。
|
1月前
|
Java 编译器 程序员
Java面试高频题:用最优解法算出2乘以8!
本文探讨了面试中一个看似简单的数学问题——如何高效计算2×8。从直接使用乘法、位运算优化、编译器优化、加法实现到大整数场景下的处理,全面解析了不同方法的原理和适用场景,帮助读者深入理解计算效率优化的重要性。
34 6
|
1月前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
66 2
|
1月前
|
Java 关系型数据库 数据库
面向对象设计原则在Java中的实现与案例分析
【10月更文挑战第25天】本文通过Java语言的具体实现和案例分析,详细介绍了面向对象设计的五大核心原则:单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则。这些原则帮助开发者构建更加灵活、可维护和可扩展的系统,不仅适用于Java,也适用于其他面向对象编程语言。
37 2
|
2月前
|
存储 Java 编译器
[Java]基本数据类型与引用类型赋值的底层分析
本文详细分析了Java中不同类型引用的存储方式,包括int、Integer、int[]、Integer[]等,并探讨了byte与其他类型间的转换及String的相关特性。文章通过多个示例解释了引用和对象的存储位置,以及字符串常量池的使用。此外,还对比了String和StringBuilder的性能差异,帮助读者深入理解Java内存管理机制。
28 0
|
1天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
1天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
2天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
12 3