← 返回题解列表

P1066 月的第十二章 官方题解

【部分分:测试点 1∼41\sim 41∼4】

  • y103y\leq 10^3 时,我们可以把已经写完的行数 xx 看作状态。从 xx 出发,枚举 1kgcd(x,y)1\leq k\leq \gcd(x,y),转移到状态 x+kx+k,代价为 11
  • x=1x=1 开始做最短路即可得到答案。所有转移始终令 xx 增大,所以也可以按照 xx 递增的顺序进行动态规划。对每个不同的 yy 记忆化答案后,不需要为重复数据重新计算。单次计算的时间复杂度为 O(y2)\mathcal{O}(y^2),空间复杂度为 O(y)\mathcal{O}(y)

【部分分:测试点 5∼85\sim 85∼8】

  • 特殊性质保证 yy 为质数。对于任意 1x<y1\leq x<y,都有 gcd(x,y)=1\gcd(x,y)=1,所以每个夜晚只能增加一行。
  • 11 增加到 yy 恰好需要 y1y-1 个夜晚,单组数据可以在 O(1)\mathcal{O}(1) 的时间内回答。这个性质提示我们:真正影响一次续写幅度的是 yy 的约数,而不需要保留所有中间位置。

【性质观察】

  • 设当前已经到达 xx,并令 d=gcd(x,y)d=\gcd(x,y)。若接下来始终增加 dd,那么到达的每个中间位置仍然是 dd 的倍数,它与 yy 的最大公约数至少为 dd,所以这些操作始终合法。
  • 因此,从约数 dd 出发到达更大的约数 ee,我们至多需要
edd\left\lceil\frac{e-d}{d}\right\rceil

个夜晚。若途中得到更大的最大公约数,就把它作为新的约数状态,继续进行同样的讨论。

【部分分:测试点 9∼129\sim 129∼12】

  • 第一档的瓶颈是保留了 1y1\sim y 的全部位置。根据上一页的观察,我们只保留 yy 的正约数。将它们从小到大写成 d1,d2,,dmd_1,d_2,\ldots,d_m,其中 d1=1d_1=1dm=yd_m=y。定义 fif_i 表示从 11 到达 did_i 所需的最少夜晚数,初值为 f1=0f_1=0
  • 枚举上一个约数状态,完整转移为
fi=min1j<i{fj+didjdj}.f_i=\min_{1\leq j<i} \left\{f_j+\left\lceil\frac{d_i-d_j}{d_j}\right\rceil\right\}.

答案为 fmf_m,不存在的状态视为正无穷。

【正解】

  • 考虑一条最优续写过程。如果某一段过程在最大公约数为 dd 时没有到达新的约数状态,就可以把这一段调整为不断增加 dd,直至抵达下一次出现的约数状态。这样不会增加夜晚数。于是总存在一条最优过程只需记录依次经过的约数。
  • 枚举两个相邻的约数状态时,上页转移恰好计算了它们之间所需的最少夜晚数,因此动态规划既不会遗漏更优过程,也不会加入非法过程。
  • 试除分解 yy 并递归生成全部约数。设 τ(y)\tau(y) 为约数个数,单组数据的时间复杂度为 O(y+τ(y)2)\mathcal{O}(\sqrt y+\tau(y)^2),空间复杂度为 O(τ(y))\mathcal{O}(\tau(y))。相同的 yy 可以直接复用答案。

【参考代码】

/*Author:EhundateghDate:2026/8/18Name:moon_no_cht.cppYou steal,I kill.*/#include <map>#include <cstdio>#include <vector>#include <climits>#include <algorithm>#define MAXV 31630using namespace std; int c,T,y,Prime[4000],cnt=0;bool Tag[MAXV];map <int,int> Ans; void Init(){    for(int i=2;i<MAXV;i++){        if(!Tag[i]) Prime[++cnt]=i;        for(int j=1;j<=cnt&&i*Prime[j]<MAXV;j++){            Tag[i*Prime[j]]=true;            if(i%Prime[j]==0) break;        }    }} void Get_Div(int Now,long long Val,vector <pair<int,int> > &Fac,vector <int> &Div){    if(Now==(int)Fac.size()){        Div.push_back((int)Val);        return;    }    for(int i=0;i<=Fac[Now].second;i++){        Get_Div(Now+1,Val,Fac,Div);        if(i<Fac[Now].second) Val*=Fac[Now].first;    }} int Calc(int y){    if(Ans.count(y)) return Ans[y];    int Temp=y;    vector <pair<int,int> > Fac;    for(int i=1;i<=cnt&&1ll*Prime[i]*Prime[i]<=Temp;i++){        if(Temp%Prime[i]) continue;        int Count=0;        while(Temp%Prime[i]==0){Temp/=Prime[i];Count++;}        Fac.push_back(make_pair(Prime[i],Count));    }    if(Temp>1) Fac.push_back(make_pair(Temp,1));     vector <int> Div;    Get_Div(0,1,Fac,Div);    sort(Div.begin(),Div.end());     vector <int> Dp(Div.size(),0);    for(int i=1;i<(int)Div.size();i++){        int Best=INT_MAX;        for(int j=i-1;j>=0;j--){            int Step=(Div[i]-1)/Div[j];            if(Step>=Best) break;            Best=min(Best,Dp[j]+Step);        }        Dp[i]=Best;    }    return Ans[y]=Dp.back();} int main(){    Init();    scanf("%d%d",&c,&T);    while(T-->0){        scanf("%d",&y);        printf("%d\n",Calc(y));    }    return 0;}