剑指 offer 篇:11 - 15

该博客是作者的刷题笔记,用Java语言对剑指Offer部分题目进行方法总结。涵盖机器人运动范围、剪绳子、二进制中1的个数、数值的整数次方等题目,介绍了各题的描述、示例,给出解题思路,如动态规划、贪心算法、位运算等,并提及代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

内容来源于自己的刷题笔记,对一些题目进行方法总结,用 java 语言实现。

11.剑指 Offer 13. 机器人的运动范围
  1. 题目描述:

    地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

    示例:

    输入:m = 2, n = 3, k = 1
    输出:3

  2. 解题思路:

    和上一道题目类似,这个方格也可以看作一个 m*n 的矩阵,同样的,在这个矩阵中,除了边界上的格子之外,其他格子都有 4 个相邻的格子。

    机器人从坐标(0,0)开始移动,当它准备进入坐标为(i,j)的格子时,通过检查坐标的位数和来判断它能否进入 4 个相邻的格子(i,j-1),(i-1,j),(i,j+1),(i+1,j)。这里可以进行改进,由于位数和只有当机器人向下或者向右移动才会增加,也就是说如果 A 点能达到,那么 A 上面的点和左边的点肯定也能达到,于是我们只需要考虑另外两个坐标即可。

  3. 代码实现:

    public int movingCount(int m, int n, int k) {
            if(m <= 0 || n <= 0 || k < 0){
                return 0;
            }
            boolean[] visited = new boolean[m*n];
            for (int i = 0;i < m*n;i ++){
                visited[i] = false;
            }
            int count = movingCountCore(k,m,n,0,0,visited);
            return count;
        }
    
        /**
         * 递归调用查找函数,我们在搜索的过程中搜索方向可以缩减为向右和向下,而不必再向上和向左进行搜索。
         * @param k
         * @param rows
         * @param cols
         * @param row
         * @param col
         * @param visited
         * @return
         */
        private int movingCountCore(int k, int rows, int cols, int row, int col, boolean[] visited) {
            int count = 0;
            if (check(k,rows,cols,row,col,visited)){
                visited[row*cols + col] = true;
                count = 1 + movingCountCore(k,rows,cols,row+1,col,visited) + movingCountCore(k,rows,cols,row,col+1,visited);
            }
            return count;
        }
    
        /**
         * 判断函数,判断机器人能否进入坐标(row,col)
         * @return
         */
        private boolean check(int k, int rows, int cols, int row, int col, boolean[] visited){
            if (row >= 0 && row < rows && col >= 0 && col < cols && !visited[row*cols+col] && (getDigitSum(row) + getDigitSum(col) <= k)){
                return true;
            }
            return false;
        }
    
        /**
         * 计算位数之和
         * @param number
         * @return
         */
        private int getDigitSum(int number){
            int sum = 0;
            while (number > 0){
                sum += number % 10;
                number /= 10;
            }
            return sum;
        }
    
