[题解]CF1073E Segment Sum
这道数位dp与其他不同的是,这个求的是满足要求的数的和,这种题型的题我们还没有做过。
以前虽然做过一些求和或者求积的题,但都是求每个满足条件的数的数位和、二进制1的个数等等的和。而这道题是对\([L,R]\)中满足条件的数直接求和,这意味着基本不会有两个状态得出相同的结果。这种情况我们应该怎么做呢?
总之先打出暴搜,参数中的\(tk\)表示到现在一共有多少个不同的数,\(sum\)表示到现在的和。
1.1 暴搜
#include<bits/stdc++.h>
#define int long long
#define mod 998244353
using namespace std;
int l,r,k,a[30];
int sta[30];
bitset<10> vis;
int dfs(int pos,bool limit,int tk,int sum,bool zero){
if(tk>k) return 0;
if(pos==0) return sum;
int rig=limit?a[pos]:9,ans=0;
for(int i=0;i<=rig;i++){
bool temp=(zero&&i==0);//是否为前导0
int presta=sta[pos];
bool previs=vis[i],add=0;
if(!temp) add=(!vis[i]),sta[pos]=i,vis[i]=1;
ans+=dfs(pos-1,limit&&i==rig,tk+add,(sum*10%mod+i)%mod,temp);
ans%=mod;
sta[pos]=presta,vis[i]=previs;
}
return ans;
}
int solve(int x){
int len=0;
while(x){
a[++len]=x%10;
x/=10;
}
return dfs(len,1,0,0,1);
}
signed main(){
cin>>l>>r>>k;
cout<<solve(r)-solve(l-1);
return 0;
}
接着我们考虑优化,但是却发现怎么也无法入手,为什么?这是因为我们始终是进行到最后再返回\(sum\),而两个状态因为前面填的位都不一样所以\(sum\)几乎不存在一样的,所以没法记忆化。
所以我们考虑另一种优化方式,也就是优化\(sum\)的计算方法,把计算到末尾再返回整个数是多少,改成返回这个数位到最低位的子串是多少。然后在循环里\(ans\)逐个累加,但是返回的值还需要加上自己这个数位啊。所以在逐个累加每一个答案的同时,累加上当前这一位\(sta[pos]*10^{pos}\)。
见下图:
为什么这样就方便记忆化了呢?因为这样每个递归的节点都只关注它后面的值,不会受前面的影响。自然我们就可以确定,如果\(pos\)相同,\(vis\)的值相同,两个状态答案一样。
这样的话暴搜代码就得重制了,见下:
1.2 改进的暴搜
#include<bits/stdc++.h>
#define int long long
#define mod 998244353
using namespace std;
int l,r,k,a[30],ph[30];
int sta[30],f[30][1050];
bitset<10> vis;
pair<int,int> dfs(int pos,bool limit,int tk,bool zero){
if(tk>k) return {0,0};
if(pos==0) return {0,1};
int rig=limit?a[pos]:9;
pair<int,int> ans={0,0};
for(int i=0;i<=rig;i++){
bool temp=(zero&&i==0);//是否为前导0
int presta=sta[pos];
bool previs=vis[i],add=0;
if(!temp) add=(!vis[i]),sta[pos]=i,vis[i]=1;
auto res=dfs(pos-1,limit&&i==rig,tk+add,temp);
ans.first=(ans.first+res.first)%mod;
ans.first+=(i*res.second%mod*ph[pos]%mod);
ans.first%=mod;
ans.second+=res.second;
sta[pos]=presta,vis[i]=previs;
}
return ans;
}
int solve(int x){
int len=0;
while(x){
a[++len]=x%10;
x/=10;
}
return dfs(len,1,0,1).first;
}
signed main(){
ph[1]=1;
for(int i=2;i<=25;i++) ph[i]=ph[i-1]*10%mod;
cin>>l>>r>>k;
cout<<solve(r)-solve(l-1);
return 0;
}
注意到我们废弃了\(sum\)参数,这是因为我们已经修改了\(sum\)的计算方法,改为存在返回值里。但返回值使用了pair<int,int>
,这是为什么呢?
first
表示到目前的\(sum\),即满足条件的数的和。second
表示到目前满足条件的数的个数。
为什么还要记个数呢?是因为我们需要知道这个子节点有多少个答案,也就是需要额外加多少个\(sta[pos]*10^{pos}\)。
根据上面我们的推导,用\(f[pos][vis]\)来记忆化即可。大小\(20*1024\)。
(这个题如果再加一问,询问一共有多少个,输出根节点的second
即可)
1.3 记忆化
#include<bits/stdc++.h>
#define int long long
#define mod 998244353
using namespace std;
int l,r,k,a[30],ph[30];
int sta[30];
pair<int,int> f[30][1050];
bitset<10> vis;
pair<int,int> dfs(int pos,bool limit,int tk,bool zero){
if(tk>k) return {0,0};
if(pos==0) return {0,1};
int visnum=vis.to_ulong();
if(!limit&&!zero&&f[pos][visnum].first!=-1){
return f[pos][visnum];
}
int rig=limit?a[pos]:9;
pair<int,int> ans={0,0};
for(int i=0;i<=rig;i++){
bool temp=(zero&&i==0);
int presta=sta[pos];
bool previs=vis[i],add=0;
if(!temp) add=(!vis[i]),sta[pos]=i,vis[i]=1;
auto res=dfs(pos-1,limit&&i==rig,tk+add,temp);
ans.first=(ans.first+res.first)%mod;
ans.first+=(i*res.second%mod*ph[pos]%mod);
ans.first%=mod;
ans.second=(ans.second+res.second)%mod;
sta[pos]=presta,vis[i]=previs;
}
if(!limit&&!zero) f[pos][visnum]=ans;
return ans;
}
int solve(int x){
int len=0;
while(x){
a[++len]=x%10;
x/=10;
}
return dfs(len,1,0,1).first;
}
signed main(){
memset(f,-1,sizeof f);
ph[1]=1;
for(int i=2;i<=25;i++) ph[i]=ph[i-1]*10%mod;
cin>>l>>r>>k;
cout<<(solve(r)-solve(l-1)+mod)%mod;
return 0;
}