与数位有关的 DP

之前写这个一直用着巨恶心的非递归写法,因为觉得这东西不怎么出现也没有去学更先进的科技,结果快初赛了才发现这东西怎么进初赛题里去了/pz

于是吓哭了赶紧来学。

记忆化搜索

数位 DP 类题目的样子就是给你一个区间 \([l,r]\),让你计数这里面满足要求的非负整数的数量。

当然,你能填的不只是数字,你还可以填字符,原理是一样的。

首先我们进行一步最常见的前缀和处理,也就是把求解 \([l,r]\) 内的数量变为求解 \([0,r],[0,l-1]\) 两区间内符合要求的数的数量,再把两值相减。

于是,我们写数位 DP 仅需求出 \([0,k]\) 内满足要求的数数量即可。

这一步处理完成之后,我们用一例题来更好的说明原理:

[ZJOI2010] 数字计数

数位 DP 时一般从高位向低位尝试填数,这样处理的目的是创造出 “自由状态”。

举例子,假设数的上限是 \(7335\),如果我们钦定最高位千位填 \(1\),那么下面 \(1\mathrm{XXX}\) 三位可以填入 \(0\sim 9\) 的任何数。

你可能会问这有什么用?不还是要一位位去填然后统计?当然是有用的,你可以把这样的 “后三位” 设为一种状态,这样当你计算出一次这样的状态,千位为 \(2,3,\cdots,6\) 的时候直接复用这个状态即可。数位 DP 的 DP 体现在这里。

还没完,千位填 \(7\) 该怎么办?这时向下一位填数的时候就只能填 \(1\sim 3\) 了,因此发现我们的状态还需要记录一个:“当前是不是处于上界”,来决定当前位可以填入的数位范围。

还没完,我们是否可以直接舍弃掉千位?当然可以,但是此时如果我们决定在下一位的百位上填数,就不可以再填 \(0\),因为填入 \(0\) 等价于决策舍弃百位。于是发现我们的状态还需记录一个:“前面有没有填入过数”,同样来决定当前位可以填入的数位范围。

你会发现上面的过程如果用正常写 DP 方程的形式去转移是很麻烦的,因为你要根据当前选择的状态来确定范围。

但是记搜写法就比较简单(虽然代码长了点),自制了下面这种填数的板子,注释很详细:

//          上限 还剩几位  条件    自由状态  前面填过吗
ll f(string num,int p,    ...   ,bool ok,bool ext){
  if(vi[p][][ok][ext])return dp[p][][ok][ext];// 记忆化
  if(p==0)return ext; // 前面所有数都决策完了,填过数才算数
  ll ans=0;
  if(!ext)ans+=f(num,p-1,...,1,0);// 决策舍弃这一位
  if(ok){// 可以自由填
    if(!ext&&p==1){// 前面没填过(现在填的是最高位)但是是最后一位
      for(int i=0;i<=9;i++){// 这时候可以放 0
        if(constraint)continue;
        ans+=f(num,p-1,...,1,1);
      }
    }
    else if(!ext){// 前面没填过,不是最后一位
      for(int i=1;i<=9;i++){// 不能放 0
        if(constraint)continue;
        ans+=f(num,p-1,...,1,1);
      }
    }
    else{// 前面填过,此时 0~9 都可以
      for(int i=0;i<=9;i++){
        if(constraint)continue;
        ans+=f(num,p-1,...,1,1);
      }
    }
  }
  else{// 不能自由填
    if(!ext&&p==1){//前面没填过(现在填的是最高位)但是是最后一位
      for(int i=0;i<=num[p]-'0';i++){// 这时候可以放 0,不能放超过这一位的数
        if(constraint)continue;
        if(i==num[p]-'0')ans+=f(num,p-1,...,0,1);// 依然顶着上界
        else ans+=f(num,p-1,...,1,1);// 不顶上界了,之后自由了 
      }
    }
    else if(!ext){// 前面没填过,不是最后一位
      for(int i=1;i<=num[p]-'0';i++){// 不能放 0,不能放超过这一位的数
        if(constraint)continue;
        if(i==num[p]-'0')ans+=f(num,p-1,...,0,1);
        else ans+=f(num,p-1,...,1,1);
      }
    }
    else{// 可以放 0,不能放超过这一位的数
      for(int i=0;i<=num[p]-'0';i++){
        if(constraint)continue;
        if(i==num[p]-'0')ans+=f(num,p-1,...,0,1);
        else ans+=f(num,p-1,...,1,1);
      }
    }
  }
  vi[p][][ok][ext]=1;// 记忆化
  dp[p][][ok][ext]=ans;
  return ans;
}

