[一本通提高数位动态规划]恨7不成妻--题解--胎教级教学

1前言

一本通提高篇的毒瘤数位dp终于要结束了
然而…我遇到了这道毒中之毒
网上的题解都是依托构思,我猜他们都是抄的代码
我要是抄代码用你发?
甚至有人直接抄袭acwing yxc老师的图片
几十篇题解凑不出完整的思路,你家题解是散装的
所有人都在劝你抄代码,只有我在写胎教级教学
本文的所有思路均有证明,终结你关于这道毒瘤题的一切疑问
所有公式均使用 L a t e x Latex Latex,包清晰
本题的难度较高,新手不要轻易尝试
建议先阅读
论数位dp–胎教级教学
B3883 [信息与未来 2015] 求回文数 数位dp题解
论进制类型的数位dp:胎教级教学
[一本通提高数位动态规划]数字游戏:取模数题解

2问题

如图
在这里插入图片描述
注意!要求的是平方和,这是本题最大的毒瘤
(出题人yyds (永远单身)
而且,先放下平方和不看,本题对于合法数的约束条件有整整 3 3 3
面对复杂的问题,我们可以使用dp式解题法
先求解子问题,再转移
我们一步一步地解决吧

3化繁为简–对于方案数的求解

(1)子问题的分解

我们发现,求解平方和的性质是如此毒瘤,以至于扰乱了整个dp过程
一步到正解过于困难,我们可以抛开这个条件,先考虑求解合法方案数

(2)数位dp-part1状态设置–利用约束条件推状态

dp的状态设置要满足两个条件
1.构成子问题,即和最终要求解的问题有一致性
2.可转移性,可以利用已经求出的状态来推新的状态
首先,对于子问题,我们转化原问题的约束条件
设一个合法方案的数值为 x x x x x x的第 i i i位为 x i x_{i} xi,则有
{ ∀ x i , x i ≠ 7 ∑ x i m o d 7 ≠ 0 x m o d 7 ≠ 0 \left \{ \begin{array}{c} \forall x_{i},x_{i}\ne7\\ \sum x_{i} mod 7 \ne 0\\ x mod 7 \ne 0 \end{array} \right. xixi=7ximod7=0xmod7=0
这三个条件互相不干扰,我们为 d p dp dp数组增加三个维度
1. x x x的最高位
2. ∑ x i m o d 7 \sum x_{i} mod 7 ximod7的值
3. x m o d 7 x mod 7 xmod7的值
然后,考虑可转移性
可以发现,我们在 x x x前面插入一位数 k k k,设 k k k为第 i i i位数,新的状态可以由之前的转移过来,(因为状态合法和不合法都要处理,为了体现可转移性,我们假定当前状态是合法的)
对于维度1,直接枚举可能的最高位(当然不能为 7 7 7
对于维度2,取 ( 7 − k ) m o d 7 (7-k) mod 7 (7k)mod7(温馨提示:在合法的情况下为 7 − k 7-k 7k,因为各位和模 7 7 7 0 0 0
对于维度3,取 ( 7 − k × 1 0 i ) (7-k \times 10^{i}) (7k×10i)
这种转移方式可以采用,但是因为要枚举当前位数 − 1 -1 1的情况
我们还要再开一维,为当前的位数
所以状态得出
d p i , j , k , l dp_{i,j,k,l} dpi,j,k,l i i i位, j j j开头,数值模 7 7 7 k k k,各为之和模 7 7 7 l l l的方案个数

(3)数位dp-part2状态转移

其实状态转移的方法,我们已经在设置状态的时候考虑好了
枚举 i , j , k , l i,j,k,l i,j,k,l,即 d p dp dp的所有维度,此外,还需枚举一个 h h h,代表前一位的情况
得状态转移方程:(这里有点绕,慢慢理解即可)
d p i , j , k , l = d p i , j , k , l + d p i − 1 , h , m o d ( k − ( 1 0 i × j ) ) , m o d ( l − j ) dp_{i,j,k,l} = dp_{i,j,k,l}+dp_{i-1,h,mod(k-(10^i \times j)),mod(l-j)} dpi,j,k,l=dpi,j,k,l+dpi1,h,mod(k(10i×j)),mod(lj)
其中 m o d ( x ) mod(x) mod(x)代表数 x x x 7 7 7取正数
逆天状态转移方程
我们再来一遍,一维一维看
维度 i i i:上一位当然为 i − 1 i-1 i1
维度 j j j:枚举的 h h h
维度 k k k j j j所在第 i i i位,值增加了 j × 1 0 i j \times 10^i j×10i变成 k k k,得这一维为 m o d ( k − ( 1 0 i × j ) ) mod(k-(10^i \times j)) mod(k(10i×j))
维度 l l l:加上一位 j j j,各位之和变为 l l l ,得 m o d ( l − j ) mod(l-j) mod(lj)
(觉得状态转移方程太复杂不可读,拆开看好一些QwQ)
看到这了,就该代码出场了
附初始化部分的代码(c++)

const long long MOD = 1e9+7; 
long long dp[20][20][10][10];//状态 
long long e[20];//预处理10的幂 
long long mmod(long long x){//模7防负数 
	return(x%7+7)%7;
}
long long n,a,b;
void init(){
	e[0] = 1;//10^0
	for(long long i = 0;i<=9;i++){//预处理个位数 
		dp[1][i][i%7][i%7] = 1-(i==7);
	}
	for(long long i = 1;i<=20;i++){
		e[i] = e[i-1]*10;
		e[i]%=7;
		for(long long j = 0;j<=9;j++){
			if(j==7){//判断7 
				continue;
			}
			for(long long k = 0;k<7;k++){
				for(long long l = 0;l<7;l++){
					for(long long h = 0;h<=9;h++){
						if(h!=7){//判断7 
							dp[i][j][k][l]+=dp[i-1][h][mmod(k-(j*e[i]))][mmod(l-j)];//状态转移方程的体现 
							dp[i][j][k][l]%=MOD;
						}
					}
				}
			}
		}
	}
}
(4)数位dp-part3利用状态求解问题

我们依旧先划分问题,举例数 23456 23456 23456
可划分为 1 − 19999 1-19999 119999 20000 − 23456 20000-23456 2000023456
首先考虑 1 − 19999 1-19999 119999区间,对于 i i i位(此处 i = 5 i = 5 i=5),枚举 0 ≤ j ≤ 9 , j ≠ 7 0 \le j \le 9,j \ne 7 0j9j=7
答案加上 d p i , j , k , l dp_{i,j,k,l} dpi,j,k,l即可,这里 k , l k,l k,l都是合法的
那怎么判断合法呢,分别处理前面的数值和各位和,就可以用来判断了
我们在写代码时可以用函数将这一步独立出来
至于 20000 − 23456 20000-23456 2000023456这个区间,向后递推处理即可,边界问题要特判,其他就没什么难的了

(5)方案数求解的代码

代码如下,记得模上 1 0 9 + 7 10^9+7 109+7,记得开 l o n g l o n g long long longlong
(作者因为没调用初始化函数调了半天)

#include<bits/stdc++.h>
using namespace std;
const long long MOD = 1e9+7; 
long long dp[30][20][10][10];//状态 
long long e[20];//预处理10的幂 
long long mmod(long long x){//模7防负数 
	return(x%7+7)%7;
}
long long n,a,b;
void init(){
	e[0] = 1;//10^0
	for(long long i = 0;i<=9;i++){//预处理个位数 
		dp[1][i][i%7][i%7] = 1-(i==7);
	}
	for(long long i = 1;i<=20;i++){
		e[i] = e[i-1]*10;
		e[i]%=7;
		for(long long j = 0;j<=9;j++){
			if(j==7){//判断7 
				continue;
			}
			for(long long k = 0;k<7;k++){
				for(long long l = 0;l<7;l++){
					for(long long h = 0;h<=9;h++){
						if(h!=7){//判断7 
							dp[i][j][k][l]+=dp[i-1][h][mmod(k-(j*e[i]))][mmod(l-j)];//状态转移方程的体现 
							dp[i][j][k][l]%=MOD;
						}
					}
				}
			}
		}
	}
}
long long get(long long i1,long long j1,long long k1,long long l1){
	long long ans = 0;
	for(int k = 0;k<7;k++){
		for(int l = 0;l<7;l++){
			if(k!=k1&&l!=l1){
				ans+=dp[i1][j1][k][l];
			}
		}
	}
	return ans;
}
long long solve(long long x){
	if(x==0){
		return 0;
	}
	long long h = x,s[1145],idx = 0,ans = 0,tmp1 = 0,tmp2 = 0;
	while(h){
		s[++idx] = h%10;
		h/=10; 
	}
	for(int i = idx;i>=1;i--){
		for(int j = 0;j<s[i];j++){
			if(j==7){
				continue;
			}
			long long k1 = mmod(-tmp1*e[i]),l1 = mmod(-tmp2);
			ans+=get(i,j,k1,l1);
		}
		if(s[i]==7){
			break;
		}
		tmp1 = tmp1*10+s[i];
		tmp2 = tmp2+s[i];
		if(i==1&&tmp1%7!=0&&tmp2%7!=0){
			ans++;
		}
	}
	return ans;
} 
int main(){
	init();
	cin>>n;
	while(n--){
		cin>>a>>b;
		long long ans = solve(b)-solve(a-1);
		cout<<ans<<endl;
	}
	return 0;
}

4问题转化–平方和的加入

(1)状态转移–问题的变化

我们的dp式解题法已经求好了状态,接下来该进行转移了
平方和…这个问题会破坏掉我们的整个求解过程
所以我们要尝试在原来求解方式的基础上改进,就要利用好平方和的性质

(2)改进算法–从状态出发

首先,要具有子问题的性质,我们不可能处理出所有符合条件的数,再开一个数组麻烦,那就开一个结构体
使原有的 d p dp dp数组不止存方案数,还存储所有合法数的平方和
对于 d p i , j , k , l dp_{i,j,k,l} dpi,j,k,l,在 i = 1 i=1 i=1的条件下显然可以直接求出平方和(就一种方案)
还记得我们求方案数时状态转移的原理吗
在原有的数前面加上一位,那么我们设原数为 x x x,新的一位为 h h h
则新的数为 h × 1 0 i + x h \times 10^{i}+x h×10i+x,表示为平方 ( h × 1 0 i + x ) 2 (h \times 10^{i}+x)^2 (h×10i+x)2
根据完全平方公式得原式等价于
( h × 1 0 i ) 2 + 2 × ( h × 1 0 i ) × x + x 2 (h \times 10^i)^2+2\times(h \times 10^i) \times x+x^2 (h×10i)2+2×(h×10i)×x+x2
进一步化简:
h 2 × 1 0 2 i + 2 × 1 0 i h x + x 2 h^2\times10^{2i} + 2\times 10^ihx + x^2 h2×102i+2×10ihx+x2
我们发现了一个极好的性质!!!, x 2 x^2 x2,这正是子问题
对于每一个新的数,我们都套用公式
设原来 n n n x x x的平方和为 s u m x sum_{x} sumx,新数 y y y的平方和为 s u m y sum_{y} sumy,则有
s u m y = n × h 2 × 1 0 2 i + 2 × 1 0 i h ( x 1 + x 2 . . . . . . + x n ) + s u m x sum_{y} = n \times h^2 \times 10^{2i}+2 \times 10^ih(x_1+x_2......+x_n)+sum_{x} sumy=n×h2×102i+2×10ih(x1+x2......+xn)+sumx
原来的方案数和平方和都用上了,好啊, 10 10 10的幂照常预处理
但是呢,意外出现了, x x x的求和我们没存过
那还想啥了,存呗
想想转移(以下所有设的未知数的意思和上文相同)
每个新数 y = h × 1 0 i + x y = h \times 10^i+x y=h×10i+x
∑ y = ∑ x + n × h × 1 0 i \sum y = \sum x+n\times h \times10^i y=x+n×h×10i
归纳以上内容,得状态转移方程(枚举的上一位依旧设为 h h h
(此处为了清晰不用 d p dp dp的结构体表示形式, c n t cnt cnt代指方案数, s u m sum sum代指求和, r e s res res代指平方和,如果觉得太复杂不可读也可以先看看后面的代码部分)
(为了更加清晰可读,前一个状态的 k , l k,l k,l,即 m o d ( k − ( 1 0 i × j ) ) , m o d ( l − j ) mod(k-(10^i \times j)),mod(l-j) mod(k(10i×j)),mod(lj)统一替换为 u , v u,v u,v
(所有上一个状态的下标都统一表示为 s 2 s2 s2,当前状态表示为 s 1 s1 s1
(公式不代表代码,取模部分这里不体现代码里会有的)
c n t s 1 = c n t s 1 + c n t s 2 cnt_{s1} = cnt_{s1}+cnt_{s2} cnts1=cnts1+cnts2
s u m s 1 = s u m s 1 + s u m s 2 + c n t s 2 × j × 1 0 i sum_{s1} = sum_{s1}+sum_{s2}+cnt_{s2}\times j \times 10^i sums1=sums1+sums2+cnts2×j×10i
r e s s 1 = r e s s 1 + c n t s 2 × j 2 × 1 0 2 i + 2 × 1 0 i j x × s u m s 2 + r e s s 2 res_{s1} = res_{s1}+cnt_{s2}\times j^2 \times 10^{2i}+2\times 10^ijx\times sum_{s2}+res_{s2} ress1=ress1+cnts2×j2×102i+2×10ijx×sums2+ress2
初始化这边的代码也一并附上,注意循环最里层的写法,直接利用指针把原 d p dp dp数组的值带入到 x x x里,多使用这些技巧可以改善码风
附初始化代码(c++)

const long long MOD = 1e9+7; 
long long e[20],g[20];//预处理10的幂
struct node{
	long long cnt,sum,res;
}dp[30][20][10][10];//状态 
long long mmod(long long x){//模7防负数 
	return (x%7+7)%7;
}
long long mmmod(long long x){//模1e9+7防负数
	return (x%MOD+MOD)%MOD;
}
long long n,a,b;
void init(){
	e[0] = 1;//10^0
	g[0] = 1;
	for(long long i = 0;i<=9;i++){//预处理个位数 
		if(i==7){
			continue;
		}
		node &u = dp[1][i][i%7][i%7];
		u.cnt++;
		u.sum+=i;
		u.res+=i*i;
	}
	for(long long i = 1;i<=20;i++){
		e[i] = e[i-1]*10;
		e[i]%=7;
		g[i] = g[i-1]*10;
		g[i]%=MOD;
		for(long long j = 0;j<=9;j++){
			if(j==7){//判断7 
				continue;
			}
			for(long long k = 0;k<7;k++){
				for(long long l = 0;l<7;l++){
					for(long long h = 0;h<=9;h++){
						if(h!=7){//判断7 
							node &v = dp[i][j][k][l],u = dp[i-1][h][mmod(k-j*e[i])][mmod(l-j)];
							v.cnt = mmmod(v.cnt+u.cnt);
							v.sum = mmmod(v.sum+1ll*j%MOD*(e[i]%MOD)%MOD*u.cnt%MOD+u.sum);
							v.res = mmmod(v.res+1ll*j%MOD*u.cnt%MOD*(e[i]%MOD)%MOD*j%MOD*(e[i]%MOD)%MOD+1ll*u.sum%MOD*2%MOD*j%MOD*(e[i]%MOD)%MOD+u.sum);
						}
					}
				}
			}
		}
	}
}
(3)利用新状态–问题再求解

问题的划分和上文相同,这里便不再赘述
上文程序中的 g e t get get函数无需大改,只是需要返回结构体变量
需要改的是 s o l v e solve solve函数
我们原来使用的将答案累加到 a n s ans ans变量上的方式可以继续沿用
为什么?因为将平方和加到一个现有的平方和的结果上,无需现有平方和结果对应的方案数和数的求和, a n s ans ans还可以是 l o n g l o n g long long longlong型的
说人话就是求 r e s res res用不着 c n t , s u m cnt,sum cnt,sum管,不用开结构体变量
至于累加平方和的公式又要再打一遍
这就是本题的毒瘤之处
其实那些公式推出来了,剩下的步骤思路难度不高,有的只是对手的折磨

(4)问题的终结–附上代码

话不多说,直接给代码(c++)

#include<bits/stdc++.h>
using namespace std;
const long long MOD = 1e9+7; 
long long e[30],g[30];//预处理10的幂
struct node{
	long long cnt,sum,res;
}dp[40][30][20][20];//状态 
long long mmod(long long x){//模7防负数 
	return (x%7+7)%7;
}
long long mmmod(long long x){//模1e9+7防负数
	return (x%MOD+MOD)%MOD;
}
long long n,a,b;
void init(){
	e[0] = g[0] = 1;//10^0
	e[1] = g[1] = 10;
	for(long long i = 0;i<=9;i++){//预处理个位数 
		if(i==7){
			continue;
		}
		node &u = dp[1][i][i%7][i%7];
		u.cnt++;
		u.sum+=i;
		u.res+=i*i;
	}
	long long pow = 10;
	for(long long i = 2;i<20;i++,pow*=10){
		e[i] = e[i-1]*10;
		e[i]%=7;
		g[i] = g[i-1]*10;
		g[i]%=MOD;
		for(long long j = 0;j<=9;j++){
			if(j==7){//判断7 
				continue;
			}
			for(long long k = 0;k<7;k++){
				for(long long l = 0;l<7;l++){
					for(long long h = 0;h<=9;h++){
						if(h!=7){//判断7 
							node &v = dp[i][j][k][l],u = dp[i-1][h][mmod(k-j*pow)][mmod(l-j)];
							v.cnt = mmmod(v.cnt+u.cnt);
							v.sum = mmmod(v.sum+1ll*j%MOD*(pow%MOD)%MOD*u.cnt%MOD+u.sum);
							v.res = mmmod(v.res+1ll*j%MOD*u.cnt%MOD*(pow%MOD)%MOD*j%MOD*(pow%MOD)%MOD+1ll*u.sum%MOD*2%MOD*j%MOD*(pow%MOD)%MOD+u.res);
						}
					}
				}
			}
		}
	}
}
node get(long long i1,long long j1,long long k1,long long l1){
	long long ans1 = 0,ans2 = 0,ans3 = 0;
	for(int k = 0;k<7;k++){
		for(int l = 0;l<7;l++){
			if(k!=k1&&l!=l1){
				node st = dp[i1][j1][k][l];
				ans1=mmmod(ans1+st.cnt);
				ans2=mmmod(ans2+st.sum);
				ans3=mmmod(ans3+st.res);
			}
		}
	}
	return {ans1,ans2,ans3};
}
long long solve(long long x){
	if(x==0){
		return 0;
	}
	long long ggg = x%MOD;
	long long h = x,s[1145],idx = 0,ans = 0,tmp1 = 0,tmp2 = 0;
	while(h){
		s[++idx] = h%10;
		h/=10; 
	}
	for(int i = idx;i>=1;i--){
		for(int j = 0;j<s[i];j++){
			if(j==7){
				continue;
			}
			long long k = mmod(-tmp1*e[i]),h = mmod(-tmp2);
			node st = get(i,j,k,h);
			ans = mmmod(ans+1ll*(tmp1%MOD)*(tmp1%MOD)%MOD*(g[i]%MOD)%MOD*(g[i]%MOD)%MOD*st.cnt%MOD+1ll*2*tmp1%MOD*(g[i]%MOD)%MOD*st.sum%MOD+st.res%MOD);
			
		}
		if(s[i]==7){
			break;
		}
		tmp1 = tmp1*10+s[i];
		tmp2+=s[i];
		if(i==1&&tmp1%7&&tmp2%7){
			ans = mmmod(ans+ggg*ggg%MOD);
		}
	}
	return ans;
} 
signed main(){
	init();
	cin>>n;
	while(n--){
		cin>>a>>b;
		long long ans = mmmod(solve(b)-solve(a-1));
		cout<<ans<<endl;
	}
	return 0;
}

5后记

我们就这样切掉了这道毒瘤题,作者认为这一题的难度完全可以评黑(下位黑或上位紫)
作者从早上 10 10 10点调到晚上 8 8 8点,终于AC,并完成了这篇博客
我觉得这一切都是值得的,我敢说我的博客比CSDN平台上的任何一篇都要详细
我可以写出更详细的题解,这就是OI事业的发展
可能某一天,我的题解也会成为"屎"一样的存在,那就证明OI的事业发展的更好了
关注CSDN@森林古猿1,我会为大家带来更多胎教级教学
本文作者是蒟蒻,如有错误请各位神犇指点
森林古猿出品,必属精品,请认准CSDN森林古猿1

  • 25
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值