回顾
对于网络赛和多校联合集训时候出的数位dp的题目看了题解能很快A出来,但是自己写的时候就有很多问题,于是自己有重新看了以前做的一些题目,有了一些很好的想法,写这篇博客的目的一是总结经验,二是给跟我一样还在迷途中的人一些指导。
这里我就拿hdu3555Bomb为例说一下我自己的想法,对于数位dp我就用dfs的写法,这个写法简便且容易理解。对于数位dp我感觉最重要的就是定义状态,但是通常不好定义,而且有时候大家会发现数位dp如果把自己定义的状态去掉1个或者2个答案也是对的,其实这里面深层的原因大家可能不懂,这里我就把今天上午埋头苦想得到的一些结论跟大家分享一下。
数位dp的我感觉可以减少状态的方式就是:如果对于当前状态不合乎题意,那么我们就没有必要继续往下dfs了,这样会白白浪费时间和空间不说,而且状态转移方程需要一个变量来保存当前状态是否合理是不是,这样写起来除了这个之外还有一些其他的问题就是必须要用状态多开一维来保存当前状态是否合理,通常用1表示合理,用0表示不合理。 HDU 3555 Bomb今天我尝试减少一个状态但是发现第三个样例出错了,找了半天终于发现原因。首先给大家看一下AC代码
HDU 3555 Bomb AC 代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL dp[70][10][2], bit[70];
LL dfs(int pos, int pre, int flag, bool limit){
if(pos < 1) return flag;
if(!limit && dp[pos][pre][flag] != -1) return dp[pos][pre][flag];
LL ret = 0;
int len = limit?bit[pos]:9;
for(int i = 0; i <= len; i++){
ret += dfs(pos-1, i, flag||(i == 9 && pre == 4), limit && i == len);
}
if(!limit) dp[pos][pre][flag] = ret;
return ret;
}
LL solve(LL n){
int len = 0;
while(n){
bit[++len] = n%10;
n /= 10;
}
return dfs(len, 0, 0, true);
}
int main(){
int T;
scanf("%d", &T);
LL n;
memset(dp, -1, sizeof(dp));
while(T--){
scanf("%I64d", &n);
printf("%I64d\n", solve(n));
}
return 0;
}
上面的那个是正确的,但是如果我们把最后一维flag给去掉呢??最初我认为其实是可以,因为我一直用中间变量flag存储本状态是否合理,但是后来发现自己错了,为什么呢?先给出wrong代码。
HDU 3555 Bomb WA 代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL dp[70][10], bit[70];
LL dfs(int pos, int pre, int flag, bool limit){
if(pos < 1) return flag;
if(!limit && dp[pos][pre] != -1) return dp[pos][pre];
LL ret = 0;
int len = limit?bit[pos]:9;
for(int i = 0; i <= len; i++){
ret += dfs(pos-1, i, flag||(i == 9 && pre == 4), limit && i == len);
}
if(!limit) dp[pos][pre][flag] = ret;
return ret;
}
LL solve(LL n){
int len = 0;
while(n){
bit[++len] = n%10;
n /= 10;
}
return dfs(len, 0, 0, true);
}
int main(){
int T;
scanf("%d", &T);
LL n;
memset(dp, -1, sizeof(dp));
while(T--){
scanf("%I64d", &n);
printf("%I64d\n", solve(n));
}
return 0;
}
上面的代码是错的,为什么呢??很明显是因为dp[i][j]已经存储状态,虽然我用了flag来表示当前状态是否合理,但是还是不行,因为根本递归不到pos<1,就因为if(!limit && dp[pos][pre] != -1) return dp[pos][pre];而输出了当前值,但是当前值是上一步状态留下的值显然是错误的,因此这样写是错误的,如果我们把这句话注释掉呢??很明显是对的,但是因为没有了中间状态,这样提交上去很明显会TLE,我试了一下果然如此,可见状态定理有多么的重要,对于这种有中间变量来保存当前是否合理的时候需要增加一维来保存,想AC代码一样,同时还有前面我说的如果前面的不合理就不必dfs了,直接跳过即可。这里以HDU 5898 odd-even number(2016 ACM/ICPC Asia Regional Shenyang Online) 为例。给出代码大家可以看出端倪。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL dp[20][20][20], bit[20];
LL dfs(int pos, bool zero, int ji, int ou, int limit){
if(pos < 1){ //如果到末位
if(!ji&&!ou) return 1;
else if(ji && !ou) return ji%2==0;
else if(!ji && ou) return ou&1;
else return (ji%2==0)&&(ou&1);
}
if(!limit && dp[pos][ji][ou] != -1) return dp[pos][ji][ou];
LL ret = 0;
int len = limit?bit[pos]:9;
for(int i = 0; i <= len; i++){
if(i&1){ //如果为奇数
if((ou&1)||(ou==0)) ret += dfs(pos-1, false, ji+1, 0, limit&&i==len);
}
else{
if(i == 0){ //如果该位置为0
if(zero){ //且有前导0
ret += dfs(pos-1, true, 0, 0, limit&&i==len);
}
else if(ji%2==0){
ret += dfs(pos-1, false, 0, ou+1, limit&&i==len);
}
}
else{
if(ji%2==0) ret += dfs(pos-1, false, 0, ou+1, limit&&i==len);
}
}
}
if(!limit) dp[pos][ji][ou] = ret;
return ret;
}
LL solve(LL x){
int len = 0;
while(x) bit[++len] = x%10, x /= 10;
return dfs(len, true, 0, 0, true);
}
int main(){
int T;
scanf("%d", &T);
LL L, R;
memset(dp, -1, sizeof(dp));
for(int kase = 1; kase <= T; kase++){
scanf("%I64d%I64d", &L, &R);
printf("Case #%d: %I64d\n", kase, solve(R)-solve(L-1));
}
return 0;
}
很明显这里dfs值只取了正确的状态才往下dfs,但是HDU3555确实没法这样,因为不到末位你不会知道是否含有49,因此无法对接下来的状态进行筛选。具体的话还是看题目的要求吧。(如果有什么说的不对的地方请各位大大指出来。)