数位DP复习笔记
前言
复习笔记第五篇。(由于某些原因(见下),放到了第六篇后面更新)CSP-S RP++.
luogu 的难度评级完全不对,所以换了顺序,换了别的题目。有点乱,见谅。要骂就骂洛谷吧,原因在T2处
由于半路换题,中间耽搁了一天等原因,所以此文的阅读体验不一定和前5篇相当,见谅。
注:此文大部分建议结合代码阅读,便于解释
0——HDU 3555 Bomb
题意
求 \(1\sim N\) 中包含子串 49
的个数。\(N\leq 2^{63}-1.\)
思路
设 \(f[i][0]\) 表示长度为 \(i\) ,不含有 49
的个数;\(f[i][1]\) 为最高位为 9
但不含 49
的个数;\(f[i][2]\) 表示含有 49
的个数。转移方程为:
对于每一个位数,预处理出 \(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]数字计数
题意
给定两个正整数 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 烦人的数学作业
感谢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 花神的数论题
题意
设 \(\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\) 的数量。
设最终答案 \(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
题意
求 \(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
题意
当一个数字,从左到右依次看过去数字没有出现先递增接着递减的“山峰”现象,就被称作 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不成妻
题意
如果一个整数满足下列三个条件之一,称为和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}\)
于是就可以 \(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....