[题解]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; }
代码中的bitset<10>
可以直接压成一个整数,这样应该会更快。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效