1.完全平方数(279 - 中)
题目描述:给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 :
输入:n = 12 输出:3 解释:12 = 4 + 4 + 4
思路:本题要求使用最少的数量,可以建立一个数组,元素的最大平方数不超过n,我们只需倒序进行搜索即可,使用深度优先遍历。
- 终止条件:n = 0,即n全部用完,更新最小值,注意当i<0,直接返回。
- 递归函数传入参数:(数组, 当前剩余值, 当前进行到的索引值,当前使用的完全平方数的个数)
注意:这里有两个优化
- 使用乘法加速获取当前元素的下一步的起始最大次数,如示例,12/4 = 3,在4这个完全平方数,下一次最大的次数为3。
- 倒序遍历过程中,对于完全平方数大于当前最小值ans的情况,进行剪枝。
代码实现:
private int ans = Integer.MAX_VALUE; public int numSquares(int n) { int num = (int)Math.sqrt(n); int[] nums = new int[num]; for (int i = 0; i < num; i++) { nums[i] = (i + 1) * (i + 1); } dfs(nums, n, nums.length - 1, 0); return ans == Integer.MAX_VALUE ? -1 : ans; } private void dfs(int[] nums, int n, int i, int count) { if (n == 0) { ans = Math.min(ans, count); return; } if (i < 0) return; int start = n / nums[i]; for (int k = start; k >= 0 && k + count < ans; --k) { dfs(nums, n - k * nums[i], i - 1, k + count); } }
2.平方数之和(633 - 中)
题目描述:给定一个非负整数 c
,你要判断是否存在两个整数 a
和 b
,使得 a^2 + b^2 = c
。
示例 :
输入:c = 5 输出:true 解释:1 * 1 + 2 * 2 = 5
思路:本题主要由两种解法,一种是类似两数之和的哈希解法,构建平方数数组;另一个则是像最大矩形面积的双指针解法,利用双指针寻找可能范围内的元素(最优解)。
注意:注意其中一个数可能是0,所以平方数组和循环条件要考虑边界。
代码实现:
// hash public boolean judgeSquareSum(int c) { int n = (int)Math.sqrt(c); int[] nums = new int[n + 1]; Set<Integer> set = new HashSet<>(); for (int i = 0; i <= n; ++i) { nums[i] = i * i; set.add(nums[i]); } for (int num : nums) { if (set.contains(c - num)) { return true; } } return false; } // 双指针 public boolean judgeSquareSum(int c) { int l = 0, r = (int)Math.sqrt(c); while (l <= r) { if (l * l + r * r == c) { return true; } else if (l * l + r * r > c) { r--; } else { l++; } } return false; }
3.鸡蛋掉落(887 - 难)
题目描述:给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。
已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。
每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。
请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?
示例 :
输入:k = 1, n = 2 输出:2 解释: 鸡蛋从 1 楼掉落。如果它碎了,肯定能得出 f = 0 。 否则,鸡蛋从 2 楼掉落。如果它碎了,肯定能得出 f = 1 。 如果它没碎,那么肯定能得出 f = 2 。 因此,在最坏的情况下我们需要移动 2 次以确定 f 是多少。
思路:这是一个经典动态规划问题,可以看一下李永乐老师的双蛋问题,比较通俗易懂。
dp[i][j]
:使用i
个鸡蛋,有j
层楼梯(注意:这里j
不表示高度,代表区间)的情况下,的最少实验的次数(约束条件)。- 第一个鸡蛋扔到第x层楼,两种状态:碎
dp[i - 1][x- 1]
或者没碎dp[i][j - x]
,最终枚举所有扔鸡蛋的情况,即枚举x。 - 对于上边找x的过程,我们可以通过二分查找进行优化。
dp[i - 1][x- 1]
是一个随x(层数)的增加而单调递增的函数,相反,dp[i][j - x]
随着层数x单调递减,我们的目标找这两函数的交点,类似于数组中找山峰/峡谷(最大/最小值),那么显然就是二分提升效率了。 - 第一个鸡蛋在哪里扔(也算一次操作),这也是状态转移方程中为什么+1.
代码实现:
public int superEggDrop(int k, int n) { // k 鸡蛋数 n 为楼层数 int[][] dp = new int[k + 1][n + 1]; for (int i = 1; i <= k; ++i) { dp[i][1] = 1; } for (int i = 1; i <= n; ++i) { dp[1][i] = i; } for(int i = 2; i <= k; ++i){ for(int j = 2; j <= n; ++j){ // 未使用二分的解法 x为所选楼层 // int min = Integer.MAX_VALUE; // for(int x = 1; x <= j; ++x){ // min = Math.min(min, Math.max(dp[i - 1][x - 1], dp[i][j - x])); // } // dp[i][j] = 1 + min; // 改用二分查找(第一次扔的层数,替换枚举) int l = 1, r = j; while(l + 1 < r){ int mid = l + r >> 1; if(dp[i-1][mid-1] > dp[i][j-mid]){ r = mid; } else { l = mid; } } int leftVal = Math.max(dp[i - 1][l - 1], dp[i][j - l]); int rightVal = Math.max(dp[i - 1][r - 1], dp[i][j - r]); dp[i][j] = 1 + Math.min(leftVal, rightVal); } } // for(int[] d:dp){ // System.out.println(Arrays.toString(d)); // } return dp[k][n]; }
4.打印从1到最大的n位数(补充)
题目描述:输入数字 n
,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
示例 :
输入: n = 1 输出: [1,2,3,4,5,6,7,8,9]
代码实现:
public int[] printNumbers(int n) { int x = (int)Math.pow(10, n); int[] nums = new int[x - 1]; for (int i = 0; i < x - 1; ++i) { nums[i] = i + 1; } return nums; }
5.自除数(728 - 易)
题目描述:自除数 是指可以被它包含的每一位数除尽的数。还有,自除数不允许包含 0 。128 是一个自除数,因为 128 % 1 == 0,128 % 2 == 0,128 % 8 == 0。
给定上边界和下边界数字,输出一个列表,列表的元素是边界(含边界)内所有的自除数。
示例 :
输入: 上边界left = 1, 下边界right = 22 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 15, 22]
代码实现:
public List<Integer> selfDividingNumbers(int left, int right) { List<Integer> ans = new ArrayList<>(); for (int i = left; i <= right; ++i) { if (selfDividingNumbers(i)) { ans.add(i); } } return ans; } // 是否为自除数 public boolean selfDividingNumbers(int num) { int i = num; while (i > 0) { int x = i % 10; if (x == 0 || num % x != 0) { return false; } i /= 10; } return true; }
6.各位相加(258 - 易)
题目描述:给定一个非负整数 num,反复将各个位上的数字相加,直到结果为一位数。
进阶: 你可以不使用循环或者递归,且在 O(1) 时间复杂度内解决这个问题吗?
示例 :
输入: 38 输出: 2 解释: 各位相加的过程为:3 + 8 = 11, 1 + 1 = 2。 由于 2 是一位数,所以返回 2。
思路:本题迭代比较简单,引入一个辅助变量tmp即可。
对于进阶问题,参考@wangliang大佬:
涉及数学上一个名词,数根。数根可以计算模运算的同余,对于非常大的数字的情况下可以节省很多时间。
数字根可作为一种检验计算正确性的方法。例如,两数字的和的数根等于两数字分别的数根的和。
另外,数根也可以用来判断数字的整除性,如果数根能被3或9整除,则原来的数也能被3或9整除。
上边的应用转化一下:
- 能够被9整除的整数,各位上的数字加起来也必然能被9整除,所以,连续累加起来,最终必然就是9。
- 不能被9整除的整数,各位上的数字加起来,结果对9取模,和初始数对9取摸,是一样的,所以,连续累加起来,最终必然就是初始数对9取摸。
看下边一个规律:
原数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 数根: 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3
结合上边的规律,对于给定的 n 有三种情况。
- n 是 0 ,数根就是 0。
- n 不是 9 的倍数,数根就是 n 对 9 取余,即 n % 9。
- n 是 9 的倍数,数根就是 9。
原数是 n
,树根就可以表示成 (n-1) % 9 + 1
,可以结合下边的过程理解。
原数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 偏移: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 取余: 0 1 2 3 4 5 6 7 8 0 1 2 3 4 5 6 7 8 0 1 2 3 4 5 6 7 8 0 1 2 数根: 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3
为什么要先偏移,再加1呢?假设如果可以被 9 整除,直接取模得到是0,按照数根的应用应该是9才对,我们可以进行偏移之后,再进行取余+1.
代码实现:
public int addDigits(int num) { if (num < 9) return num; int tmp = num; while (tmp > 9) { num = tmp; tmp = 0; while (num > 0) { tmp += num % 10; num /= 10; } } return tmp; } // 是否为自除数 public int addDigits(int num) { return (num - 1) % 9 + 1; }
7.范围求和II(598 - 易)
题目描述:给定一个初始元素全部为 0,大小为 m*n 的矩阵 M 以及在 M 上的一系列更新操作。
操作用二维数组表示,其中的每个操作用一个含有两个正整数 a 和 b 的数组表示,含义是将所有符合 0 <= i < a 以及 0 <= j < b 的元素 M[i][j]
的值都增加 1。
在执行给定的一系列操作后,你需要返回矩阵中含有最大整数的元素个数。
示例 :
输入: m = 3, n = 3 operations = [[2,2],[3,3]] 输出: 4 解释: 初始状态, M = [[0, 0, 0], [0, 0, 0], [0, 0, 0]] 执行完操作 [2,2] 后, M = [[1, 1, 0], [1, 1, 0], [0, 0, 0]] 执行完操作 [3,3] 后, M = [[2, 2, 1], [2, 2, 1], [1, 1, 1]] M 中最大的整数是 2, 而且 M 中有4个值为2的元素。因此返回 4。
思路:分析一下题目。可以确定的是最大值一定是 M[0][0]
,范围一定是数组中最小的行和最小的列。有了上边两个结论,因为本题只是统计个数。
注意:复用了变量m 和 n。
代码实现:
public int maxCount(int m, int n, int[][] ops) { for (int[] r : ops) { m = Math.min(m, r[0]); n = Math.min(n, r[1]); } return m * n; }