阅读提示
由于 dp 转移式分类情况较多,每个类别本人都单独写出了转移式,写代码时要整合一下。
思路
我们要算的是 ∑ A A ⊂ S ∑ i i ∈ T i \sum_{A}^{A\subset S}\sum_{i}^{i\in T}i ∑AA⊂S∑ii∈Ti( S S S 是包含所有操作的集合, T T T 是做完选中的操作 A A A 后的可重集合)。这种表达方式十分抽象,但是这两个求和符号给了我们两种考虑的角度。
以 A 集合(操作)为主
由于每个操作可以选中也可以不选中,所以容易想到以 d p i dp_i dpi 来表示考虑到第 i i i 个操作, f ( A ) f(A) f(A) 的和(答案)。删除操作代表着无论到哪个状态都必须知道目前的最小值,这也就意味着每做一个操作都需要记录,因为不知道什么时候可能会用到当前加入的数。这样 d p i dp_i dpi 就需要增加很多维度,几乎算是要把整个 T T T 集合记录在里面,不可行。
以 T 集合(数字)为主
谁说这两个 ∑ \sum ∑ 不能调换一下顺序?我们先枚举出现过的每个数,计算这个数会在做完多少种操作序列 A A A 后存在于 T T T 集合,对答案进行贡献。这样有了针对的对象,某个数 a i a_i ai,就不用记录所有数了,只要保证 a i a_i ai 被加入了并且没被删掉。 d p j , k dp_{j,k} dpj,k 表示考虑到第 j j j 个操作, a i a_i ai 目前排在从小到大第 k k k 名,有多少种选择操作的方案。我们先对所有状态进行分类:
还没到第 i i i 个操作( j < i j<i j<i), a i a_i ai 未被加入。
此时无论加减都无所谓,不影响 a i a_i ai,为了方便 a i a_i ai 后来加入, k k k 表示 a i a_i ai 如果现在加入,会排在第几名。
- 删除操作
如果删除了一个数那么 k k k 就会减 1 1 1。要是 k = 1 k=1 k=1 了,还能删吗?当然能, a i a_i ai 都没加入,删了, a i a_i ai 的预估排名还是 1 1 1。 k = 1 k=1 k=1 也代表着开始删 > a i >a_i >ai 的数或者集合已经空了。转移式中 d p j − 1 , k dp_{j-1,k} dpj−1,k 表示不选中这个操作,对排名无影响。 d p j , k = { d p j − 1 , k + d p j − 1 , k k = 1 d p j − 1 , k + d p j − 1 , k + 1 k > 1 \\ dp_{j,k} = \begin{cases} dp_{j-1,k}+dp_{j-1,k} & k=1 \\ dp_{j-1,k}+dp_{j-1,k+1} & k>1 \\ \end{cases} dpj,k={dpj−1,k+dpj−1,kdpj−1,k+dpj−1,k+1k=1k>1 - 加入操作
如果加入了一个比 a i a_i ai 小的数 k k k 就会加 1 1 1,加入了一个比 a i a_i ai 大的 k k k 就没有变化。那如果加入了一个相等的呢?此时需要确定一种顺序,比如说按照下标,如果数值相等,下标越小,排得越靠前。如果不确定顺序,每次有相等的数,做删除操作时都尽量不删 a i a_i ai(每次的规则都在变),会重复算,答案变大。(转移式与 j > i j>i j>i 的加入操作一起写)
现在是第 i i i 个操作( j = i j=i j=i),必须选中,将 a i a_i ai 加入。
没什么好说的, d p j , k = d p j − 1 , k dp_{j,k}=dp_{j-1,k} dpj,k=dpj−1,k。
过了第 i i i 个操作( j > i j>i j>i), a i a_i ai 已经加入了。
- 删除操作
此时如果 k = 1 k=1 k=1 可就不能删了(只能从 k + 1 k+1 k+1 到 k k k, k + 1 > 1 k+1>1 k+1>1),不然 a i a_i ai 就出去了。
d p j , k = d p j − 1 , k + d p j − 1 , k + 1 dp_{j,k} = dp_{j-1,k}+dp_{j-1,k+1} dpj,k=dpj−1,k+dpj−1,k+1 - 加入操作
和 j < i j<i j<i 的情况相同,把转移式合在一起写了。 d p j , k = { d p j − 1 , k + d p j − 1 , k − 1 a j < a i 或 ( a j = a i 且 j < i ) d p j − 1 , k + d p j − 1 , k a j > a i 或 ( a j = a i 且 j > i ) \\ dp_{j,k} = \begin{cases} dp_{j-1,k}+dp_{j-1,k-1} & a_j<a_i 或 (a_j=a_i 且 j<i) \\ dp_{j-1,k}+dp_{j-1,k} & a_j>a_i 或 (a_j=a_i 且 j>i) \\ \end{cases} dpj,k={dpj−1,k+dpj−1,k−1dpj−1,k+dpj−1,kaj<ai或(aj=ai且j<i)aj>ai或(aj=ai且j>i)
初始状态和答案
d
p
0
,
1
=
1
dp_{0,1}=1
dp0,1=1:初始状态。没有做任何操作,
a
i
a_i
ai 如果加入就排在第一个。
∑
k
=
1
k
≤
n
d
p
n
,
k
\\ \sum_{k=1}^{k\le n}dp_{n,k}
∑k=1k≤ndpn,k:总方案数。只要
a
i
a_i
ai 还在里面,不论排名。
∑
k
=
1
k
≤
n
d
p
n
,
k
×
a
i
\\ \sum_{k=1}^{k\le n}dp_{n,k}\times a_i
∑k=1k≤ndpn,k×ai:
a
i
a_i
ai 对答案的贡献。
代码
经验与教训:
- k k k 的最大值是 j + 1 j+1 j+1,因为在 a i a_i ai 还没加入的时候,做了 j j j 次添加操作,而 a i a_i ai 恰好比它们都大,加入就会排在第 j + 1 j+1 j+1 个。
- 在 j < i j<i j<i 的时候,无论如何都可以删除,并且排名不变。上面转移的时候讲了,一开始本人没发现,错得很诡异。
#include<bits/stdc++.h>
using namespace std;
#define mod 998244353
int n,a[505],op[505];
char c;
long long dp[505][505],res,ans;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>c;
op[i]=(c=='+'?1:0);
if(op[i]) cin>>a[i];
}
for(int i=1;i<=n;i++){
if(!a[i]) continue;
memset(dp,0,sizeof(dp));
dp[0][1]=1;res=0;
for(int j=1;j<=n;j++)
for(int k=1;k<=j+1;k++){
dp[j][k]=dp[j-1][k];
if(i!=j&&op[j]==1){
if(a[j]<a[i]||a[j]==a[i]&&j<i)
dp[j][k]=(dp[j][k]+dp[j-1][k-1])%mod;
else dp[j][k]=(dp[j][k]+dp[j-1][k])%mod;
}
else if(i!=j&&op[j]==0){
dp[j][k]=(dp[j][k]+dp[j-1][k+1])%mod;
if(k==1&&j<i) dp[j][k]=(dp[j][k]+dp[j-1][k])%mod;
}
}
for(int j=1;j<=n;j++) res=(res+dp[n][j])%mod;
ans=(ans+res*a[i]%mod)%mod;
}
cout<<ans<<endl;
return 0;
}
此题代码调了很久,遂写文纪念,结果文章也写了很久,这分类讨论情况是真的多。[捂脸]