而且似乎这种写法不用在意前导零?我暂时还没被前导零这一块的卡过。

小练习

[ZJOI2010] 数字计数

套上板子加上一些小学奥数就能做完这一题了!

注意清空和边界条件。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define ls p<<1
#define rs (p<<1)+1
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
array<int,10> dp[100][3][3];
bool vis[100][3][3];
ll p10[1000];
ll limi[100];
array<int,10> f(string num,int p,bool ok,bool ext){
  if(p==0)return array<int,10>{0,0,0,0,0,0,0,0,0,0};
  if(vis[p][ok][ext])return dp[p][ok][ext];
  array<int,10> ans;
  ans={0,0,0,0,0,0,0,0,0,0};
  if(!ext){
    array<int,10> res=f(num,p-1,1,0);
    for(int k=0;k<=9;k++)ans[k]+=res[k];
  }
  if(ok){
    if(!ext&&p==1){
      for(int i=0;i<=9;i++){
        array<int,10> res=f(num,p-1,1,1);
        ans[i]+=p10[p];
        for(int k=0;k<=9;k++)ans[k]+=res[k];
      }
    }
    else if(!ext){
      for(int i=1;i<=9;i++){
        array<int,10> res=f(num,p-1,1,1);
        ans[i]+=p10[p];
        for(int k=0;k<=9;k++)ans[k]+=res[k];
      }
    }
    else{
      for(int i=0;i<=9;i++){
        array<int,10> res=f(num,p-1,1,1);
        ans[i]+=p10[p];
        for(int k=0;k<=9;k++)ans[k]+=res[k];
      }
    }
  }
  else{
    if(!ext&&p==1){
      for(int i=0;i<=num[p]-'0';i++){
        array<int,10> res;
        if(i==num[p]-'0')res=f(num,p-1,0,1),ans[i]+=limi[p-1];
        else res=f(num,p-1,1,1),ans[i]+=p10[p];
        for(int k=0;k<=9;k++)ans[k]+=res[k];
      }
    }
    else if(!ext){
      for(int i=1;i<=num[p]-'0';i++){
        array<int,10> res;
        if(i==num[p]-'0')res=f(num,p-1,0,1),ans[i]+=limi[p-1];
        else res=f(num,p-1,1,1),ans[i]+=p10[p];
        for(int k=0;k<=9;k++)ans[k]+=res[k];
      }
    }
    else{
      for(int i=0;i<=num[p]-'0';i++){
        array<int,10> res;
        if(i==num[p]-'0')res=f(num,p-1,0,1),ans[i]+=limi[p-1];
        else res=f(num,p-1,1,1),ans[i]+=p10[p];
        for(int k=0;k<=9;k++)ans[k]+=res[k];
      }
    }
  }
  vis[p][ok][ext]=1;
  dp[p][ok][ext]=ans;
  return ans;
}
signed main(){
  
  p10[1]=1;
  for(int i=2;i<=15;i++)p10[i]=p10[i-1]*10;
  p10[0]=1;
  p10[1]=1;
  ll a,b;cin>>a>>b;
  a--;
  string n1=to_string(a);
  string n2=to_string(b);
  int len1=n1.size();
  int len2=n2.size();
  reverse(n1.begin(),n1.end());
  for(int i=0;i<n1.size();i++){
    if(i==0){limi[i+1]=n1[i]-'0';continue;}
    else {
      limi[i+1]=(n1[i]-'0')*p10[i+1]+limi[i];
    }
  }
  limi[0]=1; 
  for(int i=1;i<=n1.size();i++)limi[i]++;
  n1=' '+n1;
  array<int,10> ans1=f(n1,len1,0,0);
  reverse(n2.begin(),n2.end());
  for(int i=0;i<n2.size();i++){
    if(i==0){limi[i+1]=n2[i]-'0';continue;}
    else {
      limi[i+1]=(n2[i]-'0')*p10[i+1]+limi[i];
    }
  }
  limi[0]=1; 
  for(int i=1;i<=n2.size();i++)limi[i]++;
  n2=' '+n2;
  for(int i=0;i<=14;i++){
    for(int j=0;j<=1;j++){
      for(int k=0;k<=1;k++){
        dp[i][j][k].fill(0);
        vis[i][j][k]=0;
      }
    }
  }
  array<int,10> ans2=f(n2,len2,0,0);
  for(int i=0;i<=9;i++){
    cout<<ans2[i]-ans1[i]<<' ';
  }
  
  return 0;
}

