[題解]CF1073E Segment Sum

Sinktank發表於2024-04-14

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;
}

相關文章