r HDU - 3709 Balanced Numbe(数位dp解析)

题目链接https://vjudge.net/contest/355127#problem/C
Problem Description
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].
Input
The input contains multiple test cases. The first line is the total number of cases T (0 < T ≤ 30). For each case, there are two integers separated by a space in a line, x and y. (0 ≤ x ≤ y ≤ 1018).
Output
For each case, print the number of balanced numbers in the range [x, y] in a line.

Sample Input

2
0 9
7604 24324 

Sample Output

10
897

翻译
求给定区间 [l,r] 内平衡数的个数
平衡数是指,某个数以某位作为支点,此位左边的 数字*距离 的和与右边相等
例如:4139,以3位支点,左边的距离之和: 4* 2 + 1 * 1,右边的距离之和:9 *1,两边相等,所以4139为平衡数

引入数位dp
数位dp是一种计数用的dp,一般就是要统计一个区间[le,ri]内满足一些条件数的个数

传入的参数

  1. 前导0标记lead

例如我们要从 [0,1000] 找任意相邻两数相等的数,显然 111,222,888 等等是符合题意的数
但是我们发现右端点1000 是四位数
因此我们搜索的起点是 0000 ,这样三位数的记录都变成了如 0111,0222,0888 等等
而这种情况下如果我们直接找相邻位相等,则 0000 符合题意,但是 0111,0222,0888 都变成不符合题意了
所以我们要引入前导0标记

如果当前位 lead=1 而且当前位也是0,那么当前位也归为前导0, pos+1 继续搜;
如果当前位 lead=1 但当前位不是0,则本位作为当前数的最高位, pos+1 继续搜;

LL dfs(int pos,int pre,int st,....,int lead,int limt)
{
    if(pos>len)
        return st;/*剪枝*/
    if(dp[pos][pre][st]..[...]!=-1&&!limt&&!lead)
        return dp[pos][pre][st]..[...];
    LL res=0;/*暂时记录当前的方案数*/
    int up=limt?a[len-pos+1]:9;
    for(int i=0; i<=up; i++)
    {
        if(!i&&lead)
            res+=dfs(...,...,i==up&&limt);/*有前导0,并且当前位也是0*/
        else if(i&&lead)
            res+=dfs(...,...,i==up&&limt);/*有前导0,但当前位不是0,当前位就是最高位*/
        else if(/*根据题意而定的判断*/)
            res+=dfs(...,.....,i==up&&limt)
        }
}
  1. 最高位标记limt

在搜索数位时,搜索范围可能发生变化
举个例子:我们在搜索 [0,555] 的数时,显然最高位搜索范围是 0 ~ 5 ,而后面的位数的取值范围会根据上一位发生变化:
最高位是 1 ~ 4 时,第二位取值为 [0,9]
最高位是 5 时,第二位取值为 [0,5] (再往上取就超出右端点范围了)

为了分清这两种情况,我们引入了 limt 标记:

若当前位 limit=1 而且已经取到了能取到的最高位时,下一位 limit=1 ;(limt=1表示有限制,limt=0表示没有限制
若当前位 limit=1 但是没有取到能取到的最高位时,下一位 limit=0 ;
若当前位 limit=0 时,下一位 limit=0 。
我们设这一位的标记为 limit ,这一位能取到的最大值为 res ,则下一位的标记就是 i==res && limit ( i 枚举这一位填的数)

  1. dp值的记录和使用

数位dp是在记忆化搜索的框架下进行的,每当找到一种情况我们就可以这种情况记录下来,等到搜到后面遇到相同的情况时直接使用当前记录的值。
dp数组的下标表示的是一种状态,只要当前的状态和之前搜过的某个状态完全一样,我们就可以直接返回原来已经记录下来的dp值。
再举个例子
假如我们找 [0,123456] 中符合某些条件的数
假如当我们搜到1000时,dfs从下返上来的数值就是当前位是第 5 位,前一位是 0 时的方案种数,搜完这位会向上反,这是我们可以记录一下:当前位第 5 位,前一位是 0 时,有的方案种数
当我们继续 搜到 1010时,我们发现当前状态又是搜到了第 5 位,并且上一位也是 0 ,这与我们之前记录的情况相同,这样我们就可以不继续向下搜,直接把上次的dp值返回就行了。

返回的dp值必须和当前处于完全一样的状态,这就是为什么dp数组下标要记录 pos,pre 等参量了。

接着上面的例子,范围 [0,123456]
如果我们搜到了 1234 ,我们能不能直接返回之前记录的:当前第 5 位,前一位是 4 时的dp值?
答案是否定的
我们发现,这个状态的dp值被记录时,当前位也就是第 5 位的取值是 [0,9] ,而这次当前位的取值是 [0,5] ,方案数一定比之前记录的dp值要小。
当前位的取值范围为什么会和原来不一样呢?
如果你联想到了之前所讲的知识,你会发现:现在的 limit=1 ,最高位有取值的限制。
因此我们可以得到一个结论:当 limit=1 时,不能记录和取用dp值
完整代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long LL;
int a[20];
LL dp[20][20][2000];
int tot;
LL dfs(int pos,int x,int sta,int limt)
{
    if(sta<0)/*支点两边的和<0,相当于剪枝*/
        return 0;
    if(pos==0)/*每一位都枚举完了,如果sta==0,则返回1,否则返回0*/
        return sta==0;
    if(!limt&&dp[pos][x][sta]!=-1)/*最高位没有限制*/
        return dp[pos][x][sta];
    int up=limt?a[pos]:9;
    LL res=0;
    for(int i=0; i<=up; i++)
    {
        int nex=sta+i*(pos-x);/*x是支点的下标,起初pos为数的总长度,pos-x>0,随着pos的减小,pos-x<0满足了支点两边一正一负,状态累加,状态sta=0是目标状态*/
        res+=dfs(pos-1,x,nex,limt&&(i==up));
    }
    if(!limt)
        dp[pos][x][sta]=res;/*位数为pos,支点为x,两边的支点和为sta的个数*/
    return res;
}
LL solve(LL n)
{
    tot=0;
    while(n)
    {
        a[++tot]=n%10;
        n/=10;
    }
    LL sum=0;
    for(int i=1; i<=tot; i++)
        sum+=dfs(tot,i,0,1);/*数的总长度是tot,把第i为当成是支点,两边的支点和为0的方案数*/
    return sum-tot+1;/*00,000,.....,*/
}
int main()
{
    int t;
    memset(dp,-1,sizeof(dp));
    scanf("%d",&t);
    while(t--)
    {
        LL x,y;
        scanf("%lld%lld",&x,&y);
        printf("%lld\n",solve(y)-solve(x-1));/*相当于0~r区间内的数减去0~(l-1)区间内的数*/
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zaiyang遇见

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值