复习 [kuangbin带你飞]专题15 数位dp

目录

  • 1. CodeForces-55D Beautiful numbers
  • 2. hdu 4352 XHXJ's LIS
  • 3. hdu 2089 不要62
  • 4. hdu 3555 Bomb
  • 5. poj 3252 Round Numbers
  • 6. hdu 3709 Balanced Number
  • 7. hdu 3652 B-number
  • 8. hdu 4734 F(x)
  • 10. hdu 4507 吉哥系列故事――恨7不成妻
  • 11. SPOJ-BALNUM Balanced Numbers

1. CodeForces-55D Beautiful numbers

  • 此题非常经典,思路应该记住,问区间范围内能够整除它自己每一位的数有多少个
  • 首先我们知道如果一个数能整除某些数,那么它一定能够整除这些数的最小公倍数,然后注意到 1 − 9 1-9 19任意两个数的最小公倍数最多 48 48 48个,最大 2520 2520 2520,所以考虑一个三维的数位 d p dp dp
  • d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]为当前位置为 p p p,前面位的最小公倍数乘积除以 2520 2520 2520余数为 j j j,前面位的最小公倍数为 k k k,其中 k k k需离散化处理,细节见代码
#include 

using namespace std;

typedef long long ll;

int digit[30];
ll GCD(ll a, ll b){
  return b == 0 ? a : GCD(b, a % b);
}
ll LCM(ll a, ll b){
  return a / GCD(a, b) * b;
}
// dp[i][j][k]表示当前位为i, 前一个模2520的余数为j, 之前的lcm为k 
ll dp[30][2525][50];
int has[2525];
/**
 * @brief 数位dp
 * 
 * @param p 当前位置, 从高到低
 * @param prelcm 前一个最小公倍数
 * @param rem 模2520的余数
 * @param limit 是否处于限制
 * @return ll 
 */
ll Dfs(int p, int prelcm, int rem, bool limit){
  assert(prelcm <= 2520 && rem < 2520);
  if(p < 0) return rem % prelcm == 0;
  if(!limit && dp[p][rem][has[prelcm]] != -1) return dp[p][rem][has[prelcm]];
  ll ans = 0;
  int up = (limit ? digit[p] : 9);
  for(int i=0;i<=up;i++){
    int now = prelcm;
    if(i > 0) now = LCM(i, now);
    ans += Dfs(p - 1, now, (rem * 10 + i) % 2520, limit && i == up);
  }
  if(!limit) dp[p][rem][has[prelcm]] = ans;
  return ans;
}
ll solve(ll n){
  int p = 0;
  while(n > 0){
    digit[p++] = n % 10;
    n /= 10;
  }
  return Dfs(p - 1, 1, 0, true);
}
int main(){
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  int t;
  cin >> t;
  int cnt = 0;
  memset(dp, -1, sizeof dp);
  for(int i=1;i<=2520;i++){
    if(2520 % i == 0){
      has[i] = ++cnt;
    }
  }
  while(t--){
    ll l, r;
    cin >> l >> r;
    cout << solve(r) - solve(l - 1) << '\n';
  }
  return 0;
}

2. hdu 4352 XHXJ’s LIS

  • 将数看成字符串,让你找 L I S LIS LIS长度为 k k k的数有多少个
  • 核心思想是使用一个二进制数来记录当前的 L I S LIS LIS状态,这里和 L I S LIS LIS的更新思想是一样的,同时注意前导 0 0 0等细节
  • d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]表示当前是第 i i i位, L I S LIS LIS状态为 j j j,长度为 k k k的数字个数
#include 

using namespace std;

typedef long long ll;

int digit[30];
int k;

