数位dp入门题目总结——区间中满足要求的数的个数

  • 引言
  • 总体策略
  • 不要62
  • Bomb
  • B-number
  • Balanced Number

引言

在算法竞赛中,有一类求出给定区间中符合要求的数的个数问题,这类问题往往区间范围较大,无法通过枚举区间中数再判断条件这种方式来求解,数位dp就是一种解决这种方式的策略。

给出一篇写的很好地文章链接

总体策略

若区间符合可加减,
求解 [l,r] 满足条件的数个数可以通过 [0,r][0,l1] 来完成

问题转化为求解 [0,n]
对于一个小于n的数,必然从高位到低位有一位数小于n中对应位置上的数,这样我们在循环时可以通过枚举每一位数上的数字范围来求解。

不要62

题目链接

杭州人称那些傻乎乎粘嗒嗒的人为62(音:laoer)。
杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除个别的士司机和乘客的心理障碍,更安全地服务大众。
不吉利的数字为所有含有4或62的号码。例如: 62315 73418 88914
都属于不吉利号码。但是,61152虽然含有6和2,但不是62连号,所以不属于不吉利数字之列。
你的任务是,对于每次给出的一个牌照区间号,推断出交管局今次又要实际上给多少辆新的士车上牌照了。

思路:
定义 dp[i][j] 表示以j开头位数为i的满足条件的数的个数,
初始化dp[0][0] =1, 接着预处理完成赋值,包含前导0。

    dp[0][0] = 1;
    for(i=1; i<=7; i++)     //循环多少位数 
    {
        for(j=0; j<=9; j++)     //循环第i位数字取值 
        {
            for(k=0; k<=9; k++)     //循环第i-1位数字取值 
            {
                if(j!=4 && !(j==6 && k==2))
                {
                    dp[i][j] += dp[i-1][k];
                }
            }
        }
    }

递推时由低位向高位枚举,逐步求出所有dp值

通过区间相减方式区间个数问题变为[0, x]间个数问题,统计数字前用v数组记录x每一位数字值。

for(i=size; i>0; i--)
    {
        for(j=v[i-1]-1; j>=0; j--)
        {
            if(j!=4 && !(j==2 && v[i] == 6)) 
            {
                ans += dp[i][j];
            }
        }

        if(v[i-1] == 4 || (v[i] == 6 && v[i-1] == 2))   //第i位取原数字,进行下一位时判断这一位是否可行 
        {
            break;
        }
    }

统计数时由高位向低位枚举,注意边界时(即该位置取v相同数字时)取到4或和之前一位构成62时及时break。
调用时要调用solve(x+1).

程序代码:

#include
#include
using namespace std;
int dp[8][10];   //dp[i][j]  以j开头位数为i的满足条件的数的个数

int work(int x)
{
    vector<int> v;      //x各个数位上的数字 
    while(x > 0){
        v.push_back(x % 10);
        x /= 10;
    }

    int ans = 0;
    int i, j;

    int size = v.size();
    v.push_back(0);
    for(i=size; i>0; i--)
    {
        for(j=v[i-1]-1; j>=0; j--)
        {
            if(j!=4 && !(j==2 && v[i] == 6)) 
            {
                ans += dp[i][j];
            }
        }

        if(v[i-1] == 4 || (v[i] == 6 && v[i-1] == 2))   //第i位取原数字,进行下一位时判断这一位是否可行 
        {
            break;
        }
    }

    return ans;
}

int main()
{
    //freopen("tt.in", "r", stdin);
    //freopen("b", "w", stdout);
    int n, m;
    int i, j, k;

    dp[0][0] = 1;
    for(i=1; i<=7; i++)     //循环多少位数 
    {
        for(j=0; j<=9; j++)     //循环第i位数字取值 
        {
            for(k=0; k<=9; k++)     //循环第i-1位数字取值 
            {
                if(j!=4 && !(j==6 && k==2))
                {
                    dp[i][j] += dp[i-1][k];
                }
            }
        }
    }

    while(1)
    {
        cin >> n >> m;
        if(n == 0 && m == 0){
            break;
        }   

        int ansa = work(n);
        int ansb = work(m+1);

        cout << ansb - ansa << endl;

    }   

    return 0;
}

