https://leetcode.com/problems/numbers-at-most-n-given-digit-set/
https://zhanghuimeng.github.io/post/leetcode-902-numbers-at-most-n-given-digit-set/
数学方法
https://leetcode.com/problems/numbers-at-most-n-given-digit-set/discuss/168313/Java-Solution-with-explanation
Time complexity: O(|D|^log10(N))
We have a sorted set of digits
D
, a non-empty subset of {'1','2','3','4','5','6','7','8','9'}
. (Note that '0'
is not included.)
Now, we write numbers using these digits, using each digit as many times as we want. For example, if
D = {'1','3','5'}
, we may write numbers such as '13', '551', '1351315'
.
Return the number of positive integers that can be written (using the digits of
D
) that are less than or equal to N
.
Example 1:
Input: D = ["1","3","5","7"], N = 100 Output: 20 Explanation: The 20 numbers that can be written are: 1, 3, 5, 7, 11, 13, 15, 17, 31, 33, 35, 37, 51, 53, 55, 57, 71, 73, 75, 77.
Example 2:
Input: D = ["1","4","9"], N = 1000000000 Output: 29523 Explanation: We can write 3 one digit numbers, 9 two digit numbers, 27 three digit numbers, 81 four digit numbers, 243 five digit numbers, 729 six digit numbers, 2187 seven digit numbers, 6561 eight digit numbers, and 19683 nine digit numbers. In total, this is 29523 integers that can be written using the digits of D.
Approach 1: Dynamic Programming + Counting
假设数字 N 有 L 位,D 总共有 n 个数字,那么低于 L 位数的所有数字都可以随意拼成,这一部分总共有 ∑L−1i=1ni∑i=1L−1ni 个整数可以拼成。
接下来从数字 N 的最高位开始分析(第 L 位到第 1 位),如果 D 中没有数字小于等于当前位 s[i],则可以直接返回答案;
若 D 中有数字等于当前位 s[i],且有 j 个数字小于当前位 s[i],则当前答案统计 j⋅ni−1j⋅ni−1;
若 D 中没有数字等于当前为 s[i],且有 j 个数字小于当前位 s[i],则当前答案统计 j⋅ni−1j⋅ni−1,然后返回答案。
最后若退出了循环而没有返回,返回答案加一,这是因为最后等于 N 的数字需要被统计。
时间复杂度
每个数位被遍历常数次,故时间复杂度为 O(logN)O(logN)。
Tips
这类数位统计题的一般技巧是从高位开始,累加小于当前数字的所有情况,然后令当前位和限制的数字相同,继续往低位累加。
(1)首先明确一个问题,就是给的数字集合 D 是不重复的,感觉这一点还是很重要的。
(2)假设数字 N 为 K 位, 很显然,K-1 的数字一定比它小,那么能组成 K-1 位的数有多少那?答案为(K-1)^(len(D)),理解就是每一位有 len(D) 种选择,所以是它的次方个。
(3)由(2)可以求出位数为1 - (K-1) 位 的数的个数了,现在位数为 K,其比 N 小的数字怎么求?动态规划方法。
(4)–求位数为K且小于 N 的数字个数–, 思路:
(5)从数据集D中依次拿出 一个数去和N的最高位比较,如果比它小,后面各个位上的数字可以任意组合,通过求次方就可以得到。
(6)如果比它小,很显然,后面再怎么组合也是不符合要求的,所以直接舍弃。
(7)如果相等,现在也很显然,竟然最高位相等,那么就可以直接等于次高位符合要求的个数(形成子问题 – 动态规划的一个必要条件)。然后把所有的累加,就是位数为 K 且小于N的数字个数。
dp状态方程如下:
设:dp[i:] 表示,从左向开始,第 i 个数字表示最高位时的,符合要求的数字个数。
for i in range(K-1, -1, -1):
for d in D:
if d < S[i]:
dp[i] += len(D) ** (K-i-1)
elif d == S[i]:
dp[i] += dp[i+1]
Time complexity: O(log10(N))
Space complexity: O(1)
Suppose N has n digits.
less than n digits
We can use all the numbers from D to construct numbers of with length 1,2,…,n-1 which are guaranteed to be less than N.
e.g. n = 52125, D = [1, 2, 5]
format X: e.g. 1, 2, 5 counts = |D| ^ 1
format XX: e.g. 11,12,15,21,22,25,51,52,55, counts = |D|^2
format XXX: counts = |D|^3
format XXXX: counts = |D|^4
exact n digits
if all numbers in D != N[0], counts = |d < N[0] | d in D| * |D|^(n-1), and we are done.
e.g. N = 34567, D = [1,2,8]
we can make:
- X |3|^1
- XX |3| ^ 2
- XXX |3| ^ 3
- XXXX |3| ^ 3
- 1XXXX, |3|^4
- 2XXXX, |3|^4
- we can’t do 8XXXX
Total = (3^1 + 3^2 + 3^3 + 3^4) + 2 * |3|^ 4 = 120 + 162 = 282
N = 52525, D = [1,2,5]
However, if d = N[i], we need to check the next digit…
- X |3|^1
- XX |3| ^ 2
- XXX |3| ^ 3
- XXXX |3| ^ 3
- 1XXXX, |3|^4
- 2XXXX, |3|^4
- 5????
- 51XXX |3|^3
- 52???
- 521XX |3|^2
- 522XX |3|^2
- 525??
- 5251X |3|^1
- 5252?
- 52521 |3|^0
- 52522 |3|^0
- 52525 +1
total = (120) + 2 * |3|^4 + |3|^3 + 2*|3|^2 + |3|^1 + 2 * |3|^0 + 1 = 120 + 213 = 333
if every digit of N is from D, then we also have a valid solution, thus need to + 1
public int atMostNGivenDigitSet(String[] D, int N) {
char[] s = String.valueOf(N).toCharArray();
int n = s.length;
int ans = 0;
for (int i = 1; i < n; ++i)
ans += (int)Math.pow(D.length, i);
for (int i = 0; i < n; ++i) {
boolean prefix = false;
for (String d : D) {
if (d.charAt(0) < s[i]) {
ans += Math.pow(D.length, n - i - 1);
} else if (d.charAt(0) == s[i]) {
prefix = true;
break;
}
}
if (!prefix) return ans;
}
return ans + 1;
}
(数位统计) O(logN)O(logN)假设数字 N 有 L 位,D 总共有 n 个数字,那么低于 L 位数的所有数字都可以随意拼成,这一部分总共有 ∑L−1i=1ni∑i=1L−1ni 个整数可以拼成。
接下来从数字 N 的最高位开始分析(第 L 位到第 1 位),如果 D 中没有数字小于等于当前位 s[i],则可以直接返回答案;
若 D 中有数字等于当前位 s[i],且有 j 个数字小于当前位 s[i],则当前答案统计 j⋅ni−1j⋅ni−1;
若 D 中没有数字等于当前为 s[i],且有 j 个数字小于当前位 s[i],则当前答案统计 j⋅ni−1j⋅ni−1,然后返回答案。
最后若退出了循环而没有返回,返回答案加一,这是因为最后等于 N 的数字需要被统计。
时间复杂度
每个数位被遍历常数次,故时间复杂度为 O(logN)O(logN)。
Tips
这类数位统计题的一般技巧是从高位开始,累加小于当前数字的所有情况,然后令当前位和限制的数字相同,继续往低位累加。
(1)首先明确一个问题,就是给的数字集合 D 是不重复的,感觉这一点还是很重要的。
(2)假设数字 N 为 K 位, 很显然,K-1 的数字一定比它小,那么能组成 K-1 位的数有多少那?答案为(K-1)^(len(D)),理解就是每一位有 len(D) 种选择,所以是它的次方个。
(3)由(2)可以求出位数为1 - (K-1) 位 的数的个数了,现在位数为 K,其比 N 小的数字怎么求?动态规划方法。
(4)–求位数为K且小于 N 的数字个数–, 思路:
(5)从数据集D中依次拿出 一个数去和N的最高位比较,如果比它小,后面各个位上的数字可以任意组合,通过求次方就可以得到。
(6)如果比它小,很显然,后面再怎么组合也是不符合要求的,所以直接舍弃。
(7)如果相等,现在也很显然,竟然最高位相等,那么就可以直接等于次高位符合要求的个数(形成子问题 – 动态规划的一个必要条件)。然后把所有的累加,就是位数为 K 且小于N的数字个数。
dp状态方程如下:
设:dp[i:] 表示,从左向开始,第 i 个数字表示最高位时的,符合要求的数字个数。
for i in range(K-1, -1, -1):
for d in D:
if d < S[i]:
dp[i] += len(D) ** (K-i-1)
elif d == S[i]:
dp[i] += dp[i+1]
我想,第一个问题是,这到底是一个统计问题,还是一个计算问题。考虑到
N
的范围,显然不能直接遍历所有数进行统计,而是需要进行某种程度的计算。也许会涉及到组合数。(虽然事实是没有)
第二个问题是正着做还是反着做——计算只包含这些数字的数的个数,还是计算不只包含这些数字的数的个数?显然我比赛的时候完全没有想清楚这一点,但就这道题来说,大概计算包含更简单一点。
如何寻找
<=N
的合法的数?显然可以根据数的长度进行分类讨论。[1]对于长度小于len(N)
的那些,显然D
中数字的任意组合均是合法的(这一点我之前居然没有意识到……);对于长度和len(N)
相等的那些,则需要分类讨论:- 如果该数首位比
N
的首位小,则之后的数字可以任意组合;如果该数首位和N
的首位相等(当然前提是N
的首位也是一个合法数字),则需要继续讨论第2位 - 如果该数第2位比
N
的第2位小,则之后的数字可以任意组合;如果该数第2位和N
的第2位相等(当然前提是N
的第2位也是一个合法数字),则需要继续讨论第3位 - 以此类推……
于是我们就得到了一个递推算法:令
f[i]
表示<= N[i:]
的数的数量(i
从1开始编号),则1
| f[i] = (number of digits < N[i]) * (D.size)^(len - i) + (N[i] in D) * f[i + 1]
|
且最终的结果为
1
| f[1] + (D.size)^(len - 1) + (D.size)^(len - 2) + ... + D.size
|
First, call a positive integer
X
valid if X <= N
and X
only consists of digits from D
. Our goal is to find the number of valid integers.
Say
N
has K
digits. If we write a valid number with k
digits (k < K
), then there are possible numbers we could write, since all of them will definitely be less than N
.
Now, say we are to write a valid
K
digit number from left to right. For example, N = 2345
, K = 4
, and D = '1', '2', ..., '9'
. Let's consider what happens when we write the first digit.- If the first digit we write is less than the first digit of
N
, then we could write any numbers after, for a total of valid numbers from this one-digit prefix. In our example, if we start with1
, we could write any of the numbers1111
to1999
from this prefix. - If the first digit we write is the same, then we require that the next digit we write is equal to or lower than the next digit in
N
. In our example (withN = 2345
), if we start with2
, the next digit we write must be3
or less. - We can't write a larger digit, because if we started with eg.
3
, then even a number of3000
is definitely larger thanN
.
Algorithm
Let
dp[i]
be the number of ways to write a valid number if N
became N[i], N[i+1], ...
. For example, if N = 2345
, then dp[0]
would be the number of valid numbers at most 2345
, dp[1]
would be the ones at most 345
, dp[2]
would be the ones at most 45
, and dp[3]
would be the ones at most 5
.
Then, by our reasoning above,
dp[i] = (number of d in D with d < S[i]) * ((D.length) ** (K-i-1))
, plus dp[i+1]
if S[i]
is in D
.
public int atMostNGivenDigitSet(String[] D, int N) {
String S = String.valueOf(N);
int K = S.length();
int[] dp = new int[K + 1];
dp[K] = 1;
for (int i = K - 1; i >= 0; --i) {
// compute dp[i]
int Si = S.charAt(i) - '0';
for (String d : D) {
if (Integer.valueOf(d) < Si)
dp[i] += Math.pow(D.length, K - i - 1);
else if (Integer.valueOf(d) == Si)
dp[i] += dp[i + 1];
}
}
for (int i = 1; i < K; ++i)
dp[0] += Math.pow(D.length, i);
return dp[0];
}
As in Approach #1, call a positive integer
X
valid if X <= N
and X
only consists of digits from D
.
Now let
B = D.length
. There is a bijection between valid integers and so called "bijective-base-B
" numbers. For example, if D = ['1', '3', '5', '7']
, then we could write the numbers '1', '3', '5', '7', '11', '13', '15', '17', '31', ...
as (bijective-base-B
) numbers '1', '2', '3', '4', '11', '12', '13', '14', '21', ...
.
It is clear that both of these sequences are increasing, which means that the first sequence is a contiguous block of valid numbers, followed by invalid numbers.
Our approach is to find the largest valid integer, and convert it into bijective-base-
B
from which it is easy to find its rank (position in the sequence.) Because of the bijection, the rank of this element must be the number of valid integers.
Continuing our example, if
N = 64
, then the valid numbers are '1', '3', ..., '55', '57'
, which can be written as bijective-base-4 numbers '1', '2', ..., '33', '34'
. Converting this last entry '34'
to decimal, the answer is 16
(3 * 4 + 4).
Algorithm
Let's convert
N
into the largest possible valid integer X
, convert X
to bijective-base-B, then convert that result to a decimal answer. The last two conversions are relatively straightforward, so let's focus on the first part of the task.
Let's try to write
X
one digit at a time. Let's walk through an example where D = ['2', '4', '6', '8']
. There are some cases:- If the first digit of
N
is inD
, we write that digit and continue. For example, ifN = 25123
, then we will write2
and continue. - If the first digit of
N
is larger thanmin(D)
, then we write the largest possible number fromD
less than that digit, and the rest of the numbers will be big. For example, ifN = 5123
, then we will write4888
(4
then888
). - If the first digit of
N
is smaller thanmin(D)
, then we must "subtract 1" (in terms ofX
's bijective-base-B representation), and the rest of the numbers will be big.For example, ifN = 123
, we will write88
. IfN = 4123
, we will write2888
. And ifN = 22123
, we will write8888
. This is because "subtracting 1" from'', '4', '22'
yields'', '2', '8'
(can't go below 0).
Actually, in our solution, it is easier to write in bijective-base-B, so instead of writing digits of
D
, we'll write the index of those digits (1-indexed). For example, X = 24888
will be A = [1, 2, 4, 4, 4]
. Afterwards, we convert this to decimal.- Time Complexity: , and assuming is constant.
- Space Complexity: , the space used by
A
.
public int atMostNGivenDigitSet(String[] D, int N) {
int B = D.length; // bijective-base B
char[] ca = String.valueOf(N).toCharArray();
int K = ca.length;
int[] A = new int[K];
int t = 0;
for (char c : ca) {
int c_index = 0; // Largest such that c >= D[c_index - 1]
boolean match = false;
for (int i = 0; i < B; ++i) {
if (c >= D[i].charAt(0))
c_index = i + 1;
if (c == D[i].charAt(0))
match = true;
}
A[t++] = c_index;
if (match)
continue;
if (c_index == 0) { // subtract 1
for (int j = t - 1; j > 0; --j) {
if (A[j] > 0)
break;
A[j] += B;
A[j - 1]--;
}
}
while (t < K)
A[t++] = B;
break;
}
int ans = 0;
for (int x : A)
ans = ans * B + x;
return ans;
}
https://zhanghuimeng.github.io/post/leetcode-902-numbers-at-most-n-given-digit-set/
数学方法
这是一种很神奇的方法。我们不妨把
D
中的数字看成是一种新的进位表示。比如,当D = {1, 3, 5}
时,就可以把[11, 13, 51, 53]
这类的数写成[11, 12, 31, 32]
。如果能够知道<= N
的最大的这种进位表示,就可以直接计算合法的数的数量了。所以剩下的问题就是怎么找到这个进位表示。
一种方法是根据
N
的位,从高位到低位分别进行讨论。令B
表示所需的进位表示,1 <= B[i] <= D.size, 1 <= i <= len(N)
:- 如果
N[i] in D && N[i] = D[j]
,则B[i] = j
- 如果
N[i] > min(D)
,则B[i] = lower_bound(D, N[i])
,B[i+1:] = len(D)
,结束 - 如果
N[i] < min(D)
,则B[i] = 0
,并对之前的数执行类似于退位的操作,B[i+1:] = len(D)
,结束
其他情况是比较显然的。一个需要退位的例子:
D = {2, 4, 6, 8}
,N = 41265
B[1] = 2, "4"
B[2] = 0
;执行退位操作后,B[1] = 1, B[2] = 4, B[3:5] = 4, "28888"
找到这个进位表示之后就可以直接计算出比它小的数的数量了。事实上这种表示方法类似于Leetcode 171,同样没有0的位置。[1]
- N has n digits, so all numbers less than n digits are valid, which are: sum(len(D) ** i for i in range(1, n))
- The loop is to deal with all numbers with n digits, considering from N[0], N[1] back to N[n-1]. For example, N[0] is valid only for c in D if c <= N[0]. If c < N[0], then N[1], ..., N[n-1] can take any number in D but if c == N[0], then we need consider N[1], and the iteration repeats. That's why if N[i] not in D, then we don't need to repeat the loop anymore.
- Finally i==n is addressed at the end when there exists all c in D that matches N
https://leetcode.com/problems/numbers-at-most-n-given-digit-set/discuss/168313/Java-Solution-with-explanation
- Breaking the problem to finding the count of digits with exactly K digits. Iterating form K=1 to number of digits in N
- Now comes checking the count of K digits
- If number of digits is less than K, then answer is all possible permutations which would be
Math.pow(D.length,K)
- If number of digits is
0
return0
- If number of digits is exactly equal to K, then check the number of digits that can be formed by placing
D[i]
at highest position and count of K-1 digits.
- If number of digits is less than K, then answer is all possible permutations which would be
public int atMostNGivenDigitSet(String[] D, int N) {
int result = 0;
for(int i=1;i<=Integer.toString(N).length();i++){
result += helper(D,i,Integer.toString(N));
}
return result;
}
//exactly N digit
public int helper(String[] D, int K, String smax){
if (smax.equals("0")) {
return 0;
}
// String smax = Integer.toString(max);
if(smax.length()>K){
return (int)Math.pow(D.length,K);
}
int count=0;
for(int i=0;i<D.length;i++){
int char0 = Integer.parseInt(smax.charAt(0));
if(Integer.parseInt(D[i])<char0){
count += helper(D,K-1,smax);
}
if(Integer.parseInt(D[i])==char0){
if (smax.length() > 1) {
int charRem = Integer.parseInt(smax.substring(1, 2)) == 0 ? 0 : Integer.parseInt(smax.substring(1));
count += helper(D, K - 1, Integer.toString(charRem));
} else {
count++;
}
}
}
return count;
}
Solution -1: DFS (TLE)Time complexity: O(|D|^log10(N))
int atMostNGivenDigitSet(vector<string>& D, int N) {
int ans = 0;
dfs(D, 0, N, ans);
return ans;
}
private:
void dfs(const vector<string>& D, int cur, int N, int& ans) {
if (cur > N) return;
if (cur > 0 && cur <= N) ++ans;
for (const string& d : D)
dfs(D, cur * 10 + d[0] - '0', N, ans);
}