6.15训练总结
数位dp
数位dp一般来说就是一个计数,主要是针对数位来计算的,跟数本身的大小无关。而且跟区间一样非常好判断,特别明显。一般有几个特点。
- 数据范围一般非常鬼畜,从 1 ≤ n ≤ 1 e 9 1 \leq n \leq 1e9 1≤n≤1e9 或者 1 ≤ n ≤ 1 e 30 1 \leq n \leq 1 e30 1≤n≤1e30 可谓是层出不穷
- 一般都是计数,与数位有关。
那我们由浅入深,先讲一下模版HDU3555吧。
例题
HDU3555 Bomb
题目大意就是 1 1 1 到 N N N 中包含连续子串 49 49 49,有多少个。
我们就可以开这样的 dp 数组,大概如下。
- d p i , 0 dp_{i,0} dpi,0,不含 49 49 49
- d p i , 1 dp_{i,1} dpi,1 ,不含 49 49 49 但最高位为 9 9 9
- d p i , 2 dp_{i,2} dpi,2,包含 49 49 49
大概什么思路呢?先以 N = 123456 N=123456 N=123456 为例,我们
因为要一位一位地判断,总得有个顺序吧。那是从高到低,还是从低到高。我们想一想,他的 N N N 的限制如果从低到高还能判断吗?你后面太大,前面卡了怎么办?所以,顺序就是从高到低了。好,我们来模拟一下。 N = 123456 N=123456 N=123456,我们最高位是0时没有限制,随便搞。百万位为 1 1 1 时,有限制,怎么办?找下一位,然后重复这个过程:找没有限制的,全部搞完,有限制了,下一位继续做。
这是数位dp写法
#include<bits/stdc++.h>
long long dp[50][3];
void init()
{
memset(dp,0,sizeof(dp));
dp[0][0]=1;
for(int i=1;i<50;i++)
{
dp[i][0]=dp[i-1][0]*10-dp[i-1][1];//!要减去一个第i-1位为9的情况,因为如果第i位为4的话就会构成49。
dp[i][1]=dp[i-1][0];//!只让第i位为9,则就等于1x(以前不包含49的所有情况)。
dp[i][2]=dp[i-1][2]*10+dp[i-1][1];//!以前包含的49则第i位有10中可能,加上以前没有包含49但是第i-1位为9的情况。
}
}
long long solve(long long n)
{
int bit[50],len=0;
while(n)//!存储n的每一位
{
bit[++len]=n%10;
n/=10;
}
bit[len+1]=0;
long long ans=0;
bool flag=false;
for(int i=len;i;i--)//!从高位开始取,这里从len和len-1开始都是一样的。因为都不会改变ans的值
{
ans+=bit[i]*dp[i-1][2];//!加上i-1位所有包含49的情况。第i位可以填1到num[i]的数据。
if(flag)//!如果前面出现了49的情况,则后面的数据可以直接加上了。
{
ans+=bit[i]*dp[i-1][0];
}
else
{
if(bit[i]>4)//!如果没有前面没有出现49的情况,并且第i位大于4
{
ans+=dp[i-1][1];//!则加上第i位为4,第i-1位为9的所有情况
}
if(bit[i+1]==4&&bit[i]==9)//!如果出现49的情况则改变标记。
{
flag=true;
}
}
}
if(flag)
{
ans++;
}
return ans;
}
int main()
{
init();
int T;
scanf("%d",&T);
while(T--)
{
long long n;
scanf("%lld",&n);
printf("%lld\n",solve(n));
}
}
下面是记忆化搜索写法
#include<bits/stdc++.h>
using namespace std;
int digit[30];
long long dp[30][3];
int solve(long long x)
{
memset(digit,0,sizeof(digit));
int cnt=0;
while(x)
{
digit[++cnt]=x%10;
x/=10;
}
return cnt;
}
//!pos表示当前的位数
//!status中0表示不含49,1表示上一位是4,2表示含49
//!limit表示当前位是否受限制
long long solve(int pos,int status,int limit)
{
if(pos<=0)
return status==2;
if(!limit&&dp[pos][status]!=-1)
return dp[pos][status];//!这里就是说比如257,那么当你计算过了0到99之后,100到199就可以直接调用了,而2这一位是有限制的,所以不能直接调用现成的,要到下面重新计算一下
long long ans=0;
int num=limit?digit[pos]:9;
for(int i=0;i<=num; i++)
{
int nstatus=status;//!相当于else s=status;
if(status==0&&i==4)//!高位不含49,并且末尾不是4,现在末尾添加的是4返回状态1
nstatus=1;
else if(status==1&&i!=4&&i!=9)//!高位不含49,且末尾是4,现在末尾添加的不是4,返回状态0
nstatus=0;
else if(status==1&&i==9)//!高位不含49,且末尾是4,现在末尾添加9,返回状态2
nstatus=2;
ans+=solve(pos-1,nstatus,limit&&i==num);
}
if(!limit)
dp[pos][status]=ans;
return ans;
}
int main()
{
int T;
long long n;
ios::sync_with_stdio(false);
cin>>T;
while(T--)
{
memset(dp,-1,sizeof(dp));
cin>>n;
int len=solve(n);
long long sum=solve(len,0,1);
cout<<sum<<endl;
}
return 0;
}
windy数
题目大意不必多说,思路和上一题类似。
上代码。
#include<bits/stdc++.h>
using namespace std;
long long dp[15][15],ans;//!dp[i][j]表示搜到第i位,前一位是j,的limit方案和。
int a[15],len;
long long L,R;
long long dfs(int pos,int pre,int st,int limit)//!pos当前位置,pre前一位数,st判断前面是否全是0,limit最高位限制
{
if(pos>len)
return 1;//!搜完了
if(!limit&&dp[pos][pre]!=-1)
return dp[pos][pre];//!没有最高位限制,已经搜过了
long long ret=0;
int res=limit?a[len-pos+1]:9;//!当前位最大数字
for(int i=0;i<=res;i++)//!从0枚举到最大数字
{
if(abs(i-pre)<2)
continue;//!不符合题意,继续
if(st&&i==0)
ret+=dfs(pos+1,-2,1,limit&&i==res);//!如果有前导0,下一位随意
else
ret+=dfs(pos+1,i,0,limit&&i==res);//!如果没有前导0,继续按部就班地搜
}
if(!limit&&!st)
dp[pos][pre]=ret;//!没有最高位限制且没有前导0时记录结果
return ret;
}
void part(long long x)
{
len=0;
while(x)
{
a[++len]=x%10;
x/=10;
}
memset(dp,-1,sizeof dp);
ans=dfs(1,-2,1,1);
}
int main()
{
scanf("%lld%lld",&L,&R);
part(L-1);
long long minn=ans;
part(R);
long long maxn=ans;
printf("%lld",maxn-minn);
return 0;
}
SAC#1 - 萌数
求 [ l , r ] [l,r] [l,r] 内有多少个合法的正整数。一个数是合法的,当且仅当这个数字内部存在长度至少为 2 2 2 的回文子串。答案对 1 e 9 + 7 1e9+7 1e9+7 取模。
这题还是数位dp,比较模板,但是呢它让我们求带有回文子串的数字,有点多,嗯。那怎么办呢?当然就是正难则反。去求不包含的不就好了吗?OK,这题就搞定了。其余细节我放代码注释里了,自己去看。
#include<bits/stdc++.h>
#define Mod 1000000007
using namespace std;
int s1[1005],s2[1005];
int st[1005];
long long f[1005][15][15][2][2][2];
string sx,sy;
int len;
//! pre2当前位置的第前两位 pre1当前位置的第前一位
//! zero前导零
//! pos当前位置
//! limit最高为限制
//! flag如果flag为1 则当前这个数是萌的 否则它为0
//! len记录当前字符串的长度
long long dfs (int pre2,int pre1,int pos,int limit,int zero,int flag)
{
if(pos>len)//!位置都排完了
return flag;
long long ret=0;
if(f[pos][pre1][pre2][limit][zero][flag]!=-1)//!位置排完,无前导0
return f[pos][pre1][pre2][limit][zero][flag]%Mod;
int limit=limit?st[pos]:9;//!最大上限
for(int i=0;i<=limit;i++)
{
if(flag&&pre1!=-2)//!如果这个数是萌的 无需再判断了
ret=(ret%Mod+dfs(pre1,(zero&&!i)?-2:i,pos+1,limit&&i==limit,(zero&&!i)?1:0,1)%Mod)%Mod;
else
{
//!这里其实可以细分成aa和aba这两种,其他的都是复制过来的。
if(pre1==i&&pre1!=-2||pre2==i&&pre2!=-2&&pre1!=-2)
ret=(ret%Mod+dfs(pre1,(zero&&!i)?-2:i,pos+1,limit&&i==limit,(zero&&!i)?1:0,1)%Mod)%Mod;//1
else
ret=(ret%Mod+dfs(pre1,(zero&&!i)?-2:i,pos+1,limit&&i==limit,(zero&&!i)?1:0,0)%Mod)%Mod;//0
}
}
f[pos][pre1][pre2][limit][zero][flag]=(ret%Mod);//!存下状态
return ret%Mod;
}
long long solve (int le,int flag)
{
memset(st,0,sizeof(st));
memset(f,-1,sizeof(f));
len=le;
if(flag==1)
{
for(int i=1;i<=len;i++)
st[i]=s2[i];
}
else
{
for(int i=1;i<=len;i++)
st[i]=s1[i];
}
return dfs(-2,-2,1,1,1,0)%Mod;
}
int main()
{
cin>>sx>>sy;//!这里数据太大了,只能用字符串
int len1=sx.length();
for(int i=0;i<len1;i++)
{
s1[i+1]=sx[i]-'0';
}
int len2=sy.length();
for(int i=0;i<len2;i++)
{
s2[i+1]=sy[i]-'0';
}
s1[len1]--;
long long ans1=solve(len1,0);//!正难则反
long long ans2=solve(len2,1);
printf("%long longd",((ans2-ans1)%Mod+Mod)%Mod);//!一定要记得取模
return 0;
}
储能表
还行,普通的数位dp,不做过多的解释。
#include<bits/stdc++.h>
using namespace std;
const long long int N=70;
long long int f[N][2][2][2],g[N][2][2][2];
long long int n,m,k,T,p;
int main(){
scanf("%lld",&T);
while(T--)
{
memset(f,0,sizeof(f));
memset(g,0,sizeof(g));
scanf("%lld%lld%lld%lld",&n,&m,&k,&p);
g[62][1][1][1]=1;
for(int i=61;i>=1;i--){
long long int x=(n>>(i-1))&1,y=(m>>(i-1))&1,z=(k>>(i-1))&1;
for(long long int a=0;a<2;a++){
for(long long int b=0;b<2;b++){
for(long long int c=0;c<2;c++){
if(f[i+1][a][b][c] || g[i+1][a][b][c]){
for(long long int xx=0;xx<2;xx++){
for(long long int yy=0;yy<2;yy++){
long long int zz=xx^yy;
if((a && x<xx)||(b && y<yy)||(c && z>zz)){
continue;
}
long long int aa=(a && x==xx),bb=(b && y==yy),cc=(c && z==zz);
g[i][aa][bb][cc]+=g[i+1][a][b][c];
g[i][aa][bb][cc]%=p;
f[i][aa][bb][cc]+=f[i+1][a][b][c]+(zz-z+p)%p*((1ll<<(i-1))%p)%p*g[i+1][a][b][c]%p;
f[i][aa][bb][cc]%=p;
}
}
}
}
}
}
}
printf("%lld\n",f[1][0][0][0]);
}
}
花神的数论题
这题简而言之求 1 1 1 到 n n n 中每个数的二进制下的 1 1 1 的个数的累乘。
那看到二进制我们很容易想到直接二进制拆分后直接套数位dp的模板,就是我标注的部分,好,上代码。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=1e7+7;
int len;
ll n;
int a[55];
ll dp[55][55];
ll dfs(int p,int st,int limit)//!树形dp模板
{
if(p>len)
return max(st,1);
if(dp[p][st]!=-1&&!limit)
return dp[p][st];
ll ret=1;
int res;
if(limit)
res=a[len-p+1];
else
res=1;
for(int i=0;i<=res;i++)
(ret*=dfs(p+1,i==1?st+1:st,limit&&(i==res)))%=mod;
if(!limit)
dp[p][st]=ret;
return ret;
}
int main()
{
scanf("%lld",&n);
while(n)
{
a[++len]=n&1;
n>>=1;
}
memset(dp,-1,sizeof dp);
printf("%lld\n",dfs(1,0,1));
return 0;
}
UVA12517 Digit Sum
这题的dfs与之前的非常类似,就是限制改一下,前几位就是枚举数字。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int l,r,dp[50][50],a[50],siz;
void dfs()
{
for(int i=1;i<=20;++i)
{
for(int j=0;j<=9;++j)
{
for(int k=0;k<=9;++k)
{
dp[i][j]+=dp[i-1][k];
}
int s=1;
for(int k=1;k<=i-1;++k)
{
s*=10;
}
dp[i][j]+=s*j;
}
}
}
void add(int x)
{
memset(a,0,sizeof(a));
siz=0;
while(x>0)
{
a[++siz]=x%10;
x/=10;
}
}
int query(int x)
{
add(x);
int ans=0,getsum=0;
for(int i=siz;i>=1;--i)
{
for(int j=0;j<a[i];++j)
{
ans+=dp[i][j];
int p=1;
for(int k=1;k<i;++k)
{
p*=10;
}
ans+=(p*getsum);
}
getsum+=a[i];
}
ans+=getsum;
return ans;
}
signed main()
{
int T;
cin>>T;
dfs();
while(T--)
{
cin>>l>>r;
if(l==0&&r==0)
break;
cout<<query(r)-query(l-1)<<"\n";
}
return 0;
}
Palindromic Numbers
题目大意:给你 i , j i,j i,j 求两个数之间回文数的个数,注意前导零;
思路如下
- 既然是求 i , j i,j i,j 之间的个数,还是求 f j − f j − 1 f_{j}-f_{j-1} fj−fj−1;
- 利用 d p i dp_{i} dpi 表示所有 i i i 位的数有多少回文数。但是还有一个问题,如何保存呢,分奇数位和偶数位两种情况:偶数位如果保证回文,要求对称,如果是 i i i 位数,前 i / 2 i/2 i/2 位要对称于后 i / 2 i/2 i/2 位,如四位数,前 2 2 2 位数字有 10 10 10 到 99 99 99 共有 90 90 90 种情况,所以四位数共有 90 90 90 个回文数;奇数位相当于在少一位的偶数位中间插入一个数,个数可以是 0 0 0 到 9 9 9 共 10 10 10 个数任意一个;例如五位数,中间不考虑就是有 90 90 90 种情况,这时考虑中间数有 10 10 10 种,所以共有 90 ∗ 10 90*10 90∗10 种情况;
- 分步算答案
#include<bits/stdc++.h>
#define MAXN 10000005
#define Mod 10001
using namespace std;
int dight[40],tmp[40];
long long dp[40][100][100];
long long dfs(int start,int pos,int s,bool limit)
{
if(pos<0)
return s;
if(!limit&&dp[pos][s][start]!=-1)
return dp[pos][s][start];
int end;
long long ret=0;
if(limit)
end=dight[pos];
else
end=9;
for(int d=0; d<=end; ++d)
{
tmp[pos]=d;
if(start==pos&&d==0)
ret+=dfs(start-1,pos-1,s,limit&&d==end);
else if(s&&pos<(start+1)/2)
ret+=dfs(start,pos-1,tmp[start-pos]==d,limit&&d==end);
else
ret+=dfs(start,pos-1,s,limit&&d==end);
}
if(!limit)
dp[pos][s][start]=ret;
return ret;
}
long long solve(long long a)
{
memset(dight,0,sizeof(dight));
int cnt=0;
while(a!=0)
{
dight[cnt++]=a%10;
a/=10;
}
return dfs(cnt-1,cnt-1,1,1);
}
int main()
{
memset(dp,-1,sizeof(dp));
int t,cnt=1;
scanf("%d",&t);
while(t--)
{
long long x,y;
scanf("%lld%lld",&x,&y);
if(x>y)
swap(x,y);
printf("Case %d: %lld\n",cnt++,solve(y)-solve(x-1));
}
return 0;
}
Magic Numbers
题目大意
给定 m , d , l , r m,d,l,r m,d,l,r ,求符合以下条件的数 x x x 的个数:
- l ≤ x ≤ r l \leq x \leq r l≤x≤r
- x x x 的偶数位是 d d d ,奇数位不是 d d d 。
- m ∣ x m∣x m∣x
传三个参数 k , x , p k,x,p k,x,p 进入 dfs,分别表示枚举到第 k k k 位,当前数模 m m m 的余数,以及这一位填的数有没有限制,再记忆化存下即可
看似很简单,感觉不到紫题的难度,但是,还有很多注意的点。
- d d d 填的不能超过 m a x n maxn maxn
- a a a 和 d f s dfs dfs 均从高往低处搜,避免因为限制卡掉。
- 数位dp是使用了前缀和的思想,但是这道题目可能会退位,所以毒瘤的高精度不可避免了。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll mod=1e9+7;
int m,d,len,a[2005];
ll f[2005][2005];
char l[2005],r[2005];
ll dfs(int k,int x,int p)
{
if(k>len)
return x?0:1;
if(!p&&f[k][x]!=-1)
return f[k][x];
int y=p?a[k]:9;
ll res=0;
if(k&1)
{
for(int i=0;i<=y;i++)
{
if(i==d)
continue;
res=(res+dfs(k+1,(x*10+i)%m,p&&(i==y)))%mod;
}
}
else
{
if(d<=y)
res=(res+dfs(k+1,(x*10+d)%m,p&&(d==y)))%mod;
}
if(!p)
f[k][x]=res;
return res;
}
ll divide(char *s)
{
len=strlen(s+1);
for(int i=1;i<=len;i++)
a[i]=s[i]-'0';
return dfs(1,0,1);
}
bool check(char *s)
{
len=strlen(s+1);
int x=0;
for(int i=1;i<=len;i++)
{
int y=s[i]-'0';
x=(x*10+y)%m;
if(i&1)
{
if(y==d)
return false;
}
else
{
if(y!=d)
return false;
}
}
return !x;
}
int main()
{
memset(f,-1,sizeof(f));
scanf("%d%d%s%s",&m,&d,l+1,r+1);
printf("%lld\n",(divide(r)-divide(l)+check(l)+mod)%mod);
return 0;
}