思路
由题意可知这是一道组合数字的题目,可以利用数学方法来实现
数学
对于数字n
,计它的位数为k
;
计digits
的长度为len
,表示该数组中有多少个数字可供使用,且digits
中的数字时单调递增的。
数字不重复且非递减排序,就是递增排序的。
首先我们知道:
- 对于数字
n
,任意一个位数小于k
的数显然都是比n
小的。 - 当两数字位数相同时,假设有:
n
=
a
0
a
1
.
.
.
a
k
−
1
n = {a_0}{a_1}...{a_{k-1}}
n=a0a1...ak−1,
n
u
m
=
b
0
b
1
.
.
.
b
k
−
1
num = {b_0}{b_1}...{b_{k-1}}
num=b0b1...bk−1,又可以分为以下几种情况,从第一位进行分析:
- 若
b
0
<
a
0
b_0 < a_0
b0<a0,则
b
1
,
b
2
,
.
.
.
,
b
k
−
1
b_1,b_2,...,b_{k-1}
b1,b2,...,bk−1可以使用任意数字,都不会影响
num < n
的结果; - 若 b 0 = a 0 b_0 = a_0 b0=a0,那么需要递归的去继续分析 a 1 . . . a k − 1 {a_1}...{a_{k-1}} a1...ak−1与 b 1 . . . b k − 1 {b_1}...{b_{k-1}} b1...bk−1;
- 若
b
0
>
a
0
b_0 > a_0
b0>a0,那么显然是不存在
num > n
的数字组合的。
- 若
b
0
<
a
0
b_0 < a_0
b0<a0,则
b
1
,
b
2
,
.
.
.
,
b
k
−
1
b_1,b_2,...,b_{k-1}
b1,b2,...,bk−1可以使用任意数字,都不会影响
因此可以设计算法:
- 首先初始化变量:将
digits
由字符串数组转换为整形数组、将数字n
转换为整形数组,数组中每一个元素是一个一位数 - 计算位数小于
k
的数字组合数量,有: l e n + l e n 2 + . . . + l e n ( k − 1 ) len + len^2 + ... + len^{(k-1)} len+len2+...+len(k−1)个 - 计算位数等于
k
的数字组合数量, n = a 0 a 1 . . . a k − 1 n = {a_0}{a_1}...{a_{k-1}} n=a0a1...ak−1, n u m = b 0 b 1 . . . b k − 1 num = {b_0}{b_1}...{b_{k-1}} num=b0b1...bk−1,按下标从低到高进行两种分析,固定下标idx
:- 首先计算得到
digits
中第一个大于 a i d x a_{idx} aidx的数下标i
,同时i
也表示digits
数组中元素小于 a i d x a_{idx} aidx的数量,并且此时 b i d x + 1 , b i d x + 2 , . . . , b k − 1 {b_{idx+1}},{b_{idx+2}},...,{b_{k-1}} bidx+1,bidx+2,...,bk−1都可以任意选择,此时共有: i ∗ l e n ( k − 1 − i d x ) i*len^{(k-1-idx)} i∗len(k−1−idx)种组合; - 若 d i g i t s [ i ] = = a i d x digits[i] == a_{idx} digits[i]==aidx,则还需要递归的继续分析 a i d x + 1 . . . a k − 1 {a_{idx+1}}...{a_{k-1}} aidx+1...ak−1与 b i d x + 1 . . . b k − 1 {b_{idx+1}}...{b_{k-1}} bidx+1...bk−1,从而得到该情况下的答案;否则可令此时的答案为0。
- 将上述两种情况累加后返回。
- 首先计算得到
从上述算法设计中,我们发现:
digits
数组是单调递增的,并且需要对该数组进行查询,所以可以利用二分查找加速;- 有多次需要计算
len
次幂的地方存在,可以打表用于后续查询。
实现代码如下:
class Solution {
private int[] table;
public int atMostNGivenDigitSet(String[] digits, int n) {
int ans = 0;
char[] chars = String.valueOf(n).toCharArray();
//n是一个k位整数
int k = chars.length;
table = new int[k];
int len = digits.length;
//将字符串转换为整型,方便进行查找
List<Integer> digitList = Arrays.stream(digits).map(Integer::parseInt).collect(Collectors.toList());
//显然任意的 1 到 k-1 位整数都小于n
//有 len + len^2 + ... + len^(k-1) 个
int x = len;
table[0] = 1;
for (int i = 1; i < k; i++) {
ans += x;
table[i] = x;
x *= len;
}
ans += dfs(0, chars, digitList);
return ans;
}
private int dfs(int idx, char[] chars, List<Integer> digitList){
if (idx == chars.length){
return 1;
}
//分为两种情况,下标idx处小于chars[idx]与等于chars[idx]的两种情况
int a=0, b=0;
int c = chars[idx] - '0';
//有i个数小于c
int i = biSearch(c, digitList);
a = i * table[chars.length-1-idx];
if (i<digitList.size() && digitList.get(i) == c){
b = dfs(idx+1, chars, digitList);
}
return a+b;
}
//返回第一个大于等于val的下标
private int biSearch(int val, List<Integer> list){
int l = 0, r = list.size() - 1;
int mid;
while(l <= r){
mid = l + (r - l) / 2;
if (list.get(mid) < val){
l = mid + 1;
} else {
r = mid - 1;
}
}
return l;
}
}
该方法能实现的原因是
digits
数组中没有0
.
若存在0
,还需要进行额外的判断,代码会更乱。
动态规划
数位DP?参考官方题解的思路,思路见官方题解。
class Solution {
public int atMostNGivenDigitSet(String[] digits, int n) {
List<Integer> digitList = Arrays.stream(digits).map(Integer::parseInt).collect(Collectors.toList());
char[] chars = String.valueOf(n).toCharArray();
//n是一个k位整数
int k = chars.length;
int m = digits.length;
// dp[i][0]表示由digits构成且小于n的前i位的数字的个数
// dp[i][1]表示由digits构成且等于n的前i位的数字的个数
int[][] dp = new int[k+1][2];
//初始化dp[0][1] = 1
dp[0][1] = 1;
for (int i = 1; i <= k; i++) {
int c = chars[i-1] - '0'; //第i个数字
for (int j = 0; j < m; j++) {
//从digits中找满足条件的数
if(digitList.get(j) == c){
dp[i][1] = dp[i-1][1];
} else if (digitList.get(j) < c) {
//前面的数都是相等的,找到有多少个小于的
dp[i][0] += dp[i-1][1];
} else {
break;
}
}
if (i > 1){
// m * dp[i-1][0] 部分表示 dp[i-1][0]代表的数num * 10 + digits[j] 都是满足条件的
// + m 表示digits中数字直接作为一个数时显然也是满足条件的
dp[i][0] += m * dp[i-1][0] + m;
}
}
return dp[k][0] + dp[k][1];
}
}