ABC 321 F #(subset sum = K) with Add and Erase

Linear_L發表於2024-06-13

題意
有一個箱子,每次可以向裡面新增或者拿走一個數,問每次操作過後,任選箱子裡的數相加,總和等於k的方案數是多少。

思路
萌新算是學到新東西了,這其實是個可撤銷揹包的板題。
我們先考慮一個問題:
對於普通計數類dp,我們現在禁用某一個數i,我們現在想知道某一個數j有多少種方式表示(即dp[j])。那麼我們現在正常進行一次計數dp,然後撤銷使用了數字i的方案即可。

  • 若j<i,那麼dp[j]是不會變化的,因為j不可能表示為i加上其它正整數的和
  • 若j>=i,dp[j]就會包含選擇了數字i的方案。那麼這些方案一定是從dp[j-i]這個狀態轉移過來的。那麼只需要減去dp[j-i]即可。
    對於本題,若操作為向集合裡新增某個數,那麼只需要從k到x遍歷一遍dp,然後轉移方程為dp[i]=dp[i-x]+dp[i];若在集合裡刪除某個數,那麼只需要從x到k遍歷一遍dp,然後轉移方程為dp[i]=dp[i]-dp[i-x]。

難點
這個題真正值得關注的是如何理解遍歷順序(新增為倒序遍歷,刪除為正序遍歷):

  • 若新增某個數,那麼可以類比01揹包,你只是增加了一個x,dp[i]的轉移只是比原來多了一種路徑(dp[i-x]+x),所以只需要將原來的dp[i-x]再加上一次就可以了,這也是保證了dp的無後效性,所以需要從後向前遍歷。(如果你正向遍歷,你就會多加很多次你新增的x,然後只多新增了一個x)
  • 若刪除某個數,那麼此時就要考慮到前面的數的方案數對後面數的方案數的後效性了,因為你設想還是倒序遍歷,當你更新dp[i]=dp[i]-dp[i-x]時,此時的i-x若仍然大於x,那麼dp[i-x]也要發生變化,也就是先更新小的,再更新大的。(好好理解這兩個遍歷順序)

程式碼

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=998244353;
const int maxn=2e5+10;
int dp[maxn];

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(NULL);
	cout.tie(NULL);
	
	int q,k;
	cin>>q>>k;
	dp[0]=1;
	while(q--)
	{
		char s;
		int x;
		cin>>s>>x;
		if(s=='+')
		{
			for(int i=k;i>=x;i--)
			  dp[i]=(dp[i]+dp[i-x])%mod;
		}
		else 
		{
			for(int i=x;i<=k;i++)
			  dp[i]=(dp[i]-dp[i-x]+mod)%mod;
		}
		cout<<dp[k]%mod<<endl;
	}
	
	
	
	
	return 0;
}

相關文章