目录
0.题面
1.题目分析
1.1暴力枚举法
在做一道题的时候,我们最先想到的肯定是暴力的模拟法:也就是暴力枚举1~n中的每一个数字,然后判断他们中的每个数字是否相同。由于这个想法过于直白,我就直接放代码:
int countSpecialNumbers(int n)
{
int ans = 0;
for (int i = 1; i <= n; i++)
{
int state = 0, pi = i;
while (pi)
{
int last = pi % 10;
if ((state & (1 << pi)))
break;
state |= (1 << pi);
last /= 10;
}
if (!pi)
ans++;
}
return ans;
}
这里解释一下state这个变量,因为int变量一共有32位,而int最大是21亿多,也就是10位数,所以我们可以拿state二进制上的位来做哈希,检索这个数字有没有出现过。如果出现过直接跳出循环,如果没有出现过,就把那个位置的二进制值改为1。当然,这里的n是10的九次方,所以这个办法显然不能在1s之内完成计算,我们必须要另想办法。
1.2数位dp
我们可以通过分析,直接用公式算出结果,比如说我的n是四位数,那么我就可以用公式,直接算出1位数的情况,2位数的情况和3位数的情况。4位数的情况,再用跑循环和递归,这样就可以大大减少次数。当然这里的循环和递归也不是暴力跑法,详情请看原理分析。
2.数位dp原理分析
2.1位数不足时的计算
这里用五位数举例,在千位我们可以填1~9,因为如果填了0那么就不是4位数了,就是3位数了,所以千位有9种填法,接下来看百位,百位就没有0的拘束,就可以填0,但是我们这一位填的数不能与千位相同,所以就是10-1=9种,那么十位就是10-2=8,个位10-3=7。总的情况就是9*9*8*7种。
接下来看三位数,第一位还是可以1~9,也是有9种方法,第二位是0~9但是不能与第一位重复,也就是9种,第三位就是8种,一共9*9*8以此类推……两位数就是9*9。那么我们就可以尝试写出这个循环了:
int ans=0;
if (len >= 2)
{
//1位数字时就是九种情况
ans += 9;
int pow = 9;
for (int i = 2, k = 9; i < len; i++, k--)
{
pow *= k;
ans += pow;
}
}
2.2位数相同时的计算
2.2.1.预处理
在计算这个之前,我们需要有一小步的预处理。
我们可以看到,数字位数不同,后续可以填的个数是不一样的,我们不妨假设数字的长度为len,后续可以填的情况是9!/(10-len)! 但是如果我们每次计算都要用阶乘,那么既不方便,速度也不快。所以我这里就用一个数组来保存。
vector<int> d(len);
d[0]=1;
for(int i=1,k=10-len+1;i<len;i++,k++)
{
d[i]=d[i-1]*k;
}
这里d中括号中间的数字就代表后面有几个数字,d[0]就是后面还有0位数字的意思,后面有0位数字就说明这个数字已经确定了,后续没有其他可能,可能性就是1种。
2.2.2首位数字比n的首位数字小
我们还是拿这张图,如果我们首位填了4,那是不是说明我们后面的4位可以随意填,只要不与前面重复,所以我们可以直接用公式算出总数。不难发现,我们首位填1~3的情况与填4一样。那么我们首位为什么不是0开始的呢?因为从0开始就不是五位数了,而四位数我们已经在前面计算过了,如果把0算进去就会出现重复。所以一共可以填的数字就是first-1
ans+=(first-1)*d[len-1];//这里的d存的就是9!/(10-len)的值。
2.2.3首位数字与n首位数字相同
当我们第一个数字取了5时,我们第二位就不可以取比4大的数字了,那么它也就只有4种选择了,然后我们还需要分析它可以填什么数字。当第二位填了4时,后续也就不能填3了……
我们接着看这个情况,当第二位填了3时后续是不是有可以随意填了。而且我们这个情况与我们的首位填4的情况类似,所以我们就可以采取递归。
当我们填的数字原数字相同时进入递归,当数字小于原数字时直接用公式计算。
3.代码演示
int f(vector<int>& d, int n, int pow, int len, int state)
{
if (len == 0)
return 1;
int first = (n / pow) % 10;
int ans = 0;
//取的数字比当前小时,直接计算
for (int i = 0; i < first; i++)
{
if (((1 << i) & state) == 0)
{
ans += d[len - 1];
//cout<<"##"<<endl;
}
}
//相等进递归
if (((state) & (1 << first)) == 0)
ans += f(d, n, pow / 10, len - 1, state | (1 << first));
return ans;
}
int countSpecialNumbers(int n)
{
//计算n有几位
int len = 0, pn = n, first = 0, pow = 0;
//pow用来取第几位数字
while (pn)
{
if (pow == 0)
pow = 1;
else
pow *= 10;
len++;
pn /= 10;
//获取首个数字
if (pn < 10)
first = pn;
}
vector<int> d(len);
d[0] = 1;
for (int i = 1, k = 10 - len + 1; i < len; i++, k++)
{
d[i] = d[i - 1] * k;
}
int ans = 0;
//从两位开始计算
if (len >= 2)
{
//1位数字
ans += 9;
int pow = 9;
for (int i = 2, k = 9; i < len; i++, k--)
{
pow *= k;
ans += pow;
}
}
//位数相同且首数字比first小时,直接公式计算
ans += (first - 1) * d[len - 1];
int state = 0;
ans += f(d, n, pow, len, state);
return ans;
}
4.结语
如果大家对这个内容有啥疑问,欢迎在评论区友好讨论呀~