Bomb

题目链接

The counter-terrorists found a time bomb in the dust. But this time
the terrorists improve on the time bomb. The number sequence of the
time bomb counts from 1 to N. If the current number sequence includes
the sub-sequence “49”, the power of the blast would add one point. Now
the counter-terrorist knows the number N. They want to know the final
points of the power. Can you help them?

题意:
求给定区间中含有49的数字个数

思路1:
反向处理,参考上题求出不含有49的个数,再用区间个数相减。

思路2:
正向思维,定义
dp[i][j][0] 表示以j开头i位数中不含49的个数
dp[i][j][1] 表示以j开头i位数中含49的个数

若当前位和后一位不构成49, dp[i][j][0]=9k=0dp[i1][k][0] dp[i][j][1]=9k=0dp[i1][k][1]
若当前位和后一位 构成49, dp[i][j][1]=9k=0(dp[i1][k][0]+dp[i1][k][1])

统计数字时同样要注意边界条件,若之前边界出现49,则后面数字无论出没出现49都要累加。

程序代码:

#include
#include
using namespace std;
#define LL long long
LL dp[65][10][2];   //dp[i][j][0]表示以j开头i位数中不含49的个数 
                    //dp[i][j][1]表示以j开头i位数中含49的个数 

LL solve(LL x)
{
    vector v;
    while(x > 0)
    {
        v.push_back(x % 10);
        x /= 10;    
    }   

    int size = v.size();
    v.push_back(0);
    LL ans = 0;
    int i, j;
    bool pd = false;
    for(i=size; i>0; i--)
    {
        for(j=0; j1]; j++)
        {
            if(v[i] == 4 && j == 9 || pd)
            {
                ans += dp[i][j][0] + dp[i][j][1];
            }
            else{
                ans += dp[i][j][1];
            }
        }

        if(v[i] == 4 && v[i-1] == 9)    //若i位和i-1位分别为4和9,表示后面找到的位数都满足要求 
        {
            pd = true;
        }
    }

    return ans;
}

int main()
{
    //freopen("tt", "r", stdin); 
    //freopen("a", "w", stdout);
    int T;
    int i, j, k;
    LL n;
    cin >> T;

    dp[0][0][0] = 1;
    dp[0][0][1] = 0;
    for(i=1; i<=63; i++)
    {
        for(j=0; j<=9; j++)     //第i位 
        {
            for(k=0; k<=9; k++)     //第i-1位 
            {
                if(!(j==4 && k==9))
                {
                    dp[i][j][0] += dp[i-1][k][0];   
                    dp[i][j][1] += dp[i-1][k][1];
                }   

                else{
                    dp[i][j][1] += dp[i-1][k][0] + dp[i-1][k][1];
                }
            }       
        }
    }

    while(T--)
    {
        cin >> n;
        //cout << n << " ";
        cout << solve(n+1) << endl;
    }

    return 0;
}

B-number

题目链接

A wqb-number, or B-number for short, is a non-negative integer whose
decimal form contains the sub- string “13” and can be divided by 13.
For example, 130 and 2613 are wqb-numbers, but 143 and 2639 are not.
Your task is to calculate how many wqb-numbers from 1 to n for a given
integer n.

题意:
含有13且能被13整除的数个数

思路:
此题用记忆化搜索较为容易,网上流传较广的记忆化数位dp模板如下

    int dfs(int i, int s, bool e) {  
        if (i==-1) return s==target_s;  
        if (!e && ~f[i][s]) return f[i][s];  
        int res = 0;  
        int u = e?num[i]:9;  
        for (int d = first?1:0; d <= u; ++d)  
            res += dfs(i-1, new_s(s, d), e&&d==u);  
        return e?res:f[i][s]=res;  
    }  

i表示当前i位置
s表示当前状态
e表示是否是当前前缀(即后面的数字能否任意填)
只有在数字可以任意填时,才记录f数组值