Leetcode 600

转成二进制,记下上一位状态,然后套板子即可。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define ls p<<1
#define rs (p<<1)+1
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
class Solution {
public:
  int dp[50][3][3][3];
  bool vis[50][3][3][3];
  int f(string num,int p,bool n1,bool ok,bool ext){
    if(vis[p][n1][ok][ext])return dp[p][n1][ok][ext];
    if(p==0)return ext;
    ll ans=0;
    if(!ext)ans+=f(num,p-1,0,1,0);
    if(ok){
      if(!ext&&p==1){
        for(int i=0;i<=1;i++){
          if(n1==1&&i==1)continue;
          ans+=f(num,p-1,i,1,1);
        }
      }
      else if(!ext){
        for(int i=1;i<=1;i++){
          if(n1==1&&i==1)continue;
          ans+=f(num,p-1,i,1,1);
        }
      }
      else{
        for(int i=0;i<=1;i++){
          if(n1==1&&i==1)continue;
          ans+=f(num,p-1,i,1,1);
        }
      }
    }
    else{
      if(!ext&&p==1){
        for(int i=0;i<=num[p]-'0';i++){
          if(n1==1&&i==1)continue;
          if(i==num[p]-'0')ans+=f(num,p-1,i,0,1);
          else ans+=f(num,p-1,i,1,1);
        }
      }
      else if(!ext){
        for(int i=1;i<=num[p]-'0';i++){
          if(n1==1&&i==1)continue;
          if(i==num[p]-'0')ans+=f(num,p-1,i,0,1);
          else ans+=f(num,p-1,i,1,1);
        }
      }
      else{
        for(int i=0;i<=num[p]-'0';i++){
          if(n1==1&&i==1)continue;
          if(i==num[p]-'0')ans+=f(num,p-1,i,0,1);
          else ans+=f(num,p-1,i,1,1);
        }
      }
    }
    vis[p][n1][ok][ext]=1;
    dp[p][n1][ok][ext]=ans;
    return ans;
  }
  int findIntegers(int n) {
    memset(dp,0,sizeof dp);
    memset(vis,0,sizeof vis);
    string pt="";
    int p1=n;
    while(p1){
      int dig=p1&1;
      pt=pt+(char)(dig+'0');
      p1/=2;
    }
    int len=pt.size();
    pt=' '+pt;
    // cout<<pt<<'\n';
    return f(pt,len,0,0,0);
  }
};
int main(){
  
  int n;cin>>n;
  Solution s;
  cout<<s.findIntegers(n);
  
  return 0;
}

Leetcode 902

给了数字集合了,更简单连板子都不用。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define ls p<<1
#define rs (p<<1)+1
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
vector<string> dig;
class Solution {
public:
  int lim;
  vector<int> tb;
  int tr(int p,bool ok,bool exi){
    if(p==0)return exi;
    int ans=0;
    if(!exi){ans+=tr(p-1,1,0);}
    int p1=p;int n=lim;p1--;
    while(p1){
      n/=10;p1--;
    }
    int fl=n%10;
    if(ok){
      int res=1;
      for(int i=1;i<=p;i++){
        res=res*tb.size();
      }
      ans+=res;
    }
    else{
      for(int i=tb.size()-1;i>=0;i--){
        if(tb[i]==fl)ans+=tr(p-1,0,1);
        else if(tb[i]<fl)ans+=tr(p-1,1,1);
      }
    }
    return ans;
  }
  int atMostNGivenDigitSet(vector<string>& digits, int n) {
    lim=n;
    for(string s:digits){
      tb.push_back(atoi(s.c_str()));
      // cout<<atoi(s.c_str())<<' ';
    }
    // cout<<'\n';
    return tr(to_string(n).size(),0,0);
  }
};
int main(){
  
  int dl;cin>>dl;
  for(int i=1;i<=dl;i++){
    string x;cin>>x;
    dig.push_back(x);
  }
  int num;cin>>num;
  Solution s;
  cout<<s.atMostNGivenDigitSet(dig,num);
  
  return 0;
}