ll bit(int state){
  int cnt = 0;
  while(state > 0){
    state -= (state & -state);
    cnt += 1;
  }
  return cnt;
}
ll dp[30][1 << 10][12];
int upd(int x, int s){
  for(int i=x;i<10;i++){
    if(s & (1 << i)){
      return (s ^ (1 << i)) | (1 << x);
    }
  }
  return s | (1 << x);
}
ll Dfs(int p, int state, int lead, bool limit){
  if(p < 0) return bit(state) == k;
  if(!limit && dp[p][state][k] != -1) return dp[p][state][k];
  int up = (limit ? digit[p] : 9);
  ll ans = 0;
  for(int i=0;i<=up;i++){
    ans += Dfs(p - 1, (lead && i == 0) ? 0 : upd(i, state), lead && i == 0, limit && i == up);
  }
  if(!limit) dp[p][state][k] = ans;
  return ans;
}
ll solve(ll n){
  int p = 0;
  while(n > 0){
    digit[p++] = n % 10;
    n /= 10;
  }
  return Dfs(p - 1, 0, 1, true);
}
int main(){
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  int t;
  cin >> t;
  memset(dp, -1, sizeof dp);
  for(int kase=1;kase<=t;kase++){
    ll l, r;
    cin >> l >> r >> k;
    cout << "Case #" << kase << ": " << solve(r) - solve(l - 1) << '\n';
  }
  return 0;
}

3. hdu 2089 不要62

  • 排除掉4和62,设 d p [ i ] [ j ] dp[i][j] dp[i][j]表示当前为第 i i i位,前一位是 j j j的方案数
  • 按照常规记忆化搜索进行数位 d p dp dp
#include 

using namespace std;

typedef long long ll;
int digit[20];
int dp[30][30];// dp[i][j]表示当前是第i位, 前一位数是j
// p表示当前是第几位(从高到低), pre表示前一位的数字是多少, limit表示现在是否有限制
ll Dfs(int p, int pre, int limit){
  if(p < 0){
    return 1ll;
  }
  if(!limit && dp[p][pre] != -1) return dp[p][pre];
  int up = (limit ? digit[p] : 9);// 如果有限制它最多也就到digit[p]
  ll ans = 0;
  for(int i=0;i<=up;i++){
    if(i == 2 && pre == 6) continue;
    if(i == 4) continue;
    // 往后走一位, pre变为i, 如果前一位受限制且当前位是up, 那么接下来也都会受限制
    ans += Dfs(p - 1, i, limit && i == up);
  }
  return ans;// 算出来的结果是不包含4 且不包含62的数量
}
ll solve(ll n){
  int p = 0;
  while(n > 0){
    digit[p++] = n % 10;
    n /= 10;
  }
  return Dfs(p - 1, 0, true);// 第一位显然受限制
}
int main(){
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  ll n, m;
  memset(dp, -1, sizeof dp);
  while(cin >> n >> m && n + m > 0){
    cout << solve(m) - solve(n - 1) << '\n';
  }
  return 0;
}

4. hdu 3555 Bomb

  • [ 1 , n ] [1,n] [1,n]中不含49的数的个数
  • d p [ i ] [ j ] dp[i][j] dp[i][j]表示当前为第 i i i位,前一个是 j j j的方案数,是上一题的简单版本
#include 

using namespace std;

typedef long long ll;

ll dp[20][10];
int digit[20];

ll Dfs(ll p, ll pre, bool limit){
  if(p < 0) return 1ll;
  if(!limit && dp[p][pre] != -1) return dp[p][pre];
  int up = (limit ? digit[p] : 9);
  ll ans = 0;
  for(int i=0;i<=up;i++){
    if(i == 9 && pre == 4) continue;
    ans += Dfs(p - 1, i, limit && i == up);
  }
  if(!limit) dp[p][pre] = ans;
  return ans;
}
int main(){
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  int t;
  memset(dp, -1, sizeof dp);  
  cin >> t;
  while(t--){
    ll n;
    cin >> n;
    ll tmp = n;
    int p = 0;
    while(tmp > 0){
      digit[p++] = tmp % 10;
      tmp /= 10;
    }
    cout << n + 1 - Dfs(p - 1, 0, true) << '\n';
  }
  return 0;
}

5. poj 3252 Round Numbers

  • 问你范围内有多少个数二进制表示中 0 0 0的个数不少于 1 1 1的个数
  • d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]表示当前位置为 i i i,当前有 j j j 0 0 0 k k k 1 1 1
  • 将数字转换为二进制处理
  • 此题需要注意前导 0 0 0的问题
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

typedef long long ll;

