参考资料
一些个人理解&需要注意的地方:
- 数位dp中状态的选择与1~len位无关,是len+1~cnt位上的数字所共同决定的状态,正因为这样才可以在状态相同的情况下重复利用len位数字所产生的答案。
- 记忆化搜索时使用-1或单独开一个bool数组来标记当前状态是否被计算过,用0的话复杂度某些情况下不太对,会TLE。
- 是否需要lead来标记前导0请根据问题具体分析。
模板
//记忆化搜索数位DP模板
LL cnt,d[20],f[20][...];
//limit 是否有最高位限制
//lead 是否有前导零
//state 状态
//状态只与len之前的位有关 因此len之前的状态相同时可用记忆化来计算
//** 请用-1作为没算过的标记 **
LL dfs(int len,bool limit,bool lead,int state1 ...)
{
if(len==0) return ...; //边界值
if(!lead && !limit && f[len][state1][...] != -1) return f[len][state1][...];
int maxn=limit?d[len]:9; //确定上限
LL ans=0;
for(int i=0;i<=maxn;i++) //枚举当前位的数字并累加
{
if(lead&&i==0) //处理有前导0的情况
ans+=dfs(len-1,limit&&i==maxn,1,...);
else
{
if(....) //可以转移的情况
ans+=dfs(len-1,limit&&i==maxn,0,...);
}
}
if(!lead && !limit) f[len][state1][...]=ans; //记录一般情况下的答案
return ans;
}
LL solve(LL x) //计算[0,x]内符合条件的数
{
cnt=0;
while(x) d[++cnt]=x%10,x=x/10;
LL ans=0;
for(...) ans+=dfs(cnt,1,1,...);
return ans;
}
int main()
{
mst(f,-1);
...
}
数位DP泛做 & 简要题解
1.HDU2089 不要62
求给定范围内数位中不含62和4的数字的个数。
一个状态就行,即前一位是否是6,转移的顺便把4扣掉。
int cnt,d[20];
LL f[20][2];
LL dfs(int len,bool limit,bool pre)
{
if(len==0) return 1;
if((!limit) && f[len][pre]!=-1) return f[len][pre];
int maxn=limit?d[len]:9;
LL s=0;
for(int i=0;i<=maxn;i++)
{
if(i!=4&&!(pre&&i==2))
s+=dfs(len-1,limit&&i==maxn,i==6);
}
if(!limit) f[len][pre]=s;
return s;
}
LL solve(int x) //[0,x]
{
cnt=0;
while(x)
d[++cnt]=x%10,x=x/10;
return dfs(cnt,1,0);
}
int work()
{
mst(f,-1);
int l,r;
while(scanf("%d%d",&l,&r)&&l&&r)
printf("%lld\n",solve(r)-solve(l-1));
return 0;
}
2.POJ3252 Round Numbers
求[L,R]内转成二进制后数位上0的个数>=1的个数的数有多少。
两个状态,分别记录len+1~cnt位上有多少个0和1。
LL cnt,d[40],f[50][50][50];
LL dfs(int len,bool limit,bool lead,int z,int o)
{
if(len==0)
return z>=o;
if(!lead && !limit && f[len][z][o]) return f[len][z][o];
int maxn=limit?d[len]:1;
LL ans=0;
for(int i=0;i<=maxn;i++)
{
if(lead&&i==0)
ans+=dfs(len-1,limit&&i==maxn,1,z,o);
else
ans+=dfs(len-1,limit&&i==maxn,0,z+(i==0),o+(i==1));
}
if(!lead && !limit) f[len][z][o]=ans;
//if(!lead && !limit)
// cout << dbgs3(len,z,o) << " " << dbgs(ans) << endl;
return ans;
}
LL solve(LL x) //计算[0,x]内符合条件的数
{
cnt=0;
for(LL i=1;i<=x;i=i<<1)
d[++cnt]=bool(i&x);
/*for(int i=cnt;i;i--)
cout << d[i];
cout << endl;*/
return dfs(cnt,1,1,0,0);
}
int work()
{
LL l,r;
while(cin >> l >> r)
cout << solve(r) - solve(l-1) << endl;
return 0;
}
3.Luogu2657 windy数 不含前导零且相邻两个数字之差至少为2的正整数被称为windy数,求给定区间内有多少windy数。
首先默认len+1~cnt位的数字都满足条件,记录前一位的数字,转移的时候只选差>=2的即可。
LL cnt,d[20];
LL f[20][10];
//limit 最高位限制
//lead 前导0
LL dfs(int len,bool limit,bool lead,int pre)
{
if(len==0) return 1;
if(!lead && !limit && f[len][pre]!=-1) return f[len][pre];
int maxn=limit?d[len]:9;
LL s=0;
for(int i=0;i<=maxn;i++)
{
if(lead&&i==0)
s+=dfs(len-1,limit&&i==maxn,1,0);
else if(lead||abs(i-pre)>=2)
s+=dfs(len-1,limit&&i==maxn,0,i);
}
//cout << dbgs4(len,limit,pre,s) << endl;
if(!lead && !limit) f[len][pre]=s;
return s;
}
LL solve(LL x) //[0,x]
{
cnt=0;
while(x)
d[++cnt]=x%10,x=x/10;
return dfs(cnt,1,1,0);
}
int work()
{
mst(f,-1);
LL l,r;
scanf("%lld%lld",&l,&r);
printf("%lld\n",solve(r)-solve(l-1));
return 0;
}
4.CF55D Beautiful numbers
一个数被称为美丽数当它能被它数位上非0的数字整除,求给定区间内的美丽数。
这题又码了一上午。。
最开始的想法是两个状态,一个表示len+1~cnt为上数字的最小公倍数,另一个状态保存len+1~cnt位所组成的数字对他们LCM的模 ,但写了一会发现没法转移…
考虑到1/~9的LCM为2520,这意味着一个数若写成(X+2520)的形式后对1~9任意一个数取模等价于X直接对这个数取模,所以第二个状态可以表示为len+1~cnt位组成的数字对2520取模的结果。
这样还不够,开20*2520*250个LL会MLE, 观察暴搜一波发现1~9任意组合的LCM最多只有48个,hash一波压下空间就过了。
第一遍写的时候很傻逼的 用0当记忆化搜索时没算过的标记,还以为这题卡常数。。。把LCM提前算了出来。。。
LL g[10][3000];
LL gcd(LL a,LL b){return b==0?a:gcd(b,a%b);}
LL lcm(LL a,LL b){return a/gcd(a,b)*b;}
int h[3000],id=0;
void init(int num,int l)
{
if(num==0)
{
if(!h[l]) h[l]=++id;
return;
}
init(num-1,l);
init(num-1,g[num][l]);
}
//limit 是否有最高位限制
//lead 是否有前导零
LL cnt,d[20],f[20][50][3000];
LL dfs(int len,bool limit,int l,int m,LL cur)
{
if(len==0)
return cur%l==0;
if(!limit && f[len][h[l]][m]!=-1) return f[len][h[l]][m];
int maxn=limit?d[len]:9;
LL ans=0;
for(int i=0;i<=maxn;i++)
ans+=dfs(len-1,limit&&i==maxn,i==0?l:g[i][l],(cur*10+i)%2520,cur*10+i);
if(!limit) f[len][h[l]][m]=ans;
//cout << dbgs2(limit,cur) << dbgs4(len,l,m,ans) << endl;
return ans;
}
LL solve(LL x) //计算[0,x]内符合条件的数
{
cnt=0;
while(x) d[++cnt]=x%10,x=x/10;
LL ans=0;
ans+=dfs(cnt,1,1,0,0);
return ans;
}
int work()
{
for(int i=1;i<=9;i++)
for(int j=1;j<=2520;j++)
g[i][j]=lcm(i,j);
init(9,1);
//cout << dbgs(id) << endl;
LL T;
cin >> T;
while(T--)
{
LL l,r;
cin >> l >> r;
cout << solve(r)-solve(l-1) << endl;
}
return 0;
}
5.HDU3709 Balanced Number
选取数中的某一位作为平衡点,定义某一位力矩为该数位上的数乘以这一位到平衡点的距离,若平衡点左边所有数位的力矩之和等于右边的,则称这个数是平衡的。问给定区间内有多少数时平衡的。
若某一个数是平衡的,那么他的平衡点一定是唯一的,而平衡点不超过20个,所以可以枚举平衡点的位置然后用数位DP算答案。
第一个状态left为力矩和,平衡点左面为正,右边为负,第二个状态为平衡点的位置( 其实应该不算状态?),注意0一共被算了len次,统计答案时减掉len-1次即可。
//limit 是否有最高位限制
//lead 是否有前导零
LL cnt,d[40],f[20][2000][20];
LL dfs(int len,bool limit,bool lead,int left,int bp)
{
if(len==0)
return lead || left==0;
if(!lead && !limit && f[len][left][bp]!=-1) return f[len][left][bp];
int maxn=limit?d[len]:9;
LL ans=0;
for(int i=0;i<=maxn;i++)
{
if(i==0&&lead)
ans+=dfs(len-1,limit&&i==maxn,1,left,bp);
else if(left+i*(len-bp)>=0)
ans+=dfs(len-1,limit&&i==maxn,0,left+i*(len-bp),bp);
}
if(!lead && !limit) f[len][left][bp]=ans;
return ans;
}
LL solve(LL x) //计算[0,x]内符合条件的数
{
//if(x==0) return 1;
cnt=0;
while(x)
d[++cnt]=x%10,x=x/10;
LL ans=0;
for(int i=1;i<=cnt;i++)
ans+=dfs(cnt,1,1,0,i);
//cout << dbgs2(x,ans) << endl;
return ans - cnt + 1;
}
int work()
{
mst(f,-1);
LL T,l,r;
scanf("%lld",&T);
while(T--)
{
scanf("%lld%lld",&l,&r);
printf("%lld\n",solve(r)-solve(l-1));
}
return 0;
}
6.HDU -3652 B-number
求在[1,n]中有多少个数满足能被13整除并且包含子串13。
三个状态,第一个表示len+\1~cnt位所组成数字对13取模的结果,第二个表示前一位是否是数字1,第三个表示len+1~cnt位是否出现过13。
LL cnt,d[20],f[20][20][2][2];
LL dfs(int len,bool limit,int m,bool flag, bool pre, LL cur)
{
if(len==0) return flag==1&&m==0;
if(!limit && f[len][m][flag][pre]!=-1) return f[len][m][flag][pre];
int maxn=limit?d[len]:9;
LL ans=0;
for(int i=0;i<=maxn;i++)
ans+=dfs(len-1,limit&&i==maxn,(cur*10+i)%13,flag||(pre&&i==3),i==1,cur*10+i);
if(!limit) f[len][m][flag][pre]=ans;
return ans;
}
LL solve(LL x) //计算[0,x]内符合条件的数
{
cnt=0;
while(x) d[++cnt]=x%10,x=x/10;
return dfs(cnt,1,0,0,0,0);
}
int work()
{
mst(f,-1);
LL n;
while(cin >> n)
cout << solve(n) << endl;
return 0;
}
7.SPOJ-BALNUM Balanced Numbers
若一个数的数位上每一个奇数都出现了偶数次且每一个偶数都出现了奇数次则称这个数是平衡的,求给定范围内的平衡数。
状压DP+数位DP 丧病
第一个状态(二进制数)表示0~9在len+1~cnt位上出现了奇数次还是偶数次,另一个二进制数表示表示0~9在len+1~cnt位上是否出现过。
当然压成三进制也能做而且省空间,但个人感觉两个二进制可能好写一点。。。
int cnt,d[20];
ULL f[25][2050][2050];
bool calc[25][2050][2050];
bool check(int w,int v)
{
bool flag=1;
for(int i=0;i<10;i++)
{
if(!(v&(1<<i))) continue;
if( (i&1) && ((1<<i)&w) ) flag=0;
if(!(i&1) && !((1<<i)&w) ) flag=0;
}
return flag;
}
ULL dfs(int len,bool limit,bool lead,int w,int v)
{
if(len==0) return check(w,v);
if(!lead && !limit && calc[len][w][v])
return f[len][w][v];
int maxn=limit?d[len]:9;
ULL ans=0;
for(int i=0;i<=maxn;i++)
{
if(i==0&&lead) ans+=dfs(len-1,limit&&i==maxn,1,w,v);
else ans+=dfs(len-1,limit&&i==maxn,0,w^(1<<i),v|(1<<i));
}
if(!lead && !limit) f[len][w][v]=ans,calc[len][w][v]=1;
return ans;
}
ULL solve(ULL x) //计算[0,x]内符合条件的数
{
cnt=0;
while(x) d[++cnt]=x%10,x=x/10;
return dfs(cnt,1,1,0,0);
}
int work()
{
ULL T,l,r;
cin >> T;
while(T--)
{
cin >> l >> r;
cout << solve(r)-solve(l-1) << endl;
}
return 0;
}
7.HDU4734 F(x)
我们定义十进制数x的权值为f(x) = a(n)*2^(n-1)+a(n-1)*2(n-2)+…a(2)*2+a(1)*1,a(i)表示十进制数x中第i位的数字,题目给出a,b,求出0~b有多少个不大于f(a)的数。
定义状态w表示len位数字所组成的数中权值不大于w的数的数量,转移时减掉当前这位的权值就行。
但看上去这个w似乎与len+1~cnt位无关?我感觉可以解释为高位减掉他们对应位的权值后所剩下的 可供后面len位使用的权值。
LL c[20];
void init()
{
c[1]=1;
for(int i=2;i<20;i++)
c[i]=c[i-1]*2;
}
//limit 是否有最高位限制
//lead 是否有前导零
LL cnt,d[20],f[10][5000];
LL dfs(int len,bool limit,LL w)
{
//cout << dbgs4(len,limit,w,b) << endl;
if(len==0) return w>=0;
if(!limit && f[len][w]!=-1) return f[len][w];
int maxn=limit?d[len]:9;
LL ans=0;
for(int i=0;i<=maxn;i++)
if(i*c[len]<=w)
ans+=dfs(len-1,limit&&i==maxn,w-i*c[len]);
if(!limit) f[len][w]=ans;
return ans;
}
LL F(LL x)
{
LL t=0;
cnt=0;
while(x) d[++cnt]=x%10,x=x/10;
while(cnt) t+=d[cnt]*c[cnt],cnt--;
return t;
}
LL solve(LL x, LL y) //计算[0,x]内符合条件的数
{
LL t = F(y);
cnt=0;
while(x) d[++cnt]=x%10,x=x/10;
return dfs(cnt,1,t);
}
int work()
{
init();
mst(f,-1);
LL T,A,B;
cin >> T;
for(int t=1;t<=T;t++)
{
cin >> A >> B;
cout << "Case #" << t << ": ";
cout << solve(B,A) << endl;
}
return 0;
}
剩下三个恶心题先坑着。。。