Description
小 C 是一个算法竞赛爱好者,有一天小 C 遇到了一个非常难的问题:求一个序列的最大子段和。
但是小 C 并不会做这个题,于是小 C 决定把序列随机打乱,然后取序列的最大前缀和作为答案。
小 C 是一个非常有自知之明的人,他知道自己的算法完全不对,所以并不关心正确率,他只关心求出的解的期望值,现在请你帮他解决这个问题,由于答案可能非常复杂,所以你只需要输出答案乘上 (n!) 后对 (998244353) 取模的值,显然这是个整数。
注:最大前缀和的定义:(forall i in [1,n],sum_{j=1}^{i}a_j)的最大值。
Input
第一行一个正整数 (n),表示序列长度。
第二行 (n) 个数,表示原序列 (a[1..n]),第 (i) 个数表示 (a[i]) 。
Output
输出一个非负整数,表示答案。
Solution
据说是去年的签到题啊。
因为是最大前缀和,设(sum_{i=1}^p a_i) 为 (a[1..n])的最大前缀和,则该序列满足以下性质
-
(forall iin [2,p] ,sum_{j=i}^p a_j >0),不然的话你完全可以找到一个比(p)更靠前的位置来得到更大的前缀和。
-
(forall iin [p+1,n] ,sum_{j=p+1}^i a_j <=0),同理,如果不满足,你可以找到一个比(p)更靠后的位置来得到更大的前缀和。
考虑(dp),其中(sum_i=sum_{j in i} a[j])
令(F_i)表示取(i)这个集合的数,满足最大前缀和等于(sum_i),因为性质1,所以我们考虑从(p)开始倒着取数,有以下转移方程
[F_{i igcup {j}} = sum_{j
otin i } F_{i}, sum_i >0
]
(G_i)表示取(i)这个集合的数,满足性质2,
[G_{i igcup {j}} = sum_{j
otin i } G_{i} ,sum_{i igcup {j}} leq 0
]
Code
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int Mod=998244353;
int n,a[50],G[2000010],F[2000010],sum[2000010];
int main()
{
scanf("%d",&n);
for (int i=0;i<n;i++) scanf("%d",&a[i]);
for (int i=0;i<(1<<n);i++)
for (int j=0;j<n;j++)
if ((i>>j) & 1) sum[i]=(sum[i]+a[j])%Mod;
for (int i=0;i<n;i++)
F[1<<i]=1;
for (int i=0;i<(1<<n);i++)
for (int j=0;j<n;j++)
{
if ((i>>j) & 1) continue;
if (sum[i]>0) F[i|(1<<j)]=(F[i|(1<<j)]+F[i])%Mod;
}
G[0]=1;
for (int i=0;i<(1<<n);i++)
for (int j=0;j<n;j++)
{
if ((i>>j) & 1) continue;
if (sum[i|(1<<j)]<=0) G[i|(1<<j)]=(G[i|(1<<j)]+G[i])%Mod;
}
int ans=0;
for (int i=0;i<(1<<n);i++)
ans=(ans+1LL*sum[i]*F[i]%Mod*((1<<n)-i-1?G[(1<<n)-i-1]:1)%Mod+Mod)%Mod;
printf("%d
",ans);
return 0;
}