题解 P3216 【[HNOI2011]数学作业】

· · 题解

首先写出递推关系式:令s[i]为前i个数连接得到的数,c(i)表示i的位数,有s[i]= 10^c(i)+s[i-1]+i;

c(i)随i变化,无法在转移矩阵T中表示。如果c(i)固定,就可以套用矩阵快速幂的方法进行优化了!

考虑位数随着i的变化最多只会变化18次,因此可以按位数分段进行矩阵快速幂。

枚举位数,那么有:

                 {10^k,     0   ,0   

{f[i-1],i-1,1}* 1, 1 ,0 = {f[i],i,1}

                         1,     1  ,1}

这里在状态矩阵中加了一个永远为1的值,用于辅助i每次加一。

将多位数按每次加数时前一个数所乘的10的个数分为10-99,100-999。。。的数字段,方便进行矩阵乘法 。

实现代码时套模拟矩阵乘法模板和快速幂模板即可。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#define ll long long
using namespace std;
ll n,m,S[3][3]={},T[3][3]={}; 
void mmul(ll a[3][3],ll b[3][3],ll s[3][3])//模拟矩阵乘法 
{
    ll tmp[3][3]={};//此处a和s是同一数组,必须开一个数组作中转站(而不是像在重载运算符中结果数组是独立的,不受乘数影响) 
    for(int i=0;i<3;i++)
       for(int j=0;j<3;j++)
          for(int k=0;k<3;k++)
             tmp[i][j]=(tmp[i][j]+(a[i][k]%m)*(b[k][j]%m))%m;//两个长整型数相乘,一定要分别取余,否则会爆负数(超范围了) 
    for(int i=0;i<3;i++)
       for(int j=0;j<3;j++)
          s[i][j]=tmp[i][j];//在不影响乘数的前提下得出结果 
}
void cal(ll t,ll last)
{
    memset(T,0,sizeof(T));
    T[0][0]=t;//每次加数时前一个数应进的位数 
    T[1][0]=T[1][1]=T[2][0]=T[2][1]=T[2][2]=1;
    ll y=last-t/10+1;//y决定在该数字段下加数的次数 
    while(y)//快速幂模板 
    {
        if(y&1)mmul(S,T,S);//模拟矩阵乘法 
        mmul(T,T,T);
        y>>=1;
    }
}
int main()
{
    scanf("%lld%lld",&n,&m);
    S[0][0]=S[1][1]=S[2][2]=1;//初始化
    ll t=10;
    while(n>=t)
    {
        cal(t,t-1);//将多位数按每次加数时前一个数所乘的10的个数分为10-99,100-999。。。的数字段,方便进行矩阵乘法 
        t*=10;
    }
    cal(t,n);//如果n<10,就说明每次加数时只要进一位即可 
    printf("%lld",S[2][0]);
    return 0;
}
ps:管理员给个通过吧,上个题解程序连注释都没有