Leetcode 2376

把数字出没出现过状态压缩进一个数或者 bitset 里去,然后套板子即可。

由于这个题的限制只有 \(9!\) 中可能的状态,于是甚至不用记忆化。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define ls p<<1
#define rs (p<<1)+1
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
class Solution {
public:
  ll fct[40];
  int f(string num,int p,bool ok,bool ex,bitset<20> msk){
    if(p==0)return ex;
    ll ans=0;
    if(!ex)ans+=f(num,p-1,1,ex,msk);
    if(ok){
      if(ex){
        int cnt=0;
        for(int i=0;i<=9;i++)if(!msk[i])cnt++;
        ans=ans+fct[cnt]/fct[cnt-p];
      }
      else{
        int cnt=9;
        for(int i=1,ip=9;i<p;i++,ip--)cnt=cnt*ip;
        ans=ans+cnt;
      }
      return ans;
    }
    else{
      if(!ex){
        for(int i=1;i<=num[p]-'0';i++){
          if(msk[i])continue;
          if(i==num[p]-'0'){
            msk[i]=1;
            ans+=f(num,p-1,0,1,msk);
            msk[i]=0;
          }
          else{
            msk[i]=1;
            ans+=f(num,p-1,1,1,msk);
            msk[i]=0;
          } 
        }
      }
      else{
        for(int i=0;i<=num[p]-'0';i++){
          if(msk[i])continue;
          if(i==num[p]-'0'){
            msk[i]=1;
            ans+=f(num,p-1,0,1,msk);
            msk[i]=0;
          }
          else{
            msk[i]=1;
            ans+=f(num,p-1,1,1,msk);
            msk[i]=0;
          } 
        }
      }
    }
    return ans;
  }
  int countSpecialNumbers(int n) {
    fct[0]=1;
    for(int i=1;i<=15;i++)fct[i]=fct[i-1]*i;
    string num=to_string(n);
    int cnt=num.size();
    reverse(num.begin(),num.end());
    num=' '+num;
    bitset<20> mask;
    mask.reset();
    return f(num,cnt,0,0,mask);
  }
};
int main(){
  
  int n;cin>>n;
  Solution a;
  cout<<a.countSpecialNumbers(n);
  
  return 0;
}

Leetcode 2719

记录下从高到低位填的数位和,然后判断一下即可大力剪枝。

用上记忆化套上板子即可。

有个小点是这里两个数都太大了如果前缀和还得用高精度减法。

于是我们直接计算 \([0,r]\)\([0,l]\) 的答案,特判掉 \(l\) 合不合法再加入答案里就行了。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define ls p<<1
#define rs (p<<1)+1
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
class Solution {
public:
  const int mod=1e9+7;
  int minn,maxx;
  ll dp[50][500][10];
  bool vis[50][500][10];
  ll f(string num,int p,int sum,int ok){
    if(sum>maxx){
			return 0;
		}
		if(sum+p*9<minn) {
			return 0;
		}
    if(vis[p][sum][ok])return dp[p][sum][ok];
    if(p==0){
      dp[p][sum][ok]=(minn<=sum&&sum<=maxx);
      vis[p][sum][ok]=1;
      return dp[p][sum][ok];
    }
    ll ans=0;
    if(ok==1){
      if(sum>=minn&&p*9+sum<=maxx){
        ll cnt=1;
        for(int i=1;i<=p;i++){
          cnt=cnt*9;cnt%=mod;
        } 
        ans=(ans+cnt)%mod;
      }
      else{
        for(int i=0;i<=9;i++){
          ans+=f(num,p-1,sum+i,1);
          ans=ans%mod;
        }
      }
    }
    else{
      for(int i=num[p]-'0';i>=0;i--){
        if(i==num[p]-'0')
          ans+=f(num,p-1,sum+i,0);
        else 
          ans+=f(num,p-1,sum+i,1);
        ans=ans%mod;
      }
    }
    vis[p][sum][ok]=1;
    dp[p][sum][ok]=ans;
    return ans;
  }
  int count(string num1, string num2, int min_sum, int max_sum) {
    minn=min_sum;maxx=max_sum;
    for(int i=0;i<=48;i++){
      for(int j=0;j<=300;j++){
        for(int k=0;k<=1;k++){
          dp[i][j][k]=vis[i][j][k]=0;
        }
      }
    }
    reverse(num1.begin(),num1.end());
    num1=' '+num1;
    reverse(num2.begin(),num2.end());
    num2=' '+num2;
    ll ans1=f(num2,num2.size()-1,0,0);
    for(int i=0;i<=48;i++){
      for(int j=0;j<=300;j++){
        for(int k=0;k<=2;k++){
          dp[i][j][k]=vis[i][j][k]=0;
        }
      }
    }
    ll ans2=f(num1,num1.size()-1,0,0);
    int sum=0;
    for(char c:num1){
      if(c==' ')continue;
      sum+=c-'0';
    }
    return (ans1-ans2+mod+(minn<=sum&&sum<=maxx))%mod;
  }
};