int digit[70];// 2进制形式储存
ll dp[70][70][70];
/**
 * @brief 数位dp
 * 
 * @param p 当前是第几位
 * @param one 1的个数
 * @param zero 0的个数
 * @param lead 前导数是多少, 如果是0则表示有前导0, 否则没有前导0
 * @param limit 
 * @return ll 
 */
ll Dfs(int p, int one, int zero, bool lead, bool limit){
  if(p < 0){
    return zero >= one;
  }
  if(!limit && dp[p][one][zero] != -1) return dp[p][one][zero];
  ll ans = 0;
  int up = (limit ? digit[p] : 1);
  for(int i=0;i<=up;i++){
    if(i > 0){
      // 如果这一位不是0, 那么前导0就消失了
      ans += Dfs(p - 1, one + 1, zero, i, limit && i == up);
    }else{
      // 如果这一位是0, 那么判断是前导0还是普通的0位
      ans += Dfs(p - 1, one, zero + (lead ? 1 : 0), lead, limit && i == up);
    }
  }
  if(!limit) dp[p][one][zero] = ans;
  return ans;
}
ll solve(ll n){
  int p = 0;
  while(n > 0){
    digit[p++] = (n & 1);
    n >>= 1;
  }
  return Dfs(p - 1, 0, 0, 0, true);// 刚开始时候有前导0
}
int main(){
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  ll l, r;
  memset(dp, -1, sizeof dp);
  cin >> l >> r;
  cout << solve(r) - solve(l - 1) << '\n';  
  return 0;
}

6. hdu 3709 Balanced Number

  • 注意到每个数最多只能有一个这样的中心点,考虑枚举每一个中心点,然后利用左右两侧点缀和互为相反数,可得 d p dp dp设法
  • 但这样考虑忽略了前导0可能带来的问题,实际上对于 00012323 00012323 00012323这样的是不影响的,但是对于 000000 000000 000000这样的实际上是不合法的,所以需要把这些全是 0 0 0的减掉,只保留一个 0 0 0
#include 

using namespace std;

typedef long long ll;

ll dp[30][30][18 * 18 * 10];// dp[i][j][k]表示当前位置为i, 中心点为j, 前缀和为k
int digit[30];
/**
 * @brief 数位dp
 * 
 * @param p 当前位置
 * @param center 中心点
 * @param pre 前缀和, 如果最后结果为0, 说明这个数字是合法的
 * @param limit 是否处于限制位
 * @return ll 
 */
ll Dfs(int p, int center, int pre, bool limit){
  if(p < 0){
    return pre == 0;
  }
  if(pre < 0) return 0;
  if(!limit && dp[p][center][pre] != -1) return dp[p][center][pre];
  int up = (limit ? digit[p] : 9);
  ll ans = 0;
  for(int i=0;i<=up;i++){
    ans += Dfs(p - 1, center, pre + (p - center) * i, limit && i == up);
  }
  if(!limit) dp[p][center][pre] = ans;
  return ans;
}
ll solve(ll n){
  if(n < 0) return 0ll;
  int p = 0;
  while(n > 0){
    digit[p++] = n % 10;
    n /= 10;
  }
  ll ans = 0;
  for(int i=0;i<p;i++){
    ans += Dfs(p - 1, i, 0, true);
  }
  return ans - p + 1;
}
int main(){
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  int t;
  cin >> t;
  memset(dp, -1, sizeof dp);
  while(t--){
    ll l, r;
    cin >> l >> r;
    cout << solve(r) - solve(l - 1) << '\n';
  }
  return 0;
}

7. hdu 3652 B-number

  • 问数字中有 13 13 13且能够被 13 13 13整除的数字的个数,一定要注意 d p dp dp方程的设法,我一开始想的是记录前一个数字,但是这样的状态转移是错误的,因为只考虑前一个数字是有后效性的,换而使用 0 , 1 , 2 0,1,2 0,1,2三个状态记录当前字符串的状况,这样转移才是对的
#include 

using namespace std;

typedef long long ll;

int digit[35];

