[一本通提高数位动态规划]恨7不成妻--题解--胎教级教学
- 1前言
- 2问题
- 3化繁为简--对于方案数的求解
- (1)子问题的分解
- (2)数位dp-part1状态设置--利用约束条件推状态
- (3)数位dp-part2状态转移
- (4)数位dp-part3利用状态求解问题
- (5)方案数求解的代码
- 4问题转化--平方和的加入
- (1)状态转移--问题的变化
- (2)改进算法--从状态出发
- (3)利用新状态--问题再求解
- (4)问题的终结--附上代码
- 5后记
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.
⎩
⎨
⎧∀xi,xi=7∑ximod7=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
(7−k)mod7(温馨提示:在合法的情况下为
7
−
k
7-k
7−k,因为各位和模
7
7
7为
0
0
0)
对于维度3,取
(
7
−
k
×
1
0
i
)
(7-k \times 10^{i})
(7−k×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+dpi−1,h,mod(k−(10i×j)),mod(l−j)
其中
m
o
d
(
x
)
mod(x)
mod(x)代表数
x
x
x模
7
7
7取正数
逆天状态转移方程
我们再来一遍,一维一维看
维度
i
i
i:上一位当然为
i
−
1
i-1
i−1
维度
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(l−j)
(觉得状态转移方程太复杂不可读,拆开看好一些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
1−19999和
20000
−
23456
20000-23456
20000−23456
首先考虑
1
−
19999
1-19999
1−19999区间,对于
i
i
i位(此处
i
=
5
i = 5
i=5),枚举
0
≤
j
≤
9
,
j
≠
7
0 \le j \le 9,j \ne 7
0≤j≤9,j=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
20000−23456这个区间,向后递推处理即可,边界问题要特判,其他就没什么难的了
(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(l−j)统一替换为
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