Segment Sum CodeForces - 1073E (经典数位dp统计和问题)

题意:给出l,r求出区间里,满足不同数的个数小于等于k的数的和。

思路:先解决第一个问题:如何统计不同数的个数?思路很简单,因为只有0到9这10个数字,每出现一个新数字,将其用二进制状态表示出来,那么我们只要统计最后状态即可知道有多少个不同的数字。

第二个问题:如何计算和? 首先一个错误的思路会这样想,dp[pos][sta]表示枚举到pos位时,当前状态为sta的满足条件的数的和,也就是每次枚举到pos==-1位时,返回满足的数num或0。这样只能跑对第一组样例,原因很简单,我们是记忆化搜索,dp[pos][sta]

当搜索过1234和4213时,sta记录的量是相同的,所以发生了状态重复问题。

也就是说,我们采用dp[pos][sta]的状态表示,我们不能直接得出和,我们只能找出有sta这种状态的数有多少个。这里可以思考下,当我们搜索过1234 后,dp[pos][sta]已经被我们记录下来(sta=1111),当我们再次搜索1243时,就可以直接利用记忆化结果,因为1234满足,那么1243也一定满足。

所以我们采用的思路就是先统计满足状态的个数,最后再考虑如何进行计算。

对于这样数位dp的统计和问题,套路就是按位计算贡献。

比如,我们数位进行到12时,我们考虑2对最后结果的贡献,前面的12已经确定了,我们还要考虑2到底会出现多少次,也就是说,如果后面是 34,35,36......这些数都满足题意,那么我们的2的贡献必然是2*pow[pos]*cnt,cnt为后面到底还有多少个满足题意。这样就可以解决求和问题。

第三个问题:前导0问题。

只要是和统计各位上数的个数问题,都必须考虑前导0,原因是这样的,如果我们枚举0012,很明显,实际的数只是12,如果我们没有关注前导0,那么我们会把0也当成一个新的数,进而统计了0012有3个不同的数组成。

解决方案就是,dfs的同时利用zero来记录是否有前导0,如果有前导0,并且当前位还是0,那么就说明现在的数还是0,不用考虑。

#include 
using namespace std;

typedef long long ll;
const int p = 998244353;
int k, a[20], pw[20];
struct pr 
{
    int tot, sum;
    pr() {}
    pr(int a, int b) : tot(a), sum(b) {}
    void clr() { tot = sum = 0; }
    bool chk() { return ~tot && ~sum; }
} dp[20][1 << 10];

void add(pr& a, pr b, int num, int pos) 
{
    a.tot = (a.tot + b.tot) % p;
    a.sum = (a.sum + 1ll * num * pw[pos] % p * b.tot % p + b.sum) % p;//类似统计前缀和
}

pr dfs(bool lim, bool zero, int pos, int mark) 
{
    if (!pos) return pr(1, 0);
    pr& res = dp[pos][mark];
    if (lim==0&&res.chk()) return res;
    res.clr();
    for (int i = 0; i <= (lim ? a[pos] : 9); i++) 
    {
        if (__builtin_popcount(zero && !i ? 0 : mark | 1 << i) <= k) 
        {
            add(res, dfs(lim && i == a[pos], zero && !i, pos - 1, zero && !i ? 0 : mark | 1 << i), i, pos - 1);
        }
    }
    return res;
}

int calc(ll x) 
{
    int len = 0;
    __builtin_memset(dp,-1,sizeof(dp));
    //memset(dp, -1, sizeof dp);
    while (x) a[++len] = x % 10, x /= 10;
    return dfs(1, 1, len, 0).sum;
}

int main() 
{
    #ifndef ONLINE_JUDGE
        freopen("in.txt","r",stdin);
        freopen("out.txt","w",stdout);
    #endif
    pw[0] = 1;
    for (int i = 1; i < 19; i++) 
    {
        pw[i] = 10ll * pw[i - 1] % p;
    }
    ll l, r;
    scanf("%lld %lld %d", &l, &r, &k);
    return 0*printf("%d", (calc(r) - calc(l - 1) + p) % p);
}

 

你可能感兴趣的:(数位dp)