【总结】数位DP

填坑进度(3/4)

边听歌边写博客,我可真是个天才。

\(\texttt{I'm lost inside your deep blue.}\)

数位DP

简称暴搜(误

前置芝士

暴搜,记忆化搜索


导入

众所周知,我们可以用 \(dfs\) 搜每一个数位来遍历 \(1\sim99999\) 之间的每一个数,其方法如下:

#include<cstdio>
int temp[15];
void dfs(int x){
    //x为当前数字位数,x越小,位数越低(也就是说x是该数从右向左数的结果)
    //后面说的位数都默认为从右向左数
    if(!x){
        for(int i=5;i;--i) //从高位到低位输出
            printf("%d",temp[i]);//不要在意前导0那些无关紧要的细节
        putchar('\n');
        return;
    }
    for(int i=0;i<=9;++i){
        temp[x]=i;//记录答案
        dfs(x-1);
    }
    return;
}

那么,如果不是 \(1\sim 99999\),而是 \(1\sim 114514\) 呢?

\(\texttt{For example:}\) 如果第这个时候第 \(1,2,3\) 位分别为 \(1,1,4\),第 \(4\) 位就不能为 \(0\sim 9\) 了,而是 \(0\sim5\).

形象地说,在 \(1\sim 114514\) 这个范围内,有 \(110999,111999,112999,113999\) ,但是没有 \(114999\)

所以,我们在每一位分两种情况考虑:前面的每一位都顶格了,那么这一位最多只能顶格;如果前面几位有任何一位没有顶格,这一位 \(0\sim9\) 随便选。

注:因为 \(114514=0114514,0=0\) ,所以我们认为第 \(6\) 位的前面顶格了,第 \(6\) 位最多也只能顶格。

对于每一位,我们分两种情况讨论:

的是指 这一位以后 的所有位数都可以为 \(0\sim9\) 之间的任意数字。

那么,就可以愉快地按字典序遍历 \(1\sim 114514\) 之间的每一个数了。

我们先用一个数组 \(a\) 存下 \(114514\) 的每一位数字,以便顶格时判断当前位的取值范围。

\(dfs\) 中,我们用int参数 \(x\) 表示当前位数, bool 参数 \(v\) 表示前面的每一位是不是都顶格了,如果 v==true,当前最大取值就是 a[x],否则便是 \(9\)

那么代码就很容易惹:

#include<cstdio>
int cnt;
int temp[15],a[15];
void dfs(int x,bool v){
    if(!x){
        for(int i=6;i;--i)
            printf("%d",temp[i]);
        putchar('\n');
        return;
    }
    int u=v?a[x]:9;//u为上限,若前面每一位都顶格了,这位最多只能顶格
    for(int i=0;i<=9;++i){
        temp[x]=i;
        dfs(x-1,v&&(i==a[x]));//将v向下传
        //若前面有任何一位没有顶格,v即false,任何数&false=false
        //若i没有顶格,也就是下一位的前一位没有顶格
        //只有当前面每一位和当前位都顶格了,下一位的最高才能是a[x-1]
    }
    return;
}
int main(){
    int n=114514;
    while(n){
        //cnt表示位数
        a[++cnt]=n%10;
        n/=10;//倒着取走114514的每一位
    }
    dfs(cnt,1);//从最高位搜起
    return 0;
}

那么,大家可能有疑惑了,为什么要用这玩意儿?直接 \(\operatorname{for}\) 循环他不香嘛?常数也比这个 \(dfs\) 小,对吧?

氮素,众所周知,\(dfs\) 是珂以记忆化的!


例题一:不要62

Solution

【XSC062因言语过激被提出直播间】

咦?这个咋整?

我们只要在 \(dfs\) 的时候再设置一个参数 \(f\),表示这一位的前一位数。

如果f==6且循环时的i==2,就会出现一个连续的 \(62\),不应该继续 \(dfs\) 下去了。

如果i==4,那么最终的数里就会出现 \(4\),也不应该继续 \(dfs\) 了。

所以,我们珂以像酱紫写粗来!

#include<cstdio>
#define int long long
int a[15];
int l,r,cnt,la,ra;
int dfs(int x,int f,bool v){
	if(!x)return 1;
	int u=v?a[x]:9;
	int ans=0;
	for(int i=0;i<=u;++i){
		if(i==4||(i==2&&f==6))
			continue;
		ans+=dfs(x-1,i,v&&(i==a[x]));
	}
	return ans;
}
//简单易懂,老少皆宜,不写注释了。

咦咦咦?但这个并不是 \(1\sim r\),而是 \(l\sim r\) 呀!

像用前缀和求 \(l\sim r\) 的和一样,用 sum[r]-sum[l-1] 不就行了?

但是。。。第一位的 \(f\) 应该是多少呢?

本来,我们只要保证 f!=6 ,不影响答案,不过为了保险,我们设置一个不是 \(0\sim 9\) 之间的正整数。

主函数:

signed main(){
	while((~scanf("%lld%lld",&l,&r))&&(l||r)){
		cnt=0;
		while(r){
			a[++cnt]=r%10;
			r/=10;
		}
		ra=dfs(cnt,10,1);//记录 "sum[r]"
		cnt=0;
		l--;
		while(l){
			a[++cnt]=l%10;
			l/=10;
		}
		la=dfs(cnt,10,1);//记录 "sum[l-1]"
		printf("%lld\n",ra-la);//相减的结果就是 "l+(l+1)+(l+2)+...+r"
	}
	return 0;
}

然后你就可以在 \(\texttt{loj}\) 上A了。

还记得我前面说的那句话吗?

众所周知,\(dfs\) 是珂以记忆化的!

我们完全珂以用记忆化来优化这个 \(dfs\) 吖!

就像前面举的那个 \(114514\) 的例子,\(112\ \_\ \_\ \_\,,113\ \_ \ \_\ \_\) 已经确定的位中,倒数第一位(也就是 \(2\)\(3\))不是 \(6\),后三位最多都是 \(999\),后三位的答案总数一定是一样的。

所以如果两个数的答案一样,就必须要满足上面的三个条件,任意一个条件不成立,两个数的答案就不相同。

下文中,“不行”指与 \(112\ \_\ \_\ \_\,,113\ \_ \ \_\ \_\) 后二位的答案不相同。

  • \(114\ \_ \ \_\ \_\) 不行,因为后三位最多只能是 \(514\)。(而且还出现 \(4\) 了好伐?),违反了第二条。
  • \(106\ \_ \ \_\ \_\) 不行,因为如果下一位是 \(2\) ,那么这个答案就不符合题意,答案总数会减少,违反了第一条。

综上,我们使用 s[x][f]表示上一位为 \(f\) ,还剩下 \(x\) 位时的记忆化的答案,如果v==0就珂以使用或记录。

欸?有个小问题,本题是多组输入,记忆化数组需不需要清空呢?

本题的答案是不需要。因为题目限制是没有 \(4\)\(62\) ,任何数都可以使用之前记忆化的结果。

因为每道题的题目要求不同,所以每做一道题都要考虑一下这个问题。

Code

#include<cstdio>
#define int long long
int a[15];
int s[15][15];
int l,r,cnt,la,ra;
int dfs(int x,int f,bool v){
	if(!x)return 1;
	if(!v&&s[x][f])
		return s[x][f];
	int u=v?a[x]:9;
	int ans=0;
	for(int i=0;i<=u;++i){
		if(i==4||(i==2&&f==6))
			continue;
		ans+=dfs(x-1,i,v&&(i==a[x]));
	}
	if(!v)s[x][f]=ans;
	return ans;
}
signed main(){
	while((~scanf("%lld%lld",&l,&r))&&(l||r)){
		cnt=0;
		while(r){
			a[++cnt]=r%10;
			r/=10;
		}
		ra=dfs(cnt,10,1);
		cnt=0;
		l--;
		while(l){
			a[++cnt]=l%10;
			l/=10;
		}
		la=dfs(cnt,10,1);
		printf("%lld\n",ra-la);
	}
	return 0;
}

练习1-1:数字游戏

AC代码

练习1-2:Bomb

<法一>用所有情况减去“不要49”的情况

AC代码

<法二>等会再讲


例题二:windy数

双倍快乐

我去小伙子,没看出来呀,你居然是一道蓝题

Solution

不含前导0?咋搞?

我们想,如果第 \(x\) 位前面每一个数位上的数都是 \(0\) 的话:

  • 如果第 \(x\) 位不是 \(0\)
    好了,从第 \(x\) 位开始,后面每一位都不再有是前导 \(0\) 的可能
  • 如果第 \(x\) 位是 \(0\)
    那么 \(x\) 也算在前导 \(0\) 内,后面的数位还有是前导 \(0\) 的可能

所以我们再来一个参数 \(q\),记录 \(x\) 之前是不是都是 \(0\) 呗!q==true 时表示这一位不可能是前导 \(0\),否则表示这一位可能是前导 \(0\),也就是说,前面的每一位都是 \(0\)

因为题目要求每一位都要和上一位相差 \(2\),当q==false时,可以无视此要求。

快速传 \(q\) 值:q||i

  • \(i\) 不为 \(0\)\(q\)true,理所当然下一位不可能是前导 \(0\)
  • \(i\)\(0\)\(q\)true,说明 \(i\) 只是数中间的一个正常的 \(0\)
  • \(i\) 不为 \(0\)\(q\)false,因为 \(i\) 已经将最高位占领了,下一位不可能是前导 \(0\)
  • \(i\)\(0\)\(q\)false\(i\) 也算在前导 \(0\) 内,下一位可能是前导 \(0\)

还记得我们的记忆化吗?用s[x][f]来表示。如果 \(f\) 碰巧为 \(0\),那就会分为 \(f\) 是前导 \(0\)\(f\) 不是前导 \(0\) 的情况,我们记录的自然是 \(f\) 不是前导 \(0\) 的答案,当v==false时不能直接使用。

Code

#include<cstdio>
#define int long long
int a[25];
int l,r,cnt;
int s[25][15];
int abs(int x){return x<0?-x:x;} 
int dfs(int x,int f,bool v,bool q){
	if(!x)return 1;
	if(!v&&q&&s[x][f])
		return s[x][f];
	int u=v?a[x]:9;
	int ans=0;
	for(int i=0;i<=u;++i){
		if(!q||abs(i-f)>=2)
			ans+=dfs(x-1,i,v&&(i==a[x]),q||i);
	}
	if(!v&&q)s[x][f]=ans;
	return ans;
}
signed main(){
	scanf("%lld%lld",&l,&r);
	l--;
	cnt=0;
	while(r){
		a[++cnt]=r%10;
		r/=10;
	}
	int ra=dfs(cnt,0,1,0);
	cnt=0;
	while(l){
		a[++cnt]=l%10;
		l/=10;
	}
	int la=dfs(cnt,0,1,0);
	printf("%lld\n",ra-la);
	return 0;
}

例题三:数字计数

双倍快乐

Solution

想自己为什么能对比想题怎么做想得还久系列

既然每一个数都要统计,那么记忆化就必须每算一个数出现的次数清空一次。

那不是很好写嘛。由于需要统计 \(0\) 的个数,我们也不能算上前导 \(0\)

\(dfs\) 中加上两个参数 \(num\)\(sum\)\(num\) 表示当前正在搜的数,\(sum\) 表示这个数已经出现的次数。

记忆化数组呢,

我们设 s[x][sum] 为当前 \(dfs\)\(num\) 在前 \(x\) 位出现了 \(sum\) 次,后面的结果。

为毛这样设?跟 \(sum\) 有毛关系?直接 s[x] 他不香嘛!

没错,以上就是我花一个下午思考的问题,翻遍全网都没有大佬讲过这个问题。。。

经过暴力程序一顿搞+肉眼查错之后发现,会发生酱紫的情况:

输入 \(1,105\) ,输出 \(0\) 出现的次数为 \(16\)(和暴力程序对得上)

输入 \(1,110\) ,输出 \(0\) 出现的次数为 \(11\)( 。。。)

WWWW...What?发生了什么?

事情是这样的,我们这个方法还不够严谨。
比如求 \(1\sim110\)\(0\) 出现的次数,程序计算得 \(10\sim19\),也就是s[1]的值为 \(1\) (因为只有 \(10\) 中有一个 \(0\))

但是,我们计算 \(100\sim109\) 时程序会使用s[1]的值(此时满足使用条件)

But,As we all know,\(100\sim109\) 中有整整 \(10\)\(0\),而程序只计算了 \(100\) 当中的一个 \(0\)

所以我们需要往记忆化里再添一维,保证不会发生上面那一种情况。

好了,以上是我口胡的理由,总而言之,我们加一维 \(sum\) ,刚好珂以解决上面那个问题。

那么,这个问题就处理完了。。。(【笑容逐渐变态】)

然后这个问题处理完就没问题了。

Code

#include<cstdio>
#include<cstring>
#define int long long
int l,r,cnt;
int s[25][25];
int ans[25],a[25]; 
int dfs(int x,int sum,int num,bool v,bool q){
	//q => 无前导0 
	//!q => 有前导0 
	//num => 当前查询的数 
	//sum => 当前查询的数的总个数 
	if(!x)return sum;
	if(!v&&q&&~s[x][sum])
		return s[x][sum];
	int u=v?a[x]:9;
	int ans=0;
	for(int i=0;i<=u;++i){
		if(i||q)
			ans+=dfs(x-1,sum+(i==num),num,v&&(i==a[x]),1);
		else ans+=dfs(x-1,sum,num,v&&(i==a[x]),0);
	}
	if(!v&&q)s[x][sum]=ans;
	return ans;
}
int solve(int x,int w){
	cnt=0;
	memset(s,-1,sizeof(s));
	while(x){
		a[++cnt]=x%10;
		x/=10;
	}
	return dfs(cnt,0,w,1,0);
}
signed main(){
	scanf("%lld%lld",&l,&r);
	for(int i=0;i<=9;++i)
		printf("%lld ",solve(r,i)-solve(l-1,i));
	return 0;
}

例题四:B-number

题面翻译成人话:

\(B\) 数的定义:是 \(13\) 的倍数,且包含连续的 \(\texttt{"13"}\) 的数。

例如:\(130\)\(2613\)\(B\) 数,但是 \(143\) (不包含连续 \(\texttt{"13"}\)) 和 \(131\) (不是 \(13\) 的倍数) 不是 \(B\) 数。

计算 \(1\)\(n\) 之间有多少个 \(B\) 数。

温故而知新

练习1-2 处,我们提到了 Boob 的另外一种做法。

我们考虑使用二维记忆化数组 s[maxn][3] 来进行记忆化。

  1. s[x][0] 表示搜到第 \(x\) 位,前面没有出现连续的 \(49\),并且上一位不是 \(4\) 的所有情况。
  2. s[x][1]表示搜到第 \(x\) 位,前面没有出现连续的 \(49\),并且上一位是 \(4\) 的所有情况。
  3. s[x][2]表示搜到第 \(x\) 位,前面已经出现了连续的 \(49\) 的所有情况。

定义参数 \(f\) 为数组的第二维。\(dfs\) 向下延伸时

  • f==2,也就是前面已经出现过 \(49\) ,那么向下拓展时理所应当 \(f\) 也是 \(2\)
  • f==1&&i==9,也就是上一位是 \(4\) ,这一位是 \(9\) ,刚好凑成一个 \(49\) ,递归时将 \(f\) 传为 \(2\)
  • f!=2&&i==4,将 \(f\) 向下传为 \(1\)
  • 否则 \(f\)\(0\)

最后一个数搜完时,只有前面出现了 \(49\) ,也就是 f==2 时才会计算这个数。

完整代码:

#include<cstdio>
#include<cstring>
#define int long long
int a[25];
int x,T,n,cnt;
int s[25][3];
int dfs(int x,int f,bool v){
	if(!x)return f==2;
	if(!v&&(~s[x][f]))
		return s[x][f];
	int u=v?a[x]:9;
	int ans=0;
	for(int i=0;i<=u;++i){
		if(f==2||(i==9&&f==1))
			ans+=dfs(x-1,2,v&&(a[x]==i));
		else if(f!=2&&i==4)
			ans+=dfs(x-1,1,v&&(a[x]==i));
		else ans+=dfs(x-1,0,v&&(a[x]==i));
	}
	if(!v)s[x][f]=ans;
	return ans;
}
signed main(){
	scanf("%lld",&T);
	while(T--){
		scanf("%lld",&n);
		memset(s,-1,sizeof(s));
		int tmp=n; 
		cnt=0;
		while(n){
			a[++cnt]=n%10;
			n/=10;
		}
		printf("%lld\n",dfs(cnt,0,1));
	}
	return 0;
}

我们等一会儿会尝试将这种新方法应用到本题中来。

那么,取模,咋整?依次把每个搜到的数记录下来最后再取模?

假设有一个数 \(\overline{x_1x_2x_3\cdots x_n}\bmod13\),可以变形为 \((x_1\times 10^n+x_2\times 10^{n-1}+x_3\times10^{n-2}+\cdots+x_n\times10^0)\bmod13\),再变一下,就是:

\[(\cdots((((x_1\bmod13)\times10+x_2)\bmod13)\times10)\bmod13)\cdots)\bmod13 \]

\(dfs\) 时,我们增加一个参数 \(m\) ,表示当前已经搜完的前几位数 \(\bmod 13\) 的值,向后搜时,我们传(m*10+i)%13

搜完最后一个数时,只有同时满足 f==2m==0 时,也就是这个数包含连续的 \(\texttt{"13"}\) ,并且这个数被 \(13\) 整除才计算这个数。

Code

#include<cstdio>
#include<cstring>
#define int long long
int a[25];
int x,n,cnt;
int s[25][15][3];
int dfs(int x,int f,bool v,int m){
    if(!x)return f==2&&!m;
    if(!v&&(~s[x][m][f]))
        return s[x][m][f];
    int u=v?a[x]:9;
    int ans=0;
    for(int i=0;i<=u;++i){
        if(f==2||(i==3&&f==1))ans+=dfs(x-1,2,v&&(a[x]==i),(m*10+i)%13);
        else if(i==1)ans+=dfs(x-1,1,v&&(a[x]==i),(m*10+i)%13);
        else ans+=dfs(x-1,0,v&&(a[x]==i),(m*10+i)%13);
    }
    if(!v)s[x][m][f]=ans;
    return ans;
}
signed main(){
    while(~scanf("%lld",&n)){
        memset(s,-1,sizeof(s));
        int tmp=n; 
        cnt=0;
        while(n){
            a[++cnt]=n%10;
            n/=10;
        }
        printf("%lld\n",dfs(cnt,0,1,0));
    }
    return 0;
}

练习4-1

注意清空记忆化数组。

注意是数字和取模,而不是数字本身。

AC代码


例题五:起床困难综合征

双倍经验

Solution

位,,位运算???

预处理

提供一个思路:一个数的二进制位只可能是 \(0\)\(1\)

并且这个数每一位要做的操作已知。

那我们挨个挨个,假设每一位是 \(0\)\(1\) 不就行了?

我们用 number[i][0] 表示初始攻击力的第 \(i\) 位为 \(0\) 时的操作后的结果,number[i][1]表示初始攻击力的第 \(i\) 位为 \(1\) 时操作后的结果。

把这一位是 \(0\) 时操作的答案记录一下,这一位是 \(1\) 时操作的答案记录一下,再用数位DP找最大的答案。

数位DP

整个 \(dfs\) 共计 \(3\) 个参数:位数 \(x\) ,当前搜到的最大答案对应的初始攻击力 \(c\) ,最大答案 \(w\)

如果 \(c>m\),不满足题意,return;

搜完了,将最大答案 \(ans\)\(w\)\(\max\)

向下拓展时,因为没有了 \(v\) 的限制,枚举第 \(i\) 位为 \(0\)\(1\) 就行了。

然后因为这是数位,所以,假设第 \(x\) 位数为 \(i\),整个数就应该增加 \(i\times 2^x\) ,也就是 i<<x 才对。对应的,因为 number[x][i] 知识当前位的结果,答案 \(w\) 也要加上 number[x][i]<<x

所以数位DP的代码长这样:

void dfs(int x,int c,int w){
	if(c>m)return;
	if(x==-1){
		ans=max(ans,w);
		return;
	}
	for(int i=0;i<=1;++i)
		dfs(x-1,c+(i<<x),w+(number[x][i]<<x));
	return;
}

然而这样会 T 。

所以我们需要剪枝。

我们想,如果 \(w<ans\) ,可能是发生了什么?

差不多就是 \(ans\)\(11\ \_ \ \_\) 更新,而此时的 \(w\)\(10\ \_ \ \_\)

\(ans\) 最少是 \(1100\)\(w\) 最多是 \(1011\)\(\max\{w\}<\min\{ans\}\)\(ans\) 永远不会被 \(w\) 更新。

所以当 \(w<ans\) 时,return

Code

#include<cstdio>
#define int long long
int a[35];
char op[5];
int number[35][2];
int n,m,cnt,t,ans;
void dfs(int x,int c,int w){
	if(c>m||w<ans)return;
	if(x==-1){
		ans=w;
		return;
	}
	for(int i=0;i<=1;++i)
		dfs(x-1,c+(i<<x),w+(number[x][i]<<x));
	return;
}
signed main(){
	scanf("%lld%lld",&n,&m);
	for(int i=0;i<31;++i)
		number[i][1]=1;
	for(int i=1;i<=n;++i){
		scanf("%s%lld",op,&t);
		for(int j=0;j<31;++j){
			bool c=(t>>j)&1;
			if(op[0]=='A'){
				number[j][0]&=c;
				number[j][1]&=c;
			}
			else if(op[0]=='O'){
				number[j][0]|=c;
				number[j][1]|=c;
			}
			else{
				number[j][0]^=c;
				number[j][1]^=c;
			}
		}
	}
	dfs(30,0,0);
	printf("%lld",ans);
	return 0;
}

没有练习,咕咕。


end.

posted @ 2020-12-29 11:17  XSC062  阅读(141)  评论(0编辑  收藏  举报