luogu3281[SCOI2013]数数

该博客介绍了SCOI2013数数问题的解决思路,探讨了如何利用动态规划记录每个开头处的前缀和,并通过枚举开头和长度来确定子串。博主详细讲解了状态转移方程,并提供了代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

链接

  https://www.luogu.org/problem/show?pid=3281

题解

  不太好想。。
  如果我们直接记录字串的和,转移会不太好想(反正我是没想出来)。考虑确定子串的因素可以是开头和长度,所以记录每个开头处的所有前缀的和,统计答案时枚举开头即可。
  f[i][0/1]表示长度为i的数的所有前缀的和(允许前导0的出现)。
  假设f[i-1][0]已经求出,我们枚举最高位的数字 now[0,B1] ,我们要求的是所有以 now 为开头的那些前缀数字的和,既然最高位已经确定,就是说后面的位要从0循环到 Bi11 ,一共 Bi1 个数,每个数中, now 都出现了,且每次都作为 i 个串的开头,对答案的贡献分别是now×B0 ~ now×Bi1 ,所以now这位对答案的总贡献是 now×Bi1i1l=0Bl 。我们对 B 的幂进行预处理,对B的幂的前缀和进行预处理,但是还要枚举 now 会T,在因为 now 的枚举区间是固定的且成等差数列,可以直接使用等差数列求和公式。去掉 now 之后剩下的前缀的和就是一个相同的子问题了,直接对 f[i][0] 加上 B×f[i1][0] 就好。对f[i][1]进行类似的分析,可以最终得到:
  

f[i][0]=B×f[i1][0]+B(B1)2Bi1S[i1]  f[i][1]=num[i]×f[i1][0]+num[i](num[i]1)2n[i1]S[i1]+f[i1][1]+num[i](n[i1]+1)S[i1]

  其中 num[i] 表示输入的i这位上的数字, n[i] 表示后 i 位组成的数字,S[i]表示 B 0 i 次幂的和。(都是在模意义下的)
  前面的随便一推就推出来了,难点在于怎样统计答案。
  考虑答案中允许有前导0出现,但是出现前导0串的前提是这这整个串不包含前导0。比如103,需要统计03这样的串,但是003中,不能统计003和03的和。
  设m[i]表示i这位到最高位上的连续数字是多少。
  根据最初的思想,要把每个位置开头的串的和全部统计进ans里。那么枚举开头位置i,明显地,当更高位上全是0的时候,当前位置必须从1开始,更高位上是1到 m[i+1]1 的时候,当前位置可以无限制,用 f[i][0] ,当更高位上达到 m[i+1] 时,当前必须用 f[i][1] 。首先第一种情形可以舍去,因为一串连续的0之后必定有一个非0,而这成了子问题,可以只考虑前面的数大于0的情况。考虑当前面是 [1,m[i+1]1] 时,当前位对答案的贡献都是 f[i][0] ,一共贡献 m[i+1]1 次(要和0取max),前面是m[i+1]时,当前位对答案的贡献是 f[i][1] 。最终,
ans=i=1Nmax(0,m[i+1]1)×f[i][0]+f[i][1]

  预处理m数组,统计答案是O(N)的。
  这应该是我独立想出来的较麻烦的题目了吧。

代码

#include <cstdio>
#include <algorithm>
#define maxn 300000
#define mod 20130427
#define ll long long
using namespace std;
ll B, b[maxn], s[maxn];
void init()
{
    ll i;
    b[0]=s[0]=1;
    for(i=1;i<=200000;i++)b[i]=b[i-1]*B%mod, s[i]=(s[i-1]+b[i])%mod;
}
ll dp(ll *num)
{
    ll N=*num, i, n[maxn]={0}, f[maxn][2]={0}, ans=0, m[maxn]={0};
    for(i=1;i<=N;i++)n[i]=(num[i]*b[i-1]+n[i-1])%mod;
    for(i=N;i>=1;i--)m[i]=(m[i+1]*B+num[i])%mod;
    for(i=1;i<=N;i++)
    {
        f[i][0]=(B*f[i-1][0]%mod+B*(B-1)/2%mod*b[i-1]%mod*s[i-1])%mod;
        f[i][1]=(num[i]*f[i-1][0]%mod+num[i]*(num[i]-1)/2%mod*b[i-1]%mod*s[i-1])%mod;
        f[i][1]=(f[i][1]+f[i-1][1]+num[i]*(n[i-1]+1)%mod*s[i-1])%mod;
        ans=(ans+max((ll)0,m[i+1]-1)*f[i][0]%mod+f[i][1])%mod;
        ans%=mod;
    }
    return ans;
}
void reverse(ll *num)
{for(ll i=1;i<*num-i+1;i++)swap(num[i],num[*num-i+1]);}
void dec(ll *num)
{
    num[1]--;
    for(ll i=1;i<=*num;i++)if(num[i]<0)num[i]+=B, num[i+1]--;
    if(num[*num]==0)--*num;
}
int main()
{
    ll num[maxn], i, ans;
    scanf("%lld",&B);
    init();
    scanf("%lld",num);
    for(i=1;i<=*num;i++)scanf("%lld",num+i);
    reverse(num), dec(num);
    ans=-dp(num);
    scanf("%lld",num);
    for(i=1;i<=*num;i++)scanf("%lld",num+i);
    reverse(num), ans+=dp(num);
    printf("%lld",(ans+mod)%mod);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值