【SPOJ KPSUM】The sum(数位DP)

题目大意:

把1到N的数按顺序写出来,在每两个数位之间交替地添加+-号,求最后的和。如下:
1,2,3,4,5,6,7,8,9,10,11,12→1-2+3-4+5-6+7-8+9-1+0-1+1-1+2=5

题解

两个dp数组

我们需要两个dp数组来帮助我们解决问题。

dp

dp[i][j]表示i位数,第i位为j的数,开头为正,写成题目形式的总和。
dp[2][2] = (2-0)+(2-1)+(2-2)+(2-3)+(2-4)+(2-5)+(2-6)+(2-7)+(2-8)+(2-9)
dp[3][1] = (1-0+0)+(1-0+1)+(1-0+2)+(1-0+3)+...+(1-9+8)+(1-9+9)
所以,当前状态等于j减去上一位状态(当前为正,上一位就为负),而第i位的j就要加 10i1 次。

dp[i][j]=k=09j×10i1dp[i1][k]

dp2

我们还需要一个dp2[i],表示前i位数所有转换为题目形式的答案。
dp2[2] = 1-2+3-4+5-6+7-8+9-1+0-1+1-1+2-...-9+6-9+7-9+8-9+9
所以dp2[i]等于dp2[i-1]加上刚好i位的数计算的结果。

如果i为偶数,则i位数的最高位符号一定为-,只需要减去上所有dp[i][k]即可。注意:k必须从1开始,为0不叫真正的“刚好i位”。

dp2[i]=dp2[i1]k=19dp[i][k]

如果i为奇数,i位数+-一定是交替的,像-1+0-0+1-0+1-1+0-2+1-0+3…
可以发现,除个位以外的数位,都在偶数次加减后抵消掉,只有个位可以对答案有贡献,没两个i位数可以对答案贡献1。(如102与103,-1+0-2+1-0+3=1)
i位数一共有 10i10i1 个,所以总贡献为 10i10i12
dp2[i]=dp2[i1]+(10i10i1)/2

利用两个dp数组求解

要分两种情况,一种是偶数个数位,一种是奇数个数位。

偶数个数位

因为偶数个数位,每一个数位的符号是固定的,最高位一定是-,然后第二位为+…
从高位到低位枚举数位i,枚举每一个小于当前位数字的数字j,用f表示当前的符号,tmp记录前面已经枚举过的数位计算出的结果。 ans+=dp[i][j]×f+tmp×10i1 ,即加上i位数开头为j的全部结果,添上f符号,再加上已经枚举过的大于i的数位的结果,计算 10i1 次。
如果i为最高位,j为0,因为不能有前导0,则此时 ans+=dp2[i1] ,即把位数小于i的结果全部加上。

奇数个数位

用同样的方法枚举,如果i为最高位,j为0的情况同上 ans+=dp2[i1] ,其它情况如下:
我们假设最高位符号为+,然后-…用tmp同样的记录前面的结果,f记录符号。因为数字交替加减,个位以上的都将被抵消,理由与计算dp2时相同。
所以当i>1时,个位的贡献为 10i12 ,即 ans+=10i12 (因为枚举的i,j表示有i位,第i位为j,此时包含 10i1 个数)
当i=1时,可直接手动计算,len表示数的位数,如果 len=1(mod2)&&(j=1(mod2)) ,则最高位符号为+, ans+=tmp+j ,否则为-, ans+=tmpj

(看不懂的看代码)

代码:

#include<cstdio>
#include<cstring>
const int MAXD=17,MAXN=10;
long long dp[MAXD][MAXN],dp2[MAXD],pow10[MAXD]={1,10};
void Init()
{
    for(int j=0;j<MAXN;j++)
        dp[1][j]=j;
    dp2[1]=5;
    pow10[0]=1;
    for(int i=2;i<MAXD;i++)
    {
        pow10[i]=pow10[i-1]*10LL;
        dp2[i]+=dp2[i-1];
        if(i%2==1)
            dp2[i]+=(pow10[i]-pow10[i-1])/2;
        for(int j=0;j<MAXN;j++)
        {
            for(int k=0;k<MAXN;k++)
                dp[i][j]+=1LL*j*pow10[i-2]-dp[i-1][k];
            if(j>0&&i%2==0)
                dp2[i]-=dp[i][j];
        }
    }
}
long long solve(char num[],int len)
{
    long long ret=0;
    int tmp=0,f;
    if(len%2==0)
    {
        f=-1;
        for(int i=len;i>0;i--)
        {
            for(int j=0;j<num[len-i]-'0';j++)
                if(i==len&&j==0)
                    ret+=dp2[i-1];
                else
                    ret+=dp[i][j]*f+tmp*pow10[i-1];
            tmp+=(num[len-i]-'0')*f;
            if(f==-1)f=1;
            else f=-1;
        }
        ret+=tmp;
    }
    else
    {
        f=1;
        for(int i=len;i>0;i--)
        {
            for(int j=0;j<num[len-i]-'0';j++)
                if(i==len&&j==0)
                    ret+=dp2[i-1];
                else
                {
                    if(i>1)
                        ret+=pow10[i-1]/2;
                    else
                    {
                        if(len%2==1&&j%2==1)
                            ret+=tmp+j;
                        else
                            ret+=-tmp-j;
                    }
                }
            tmp+=(num[len-i]-'0')*f;
            if(f==-1)f=1;
            else f=-1;
        }
        if(len%2==1&&(num[len-1]-'0')%2==1)
            ret+=tmp;
        else
            ret+=-tmp;
    }
    return ret;
}
int main()
{
    Init();
    char N[MAXD]="";
    int len=0;
    while(1)
    {
        scanf("%s",N);
        len=strlen(N);
        if(len==1&&N[0]=='0')
            break;
        printf("%lld\n",solve(N,len));
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值