内容来源于自己的刷题笔记,对一些题目进行方法总结,用 java 语言实现。
11.剑指 Offer 13. 机器人的运动范围
-
题目描述:
地上有一个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 -
解题思路:
和上一道题目类似,这个方格也可以看作一个 m*n 的矩阵,同样的,在这个矩阵中,除了边界上的格子之外,其他格子都有 4 个相邻的格子。
机器人从坐标(0,0)开始移动,当它准备进入坐标为(i,j)的格子时,通过检查坐标的位数和来判断它能否进入 4 个相邻的格子(i,j-1),(i-1,j),(i,j+1),(i+1,j)。这里可以进行改进,由于位数和只有当机器人向下或者向右移动才会增加,也就是说如果 A 点能达到,那么 A 上面的点和左边的点肯定也能达到,于是我们只需要考虑另外两个坐标即可。
-
代码实现:
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. 剪绳子
-
题目描述:
给你一根长度为 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 -
解题思路:
首先对绳子的长度进行划分:
- 当 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的绳子。
-
代码实现:
/** * 动态规划 * * 当 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
-
题目描述:
给你一根长度为 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 -
解题思路:
与上一题不同的是,这个时候不能使用数组进行存储了,由于出现了大数的情况。
大数求余解法:大数越界: 当 a 增大时,最后返回的 3^a,大小以指数级别增长,可能超出 int32 甚至 int64 的取值范围,导致返回值错误。
大数求余问题: 在仅使用 int32 类型存储的前提下,正确计算 x^a,对p求余(即 x^a ⊙ p )的值。
解决大数问题有两种方法:循环求余和快速幂求余,但本质上都是基于贪心算法,后者会更快。
-
代码实现:
/** * 循环求余 * @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 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。
示例:
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 ‘1’。 -
解题思路:
- 暴力遍历:将该数转为字符数组,然后遍历这个数组,一旦发现元素为1,计数值加1。
- 位运算:
- 逐位判断:1 & 1 = 1,每一位都与1进行相与运算,一旦发现为1,计数值加1,这个时候可以用右移来减少数的位数。
- 巧用 (n - 1) & n:二进制数字 n 最右边的 1 变成 0 ,其余不变。
-
代码实现:
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. 数值的整数次方
-
题目描述:
实现函数 double Power(double base, int exponent),求 base 的 exponent 次方。不得使用库函数,同时不需要考虑大数问题。
示例:
输入: 2.00000, 10
输出: 1024.00000 -
解题思路:
快速幂:
当 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 ;
-
代码实现:
/** * 当 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; }