【洛谷P5369】最大前缀和
题目
题目链接:https://www.luogu.com.cn/problem/P5369
小 C 是一个算法竞赛爱好者,有一天小 C 遇到了一个非常难的问题:求一个序列的最大子段和。
但是小 C 并不会做这个题,于是小 C 决定把序列随机打乱,然后取序列的最大前缀和作为答案。
小 C 是一个非常有自知之明的人,他知道自己的算法完全不对,所以并不关心正确率,他只关心求出的解的期望值,现在请你帮他解决这个问题,由于答案可能非常复杂,所以你只需要输出答案乘上 \(n!\) 后对 \(998244353\) 取模的值,显然这是个整数。
注:最大前缀和的定义:\(\forall i \in [1,n]\),\(\sum_{j=1}^{i}a_j\) 的最大值。
\(n\leq 20\)。
思路
记 \(sum[s]\) 表示集合 \(s\) 中所有元素之和。设 \(f[s]\) 表示集合为 \(s\) 时,最大前缀和恰好为 \(sum[s]\) 的方案数。\(g[s]\) 表示集合为 \(s\) 时,最大前缀和为 \(0\) 的方案数。
那么答案显然就是
\[\sum^{2^n-1}_{i=1}sum[i]\times f[i]\times g[2^n-1-i]
\]
接下来考虑转移。
如果 \(sum[s]\geq 0\),那么我们可以通过在集合 \(s\) 前加入一个数字 \(i\) 来转移到 \(f[s\ \rm xor\ i]\)。否则无法对 \(f[s\ \rm xor\ i]\) 产生贡献。
如果 \(sum[s]<0\),那么对于一个在 \(s\) 里的数 \(i\),\(g[s]\) 可以通过 \(g[s\ \rm xor\ i]\) 转移过来。
时间复杂度 \(O(2^nn)\)。
代码
#include <bits/stdc++.h>
using namespace std;
const int N=25,M=(1<<20),MOD=998244353;
int n,ans,lim,a[N],f[M],g[M],sum[M];
int main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
scanf("%d",&a[i]),f[1<<i-1]=1;
g[0]=1; lim=1<<n;
for (int s=1;s<lim;s++)
{
for (int i=1;i<=n;i++)
if (s&(1<<i-1)) sum[s]+=a[i];
if (sum[s]<0)
for (int i=1;i<=n;i++)
if (s&(1<<i-1)) g[s]=(g[s]+g[s^(1<<i-1)])%MOD;
if (sum[s]>=0)
for (int i=1;i<=n;i++)
if (!(s&(1<<i-1))) f[s|(1<<i-1)]=(f[s|(1<<i-1)]+f[s])%MOD;
}
for (int i=0;i<lim;i++)
ans=(ans+1LL*sum[i]%MOD*f[i]%MOD*g[(lim-1)^i])%MOD;
cout<<(ans%MOD+MOD)%MOD;
return 0;
}