12.剑指 Offer 14- I. 剪绳子
  1. 题目描述:

    给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n 都是整数,n>1并且 m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为 2、3、3 的三段,此时得到的最大乘积是 18。

    示例:

    输入: 2
    输出: 1
    解释: 2 = 1 + 1, 1 × 1 = 1

  2. 解题思路:

    首先对绳子的长度进行划分:

    • 当 n≤3 时,按照规则应不切分,但由于题目要求必须剪成 m>1 段,因此必须剪出一段长度为 1 的绳子,即返回 n - 1 。
    • 当 n>3 时,这个时候有两种思路:
      • 动态规划:定义一个数组,数组长度由绳子长度决定,数组中每个元素为当绳子长度为 n 时的乘积最大值,类似于循环的内部逻辑类似查找数组元素的最大值,一旦发现更大就替换。
      • 贪心算法:该解法较难想到,得对数学有一定的敏感性,首先我们需要意识到:长度大于3的绳子,可以看成长度为1,2,3的集合,但其中,哪个值多会比较好?由于绳子切成1的时候,只是浪费了一个乘数的位置,则排除;比较2和3,找它们的最小公倍数6,当6分成3个2和3个2的时候,前者更大,于是得出结论:在能整除的情况下,尽量让3的数量更多。将绳子分成 3*a + b,但这个时候需要考虑到一种特殊情况,如果分到最后,剩余4,就得按2个2进行切割。即:当 b=0 时,意味着被整除,都分割成3;当 b=1 时,意味着有剩下4的绳子,分割成2段2和剩下都为3的绳子;当 b=2 时,分割成1段2和剩下都为3的绳子。
  3. 代码实现:

    /**
         * 动态规划
         *
         * 当 n≤3 时,按照规则应不切分,但由于题目要求必须剪成 m>1 段,因此必须剪出一段长度为 1 的绳子,即返回 n - 1 。
         *
         * 子问题的最优解存储在数组 products 里,第 i 个元素表示把长度为 i 的绳子剪成若干段后各段长度乘积的最大值,即 f(i)
         * @param n
         * @return
         */
        public int cuttingRope(int n) {
            if (n <= 3){
                return n-1;
            }
            int[] products = new int[n+1];
            products[0] = 0;
            products[1] = 1;
            products[2] = 2;
            products[3] = 3;
            int max;
            for (int i = 4;i <= n;i++){
                max = 0;
                for (int j = 1;j <= i/2;j ++){
                    int product = products[j] * products[i-j];
                    if (max < product)
                        max = product;
                    products[i] = max;
                }
            }
            return products[n];
        }
    
        /**
         * 贪心算法
         *
         * 当 n≤3 时,按照规则应不切分,但由于题目要求必须剪成 m>1 段,因此必须剪出一段长度为 1的绳子,即返回 n - 1 。
         * 当 n>3 时,求 n 除以 3 的 整数部分 a 和 余数部分 b (即 n = 3a + b ),并分为以下三种情况:
         * 当 b = 0 时,直接返回 3^a
         * 当 b = 1 时,要将一个 1 + 3 转换为 2+2,因此返回 3^{a-1} * 4
         * 当 b = 2 时,返回 3^a * 2
         *
         * @param n
         * @return
         */
        public int cuttingRope1(int n) {
            if(n <= 3) return n - 1;
            int a = n / 3, b = n % 3;
            if(b == 0) return (int)Math.pow(3, a);
            if(b == 1) return (int)Math.pow(3, a - 1) * 4;
            return (int)Math.pow(3, a) * 2;
        }
    
