HOJ 11104题解(简单DP)

题目传送门:HOJ11104

额,你湖OJ需要内网才能进,所以,还是得贴一下题目啊:

简单翻译一下吧,就是说,给你两个数n和m,求不大于n的各数位和不大于m的自然数有多少个。n是【0,1e9),m是[0,100)

一看这一题,数位DP!!然后注意到一个问题,n只有1e9,不符合社会主义核心价值观,额,不对,应该是这大小不像数位DP的规模,数位DP起码得1e18吧(那种特别简单的除外),然后就否定了数位DP的想法(其实是我因为想不出状态转移方程~~)。队友又提供了一个想法:二维背包——背包的限制有两个:m和n,然后每一位当成一个空间。貌似很有道理的样子,但很快又被否定,1e9的背包,即使不超时,数组我也开不了啊,要知道背包容量多大,就得开多大的数组啊。。。

再后来,我想到一个题目:https://blog.csdn.net/q1410136042/article/details/80057922

虽然这两题貌似并没多大关系,但这一题我印象很深——它让我对DP有了新的理解(具体也说不上来,总之,没有那一题我是解不了这一题的)

言归正传,分析一波。

(1)先不考虑这个数是不是比n大,只考虑位数不超过i,以及位数和为j的自然数有多少个,以dp[i][j]表示。根据dp的思想(多阶段决策),dp[i][]可由dp[i-1][]转移过来。假设增加的那一位为k,则dp[i][j]=dp[i-1][j-k],而增加的一位可能是0~9,所以dp[i][j] = Σdp[i-1][j-k](0<=k<=min(j,9))

(2)第一步完全可以预处理出来,然后考虑n的大小。继续这时候仍利用dp[][],设n的位数为l,则当i>l时,dp[i][]此时是不用计算的,当i<l时,完全不用担心会比n大,唯一需要分析的也就是当i==l的了。根据(1)中求出的dp[][]可以先算出∑dp[i][k](0<=k<=m),然后去掉比n大的部分。怎么找到这部分呢?举个栗子,12345,首先考虑万位,万位为2~9的都会比n大,这部分为∑ ∑ dp[4][j-k](2<=k<=9, k <= j <= m),这个自己想一下应该是能理解的,就不多做解释了。显然,当万位为1,还是会可能出现比n大的数字,这时就要考虑千位了,千位为3~9的都会比n大,这部分为∑ ∑dp[3][j-1-k](3<=k<=9,k+1<=j<=m),j-1-k里的1便是万位上面已经确定的1了,由于这一位已经确定,算后面三位的数位和,必然要不止是减掉千位的k,万位的1也是从总的数位和里面减去的。再往后的过程不再赘述,基本就这样了

AC代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<string>
#include<cmath>
#define ll long long

using namespace std;
ll dp[10][100]; //dp[i][j]长度不超过i的,数位和为j的数字数量

int main()
{
    #ifdef AFei
    freopen("in.c", "r", stdin);
    #endif // AFei
    for(int i = 1; i < 100; ++ i)
        dp[0][i] = 0;
    for(int i = 0; i < 10; ++ i)
        dp[i][0] = 1;
    for(int i = 1; i < 10; ++ i)
        for(int j = 1; j < 100; ++ j)
            for(int k = 0; k <= 9 && k <= j; ++ k)  //新增的最高位为0~9
                dp[i][j] += dp[i-1][j-k];
    int p[10] = {1};
    for(int i = 1; i < 10; ++ i)    //计算10^i
        p[i] = p[i-1] * 10;

//    for(int i = 0; i <= 3; ++ i)
//    {
//        for(int j = 0; j <= 3; ++ j)
//            printf("(%d, %d)=%d\t", i, j, dp[i][j]);
//        cout << endl;
//    }
    int n, m;
    while(~scanf("%d%d", &n, &m))
    {
        if(n == 0)
        {
            printf("1\n");
            continue;
        }
        int l = log10(n) + 1;
        ll cnt = 0;
        for(int j = 0; j <= m; ++ j)
            cnt += dp[l][j];    //所有长度不超过l,数位和不超过m的数字数量
        int w[10] = {0};        //将n每一位分出来
        for(int i = 0; i < l; ++ i)
            w[i] = n / p[l-1-i] % 10;
        int tmp = 0;
//        cout << "cnt = " << cnt << endl;
        for(int i = 0; i < l; ++ i)
        {
            for(int j = w[i]+1; j <= 9 && j <= m-tmp; ++ j)
            {
                for(int k = 0; k <= m-j-tmp; ++ k)
                    cnt -= dp[l-i-1][k];
            }
            tmp += w[i];
        }
        printf("%lld\n", cnt);
    }
    return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值