应用在这题上,定义
dp[i][j][mod][has]表示第i个位置 以j开头 和13余数为mod 是否含有13的数个数

程序代码:

#include
#include
#include 
using namespace std;
int dp[11][10][13][2];  //dp[i][j][mod][has]:第i个位置 以j开头 和13余数为mod 是否含有13的数个数 
vector<int> v;

int dfs(int pos, int k, int mod, int has, bool limit)
{
    if(pos == 0){
        return mod == 0 && has; 
    }   

    if(!limit && dp[pos][k][mod][has] != -1)
    {
        return dp[pos][k][mod][has];
    }

    int num = limit? v[pos-1]: 9;
    int i;
    int ans = 0;
    for(i=0; i<=num; i++)
    {
        if(k==1 && i==3){
            ans += dfs(pos-1, i, (mod*10+i)%13, 1, limit&&i==num);
        }
        else{
            ans += dfs(pos-1, i, (mod*10+i)%13, has, limit&&i==num);
        }
    }

    if(!limit){
        dp[pos][k][mod][has] = ans;
    }
    return ans;
}

int solve(int x)
{
    v.clear();
    while(x){
        v.push_back(x % 10);
        x /= 10;
    }

    return dfs(v.size(), 0, 0, 0, true);
}

int main()
{
    int n;
    int i, j;
    memset(dp, -1, sizeof(dp));
    while(cin >> n)
    {
        //memset(dp, -1, sizeof(dp));
        cout << solve(n) << endl;
    }

    return 0;
 } 

Balanced Number

题目链接

A balanced number is a non-negative integer that can be balanced if a
pivot is placed at some digit. More specifically, imagine each digit
as a box with weight indicated by the digit. When a pivot is placed at
some digit of the number, the distance from a digit to the pivot is
the offset between it and the pivot. Then the torques of left part and
right part can be calculated. It is balanced if they are the same. A
balanced number must be balanced with the pivot at some of its digits.
For example, 4139 is a balanced number with pivot fixed at 3. The
torqueses are 4*2 + 1*1 = 9 and 9*1 = 9, for left part and right part,
respectively. It’s your job to calculate the number of balanced
numbers in a given range [x, y].

题意:
找出区间内平衡数的个数,所谓的平衡数,就是以这个数字的某一位为支点,另外两边的数字大小乘以力矩之和相等,即为平衡数
4139 选择3为支点. 4*2 + 1*1 = 9 and 9*1 = 9 ,即为平衡数

思路:
定义dp[i][k][s] 表示记录以k为支点 正在填第i位数字时 力矩和为s的数字个数

程序代码:

#include
#include
#include
using namespace std;
#define LL long long 
LL dp[20][20][4000];
vector<int> v;

LL dfs(int pos, int k, int s, bool isuse)   //pos位置  k支点 s力矩和 
{
    if(pos == 0){
        return s==0;
    }   
    if(s < 0){
        return 0;
    }
    if(!isuse && dp[pos][k][s] != -1){
        return dp[pos][k][s];
    }

    int i;
    LL ans = 0;
    int choice = isuse? v[pos-1]:9;
    for(i=0; i<=choice; i++)
    {
        int news = s + (pos-k) * i;
        ans += dfs(pos-1, k, news, isuse && i==choice);
    }

    if(!isuse){
        dp[pos][k][s] = ans;
    }
    return ans;
}

LL solve(LL x)
{
    v.clear();
    while(x)//注意填x>0的话-1不满足,所以填x
    {
        v.push_back(x % 10);
        x /= 10;    
    }   

    int i;
    LL ans = 0;
    for(i=1; i<=v.size(); i++)
    {
        ans += dfs(v.size(), i, 0, true);
    }

    return ans - (v.size()-1);
}

int main()
{
    int T;
    freopen("tt", "r", stdin);
    freopen("a", "w", stdout); 
    int i, j;
    cin >> T;
    memset(dp, -1, sizeof(dp));
    while(T--)
    {
        LL x, y;
        cin >> x >> y;
        cout << solve(y) - solve(x - 1) << endl;
    }

    return 0;
}

你可能感兴趣的:(算法竞赛)