今天在leetcode上面遇到了一道题:
编写一个方法,计算从 0 到 n (含 n) 中数字 2 出现的次数。
这道题最好的解法是动态规划。分析如下:设dp[i]标志这个数字前i位中出现了2的次数,那么我们需要求的是dp[i]和的dp[i - 1]的关系。比如说一个数字2849,那么dp[2]就表示0~49中出现2的数量,dp[3]就表示0~849中出现2的数量,dp[4]就表示0~2849中出现2的数量。分析如下:
因为我们要求的是2出现的次数,那么根据第i位的数字,我们可以分类讨论:
1. 第i位是0
例如:n = 102, 分析dp[2]和dp[1]的关系,即dp(02)与dp(2) (02就是2,两者的结果是相同的,但是0又是不可或缺的)
第i位是0,该位取值范围只有这一种可能,2的数量也不可能增加,由此可得
dp[02] = dp[2]
2. 第i位是1
例如:n = 178,分析dp[3]和dp[2]的关系,即dp(178)与dp(78)(括号就表示range(0~178),实际上的dp[3]和dp[2]的关系)第3位是1,该位可能取0,1两种情况:
dp[3] = 当第3位是0,1-2位取00~99时2的次数 + 当第3位是1, 1-2位取00~78时2的次数
dp[3] = dp(99) + dp[2]
dp(178) = dp(99) + dp(78)
3. 第i位是2
例如:n = 233, 分析dp[3]和dp[2]的关系,即dp(233)与dp(33)
dp[3] = 第3位取0-1,1-2位取00~99时2的次数 + 第3位是2,1-2位取00~33时2在1-2位出现的次数 + 第3位是2,1-2位取00~33时2在第3位出现的次数.
dp[3] = 2 * dp(99) + dp[2] + 34(实际上是33 + 1)
dp(233) = 2 * dp(99) + dp(33) + 34
4 第i位大于2
以 n = 478为例,分析dp[3]和dp[2]的关系,即dp(478)与dp(78)
dp[3] = 第3位取0-3,1-2位取00-99时2出现在1-2位的次数 + 第3位取4,1-2位取00-78时2的次数 + 第3位取2,1-2位取00-99时2出现在第3位的次数
dp[3] = 4 * dp(99) + dp[2] + 100
总结来看,一个数字n,假设第i位为k, 那么dp[i] = k * dp(99) + dp[i - 1] + n % pow(10, i - 1) + 1(如果k==2) + pow(10, i -1)(如果k > 2)。
所以我们存两种东西:一种是真正的dp[i][0]表示前i位数字包含2的数量,第二种是dp[i][1]表示99...(一共i个9)...99包含的2的数量,也就是dp(99...(一共i个9)...99).
代码如下:
class Solution {
public:
int numberOf2sInRange(int n) {
int digit = getdigit(n);
vector<vector<int>> dp(digit + 1, vector<int>(2, 0));//dp[i][0]表示从一共i位数字情况下出现2的个数,di[i][1]表示99...9一共i个9的情况2的数量
dp[1][0] = (n % 10 <= 1) ? 0 : 1;
dp[1][1] = 1;
for (int i = 2; i <= digit; i++){
int k = getk(n, i);
int cnt = k * dp[i - 1][1] + dp[i - 1][0];
if (k == 2){
cnt += (n % (int)pow(10, i - 1)) + 1;
}
else if (k > 2){
cnt += (int)pow(10, i - 1);
}
dp[i][0] = cnt;
//try{
dp[i][1] = 10 * dp[i - 1][1] + (int)pow(10, i - 1);
//}
//catch(exception& e){
//}
}
return dp[digit][0];
}
//获得n有多少位数
int getdigit(int n){
int cnt = 0;
while (n > 0){
n /= 10;
cnt++;
}
return cnt;
}
//获得数字n 从右边第k位置的数字是多少
int getk(int n, int k){
for (int i = 1; i < k; i++) n /= 10;
return n % 10;
}
};