int dp[35][3][13];// dp[i][j][k] 当前第i位, 前一个状态是j
// j=0表示普通状态, j=1表示前一个是1, j=2表示前面有字符串是13, 模数为k
int Dfs(int p, int pre, int md, bool limit){
  if(p < 0){
    return pre == 2 && md == 0;
  }
  if(!limit && dp[p][pre][md] != -1) return dp[p][pre][md];
  int up = (limit ? digit[p] : 9);
  int ans = 0;
  for(int i=0;i<=up;i++){
    int tmp = pre;
    if(pre == 0 && i == 1) tmp = 1;
    else if(pre == 1){
      if(i == 3) tmp = 2;
      else if(i == 1) tmp = 1;
      else{
        tmp = 0;
      }
    }
    ans += Dfs(p - 1, tmp, (md * 10 + i) % 13, limit && i == up);
  }
  if(!limit) dp[p][pre][md] = ans;
  return ans;
}
int solve(int n){
  int p = 0;
  while(n > 0){
    digit[p++] = n % 10;
    n /= 10;
  }
  return Dfs(p - 1, 0, false, true);
}
int main(){
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  int n;
  memset(dp, -1, sizeof dp);
  while(cin >> n){
    cout << solve(n) << '\n';
  }
  return 0;
}

8. hdu 4734 F(x)

  • 让你找权重不大于 F ( A ) F(A) F(A)的数字的个数
  • 如果设 d p [ i ] [ j ] dp[i][j] dp[i][j]表示当前为第 i i i位,权重为 j j j的方案数,应对单组数据还行,多测无法清空
  • 考虑一种 d p dp dp方式,能够让它的每一个状态能够应对所有数字而不是单一范围
  • d p [ i ] [ j ] dp[i][j] dp[i][j]表示当前位是 i i i j = s u m a − s u m j=sum_a-sum j=sumasum s u m sum sum表示当前前缀的权重,这样是一般化的 d p dp dp方程,对于所有满足情况的方案数都是唯一的
#include 

using namespace std;

typedef long long ll;

int digit[35];
int dp[35][42000];
int now_a;
int Dfs(int p, int now, bool limit){
  if(now_a < now) return 0;
  if(p < 0) return now <= now_a;
  assert(now_a - now < 42000);
  if(!limit && dp[p][now_a - now] != -1) return dp[p][now_a - now];
  int up = (limit ? digit[p] : 9);
  int ans = 0;
  for(int i=0;i<=up;i++){
    ans += Dfs(p - 1, now + (1 << p) * i, limit && i == up);
  }
  if(!limit) dp[p][now_a - now] = ans;
  return ans;
}
int solve(int n){
  int p = 0;
  while(n > 0){
    digit[p++] = n % 10;
    n /= 10;
  }
  return Dfs(p - 1, 0, true);
}
int main(){
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  int t;
  cin >> t;
  memset(dp, -1, sizeof dp);     
  for(int kase=1;kase<=t;kase++){
    int a, b;
    cin >> a >> b;
    now_a = 0;
    int k = 1;
    while(a > 0){
      now_a += k * (a % 10);
      a /= 10;
      k <<= 1;
    }
    cout << "Case #" << kase << ": " << solve(b) << '\n';
  }
  return 0;
}

10. hdu 4507 吉哥系列故事――恨7不成妻

  • 求平方和的数的个数,数位 d p dp dp关键在于加上一位之后的影响,我们要记录之前枚举过了的位的和,以及平方和,再根据满足条件的数的个数来计算加上这一位对于答案的影响
#include 

using namespace std;

typedef long long ll;

const int MOD = 1e9 + 7;