13.剑指 Offer 14- II. 剪绳子 II
  1. 题目描述:

    给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m - 1] 。请问 k[0]k[1]…k[m - 1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

    答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

    示例:

    输入: 2
    输出: 1
    解释: 2 = 1 + 1, 1 × 1 = 1

  2. 解题思路:

    与上一题不同的是,这个时候不能使用数组进行存储了,由于出现了大数的情况。

    大数求余解法:大数越界: 当 a 增大时,最后返回的 3^a,大小以指数级别增长,可能超出 int32 甚至 int64 的取值范围,导致返回值错误。

    大数求余问题: 在仅使用 int32 类型存储的前提下,正确计算 x^a,对p求余(即 x^a ⊙ p )的值。

    解决大数问题有两种方法:循环求余和快速幂求余,但本质上都是基于贪心算法,后者会更快。

  3. 代码实现:

    /**
         * 循环求余
         * @param n
         * @return
         */
        public int cuttingRope(int n) {
            if(n <= 3) return n - 1;
            long res=1L;
            int p=(int)1e9+7;
            //贪心算法,优先切三,其次切二
            while(n>4){
                res=res*3%p;
                n-=3;
            }
            //出来循环只有三种情况,分别是n=2、3、4
            return (int)(res*n%p);
        }
    
        /**
         * 快速幂求余
         * @param n
         * @return
         */
        public int cuttingRope1(int n) {
            if(n <= 3)
                return n - 1;
            int b = n % 3, p = 1000000007;
            long ret = 1;
            int lineNums=n/3;           //线段被我们分成以3为大小的小线段个数
            for(int i=1;i<lineNums;i++) //从第一段线段开始验算,3的ret次方是否越界。注意是验算lineNums-1次。
                ret = 3*ret % p;
            if(b == 0)
                return (int)(ret * 3 % p);   //刚好被3整数的,要算上前一段
            if(b == 1)
                return (int)(ret * 4 % p);   //被3整数余1的,要算上前一段
            return (int)(ret * 6 % p);       //被3整数余2的,要算上前一段
        }
    
14.剑指 Offer 15. 二进制中1的个数
  1. 题目描述:

    请实现一个函数,输入一个整数(以二进制串形式),输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。

    示例:

    输入:00000000000000000000000000001011
    输出:3
    解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 ‘1’。

  2. 解题思路:

    • 暴力遍历:将该数转为字符数组,然后遍历这个数组,一旦发现元素为1,计数值加1。
    • 位运算:
      • 逐位判断:1 & 1 = 1,每一位都与1进行相与运算,一旦发现为1,计数值加1,这个时候可以用右移来减少数的位数。
      • 巧用 (n - 1) & n:二进制数字 n 最右边的 1 变成 0 ,其余不变。
  3. 代码实现:

    public int hammingWeight(int n) {
            String result = Integer.toBinaryString(n);
            int sum = 0;
            for (int i = 0;i < result.length();i++){
                char ch = result.charAt(i);
                if (ch == '1'){
                    sum++;
                }
            }
            return sum;
        }
    
        /**
         * 初始化数量统计变量 res = 0。
         * 循环逐位判断: 当 n = 0 时跳出。
         * res += n & 1 : 若 n&1=1 ,则统计数 res 加一。
         * n >>>= 1 : 将二进制数字 n 无符号右移一位( Java 中无符号右移为 ">>>" ) 。
         * 返回统计数量 res 。
         *
         * @param n
         * @return
         */
        public int hammingWeight1(int n){
            int res = 0;
            while(n != 0) {
                res += n & 1;
                n >>>= 1;
            }
            return res;
        }
    
        /**
         * 把一个整数减去1,再和原整数做与运算,会把该整数最右边的1变成0,其他逻辑与上一张情况一致。
         * @param n
         * @return
         */
        public int hammingWeight2(int n){
            int count = 0;
            while (n != 0){
                ++count;
                n = (n-1) & n;
            }
            return count;
        }
    
15.剑指 Offer 16. 数值的整数次方
  1. 题目描述:

    实现函数 double Power(double base, int exponent),求 base 的 exponent 次方。不得使用库函数,同时不需要考虑大数问题。

    示例:
    输入: 2.00000, 10
    输出: 1024.00000

  2. 解题思路:

    快速幂:

    当 n 为偶数: x^n = (x2){n//2}x ;
    当 n 为奇数: x^n = x(x2){n//2}x,即会多出一项 x ;

    根据二分推导,可通过循环 x = x^2 操作,每次把幂从 n 降至 n//2 ,直至将幂降为 0 ;
    设 res=1 ,则初始状态 x^n = x^n * res。在循环二分时,每当 n 为奇数时,将多出的一项 x 乘入 res ,则最终可化至 x^n = x^0 * res = res,返回 res 即可。

    将其转化为位运算:

    • 向下整除 n // 2 等价于 右移一位 n >> 1 ;
    • 取余数 n % 2 等价于 判断二进制最右一位值 n & 1 ;
  3. 代码实现:

    /**
         * 当 x = 0 时:直接返回 0 (避免后续 x = 1 / x 操作报错)。
         * 初始化 res = 1;
         * 当 n < 0 时:把问题转化至 n≥0 的范围内,即执行 x = 1/x,n = - n;
         * 循环计算:当 n = 0 时跳出;
             * 当 n&1=1 时:将当前 x 乘入 res (即 res *= x );
             * 执行 x = x^2x(即 x *= x);
             * 执行 n 右移一位(即 n >>= 1)。
         * 返回 res 。
         * @param x
         * @param n
         * @return
         */
        public double myPow(double x, int n) {
            if(x == 0) return 0;
            long b = n;
            double res = 1.0;
            if(b < 0) {
                x = 1 / x;
                b = -b;
            }
            while(b > 0) {
                if((b & 1) == 1) res *= x;
                x *= x;
                b >>= 1;
            }
            return res;
        }
    
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值