前言

复习笔记第五篇。(由于某些原因(见下),放到了第六篇后面更新)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....

数位DP复习笔记的更多相关文章

  1. 数位DP学习笔记

    数位DP学习笔记 什么是数位DP? 数位DP比较经典的题目是在数字Li和Ri之间求有多少个满足X性质的数,显然对于所有的题目都可以这样得到一些暴力的分数 我们称之为朴素算法: for(int i=l_ ...

  2. 状压DP复习笔记

    前言 复习笔记第4篇.CSP RP++. 引用部分为总结性内容. 0--P1433 吃奶酪 题目链接 luogu 题意 房间里放着 \(n\) 块奶酪,要把它们都吃掉,问至少要跑多少距离?一开始在 \ ...

  3. 斜率优化DP复习笔记

    前言 复习笔记2nd. Warning:鉴于摆渡车是普及组题目,本文的难度定位在普及+至省选-. 参照洛谷的题目难度评分(不过感觉部分有虚高,提高组建议全部掌握,普及组可以选择性阅读.) 引用部分(如 ...

  4. 数位DP复习小结

    转载请注明原文地址http://www.cnblogs.com/LadyLex/p/8490222.html 之前学数位dp的时候底子没打扎实 虚的要死 这次正好有时间……刷了刷之前没做的题目 感觉自 ...

  5. bzoj 1026: [SCOI2009]windy数 & 数位DP算法笔记

    数位DP入门题之一 也是我所做的第一道数位DP题目 (其实很久以前就遇到过 感觉实现太难没写) 数位DP题目貌似多半是问从L到R内有多少个数满足某些限制条件 只要出题人不刻意去卡多一个$log$什么的 ...

  6. 数位DP 学习笔记

    前言:鸣谢https://www.luogu.com.cn/blog/virus2017/shuweidp.感谢大佬orz ----------------------------- [引入] 首先要 ...

  7. 数位dp 笔记

    目录 数位dp 笔记 解决的问题 & 主体思想 入门 -- windy数 绕一个弯 -- 萌数 the end? -- 恨7不成妻 小心细节 [SDOI2016]储能表 复杂度起飞 [AHOI ...

  8. 算法笔记--数位dp

    算法笔记 这个博客写的不错:http://blog.csdn.net/wust_zzwh/article/details/52100392 数位dp的精髓是不同情况下sta变量的设置. 模板: ]; ...

  9. 「笔记」数位DP

    目录 写在前面 引入 求解 特判优化 代码 例题 「ZJOI2010」数字计数 「AHOI2009」同类分布 套路题们 「SDOI2014」数数 写在最后 写在前面 19 年前听 zlq 讲课的时候学 ...

随机推荐

  1. Kafka 生产者分区策略

    分区策略 1)分区的原因 (1)方便在集群中扩展,每个 Partition 可以通过调整以适应它所在的机器,而一个 topic 又可以有多个 Partition 组成,因此整个集群就可以适应任意大小的 ...

  2. async await 你真的用对了吗?

    大部分同学了解Promise,也知道async await可以实现同步化写法,但实际上对一些细节没有理解到位,就容易导致实际项目中遇到问题. 开始先抛结论,下文将针对主要问题点进行论述. 1.所有as ...

  3. Python_opencv库

    1.车牌检测 ''' 项目名称:opencv/cv2 车牌检测 简介: 1.训练级联表 ***.xml [跳过...] 2.用如下代码加载级联表和目标图片识别车牌 注:推荐用anconda安装open ...

  4. Python_faker (伪装者)创建假数据

    faker (伪装者)创建假数据 工作中,有时候我们需要伪造一些假数据,如何使用 Python 伪造这些看起来一点也不假的假数据呢? Python 有一个包叫 Faker,使用它可以轻易地伪造姓名.地 ...

  5. Python _PyQt5 【总】

    http://www.cnblogs.com/archisama/p/5442071.html QtCore QtGui QtWidgets QtMultimedia QtBluetooth QtNe ...

  6. 如何给input或textarea文字加背景色

    需求说明 如果要实现一个需求,如下图,在一个textarea中加入文字加背景色,该怎么处理呢? 答案:"很简单啊!直接给textarea加个background-color的背景颜色啊!&q ...

  7. deepin 安装最新版node

    安装npm sudo apt install npm 安装node sudo npm install -g n 升级node到稳定版 sudo n stable 升级到最新版 sudo n lates ...

  8. HDU100题简要题解(2030~2039)

    HDU2030 汉字统计 题目链接 Problem Description 统计给定文本文件中汉字的个数. Input 输入文件首先包含一个整数n,表示测试实例的个数,然后是n段文本. Output ...

  9. Linux Shell 错误: $'\r': command not found错误解决

    在Linux下执行程序最省事的方式就是将系统的执行流程封装成一个shell脚本,上传到linux环境中后就可以直接执行了,但是今天在具体实施的时候出现了错误 $'\r': command not fo ...

  10. 深度分析:面试90%被问到的 Session、Cookie、Token,看完这篇你就掌握了!

    Cookie 和 Session HTTP 协议是一种无状态协议,即每次服务端接收到客户端的请求时,都是一个全新的请求,服务器并不知道客户端的历史请求记录:Session 和 Cookie 的主要目的 ...