bzoj1799(洛谷4127)同类分布(月之谜)
题目:https://www.lydsy.com/JudgeOnline/problem.php?id=1799
https://www.luogu.org/problemnew/show/P4127
经典dp!
一个数能被它的各位和整除,在L-R内有多少个。
1.数位dp的套路,先预处理出第i位、后面任意的所有情况。
因为涉及整除,所以状态有“模当前数余几”;
因为涉及各位和,所以状态有“i位和为j”和“模k”;
好了我们有了一份会MLE的四维代码,而且有会超时的18位预处理,答案好歹是正确的。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; typedef long long ll; const int sx=18*9; ll tmp,f[19][165][165][165],cnt1,cnt2; int lmm,a[19]; ll pw(int k) { ll mul=1; for(int i=1;i<=k;i++) mul*=10; return mul; } void pre() { for(int k=1;k<=sx;k++) f[0][0][k][0]=1; int lm=0; for(int i=1;i<=18;i++) { lm=i*9; for(int j=0;j<=lm;j++) for(int k=1;k<=sx;k++) for(int l=0;l<k;l++) for(int p=0;p<=9&&j-p>=0;p++) f[i][j][k][l]+=f[i-1][j-p][k][((l-p*pw(i-1))%k+k)%k]; // if(i==1&&j==p&&k==p)printf("j=%d k=%d l=%d p=%d ff=%lld\n",j,k,l,p,f[i][j][k][l]); } } void chl() { lmm=0; while(tmp) { a[++lmm]=tmp%10; tmp/=10; } } ll calc() { int t=0; ll cnt=0,lj=0; for(int i=lmm;i;i--) { int ll=a[i];if(i==1)ll++; for(int p=0;p<ll;p++) for(int s=max(1,t+p);s<=sx;s++) cnt+=f[i-1][s-t-p][s][(s-(lj+p*pw(i-1))%s)%s]; t+=a[i];lj+=a[i]*pw(i-1); } return cnt; } int main() { scanf("%lld",&tmp);tmp--; pre(); chl(); cnt1=calc(); // printf("(%lld)\n",cnt1); scanf("%lld",&tmp); chl(); cnt2=calc(); // printf("(%lld)\n",cnt2); printf("%lld",cnt2-cnt1); return 0; }
在bzoj 50s 下应该还不会超时,考虑解决MLE。
2.发现pre转移的时候,模数s是固定的,即每个状态由模数相同的状态转移过来。
所以只要把模数s的枚举放在最外面,就可以省掉这一维,代价是每一个s都要预处理一遍。
尽可能优化一点。
1)发现L和R的预处理有一部分是重复的,所以把R - L这一部分也在循环里实现而不是最后再减;
2)(微弱)其实预处理到lm[1]-1位就行了。
bzoj上能过了(30s+),洛谷上超时一个点,而且其他点好慢……
#include<iostream> #include<cstdio> #include<cstring> using namespace std; typedef long long ll; ll f[22][180][180],l,r,mul[22],ans; int lm[3],a[3][22],sx; void pw() { mul[0]=1; for(int i=1;i<=lm[1];i++) mul[i]=mul[i-1]*10; } void pre(int s) { memset(f,0,sizeof f); f[0][0][0]=1;// int lmm=0; for(int i=1;i<=lm[1];i++) { lmm=min(s,i*9); for(int j=0;j<=lmm;j++) for(int l=0;l<s;l++)//l<j是错的(为何) for(int p=0;p<=9&&j-p>=0;p++) f[i][j][l]+=f[i-1][j-p][((l-p*mul[i-1])%s+s)%s];//考虑l-p*pw(i-1)<0 } } void chl(int k,ll tmp) { while(tmp) { a[k][++lm[k]]=tmp%10; tmp/=10; } } ll calc(int k,int s) { ll cnt=0; // printf("s=%d\n",s); int t=0;ll lj=0; for(int i=lm[k];i;i--) { int llm=a[k][i];if(i==1)llm++; for(int p=0;p<llm&&s-t-p>=0;p++) cnt+=f[i-1][s-t-p][(s-(lj+p*mul[i-1])%s)%s];//考虑lj+p*pw(i-1)==0 t+=a[k][i];lj+=a[k][i]*mul[i-1]; // printf("i=%d cnt=%d\n",i,cnt); } return cnt; } int main() { scanf("%lld%lld",&l,&r); chl(0,l-1);chl(1,r); sx=(lm[1]-1)*9+a[1][lm[1]];pw(); for(int s=1;s<=sx;s++) { pre(s); ans+=calc(1,s)-calc(0,s); } printf("%lld",ans); return 0; }
3.发现大家写的都是记忆化深搜。
为什么比较快呢?因为它不需要把一些用不到的状态也预处理出来。
但具体是什么我也不清楚。详见代码?
然后发现大牛的代码:写得好好!于是近乎抄了下来。
这里只要有最后那个d=0时的限制就不用记录各位和为j了,只要最后整个各位和等于s就行。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; typedef long long ll; ll input() { int f=1;ll s=0;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){s=s*10+ch-'0';ch=getchar();} return s*f; } const int N=165,M=20; ll x,y,dp[2][M][N][N]; int cnt,a[M],dfn[2][M][N][N],tot,mod; ll calc(bool p,int d,int s,int m)//p为有无限制 ,d是第几位,s是模数也是各位和 ,m是前面填好的东西%s余数 { if(!d)return (!s&&!m);//初值,第0位时仅当s为0(个位填好后整个的各位和为s)、m为0(整除)时为1 if(dfn[p][d][s][m]==tot)return dp[p][d][s][m]; dfn[p][d][s][m]=tot;ll tmp=0; int l=max(0,s-9*(d-1)),r=min(((p)?9:a[d]),s);//r<=s for(int i=l;i<=r;i++)tmp+=calc(p|(i<a[d]),d-1,s-i,(m*10+i)%mod);//i是当前位填几 return dp[p][d][s][m]=tmp; } ll solve(ll ret) { for(cnt=0;ret;ret/=10)a[++cnt]=ret%10; ll ans=0;int sx=cnt*9; for(mod=1;mod<=sx;mod++)tot++,ans+=calc(0,cnt,mod,0); return ans; } int main() { x=input();y=input(); return printf("%lld",solve(y)-solve(x-1)),0; }
有点奇怪的是下面这份代码数组范围应该都够,但仅改数组范围就能修复一个RE(变成上边那个TLE),也就是说数组越界?
#include<iostream> #include<cstdio> #include<cstring> using namespace std; typedef long long ll; ll f[19][165][165],l,r,mul[19],ans; int lm[2],a[2][19],sx; void pw() { mul[0]=1; for(int i=1;i<=lm[1];i++) mul[i]=mul[i-1]*10; } void pre(int s) { memset(f,0,sizeof f); f[0][0][0]=1;// int lmm=0; for(int i=1;i<=lm[1];i++) { lmm=min(s,i*9); for(int j=0;j<=lmm;j++) for(int l=0;l<s;l++)//l<j是错的(为何) for(int p=0;p<=9&&j-p>=0;p++) f[i][j][l]+=f[i-1][j-p][((l-p*mul[i-1])%s+s)%s];//考虑l-p*pw(i-1)<0 } } void chl(int k,ll tmp) { while(tmp) { a[k][++lm[k]]=tmp%10; tmp/=10; } } ll calc(int k,int s) { ll cnt=0; // printf("s=%d\n",s); int t=0;ll lj=0; for(int i=lm[k];i;i--) { int llm=a[k][i];if(i==1)llm++; for(int p=0;p<llm&&s-t-p>=0;p++) cnt+=f[i-1][s-t-p][(s-(lj+p*mul[i-1])%s)%s];//考虑lj+p*pw(i-1)==0 t+=a[k][i];lj+=a[k][i]*mul[i-1]; // printf("i=%d cnt=%d\n",i,cnt); } return cnt; } int main() { scanf("%lld%lld",&l,&r); chl(0,l-1);chl(1,r); sx=(lm[1]-1)*9+a[1][lm[1]];pw(); for(int s=1;s<=sx;s++) { pre(s); ans+=calc(1,s)-calc(0,s); } printf("%lld",ans); return 0; }
真是经典。以后也许要回顾回顾了呢。
人家的代码好好……