int main(){
  
  string n1,n2;
  int mis,mas;
  cin>>n1>>n2;
  cin>>mis>>mas;
  Solution s;
  cout<<s.count(n1,n2,mis,mas);

  return 0;
}

SDOI2014 数数

这个状态我们要求比较高,既要能够转移还要支持判断当前是否出现了可能的模式串,看一圈下来只有 ACAM 能够胜任这活了。

考虑我们是怎么用 ACAM 判断主串中是否出现过模式串的,就是不断跳 fail,如果找到一个位置被标记为一个子串的末尾就代表主串的一个后缀出现了一个模式串。

我们还知道把 ACAM 的 fail 指针拿出来是棵树,于是直接用 DFS 预处理出哪些 ACAM 上的点跳 fail 会跳到至少一个被标记的点。这些点是不合法的。

于是我们直接把状态设为 ACAM 上点的编号,这样就可以快速判断合法也可以转移了。

转了一圈题解区好像全是大力 DP 没有这种做法?

code

Show me the code
#define psb push_back
#define mkp make_pair
#define ls p<<1
#define rs (p<<1)+1
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=1e4+5;
int son[N][20],cnt[N];
int fil[N],ans[N];
int id=0,tap=0;
int vis[N];
int acur[N];
vector<int> e[N];
void ins(string s){
  int p=0;
  for(int i=0;i<s.size();i++){
    if(!son[p][s[i]-'0']){
      son[p][s[i]-'0']=++id;
    }
    p=son[p][s[i]-'0'];
  }
  vis[p]=1;
  return void();
}
int inner[N];
void b(){
  queue<int> q;
  for(int i=0;i<=9;i++)if(son[0][i])q.push(son[0][i]);
  while(q.size()){
    int u=q.front();q.pop();
    for(int i=0;i<=9;i++){
      if(son[u][i]){
        fil[son[u][i]]=son[fil[u]][i];
        e[son[fil[u]][i]].push_back(son[u][i]);
        e[son[u][i]].push_back(son[fil[u]][i]);
        inner[son[fil[u]][i]]++;
        q.push(son[u][i]);
      }
      else son[u][i]=son[fil[u]][i];
    }
  }
  return void();
}
void dfs(int u,int fa,bool c){
  c|=vis[u];
  acur[u]=c;
  for(int v:e[u]){
    if(v==fa)continue;
    dfs(v,u,c);
  }
  return void();
}
const int mod=1e9+7;
int dp[1250][1505][2][2];
bool vi[1250][1505][2][2];
ll f(string num,int p,int pos,bool ok,bool ext){
  if(vi[p][pos][ok][ext])return dp[p][pos][ok][ext];
  if(p==0)return ext;
  ll ans=0;
  if(!ext)ans+=f(num,p-1,pos,1,0);
  ans%=mod;
  if(ok){
    if(!ext&&p==1){
      for(int i=0;i<=9;i++){
        if(acur[son[pos][i]])continue;
        ans+=f(num,p-1,son[pos][i],1,1);
        ans=ans%mod;
      }
    }
    else if(!ext){
      for(int i=1;i<=9;i++){
        if(acur[son[pos][i]])continue;
        ans+=f(num,p-1,son[pos][i],1,1);
        ans=ans%mod;
      }
    }
    else{
      for(int i=0;i<=9;i++){
        if(acur[son[pos][i]])continue;
        ans+=f(num,p-1,son[pos][i],1,1);
        ans=ans%mod;
      }
    }
  }
  else{
    if(!ext&&p==1){
      for(int i=0;i<=num[p]-'0';i++){
        if(acur[son[pos][i]])continue;
        if(i==num[p]-'0')ans+=f(num,p-1,son[pos][i],0,1);
        else ans+=f(num,p-1,son[pos][i],1,1);
        ans=ans%mod;
      }
    }
    else if(!ext){
      for(int i=1;i<=num[p]-'0';i++){
        if(acur[son[pos][i]])continue;
        if(i==num[p]-'0')ans+=f(num,p-1,son[pos][i],0,1);
        else ans+=f(num,p-1,son[pos][i],1,1);
        ans=ans%mod;
      }
    }
    else{
      for(int i=0;i<=num[p]-'0';i++){
        if(acur[son[pos][i]])continue;
        if(i==num[p]-'0')ans+=f(num,p-1,son[pos][i],0,1);
        else ans+=f(num,p-1,son[pos][i],1,1);
        ans=ans%mod;
      }
    }
  }
  ans=ans%mod;
  vi[p][pos][ok][ext]=1;
  dp[p][pos][ok][ext]=ans;
  return ans;
}
int main(){
  
  string n;cin>>n;
  int m;cin>>m;
  int n0=1;
  for(int i=1;i<=m;i++){
    string t;cin>>t;
    if(t=="0")n0=0;
    ins(t);
  }
  for(int i=0;i<=id;i++){
    if(vis[i])acur[i]=1;
  }
  b();
  dfs(0,-1,0);  
  reverse(n.begin(),n.end());
  int len=n.size();
  n=' '+n;
  cout<<(f(n,len,0,0,0)-n0+mod)%mod;

  return 0;
}

