数位DP复习笔记

前言

复习笔记第五篇。(由于某些原因(见下),放到了第六篇后面更新)CSP-S RP++.

luogu 的难度评级完全不对,所以换了顺序,换了别的题目。有点乱,见谅。要骂就骂洛谷吧,原因在T2处

由于半路换题,中间耽搁了一天等原因,所以此文的阅读体验不一定和前5篇相当,见谅。

注:此文大部分建议结合代码阅读,便于解释

0——HDU 3555 Bomb

link

题意

\(1\sim N\) 中包含子串 49 的个数。\(N\leq 2^{63}-1.\)

思路

\(f[i][0]\) 表示长度为 \(i\) ,不含有 49 的个数;\(f[i][1]\) 为最高位为 9 但不含 49 的个数;\(f[i][2]\) 表示含有 49 的个数。转移方程为:

\[f[i][0]=10\times f[i-1][0]-f[i-1][1]\\\\ f[i][1]=f[i-1][0]\\\\ f[i][2]=10\times f[i-1][2]+f[i-1][1] \]

对于每一个位数,预处理出 \(f\) 数组,然后按位分割 \(n\) 并进行统计。具体见代码注释。

代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll f[30][3];
int a[30];

void init()
{
	f[0][0]=1; f[0][1]=f[0][2]=0;
	for ( int i=1; i<25; i++ )
	{
		f[i][0]=10*f[i-1][0]-f[i-1][1];
		f[i][1]=f[i-1][0];
		f[i][2]=10*f[i-1][2]+f[i-1][1];
	}
}

ll calc( ll x )
{
	int len=0;
	while ( x ) a[++len]=x%10,x/=10;
	a[len+1]=0; ll res=0; bool fl=0;
	for ( int i=len; i>=1; i-- )
	{
		res+=f[i-1][2]*a[i];	//先加上低位上面有49的个数。
		if ( fl ) res+=f[i-1][0]*a[i];		//之前如果出现过49,那么加上长度为 i-1 的不符合的个数
		else if ( a[i]>4 ) res+=f[i-1][1];	//之前没有出现过49,但是这一位可以填4,那么累加上之前有9的情形
		if ( a[i+1]==4 && a[i]==9 ) fl=1;	//判断是否出现过49
	}
	if ( fl ) res++;	//加上本身
	return res;
}

int main()
{
	int T; scanf( "%d",&T );
	init();
	while ( T-- )
	{
		ll x; scanf( "%lld",&x );
		printf( "%lld\n",calc( x ) );
	}

	return 0;
}

1——P2602 [ZJOI2010]数字计数

link

题意

给定两个正整数 a 和 b,求在 \([a,b]\) 中的所有整数中,每个数码(digit)各出现了多少次。\(a,b\leq 1e12\)

思路

问题转化为在 \([1,n]\) 中求出数码出现次数,前缀和的思想,相减即可。其实这样的题更多的不是统计而是填数的思想。

具体如何处理统计见代码注释,结合代码理解。

注:数据范围很小,本来是放在后面的,但是发现T1反倒是加强版所以就挪到前面来了,下一题是加强。代码是下一题贺过来的,所以可能有些多此一举的操作。不过注释都放在这题上了。

