P5369 [PKUSC2018]最大前缀和
题意
给定一个序列 \(a\),求 \(a\) 的所有排列的最大前缀和的和。
\(1\le n\le 20\)。
Solution
考虑到 \(n\) 很小的性质,想到状压。
先考虑一手,最大前缀和应该满足什么条件。一段前缀 \([1,i]\) 为最大前缀和,当且仅当,在 \([1,i]\) 的任意一段后缀都不为负,并且 \([i+1,n]\) 的任意一段前缀都是负的。
止步于此。实际上好好利用这个性质就可以轻松设计状态了。
\(\bigstar\) 我们把最后的答案拆成某一段最大前缀和,和除了这个前缀和外的后缀。那么前缀满足最大前缀和等于这个前缀中所有数的和,而后缀满足最大前缀和为负。
我们令 \(sum_S\) 表示集合 \(S\) 中的数的和,\(f_S\) 表示对集合 \(S\) 中的数进行排列后最大前缀和等于 \(sum_S\) 的方案数,\(g_S\) 表示对 \(S\) 中的数进行排列后最大前缀和为负的方案数。
考虑转移。先考虑 \(g\) 的转移,随便挑一个数放到最后,这样前面整段仍然需要满足最大前缀和为负,然后最后那个数需要满足它的权值加上前面整段的权值仍然为负即可。
然后考虑 \(f\)。考虑刷表。如果当前的和是负数,那么肯定不能通过在前面加某一个数来转移到更大的集合。所以能转移时和一定是正数。如果是正数,那么在前面加上任何数都可以转移。这样就可以通过刷表的方式转移 \(f\) 了。
最终答案应该是:
\[\sum_{S}sum_S\times f_S\times g_{\overline{S}}
\]
Code
// Problem:
// P5369 [PKUSC2018]最大前缀和
//
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P5369
// Memory Limit: 500 MB
// Time Limit: 2000 ms
#include<bits/stdc++.h>
#define ll long long
#define inf (1<<30)
#define INF (1ll<<60)
#define pb emplace_back
#define pii pair<int,int>
#define mkp make_pair
#define fi first
#define se second
#define all(a) a.begin(),a.end()
#define siz(a) (int)a.size()
#define clr(a) memset(a,0,sizeof(a))
#define rep(i,j,k) for(int i=(j);i<=(k);i++)
#define per(i,j,k) for(int i=(j);i>=(k);i--)
#define pt(a) cerr<<#a<<'='<<a<<' '
#define pts(a) cerr<<#a<<'='<<a<<'\n'
#define int long long
using namespace std;
const int MAXN=(1<<20)+10;
const int MOD=998244353;
int lbt(int x){return x&(-x);}
int a[22],f[MAXN],g[MAXN],sum[MAXN];
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
int n;cin>>n;
rep(i,1,n) cin>>a[i];
int lmt=(1<<n)-1;
rep(i,1,lmt)
sum[i]=sum[i-lbt(i)]+a[__lg(lbt(i))+1];
g[0]=f[0]=1;
rep(i,0,lmt){
if(i!=0){
rep(j,1,n) if((i>>(j-1))&1){
if(a[j]+sum[i-(1<<(j-1))]<0)
g[i]=(g[i]+g[i-(1<<(j-1))])%MOD;
}
}if(i!=lmt){
if(sum[i]>=0)
rep(j,1,n) if(!((i>>(j-1))&1)){
(f[i+(1<<(j-1))]+=f[i])%=MOD;
}
}
}
int ans=0;
rep(i,0,lmt) ans=(ans+sum[i]*f[i]%MOD*g[lmt^i]%MOD)%MOD;
cout<<(ans%MOD+MOD)%MOD<<'\n';
return 0;
}