数位dp(统计特殊整数)

文章介绍了在编程题目中,如何使用暴力枚举和数位动态规划(dp)算法解决特殊数字计数问题,包括位数不足时的计算、位数相同时的处理,并给出了详细的代码演示和递归逻辑分析。
摘要由CSDN通过智能技术生成

目录

0.题面

1.题目分析

1.1暴力枚举法

1.2数位dp

2.数位dp原理分析

2.1位数不足时的计算

2.2位数相同时的计算

2.2.1.预处理

2.2.2首位数字比n的首位数字小

2.2.3首位数字与n首位数字相同

3.代码演示

4.结语


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.结语

如果大家对这个内容有啥疑问,欢迎在评论区友好讨论呀~

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值