[题解]CF1073E Segment Sum

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}\)

见下图:
image

为什么这样就方便记忆化了呢?因为这样每个递归的节点都只关注它后面的值,不会受前面的影响。自然我们就可以确定,如果\(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;
}
posted @ 2024-04-14 10:52  Sinktank  阅读(11)  评论(0编辑  收藏  举报
★CLICK FOR MORE INFO★ TOP-BOTTOM-THEME
Copyright © 2023 ~ 2024 Sinktank - 1328312655@qq.com
Illustration from 稲葉曇『リレイアウター/Relayouter/中继输出者』,by ぬくぬくにぎりめし.