P3413 SAC#1 - 萌数

小 trick 是找题目限制的充分必要条件,其实一个数 \(s\) 是萌数,意味着它就一定存在一个数位 \(i\) 使得 \(s_i=s_{i-1}\)\(s_i=s_{i-2}\),反之也成立。

于是记录前两个数的状态即可。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define ls p<<1
#define rs (p<<1)+1
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int mod=1e9+7;
ll dp[1010][15][15][3][3];
bool vis[1010][15][15][3][3];
ll f(string num,int p,int n1,int n2,bool ok,bool ext){
  if(vis[p][n1][n2][ok][ext])return dp[p][n1][n2][ok][ext];
  if(p==0)return ext;
  ll ans=0;
  if(!ext){ans+=f(num,p-1,-1,-1,1,0);ans%=mod;}
  if(ok){
    if(!ext&&p==1){
      for(int i=0;i<=9;i++){
        if(i==n1||i==n2)continue;
        ans+=f(num,p-1,n2,i,1,1);ans%=mod;
      }
    }
    else if(!ext){
      for(int i=1;i<=9;i++){
        if(i==n1||i==n2)continue;
        ans+=f(num,p-1,n2,i,1,1);ans%=mod;
      }
    }
    else{
      for(int i=0;i<=9;i++){
        if(i==n1||i==n2)continue;
        ans+=f(num,p-1,n2,i,1,1);ans%=mod;
      }
    }
  }
  else{
    if(!ext&&p==1){
      for(int i=0;i<=num[p]-'0';i++){
        if(i==n1||i==n2)continue;
        if(i==num[p]-'0')ans+=f(num,p-1,n2,i,0,1);
        else ans+=f(num,p-1,n2,i,1,1);
        ans%=mod;
      }
    }
    else if(!ext){
      for(int i=1;i<=num[p]-'0';i++){
        if(i==n1||i==n2)continue;
        if(i==num[p]-'0')ans+=f(num,p-1,n2,i,0,1);
        else ans+=f(num,p-1,n2,i,1,1);
        ans%=mod;
      }
    }
    else{
      for(int i=0;i<=num[p]-'0';i++){
        if(i==n1||i==n2)continue;
        if(i==num[p]-'0')ans+=f(num,p-1,n2,i,0,1);
        else ans+=f(num,p-1,n2,i,1,1);
        ans%=mod;
      }
    }
  }
  dp[p][n1][n2][ok][ext]=ans;
  vis[p][n1][n2][ok][ext]=1;
  return ans;
}
int main(){
  
  string n1,n2;
  cin>>n1>>n2;
  int len1=n1.size();
  int len2=n2.size();
  reverse(n1.begin(),n1.end());
  ll tt1=0,tt2=0,poow=1;
  for(int i=0;i<n1.size();i++){
    tt1=tt1+poow*(n1[i]-'0');
    tt1%=mod;
    poow=poow*10%mod;
  }
  n1=' '+n1;
  reverse(n2.begin(),n2.end());
  poow=1;
  for(int i=0;i<n2.size();i++){
    tt2=tt2+poow*(n2[i]-'0');
    tt2%=mod;
    poow=poow*10%mod;
  }
  n2=' '+n2;
  ll ans2=(tt2-f(n2,len2,-1,-1,0,0)+mod)%mod;
  memset(dp,0,sizeof dp);
  memset(vis,0,sizeof vis);
  ll ans1=(tt1-f(n1,len1,-1,-1,0,0)+mod)%mod;
  bool ok=0;
  for(int i=2;i<=len1;i++){
    if(n1[i]==n1[i-1]||n1[i]==n1[i-2]){ok=1;break;}
  }
  cout<<(ans2-ans1+ok+mod)%mod;
  
  return 0;
}

