整理自Leetcode大佬灵神(灵茶山艾府)的板子,感谢大佬的题解ψ(`∇´)ψ
大佬
以力扣的2376题为例:
我们先去看当n = 123
为例子时的思路,可以把问题看作是f(i, mask)
然后一共有三个位置i
,往三个位置填数字(mask
是为了防止位上的数字出现重复的约束条件,本文为了能够记忆化搜索,将mask用10个位的int数字1024
来代替vector
。
【第三个参数】我们顺着思路向下分析:
因此,当i = 0
,这个位置的数字可以是0, 1
;当i = 1
,这个位置的数字可以是0,1,2
或者0-9
;当i=2
时,这个位置的数字可以是0, 1, 2
或者0-9
;`
i=1
时能否直接0-9
都是根据i=0
是否贴合n相应位的大小(题中为1)来决定的。i=2
时能否直接0-9
时根据i=0
和i=1
是否贴合n相应位的大小(题中为2)来决定的。故,我们需要第三个参数,来表示当前的i
是否要收到限制。因此,引入bool型的isLimit
参数。
【第四个参数】与本题约束条件相碰撞的一种情况:前导零!题目中要求数字不允许重复,但是当010
出现时,明明是合法的,但是会被约束条件所剪枝。因此我们引入一个新的布尔参数isNum
,其表示i
前面是否填了数字:
false
,代表当前为可以跳过(继续保持false),或者填入至少为1的数字。true
,代表前面已经有数字了,因此只能填0-9
的数字因此,思路可以分为下面:
is_limit
涉及到了数和位的比较,因此需要将n转化成string,并且加入到dp递归的参数中。结束关系
,当位置i
到了n
的长度了,那么就到了终点了,此时返回is_num
因为is_num=0
意味着没有取过数,自然方法数为0。is_num
是否跳过,先把跳过该位的情况递归求出来is_limit
,确定接下来回溯选择的范围的上界is_num
来决定回溯选择的范围的下界【初始dp的情况】注意一开始的is_limit
应为true
(因为第一位一定不是0-9
,除非n的最高位是9,但这也是收到限制),而is_num
应为false
(因为第一位也可以是0,进行跳过)。
class Solution {
public:
//这里的第一个10是因为n的范围不超过1e9
//第二个1024代表10个位,正好数字的范围是0-9
int arr[10][1024][2][2];
int countSpecialNumbers(int n) {
string n_s = to_string(n);
return dp(0, 0, true, false, n_s);
}
int dp(int i, int mask, bool is_limit, bool is_num, string n) {
//1.结束条件
if(i == n.size()) return is_num;
//2.记忆化搜索
if(arr[i][mask][is_limit][is_num] != 0) return arr[i][mask][is_limit][is_num];
//3.前面是否已经有数字了,没有才能跳过
int ans = 0;
//因为跳过了,因此后面既不受限制,前面也没出现过数字
if(!is_num) ans += dp(i+1, mask, false, false, n);
//4.确定该位数的上下界
int up = 9, down = 0;
if(is_limit) up = n[i] - '0';
if(!is_num) down = 1;
//开始尝试遍历填入这个位置的值
for(int j = down; j <= up; ++j) {
//dp里的第二个参数是将mask写入,防止重复
//第三个参数,只有前面都是限制的,这次也到上限才变
//这次因为赋数了,因此第四个参数为true
if(((mask >> j) & 1) == 0) ans += dp(i+1, mask|(1<<j),is_limit && (j == up), true, n);
}
//做记录
arr[i][mask][is_limit][is_num] = ans;
return ans;
}
};
这个题没有约束条件,因此可以不使用mask
。其实只是需要修改回溯遍历那里,因为那里选择的数字只能是digits
数组里的数字,并不是0-9任意选
。
class Solution {
public:
//因为n的范围是1e9
int arr[10][2][2];
int atMostNGivenDigitSet(vector<string>& digits, int n) {
string n_s = to_string(n);
return dp(0, true, false, n_s, digits);
}
int dp(int i, bool is_limit, bool is_num, string n, vector<string>& digits) {
//1
if(i == n.size()) return is_num;
//2
if(arr[i][is_limit][is_num] != 0) return arr[i][is_limit][is_num];
//3
int ans = 0;
if(!is_num) ans += dp(i+1, false, false, n, digits);
//4确定该位的上下限
int up = 9, down = 0;
if(is_limit) up = n[i] - '0';
if(!is_num) down = 1;
for(auto &ch: digits) {
//仅这里需要选择,只能选择digit数组里面的数字,并且要保证在上下界里面
int num = stoi(ch);
if(num <= up && num >= down)ans += dp(i+1, is_limit && (num == up), true, n, digits);
}
arr[i][is_limit][is_num] = ans;
return ans;
}
};
首先,因为没有约束条件,因此is_num
参数(前面有没有0都无所谓)和mask
参数是不需要的。
这样,只有两个变量了,一个是统计填数的位置,另一个是is_limit
,然而只靠这两个是没办法实现记忆化的,那么就会导致速度大大降低!
由于是统计1的个数,因此我们需要多一个变量来记录1 的个数。这样从后往前递归,就能记住当递归到该i
位置时,已有cnt
个,最后将有ans
个,下一次,再到i
时,已有cnt
个就不用再往后递归了,因为肯定时之前记录的ans
个。
class Solution {
public:
//此处的32是因为n最多只有31个1
int arr[10][2][32];
int countDigitOne(int n) {
return dp(0, true, 0, to_string(n));
}
int dp(int i, bool is_limit, int cnt, string n) {
//1
if(i == n.size()) return cnt;
//2
if(arr[i][is_limit][cnt] != 0) return arr[i][is_limit][cnt];
//4
int up = 9, ans = 0;
if(is_limit) up = n[i] - '0';
for(int j = 0; j <= up; ++j) {
//如果j是1,则统计1的个数的变量cnt加1
ans += dp(i+1, is_limit && (j == up), cnt+(j==1), n);
}
arr[i][is_limit][cnt] = ans;
return ans;
}
};