问题描述:
- 给定一个按非递减顺序排列的数字数组
digits
。- 你可以用任意次数
digits[i]
来写的数字,例如如果digits = ['1','3','5']
,我们可以写数字,如13
,551
和1351315
。
- 你可以用任意次数
- 返回可以生成的小于或等于给定整数
n
的正整数的个数。
核心思路:
- 该题考察了数位 dp,看了题解,学习了如何套用模板来写记忆化搜索。【具体看参考内容链接,用模板写两道类型题加深印象即可】
- 模板中有很多值得注意的细节,可以结合文字和代码中注释加以理解。
- 首先要理解要如何去做该题,所有满足条件正整数需要小于等于
n
,因此可以反过来想,所有小于等于n
的数字中,有多少个满足条件呢?【在该题中,所谓的满足条件就是数字中每一位数均存在于集合digits
中】- 当然不需要将
[1, n]
中所有数字都进行遍历,我们可以将n
的位数m
作为构造的条件,也就是说,记忆化搜索只需要维护一个状态idx
来记录当前的位数,idx
的取值范围为[0, m)
,此时我们只需要一直记录用高idx
位来构造数字的结果即可(注意是高idx
位)。【记忆化搜索维护的状态其实就是动态规划的状态,该题只有一个状态,所以代码中的 dp 数组只有一维】
- 当然不需要将
- 而下面的代码模板中,dfs 函数还维护了两个变量
is_limit
和is_num
,这两个变量很关键,简单来说,这两个变量是用于停止条件以及剪枝。- 介绍
is_limit
变量的作用:首先要理解,在当前位idx
上,插入的数字可能是0~9
,但有一种情况下,当前位上的数字选择可能会使数字超过n
,那就是此前所有位上的选择都和n
上对应位的选择是一样的;举例子来说,n = 123
,如果前两位选择为12
,则当前第三位只能选择0~3
,但如果前两位选择为11
,则当前第三位能够选择0~9
;因此is_limit
就是用来记录前面数位的选择是否受到了n
的约束。【当is_limit = true
,则说明此前数位受到了n
的约束】 - 介绍
is_num
变量的作用:该变量其实就是用来确定此前的数位是否跳过了构造,因此可以在此前一直跳过,一旦开始构造,后面就不能再跳过;换句话说,当n = 1234
时,我们想要构造一个数为34
则需要跳过高两位。【当is_num = false
则说明目前仍然未有开始构造数字】
- 介绍
- 代码里面还有很多细节,其他细节可看参考内容链接。
代码实现:
- 记忆化搜索解法代码实现如下:【模板来自参考内容】
class Solution { public: int atMostNGivenDigitSet(vector<string>& digits, int n) { string s = to_string(n); int m = s.size(); vector<int> dp(m, -1); function<int(int, bool, bool)> dfs = [&] (int idx, bool is_limit, bool is_num) -> int { if(idx == m) return is_num; // 来到尾后位置,需要用标志 is_num 来判断是否已经形成数字,如果为 true 则返回 1 if(!is_limit and is_num and dp[idx] > 0) return dp[idx]; int ans = 0; if(!is_num) // 如果仍然没有生成数字,就可以尝试跳过,构造更少位数的数字 ans += dfs(idx+1, false, false); // 此时必然没有受约束,所以两个标志都置为 false 即可 char hi = is_limit ? s[idx] : '9'; for(auto& lo : digits) // 注意 lo 为 string,而 hi 为 char { if(lo[0] > hi) break; ans += dfs(idx+1, is_limit and lo[0] == hi, true); } if(!is_limit and is_num) dp[idx] = ans; return ans; }; return dfs(0, true, false); } };