SCOI2009 windy 数(加强版)

数位 DP 经典题,直接做就可以了。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define ls p<<1
#define rs (p<<1)+1
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
ll dp[20][20][3][3];
bool vis[20][20][3][3];
ll f(string num,int p,int lst,int ok,int ext){
  if(vis[p][lst][ok][ext])return dp[p][lst][ok][ext];
  if(p==0)return ext;
  ll ans=0;
  if(!ext) ans+=f(num,p-1,lst,1,0);
  if(ok){
  	if(ext){
  		for(int i=0;i<=9;i++){
      	if(ext&&abs(i-lst)<=1)continue;
      	ans+=f(num,p-1,i,1,1);
    	}
		}
		else{
			if(p==1){
				for(int i=0;i<=9;i++){
      		if(ext&&abs(i-lst)<=1)continue;
      		ans+=f(num,p-1,i,1,1);
    		}
			} 
			else{
				for(int i=1;i<=9;i++){
      		if(ext&&abs(i-lst)<=1)continue;
      		ans+=f(num,p-1,i,1,1);
    		}
			}
		}
  }
  else{
    if(!ext){
      if(p==1){
        for(int i=0;i<=num[p]-'0';i++){
          if(i==num[p]-'0'){
            ans+=f(num,p-1,i,0,1);
          }
          else ans+=f(num,p-1,i,1,1);
        }
      }
      else{
        for(int i=1;i<=num[p]-'0';i++){
          if(i==num[p]-'0'){
            ans+=f(num,p-1,i,0,1);
          }
          else ans+=f(num,p-1,i,1,1);
        }
      }
    }
    else{
      for(int i=0;i<=num[p]-'0';i++){
        if(abs(i-lst)<=1)continue;
        if(i==num[p]-'0'){
          ans+=f(num,p-1,i,0,1);
        }
        else ans+=f(num,p-1,i,1,1);
      }
    }
  }
  vis[p][lst][ok][ext]=1;
  dp[p][lst][ok][ext]=ans;
  return ans;
}
int main(){
  
  ll a,b;cin>>a>>b;
  a--;
  string n1,n2;
  n1=to_string(a);
  n2=to_string(b);
  int l1=n1.size();
  int l2=n2.size();
  reverse(n1.begin(),n1.end());
  reverse(n2.begin(),n2.end());
  n1=' '+n1;n2=' '+n2;
  ll ans1=f(n2,l2,0,0,0);
  memset(dp,0,sizeof dp);
  memset(vis,0,sizeof vis);
  ll ans2=f(n1,l1,0,0,0);
  cout<<ans1-ans2;
  
  return 0;
}

CF855E

b 进制拆开两个端点之后一样填数即可,状态是个数出现的奇偶性,压到一个数里就行了。

code

Show me the code

PA 2020 Liczba Potyczkowa

小 trick 就是一个数能同时被一堆数整除等价于能被这一堆数的最小公倍数整除。

于是我们只需要记原数 \(\bmod \ 2520\) 的值(\(2520\)\(1\sim 9\)\(\operatorname{lcm}\)),再记录下填入的数的 \(\operatorname{lcm}\) 就行了。

但是其实所有可能的填入的数的 \(\operatorname{lcm}\) 的取值只有不到 \(60\) 种,于是我们离散存这个状态即可。

code

Show me the code

posted @ 2025-08-26 20:46  hm2ns  阅读(10)  评论(0)    收藏  举报