题目链接
题目大意: 求区间 l — r l—r l—r的数字和。但是数字和定义为每个数字不同数位的和,如123=1+2+3=6。
解题思路: l , r l,r l,r的范围是 1 0 18 10^{18} 1018。暴力枚举复杂度是 O ( r − l ) O(r-l) O(r−l),显然不行。我们换一个方向枚举:枚举数码1-9在 l l l到 r r r出现的总次数,再分别和当前枚举的数码相乘就是所求答案。比如区间 97 − 102 : 97 , 98 , 99 , 100 , 101 , 102 97-102:97,98,99,100,101,102 97−102:97,98,99,100,101,102:1出现3次,9出现4次,2,7,8均只出现1次。答案为 1 × 3 + 9 × 4 + 2 + 7 + 8 1×3+9×4+2+7+8 1×3+9×4+2+7+8。同时利用前缀和思想,先计算 0 0 0到 r r r之间的数字和,再计算 0 0 0到 l − 1 l-1 l−1之间的数字和,再用前者减去后者即为 l l l到 r r r之间的数字和。
数位dp一般是求区间 l l l到 r r r之间符合限制条件的数字个数。 l l l和 r r r都是达到 long long 级别的数,无法直接枚举求答案;要求数字的限制条件一般是针对数字的数位的(数位指一个数中每一个数字所占的位置,这些位置,都叫做数位。从右端算起,第一位是“个位”,第二位是“十位”…数位总和为位数,如1234为四位数),所以解决这种问题,不会一个数字一个数字看,而是枚举数位后捕获一大把合法的数字。
状态设计: 最低位标记为第1位,当前枚举数码为 d i g i t digit digit, d p [ i ] [ j ] dp[i][j] dp[i][j]表示从最高位到i位时 d i g i t digit digit的个数为 j j j时总的个数为多少。因为是从高位到低位枚举的,因此枚举完最后一个位置时,相当于枚举了一个完整的数字;而刚枚举最高位置时,此状态包含了所有数字。因此我们的答案就是枚举最高位置时的状态对应的数字和。
举个例子(假设我们只考虑当前数位最多有4位,能用的数字只有1和2,以此来简化问题)。
假设现在统计的 d i g i t digit digit是1,我们枚举了前两位为 12 12 12,此时计算的就是 d p [ 3 ] [ 1 ] dp[3][1] dp[3][1],表示从最高位到第3位时1(当前 d i g i t digit digit)的个数为1时总的个数。那么后两位有4种情况可以选择: 11 , 12 , 21 , 22 11,12,21,22 11,12,21,22。
第一种情况 11 11 11:相当于枚举了第一个合法的数字: 1211 1211 1211, d i g i t 1 digit 1 digit1 共3个。
第二种情况 12 12 12:相当于枚举了第二个合法的数字: 1212 1212 1212, d i g i t 1 digit 1 digit1 共2个。
第三种情况 21 21 21:相当于枚举了第三个合法的数字: 1221 1221 1221, d i g i t 1 digit 1 digit1 共2个。
第四种情况 22 22 22:相当于枚举了第四个合法的数字: 1222 1222 1222, d i g i t 1 digit 1 digit1 共1个。
因此 d p [ 3 ] [ 1 ] dp[3][1] dp[3][1]可以算出为:3+2+2+1=8。
接下来我们枚举前两位为 21 21 21,此时计算的还是 d p [ 3 ] [ 1 ] dp[3][1] dp[3][1],仍然表示从最高位到第2位时1的个数为1时的总的个数。而这个答案之前已经计算过了,没有继续往低位枚举的必要,因为后面4种情况枚举之后如下:
第一种情况 11 11 11:相当于枚举了第一个合法的数字: 2111 2111 2111, d i g i t 1 digit 1 digit1 共3个。
第二种情况 12 12 12:相当于枚举了第二个合法的数字: 2112 2112 2112, d i g i t 1 digit 1 digit1 共2个。
第三种情况 21 21 21:相当于枚举了第三个合法的数字: 2121 2121 2121, d i g i t 1 digit 1 digit1 共2个。
第四种情况 22 22 22:相当于枚举了第四个合法的数字: 2122 2122 2122, d i g i t 1 digit 1 digit1 共1个。
因此 d p [ 3 ] [ 1 ] dp[3][1] dp[3][1]仍然为:3+2+2+1=8。
最高位没有数字,第2位为1,此时仍然计算 d p [ 2 ] [ 1 ] dp[2][1] dp[2][1],仍然是8 ( 111 , 112 , 121 , 122 ) (111,112,121,122) (111,112,121,122)。
当从最高位到第 i i i位时 d i g i t digit digit个数为 j j j,如果 d p [ i ] [ j dp[i][j dp[i][j已经计算过了,就不必再次计算,直接使用之前保存过的值就好,这就是记忆化搜索( ̄y▽, ̄)╭ 。
但是如果最大的数字为2112,那么枚举前两位是21的时候接下去那一位只能是1(如果是2就超过最大数了),因此之前计算好的 d p [ 3 ] [ 1 ] dp[3][1] dp[3][1]不能使用,因为后面无法枚举完整的4种情况,只能老老实实往下算。在程序中用1个 b o o l bool bool变量 l i m i t limit limit来判断这种情况,如果枚举到当前数位 l i m i t limit limit为真,则不能使用已经保存的相应 d p dp dp值;若当前 l i m i t limit limit为真,且当前数位枚举的数字恰好和最大值对应数位的数字一致,则继续向更低位枚举时的 l i m i t limit limit也为真。但其实有 l i m i t limit limit限制的状态规模并不像有 l i m i t limit limit限制的那么多,这就像一些零零碎碎的状态需要另外处理,只要有限制,就既不能使用之前计算好的 d p dp dp值,这个状态自食其力地更新完也不能给 d p dp dp值用。
代码展示:
#include
#define LL long long
const int mod=1e9+7;
using namespace std;
int num[20],digit;
LL dp[20][20];//1ong long的类型最多有18位
LL dfs(int pos,bool limit,int sum){
LL ans=0;
if(pos==0)return sum;
if(!limit&&dp[pos][sum]!=-1)return dp[pos][sum];
int up=limit?num[pos]:9;//根据最大的数字确定上界
for(int i=0;i<=up;i++){
ans=(ans+dfs(pos-1,limit&&(up==i),sum+(i==digit)))%mod;
}
if(!limit)return dp[pos][sum]=ans;
return ans;
}
LL solve(LL cur){
num[0]=0;//num[0]表示cur的位数
LL res=0;
while(cur){
num[++num[0]]=cur%10;
cur/=10;
}
for(int i=1;i<=9;i++){
memset(dp,-1,sizeof dp);
digit=i;
res=(res+dfs(num[0],1,0)*i%mod)%mod;
}
}
int main(){
int T;cin>>T;
while(T--){
LL l,r;cin>>l>>r;
cout<<(solve(r)-solve(l-1)+mod)%mod<<endl;//取模之后可能r比l-1的数字和更小,加上mod保证是正数
}
}
题目链接
这道题和例题1很类似,只是此处需要判断二进制0和1的个数,因此前导零会产生影响,还需要一个 b o o l bool bool变量 l e a d lead lead表示当前枚举的数字是否含有前导0;同时需要两个状态:从高位到低位枚举二进制的0个数和1个数。
因此状态设计成 d p [ p o s ] [ z e r o ] [ o n e ] dp[pos][zero][one] dp[pos][zero][one]表示枚举到第 p o s pos pos位且从最高位到最低位的0个数为 z e r o zero zero,1个数为 o n e one one时符合条件的数字个数。
我们仍然举一个简单的例子来说明前导0的影响:假设当前数位为6,最大数字二进制为111111。
当前枚举到101,枚举位置为4,0个数为1,1个数为2,即计算 d p [ 4 ] [ 1 ] [ 2 ] dp[4][1][2] dp[4][1][2]。101后三位可以枚举为 000 , 001 , 010 , 011 , 100 , 101 , 110 , 111 000,001,010,011,100,101,110,111 000,001,010,011,100,101,110,111即枚举了数字 a 101000 , b 101001 , c 101010 , d 101011 , e 101100 , f 101101 , g 101110 , h 101111 a 101000,b 101001,c 101010,d 101011,e 101100,f 101101,g 101110,h 101111 a101000,b101001,c101010,d101011,e101100,f101101,g101110,h101111其中共 a , b , c , e a,b,c,e a,b,c,e 4个数字满足条件,因此更新 d p [ 4 ] [ 1 ] [ 2 ] dp[4][1][2] dp[4][1][2]为4。
继续枚举前3位为110,仍然为 d p [ 4 ] [ 1 ] [ 2 ] dp[4][1][2] dp[4][1][2],可以直接返回已经更新好的值。
当枚举前3位为011,似乎仍然是计算 d p [ 4 ] [ 1 ] [ 2 ] dp[4][1][2] dp[4][1][2],但此时前面的0并不能算是数字二进制里面的0,计算出来满足条件的数字个数会比 d p [ 4 ] [ 1 ] [ 2 ] dp[4][1][2] dp[4][1][2]更少,因为0的个数实际不包括前面的0。我们将第一位非零数字前面的零称为前导零,在状态和零的数量有关,并且前导零是不被统计的时候,就不能使用计算好的 d p dp dp值,也不用计算好的答案更新看似对应的 d p dp dp值。
#include
using namespace std;
int dp[33][33][33],num[33];
int dfs(int pos,bool limit,bool lead,int zero,int one){
if(!pos){
return zero>=one;
}
int ans=0;
if(!limit&&!lead&&dp[pos][zero][one]!=-1)return dp[pos][zero][one];
int up=limit?num[pos]:1;
for(int i=0;i<=up;i++)
ans+=dfs(pos-1,limit&&num[pos]==i,lead&&i==0,zero+(!lead&&i==0),one+(i==1));
if(!limit&&!lead)return dp[pos][zero][one]=ans;
return ans;
}
int solve(int cur){
num[0]=0;
memset(dp,-1,sizeof dp);
while(cur){
num[++num[0]]=cur&1;
cur>>=1;
}
return dfs(num[0],1,1,0,0);
}
int main(){
int l,r;cin>>l>>r;
cout<<solve(r)-solve(l-1)<<endl;
return 0;
}
d f s + d p dfs+dp dfs+dp,也就是记忆化搜索。这可以理解成一颗树,根就是我们求解的答案,而这棵树不同于普通树的点在于,它有很多重复的子树,这些子树可以在第一次计算之后保存下来,后面第二次第三次使用的时候只需要用之前第一次保存的子树答案。
#include
#include
using namespace std;
#define ll long long
const int mod=1e9+7;
ll dp[位数+1][state1][state2]...;//1
int num[位数+1];
ll dfs(int pos,bool limit,bool lead,int state){//2
if(pos==0)return 值;//3
if(!limit&&!lead&&dp[pos][state]!=-1)return dp[pos][state];//4
int up=9;if(limit)up=num[pos];//根据是否有限制及最大的数字,更新当前枚举数位数字的上界
for(int i=0;i<=up;i++){
ans+=dfs(pos-1,(i==up)&&limit,lead&&(i==0),updated state);
}
if(!limit&&!lead)dp[pos][state]=ans;
return ans;
}
ll solve(ll x){
memset(dp,-1,sizeof dp);
num[0]=0;
while(x){
num[++num[0]]=x%10;
x/=10;
}
return dfs(num[0],1,0,original state);
}
int main(){
ll l,r;cin>>l>>r;
cout<<solve(r)-solve(l-1)<<endl;
return 0;
}
题单链接
题目链接
从洛谷的题解里学到了另外一种状态表示的方法,直接将前导零和上界限制直接作为状态的一部分,这样在更新dp的时候就不用费心判断是否可以使用和更新。
#include
using namespace std;
int num[20],dp[20][20][2][2];
//什么前导0和限制都不需要考虑了!
int dfs(int pos,bool limit,bool lead,int last){
if(!pos)return 1;
if(dp[pos][last][limit][lead]!=-1)return dp[pos][last][limit][lead];
int ans=0;
int up=limit?num[pos]:9;
for(int i=0;i<=up;i++){
if(abs(i-last)>=2||lead)
ans+=dfs(pos-1,limit&&(i==num[pos]),lead&&(i==0),i);
}
return dp[pos][last][limit][lead]=ans;
}
int solve(int x){
num[0]=0;
while(x){
num[++num[0]]=x%10;
x/=10;
}
memset(dp,-1,sizeof dp);
return dfs(num[0],1,1,11);
}
int main(){
int l,r;cin>>l>>r;
cout<<solve(r)-solve(l-1)<<endl;
return 0;
}
题目链接
如果不考虑很大的数,最朴素的方法就是将这些数全部保存下来,然后用二分查找第k大的数是哪个。但是数字很大,因此我们在二分查找的时候只要知道此数字是第几大就好。
用 s o l v e ( x ) solve(x) solve(x)表示x是第几个数。 d p [ p o s ] [ s u m ] [ l i m i t ] [ o k ] dp[pos][sum][limit][ok] dp[pos][sum][limit][ok]表示从高枚举到低位,最后面的连续6个数为 s u m sum sum,有无上界限制 l i m i t limit limit,以及当前枚举是否已经合法 o k ok ok 的合法数字个数。
#include
#define LL long long
using namespace std;
LL dp[20][20][2][2];
int cnt,num[20];
LL dfs(int pos,int sum,bool limit,bool ok){
if(!pos)return ok;
LL &ans=dp[pos][sum][limit][ok];
if(ans!=-1)return ans;
ans=0;
int up=limit?num[pos]:9;
for(int i=0;i<=up;i++){
int cur=(i==6)?sum+1:0;
ans+=dfs(pos-1,cur,limit&&(i==num[pos]),ok||cur>=3);
}
return ans;
}
LL solve(LL x){
num[0]=0;
while(x)num[++num[0]]=x%10,x/=10;
memset(dp,-1,sizeof dp);
return dfs(num[0],0,1,0);
}
int main(){
int T;cin>>T;
while(T--){
cin>>cnt;
LL l=1,r=1e18,mid,ans;
while(l<=r){
mid=(l+r)>>1;
if(solve(mid)<cnt){//直接找最小的那个
l=mid+1;
}
else {
r=mid-1;
ans=mid;
}
}
cout<<ans<<endl;
}
return 0;
}