atcoder abc158 E - Divisible Substring

atcoder abc158 E - Divisible Substring

先看题面

在这里插入图片描述    题目的意思是,给定字符串和一个质数P,求字符串当中的子串被P所整除的个数。

    写一下心路历程,一开始看到题目直接莽dp(最近dp上头)
dp[i][j]=sum{dp[i1][k]}k{(10k+s[i]0)%P=j}dp[i][j]=sum\{dp[i-1][k]\} \qquad k\in\{(10*k+s[i]-'0')\%P=j \}

    意义是以第i个字符为结尾的所有字符串所对应的数字被P除,余数为j的个数,上面的公式是由i层遍历j,去找到数值k将其相加,事实上,我们可以反一下,我们不遍历j,去遍历余数,也就是根据公式

dp[i][(10k+s[i]0)%P]+=dp[i1][k]k[0,P1]dp[i][(10*k+s[i]-'0')\%P]+=dp[i-1][k] \qquad k\in[0,P-1]

    k为余数,这样子遍历一遍i-1层的同时,就完成了i层的赋值。

    看了看数据范围,dp数组空间可以优化成只用2N,因为每一次都只用前一层,只存两层就够了,所以空间复杂度能接受,算了一下时间复杂度,大概。。。不行,但是仍然要试试看 (当时觉得这个dp能优化) 此时时间复杂度为o(NP)

于是写出了令人窒息的dp

#include<bits/stdc++.h>
using namespace std;
char s[100000*2+5];
long long dp[2][10000+5];
int n,P;
int main(){
    cin>>n>>P;
    long long sum=0;
    scanf("%s",s);
    for(int i=0;i<n;i++){
        int t=s[i]-'0';
        memset(dp[i%2],0,P*sizeof (long long));
        for(int j=0;j<P;j++)
            dp[i%2][(j*10+t)%P]+=dp[(i+1)%2][j];
        dp[i%2][t%P]++;
        sum+=dp[i%2][0];
    }
    printf("%lld\n",sum);

}

    上交之后有几个大案例超时。。。。虽然也在意料之中。。。你问我第一个公式有什么用?可能这种形式的dp有大佬能优化吧。。。还是先留着吧,没准日后有用。。。。

    事后查看大佬的代码,我人蒙了。。。

#include <iostream>
using namespace std;
long N,P,ans,cur,t=1,cnt[10010],i;string S;
int main()
{
    cin>>N>>P>>S;
    if(P==2||P==5){
        for(i=0;i<N;i++)if((S[i]-'0')%P==0)ans+=i+1;
    }else{
        cnt[0]=1;
        for(i=N-1;i>=0;i--){
            (cur+=(S[i]-'0')*t)%=P;
            ans+=cnt[cur]++;
            t=t*10%P;
        }
    }
    cout<<ans;
}

着实nb的代码。我看不懂,于是研究了一上午,终于看懂了。

首先,大佬很巧妙地运用了%的性质

(ab)%p=(a%pb%p+p)%ps.t.a>b(a-b)\%p=(a\%p-b\%p+p)\%p \qquad s.t. \qquad a>b

    首先要确定一点,就是对于一个字符串abcdef...,如果abcdef..%p==ef..%p,那么我们可以认为abcd0..%p==0,怎么证明呢,上一行等式,移项,同加p,同%p,可得(abcde..%p-e..%p+p)%p=p%p=0,再运用%的性质,可得(abcde..-e..)%p=0

    但是我们想要知道的是abcd能不能被p整除,而不是abcd0%p,对于任意的字符串相减之后,我们只能知道abcd…*10^k次能不能被p整除,要通过某一种方法转换,设中间的字符串所对应的数字为M,现在我们已知(M*10^k)%p==0,运用%的乘法性质展开,得((M%p)*(10^k%p))%p==0,括号内部左边就是我们想知道的,我们想知道他会不会等于0。如果等式成立,那么只有三种可能

  1. M%p=0
  2. 10^k%p=0
  3. (M%p)*(10^k%p)是p的倍数,且不等于0

    但是因为p是质数只能被分解为1*p,且(M%p)和(10^k%p)都小于p,那么不存在一个数=p,所以必然不能满足第三点。

    第一种可能即为我们所希望的,我们只要再考虑一下第二种情况,并将其排除第二种情况成立的时候排除,剩下的就是符合我们第一种可能的所有情况。考虑第二种可能,%左边的数分解质因数必然分解为2^k*5^k,也就是说质因子只有k个2和5,若想要第二种可能不成立,则p只要不等于2或5即可,换句话说,我们只要特判p=2或5的情况,除此以外,其他情况下(M*10^k)%p==0M%p==0的答案一样。

    那么方法就很明显了,在p!=2或5的时候,找到满足abcdef..%p==ef..%p的情况,那么此中间字符串一定能被p整除。所以可以从最右边开始记录余数,cnt[i]的意义是以i为余数的字符串所对应的数字个数,每次计算余数结束之后,就给答案加上cnt[i],再进行cnt[i]++,因为之前出现过的位置必然也能和当前位置组合成能够被整除的字符串。有一个要注意的地方就是,cnt[0]必须要赋初值为1,因为如果余数为0,说明本身就能被整除,无需其他位置也能被整除。

    刚才考虑p!=2或5的一般情况,现在考虑p==2或5的特殊情况,我们都知道p为2或5的时候,p的倍数有一个有趣的性质,p的倍数的最后一位必然是2或5!也就是说,如果遇到p=2或5,只要遍历字符串,遇到2或5的时候,给答案每次都加上当前下标+1即可。举个例子,125,p=5时,答案是3,p=2时答案是。

    在此深深感受到了出题人的nb,所有条件正好全用上,我一开始真没想到质数的特殊,在整个解法中质数贯穿全部。

关于为什么不能从头开始遍历

    一开始我也很疑惑,但其实看了推导过程就会发现,这是由公式决定的,举个例子,字符串abcdef,已知如果从头开始遍历,公式会变为判断(abcd-a000)%p是否为0,括号内部左边易求,右边不易求需要对每个值进行*10^k之后取模,会超时。

    如果从尾部开始遍历,判断(abcd-bcd)%p是否为0,左右都易求。所以只能从尾部开始遍历。

以下是我写的代码,大同小异,几乎一样。

#include<bits/stdc++.h>
using namespace std;
char s[100000*2+5];
int ys[10000+5];
int n,P;
int main(){
    cin>>n>>P;
    long long sum=0;
    scanf("%s",s);
    int t=1,v=0;
    if(P==2||P==5){
        for(int i=0;i<n;i++)
            if((s[i]-'0')%P==0)
                sum+=i+1;
    }
    else{
        ys[0]=1;
        for(int i=n-1;i>=0;i--){
            v+=t*(s[i]-'0');
            v%=P;
            sum+=ys[v];
            ys[v]++;
            t*=10;
            t%=P;
        }
    }

    printf("%lld\n",sum);

}

总结

    上面方法完美用上了质数的性质,但是如果题目不给质数的条件,这个方法就不能适用,但是我一开始TLE的代码,却可以解决,使用了动态规划。当然dp优化那么多,本人才疏学浅还不能优化他,也许将来有一天可以,所以先放着。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章