这道题就是用来告诉我们 ZJOI的某些题也是可以爆切的(

代码

#include <bits/stdc++.h>
#define ll unsigned long long
using namespace std;
const int N=2e5+10;
ll ans,a[N],cnt[N],r1[N],r2[N],x[N];            

void count( ll num,ll *s )             
{
        int len=0; ll sav=num;
        while ( num ) a[++len]=num%10,num/=10;         
        num=sav;
        for ( int i=len; i>=1; i-- )
        {
                for ( int j=0; j<=9; j++ )         //1~i-1位的贡献乘上当前位的方案数
                        s[j]+=a[i]*cnt[i-1];
                for ( int j=0; j<a[i]; j++ )		//作为第 i 位的 0~a[i]-1的贡献
                        s[j]+=x[i-1];
                num-=a[i]*x[i-1];
                s[a[i]]+=num+1; s[0]-=x[i-1];		//a[i]加上作为当前位的贡献,处理前导0
        }
}

int main()
{
        x[0]=1;
        for ( int i=1; i<=19; i++ )             //预处理10的幂次和
        {
                cnt[i]=(cnt[i-1]*10)+x[i-1];            //1~10^i-1出现次数
                x[i]=x[i-1]*10;
        }
        memset( a,0,sizeof(a) ); 
        memset( r1,0,sizeof(r1) ); memset( r2,0,sizeof(r2) );
        ll x,y; scanf( "%llu%llu",&x,&y );

        count( x-1,r1 ); count( y,r2 ); ans=0;
        for ( int i=0; i<=9; i++ )
                printf( "%lld ",r2[i]-r1[i] );

        return 0;
}

2——P4999 烦人的数学作业

link

感谢18年的老王供题,成为luogu最水的数位DP,唯一一道绿题

题意

给定一个区间 \([L,R]\) ,求其中每个数的数字和。 \(1\leq L\leq R\leq 1e18\) ,答案 \(\mod 1e9+7\)

思路

ZJOI的加强版,紫题变成绿题……无语。这是难度评级又不是来源评级啊。

求数字和,那么就是求 \(count(i\in [L,R])\times i,1\leq i\leq 9\) ,前缀和即可。复杂度 \(O(\log n)\)

貌似要开 unsigned long long ,我也不知道我哪里溢出了。注意 ull 的输出格式是 %llu.

代码

#include <bits/stdc++.h>
#define ll unsigned long long
using namespace std;
const int N=2e5+10,mod=1e9+7;
ll ans,a[N],cnt[N],r1[N],r2[N],x[N];            

void count( ll num,ll *s )             
{
        int len=0; ll sav=num;
        while ( num ) a[++len]=num%10,num/=10;        
        num=sav;
        for ( int i=len; i>=1; i-- )
        {
                for ( int j=0; j<=9; j++ )             
                        s[j]+=a[i]*cnt[i-1];
                for ( int j=0; j<a[i]; j++ )
                        s[j]+=x[i-1];
                num-=a[i]*x[i-1];
                s[a[i]]+=num+1; s[0]-=x[i-1];
        }
}

int main()
{
        int T; scanf( "%d",&T );
        
        x[0]=1;
        for ( int i=1; i<=19; i++ )             
        {
                cnt[i]=(cnt[i-1]*10)+x[i-1];      
                x[i]=x[i-1]*10;
        }

        while ( T-- )
        {
                memset( a,0,sizeof(a) ); 
                memset( r1,0,sizeof(r1) ); memset( r2,0,sizeof(r2) );
                ll x,y; scanf( "%llu%llu",&x,&y );

                count( x-1,r1 ); count( y,r2 ); ans=0;
                for ( int i=1; i<=9; i++ )
                        (ans+=1ll*i*(r2[i]-r1[i]+mod)%mod)%=mod;

                printf( "%llu\n",ans%mod );
        }       

        return 0;
}

3——P4317 花神的数论题

link

题意

\(\text{sum}(i)\) 表示 \(i\) 的二进制表示中 \(1\) 的个数。给出一个正整数 \(N\) ,求 \(\prod_{i=1}^{N}\text{sum}(i)\)

思路

换一种角度看这个乘积,会发现就相当于统计出 \(1\sim N\) 中 1 的个数为 \(k\) 的数量 \(cnt_k\) ,然后 \(\prod k^{cnt_k}\) 即可。

(怎么那么水啊,这都什么垃圾紫题,题白挑了)为了让这道题更有价值,代码实现非常的神仙。Orz粉兔。

粉兔的代码看了很久才理解……luogu上至今没有看到公开的详解。

这里注释的是我认为正确的理解,若有差错还请指正。

代码

#include <cstdio>
#define ll long long
const ll mod=1e7+7;
ll n,ans=1,cnt,f[50];

ll power( ll a,ll b )
{
	ll res=1;
	for ( ; b; b>>=1,a=a*a%mod )
		if ( b&1 ) res=res*a%mod;
	return res;
}

int main()
{
	scanf( "%lld",&n );

	cnt=0; f[0]=0;
	for ( int len=49; ~len; --len )
	{
		for ( int i=49; i; --i )			
			f[i]+=f[i-1];
		if ( n>>len&1 ) f[cnt]++,cnt++;			
        //cnt记录的是除了现在这一位,之前有的1的个数,f[cnt]++表示,这一位的1产生了一种使得前面的1全部能取到的方案。
	}
	f[cnt]++;		//加上本身
//之前一直想不明白,如果这样枚举,为什么能直接从49开始。
//一开始的想法是预支最高位的1,这样当前每次加一位就能取1,对应 f[i-1] 到 f[i] 的转移
//但是这样有个问题,就是最高位没有1了怎么办,这样预支无效,答案就会偏大
//后来发现,关键在外层循环。当位数大于二进制下n的位数的时候,f始终为0,最后一句if 不会执行,也就不会出现上述问题。
//一旦开始累加出现了值,那么一定就是有高位可以预支了。否则 if 中的等号不会成立。
	for ( int i=1; i<=49; ++i )
		ans=ans*power( i,f[i] )%mod;
	
	printf( "%lld",ans );
	return 0;
}

4——P6218 [USACO06NOV] Round Numbers S

题目链接 luogu

题意

问区间 \([l,r]\) 中有多少个数的二进制表示中,0 的数目不小于 1 的数目。

思路

数位DP。

可以发现,这一题和上一题都有“区间询问满足某种条件的数的数量”的形式,而且和数的数位有关,可以把这个总结为数位DP的一种常见类型。

\(f[i][j][k]\) 表示 \(i\) 位二进制,有 \(j\) 个1,最后一位为 \(k\) 的数量。

\[f[i][j][0]=f[i-1][j][0]+f[i-1][j][1](j<i) \\\\ f[i][j][1]=f[i-1][j-1][0]+f[i-1][j-1][1](j>0) \\\\ f[1][0][0]=1,f[1][1][1]=1 \]

设最终答案 \(g(x)\) 表示区间 \([1,x)\) 的答案个数,那么所求的给定区间答案就是 \(g(R+1)-g(L)\) .把 \(x\) 转成二进制,并设长度为 \(len\).

首先将长度小于 \(len\) 的累加(因为位数小于的都可以按位数直接统计)。然后考虑位数相等的(这时候,你统计出来的数显然最高位都是1.)对除了首位的所有位,判断是否为1.如果是,那么存在一些数,长度为 \(len\) ,值小于 \(x\) ,且二进制表示中这一位开始比 \(x\) 小。统计即可。(非常经典的套路,类似“余数”一样的统计。)

代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=40;
ll f[N][N][2];
int a[N],b[N],lena,lenb;

ll solve( int *s,int len )
{
        ll res=0; int cnt0=0,cnt1=1;
        for ( int i=len-1; i>=1; i-- )
         for ( int j=0; j<=(i>>1); j++ )
                res+=f[i][j][1];
        for ( int i=len-1; i>=1; i-- )
        {
                if ( s[i] ) for ( int j=0; j<=i; j++ )
                        if ( cnt0+i-j>=cnt1+j ) res+=f[i][j][0];
                if ( s[i] ) cnt1++;
                else cnt0++;
        }
        return res;
}

int main()
{
        int t1,t2; scanf( "%d%d",&t1,&t2 ); t2++;

        for ( ; t1; t1>>=1 ) a[++lena]=t1&1;
        for ( ; t2; t2>>=1 ) b[++lenb]=t2&1;
        while ( !a[lena] ) lena--;
        while ( !b[lenb] ) lenb--;

        f[1][0][0]=f[1][1][1]=1;
        for ( int i=2; i<=lenb; i++ )
         for ( int j=0; j<=i; j++ )
         {
                 if ( j<i ) f[i][j][0]=f[i-1][j][1]+f[i-1][j][0];
                 if ( j ) f[i][j][1]=f[i-1][j-1][0]+f[i-1][j-1][1];
         }

        printf( "%lld",solve(b,lenb)-solve(a,lena) );

        return 0;
}

5——HDU3652 B-number

link

题意

\(1\sim n\) 中含有 13 且被13整除的数的个数。\(n\leq 1e9\)

思路

含有 13 之前已经有过了,很好处理;考虑如何处理被13整除。那么可以在原有的 dp 上加一维,设 \(f[i][j][k]\) 表示 \(i\) 位数,\(k=0\) 表示不含 13\(k=1\) 表示最高位为 3\(k=2\) 表示含有 13 .

如果直接设 \(j\) 表示是否被13整除的话,没法转移。考虑设 \(j\) 为当前数 \(\mod 13\) 的余数,问题就迎刃而解了。

代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll f[15][13][3],a[15],n,len;

void init()
{
	memset( f,-1,sizeof(f) );
	ll sav=n; len=0;
	for ( ; sav; sav/=10 ) a[++len]=sav%10;
	a[len+1]=0;
}

ll dfs( ll pre,ll pos,ll mo,ll fl,bool lim )
{
	if ( pos==0 ) return (fl==2 && mo==0);
	if ( !lim && f[pos][mo][fl]!=-1 ) return f[pos][mo][fl];
	ll ceil=lim ? a[pos] : 9,res=0;
	for ( int i=0; i<=ceil; i++ )
	{
		int n1,nmo=(mo*10+i)%13;
		if ( fl==2 || pre==1 && i==3 ) n1=2;
		else if ( i==1 ) n1=1;
		else n1=0;
		res+=dfs( i,pos-1,nmo,n1,lim&&i==ceil );
	}
	if ( !lim ) f[pos][mo][fl]=res;
	return res;
}

int main()
{
	while ( cin>>n )
	{
		init(); printf( "%lld\n",dfs( -1,len,0,0,1 ) );
	}
}

6——HDU6148 Valley Numer

link

题意

当一个数字,从左到右依次看过去数字没有出现先递增接着递减的“山峰”现象,就被称作 Valley Number。求 \(1\sim n\) 中的 VN个数。

\(len(N)\leq 100,\mod 1e9+7\)

思路

很典型的一道数位DP。其实这种问题基本就是,只要细节不出问题,考虑全面就好了 哪像某些毒瘤

对于一个状态,记录三个信息:

  • 当前位置 pos
  • 前导数字 pre
  • 增减性 state ,0表示不确定增或者减,1表示增,2表示减

在 dfs 里面记录额外两个信息:是否处于“前导0序列”中,和之前的每一位是否都去了上限(一旦有一位没取,那么后面就可以任意填写而不需要担心边界问题,所以是 &&

具体如何计数见代码注释。

代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=1e9+7,N=110;
int n,a[N],pos;
char s[N];
ll f[N][10][3];
//pos,pre,0无增减,1增,2减

ll dfs( int pos,int pre,int state,bool lead,bool lim )          //位置,前导,状态,前导0,上限
{
        if ( pos==-1 ) return lead ? 0 : 1;
        if ( !lead && !lim && f[pos][pre][state] ) return f[pos][pre][state];
        int up=lim ? a[pos] : 9; ll res=0;
        for ( int i=0; i<=up; i++ )
        {
                if ( lead )
                {
                        if ( i==0 ) res=(res+dfs( pos-1,0,0,1,0 ))%mod;
                        //还是前导0,继续
                        else res=(res+dfs( pos-1,i,0,0,(i==a[pos] && lim) ))%mod;   
                        //这一位不是0了,那么要判断这一位的上限和之前有没有上限
                }
                else
                {
                        if ( i<pre )		//减
                        {
                                if ( state==1 ) continue;
                                res=(res+dfs( pos-1,i,2,0,lim && i==a[pos]) )%mod;
                        }
                        else if ( i==pre ) res=(res+dfs( pos-1,i,state,0,lim && i==a[pos] ) )%mod;		
                    //不增不减,注意这里要继承之前的增减性而不能简单为0
                        else res=(res+dfs( pos-1,i,1,0,lim && i==a[pos] ))%mod;		//增
                }
        }
        if ( !lead && !lim ) f[pos][pre][state]=res%mod;
        return res;
}

int main()
{
        int T; scanf( "%d",&T );
        while (T--)
        {
                scanf( "%s",s );
                int len=strlen(s); pos=0;
                for ( int i=len-1; i>=0; i-- )
                        a[pos++]=s[i]-'0';
                printf( "%lld\n",dfs( pos-1,0,0,1,1 )%mod );
        }

        return 0;
}

7——HDU4507 恨7不成妻

link

题意

如果一个整数满足下列三个条件之一,称为和7有关:

  • 某一位是7
  • 每一位的和是7的倍数
  • 本身是7的倍数

求在区间 \([L,R]\) 中与7无关的数的平方和。\(\mod 1e9+7\)

思路

看上去很水,不过是稍微复杂和加强了一点。大体想法可以参考T5,第一个条件直接维护,第二个条件就记录到当前位为止,数位和模 7 的余数,第三个条件同 T5,记录当前模 7的余数即可。

……诶等等,你是不是忽略了什么?平方和呢?

于是你不幸地发现,你还要再记录一些东西:平方和,和,以及个数本身。为什么呢?推个式子。

设当前位填的是 \(i\) ,后面 \(i-1\) 位子状态得到的答案是 \(tmp\) (包含个数,一次方和,二次方和)

对于个数,显然 \(res.cnt+=tmp.cnt;\)

对于一次方和 \(s_1\)\(res.s1+=tmp.s1+i\times 10^{pos-1}\times tmp.cnt\)

对于二次方和,设 \(tmp.s2=x_1^2+...+x_{cnt}^2\) ,每个数加上 \(add=i\times 10^{pos-1}\)

\[(x_1+add)^2+(x_2+add)^2+...+(x_{cnt}+add)^2\\\\ =(\sum x_{1\to cnt}^2)+2\times add\times (\sum x_{1\to cnt})+cnt\times add\\\\ =tmp.s2+2\times add\times tmp.s1+add^2\times tmp.cnt \]

于是就可以 \(O(1)\) 计算了。细节部分见代码。

代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const ll N=21;
const ll mod=1e9+7;
struct node
{
        ll cnt,s1,s2;
}f[N][7][7];
ll x[N],a[N],n;

node dfs( ll pos,bool lim,ll state,ll now)
//位,前面的位是否取满,数位和,数本身
{
        if ( pos==0 )
        {
                if ( now && state ) return (node){1,0,0};
                else return (node){0,0,0};
        }
        if ( !lim && f[pos][state][now].s1 ) return f[pos][state][now];
        //前面没有卡满(也就是每一位都取最大值),答案算过,那么直接记忆化。
        ll up=lim ? a[pos] : 9;        //如果之前取了最大值,那么只能 a[pos] ,否则随便填都不会大于n
        node res; res=(node){0,0,0};
        for ( ll i=0; i<=up; i++ )
        {
                if ( i==7 ) continue;
                ll st=(state+i)%7,sm=(now*10+i)%7;
                node tmp=dfs( pos-1,(lim && i==up),st,sm );
                ll add=i*x[pos-1]%mod;
                (res.cnt+=tmp.cnt)%=mod;
                (res.s1+=(tmp.s1+add*tmp.cnt%mod))%=mod;
                (res.s2+=tmp.s2)%=mod; (res.s2+=2ll*tmp.s1*add%mod)%=mod;
                (res.s2+=add*add%mod*tmp.cnt%mod)%=mod;
        }
        if ( !lim ) f[pos][state][now]=res;
        return res;
}

ll solve( ll num )
{
        n=0; memset( a,0,sizeof(a) );
        for ( ; num; num/=10 )
                a[++n]=num%10;
        return dfs( n,1,0,0 ).s2;
}

int main()
{
        ll T; scanf( "%lld",&T ); x[0]=1;
        for ( ll i=1; i<=18; i++ )
                x[i]=x[i-1]*10%mod;
        while ( T-- )
        {
                ll l,r; scanf( "%lld%lld",&l,&r );
                printf( "%lld\n",(solve(r)-solve(l-1)+mod)%mod );
        }
        
        return 0;
}

Last

To be continue....

posted @ 2020-11-03 21:42  MontesquieuE  阅读(440)  评论(0编辑  收藏  举报