const int N = 20;
int digit[N];
ll pow10[N];
struct st{
  ll cnt;// 满足条件的数的个数
  ll sum;// 满足条件的数的和
  ll sum2;// 满足条件的数的平方和
  st(){}
  st(ll cnt, ll sum, ll sum2){
    this->cnt = cnt;
    this->sum = sum;
    this->sum2 = sum2;
  }
}dp[N][N][N];
st Dfs(int p, int md, int tot, bool limit){
  if(p < 0){
    return st(md != 0 && tot != 0, 0, 0);
  }
  if(!limit && dp[p][md][tot].cnt != -1){
    return dp[p][md][tot];
  }
  int up = (limit ? digit[p] : 9);
  st ans(0, 0, 0);
  for(int i=0;i<=up;i++){
    if(i == 7) continue;
    auto tmp = Dfs(p - 1, (i + md * 10) % 7, (i + tot) % 7, i == up && limit);
    ans.cnt += tmp.cnt;
    ans.sum += (tmp.sum + 1ll * i * pow10[p] % MOD * tmp.cnt % MOD) % MOD;
    ans.sum2 += ((tmp.sum2 + 2ll * i * pow10[p] % MOD * tmp.sum % MOD) % MOD + 1ll * i * tmp.cnt % MOD * pow10[p] % MOD * i % MOD * pow10[p] % MOD) % MOD;
    ans.cnt %= MOD;
    ans.sum %= MOD;
    ans.sum2 %= MOD;
  }
  if(!limit) dp[p][md][tot] = ans;
  return ans;
}
ll solve(ll n){
  int p = 0;
  while(n > 0){
    digit[p++] = n % 10;
    n /= 10;
  }
  return Dfs(p - 1, 0, 0, true).sum2;
}
int main(){
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  int t;
  pow10[0] = 1ll;
  for(int i=1;i<=18;i++){
    pow10[i] = pow10[i - 1] * 10 % MOD;
  }
  memset(dp, -1, sizeof dp);
  cin >> t;
  while(t--){
    ll l, r;
    cin >> l >> r;
    cout << ((solve(r) - solve(l - 1)) % MOD + MOD) % MOD << '\n';
  }
  return 0;
}

11. SPOJ-BALNUM Balanced Numbers

  • 问你有多少个数的每个数位奇数出现偶数次,偶数出现奇数次
  • 关键点在于如何去记录这个信息,实际上可以使用一个三进制数,很巧妙,如果某一位是 1 1 1说明这位出现了奇数次, 2 2 2说明出现偶数次, 0 0 0说明没出现过
  • 然后我们设 d p [ i ] [ j ] dp[i][j] dp[i][j]表示当前位为 i i i,前面的三进制数的状态为 j j j,因为 3 10 < 60000 3^{10}\lt60000 310<60000,所以完全开的下
#include 

using namespace std;

typedef long long ll;

int digit[20];
ll dp[20][60000];

bool ck(int st){
  int p = 0;
  while(st > 0){
    if(st % 3 == 1 && p % 2 == 1){
      return false;
    }
    if(st % 3 == 2 && p % 2 == 0){
      return false;
    }
    p += 1;
    st /= 3;
  }
  return true;
}
int modify(int st, int pos){
  int p = 0;
  int ans = 1;
  int x = st;
  while(x > 0){
    if(pos == p){
      break;
    }
    x /= 3;
    p += 1;
    ans *= 3;
  }
  while(pos != p){
    ans *= 3;
    p += 1;
  }
  if(x % 3 <= 1) st += ans;
  else st -= ans;
  return st;
}
ll Dfs(int p, int st, bool lead, bool limit){
  if(p < 0){
    return ck(st);
  }
  if(!limit && dp[p][st] != -1) return dp[p][st];
  int up = (limit ? digit[p] : 9);
  ll ans = 0;
  for(int i=0;i<=up;i++){
    ans += Dfs(p - 1, (lead && i == 0 ? st : modify(st, i)), lead && i == 0, limit && i == up);
  }
  if(!limit) dp[p][st] = ans;
  return ans;
}
ll solve(__int128_t n){
  int p = 0;
  while(n > 0){
    digit[p++] = n % 10;
    n /= 10;
  }
  return Dfs(p - 1, 0, true, true);
}
int main(){
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  int t;
  cin >> t;
  memset(dp, -1, sizeof dp);
  while(t--){
    __int128_t a = 0, b = 0;
    string s;
    cin >> s;
    int len = s.length();
    for(int i=0;i<len;i++){
      a *= 10;
      a += s[i] - '0';
    }
    cin >> s;
    len = s.length();
    for(int i=0;i<len;i++){
      b *= 10;
      b += s[i] - '0';
    }
    cout << solve(b) - solve(a - 1) << '\n';
  }
  return 0;
}

你可能感兴趣的:(#,专项训练,深度优先,算法,图论)