loj 6433 「PKUSC2018」最大前缀和 题解【DP】【枚举】【二进制】【排列组合】
这是个什么集合DP啊…
想过枚举断点但是不会处理接下来的问题了…
我好菜啊
题目描述
小 C 是一个算法竞赛爱好者,有一天小 C 遇到了一个非常难的问题:求一个序列的最大子段和。
但是小 C 并不会做这个题,于是小 C 决定把序列随机打乱,然后取序列的最大前缀和作为答案。
小 C 是一个非常有自知之明的人,他知道自己的算法完全不对,所以并不关心正确率,他只关心求出的解的期望值,现在请你帮他解决这个问题,由于答案可能非常复杂,所以你只需要输出答案乘上 \(n!\) 后对 \(998244353\) 取模的值,显然这是个整数。
注:最大前缀和的定义:\(\forall i \in [1,n],\sum_{j=1}^{i}a_j\) 的最大值。
输入格式
第一行一个正整数 \(n\),表示序列长度。
第二行 \(n\) 个数,表示原序列 \(a[1..n]\),第 \(i\) 个数表示 \(a[i]\) 。
输出格式
输出一个非负整数,表示答案。
样例输入
2 -1 2
样例输出
3
数据范围与约定
对于 \(10\%\) 的数据,有 \(1\leq n\leq 9\)。
对于 \(40\%\) 的数据,有 \(1\leq n\leq 15\)。
另有 \(10\%\) 的数据,满足 \(a\) 中最多只有一个负数。
另有 \(10\%\) 的数据,满足 \(|a[i]|\leq 2∣\)。
对于 \(100\%\) 的数据,满足 \(1\leq n\leq 20,\sum_{i=1}^{n}|a[i]|\leq 10^9\)。
题解
看到数据范围,这道题显然不是 \(O(n!)\),我们要把一个全排列转化为一个部分排列,对于部分排列统计一些答案,从而做到 \(O(2^n)\) 级别。
如果我们单独考虑对最大前缀和贡献,那么针对每个元素,我们无法判断它到底在最大前缀和中出现了几次。
这时我们需要研究最大前缀和的意义和等价条件。(被分割线隔开的主要是证明)
下文中 \(sum[l,r]\) 指 \(\sum_{i=l}^r a_i\),\(sum[S]\) 指 \(\sum_{i\in S}a_i\)。
对于一个序列,如果 \(sum[1,k]=\max_{1\le i\le n} \{sum[1,i]\}\) ,那么说明对于任意 \(j>k\),都不存在 \(sum[1,j]>sum[1,k]\)。由此推得不存在 \(sum[k+1,j]>0\)。
同时,对于任意 \(1\le j<k\),都不存在 \(sum[1,j]>sum[1,k]\),说明 \(sum[1,k]\) 是 \(a[1,k]\) 的最大前缀和。
这时我们用归纳法来推导它的性质。
已知 \(sum[i,k]\) 是 \(a[i,k]\) 的最大前缀和。
因此对于任意 \(i\le j\le k\),已经满足 \(sum[i,j]\le sum[i,k]\)。
设 \(S_1=\{sum[i,i],sum[i,i+1],\cdots,sum[i,k]\}\),那么 \(\forall x\in S_1\) 都有 \(x\le sum[i,k]\)。
设 \(S_2=\{sum[i-1,i-1],sum[i-1,i],\cdots,sum[i-1,k]\}\),显然 \(|S_1|+1=|S_2|\)。
而 \(S_2\) 的后面 \(|S_2|-1\) 项都是 \(S_1\) 的相应项 \(+a_{i-1}\),因此仍然满足 \(x\le sum[i,k]+a_{i-1}=sum[i-1,k]\)。
只需要第一项 \(sum[i-1,i-1]\) 满足 \(\le sum[i-1,k]\) 的条件就可以了。
所以 \(sum[1,k]\) 是最大前缀和的条件是(此处认为区间左右端点可以相等)
- \(\forall i\in[1,k],a_i\le sum[i,k]\),
- \(\forall i\in(k,n],sum[k+1,i]<0\)。
后者要求小于 \(0\) 而不是小于等于,原因在于当 \(i\in(k,n]\) 中多次出现 \(sum[k+1,i]=0\) 的情况时,进行了冗余的计算。实际上这一步计算会在后面那些元素被枚举到自己位置时算出来并做出贡献。
考虑用 \(f[S]\) 表示选出 \(S\) 这个集合中的元素,他们的全排列中满足条件 1 的排列个数。
排列可以用二进制集合枚举表示出来。
我们枚举每个集合 \(S\) 和元素 \(i\)。当 \(i\notin S\) 时我们就把 \(i\) 插到 \(S\) 的最前面,此时认为集合 \(S\) 无序。如果满足 \(a_i\le a_i+sum[S]\),则令 \(f[S]=f[S]+f[S\cup\{i\}]\)。
为什么说把 \(i\) 插到 \(S\) 前面呢?因为对第一个式子的归纳意义,后面的一定满足条件,只需要考虑新插入进来的元素是否满足。
那这样实际上我们也是遍历了所有排列。因为 \(S\) 的构造过程比较有趣:
设 \(S=\tt 1111\),它可以从 \(T_1=\mathtt{0111},T_2=\mathtt{1011},T_3=\mathtt{1101},T_4=\mathtt{1110}\) 转移过来。
我们特殊考虑 \(T_1\),假定 \(T_1\) 已经表示了 \(\tt 0111\) 的全排列,那么从 \(T_1\) 转移到 \(S\) 的就只有第一个元素在 \(S\) 的第一个位置这种情况了。
而第一个元素不在 \(S\) 的第一个位置时,一定在 \(T_2,T_3,T_4\) 枚举到了。
所以 \(S\) 遍历了 \(\tt 1111\) 的全排列。
同理,用 \(g[S]\) 表示选出 \(S\) 这个集合的元素,这些元素的全排列满足条件 \(2\) 的个数。
最后我们也不需要枚举断点,只需要枚举集合,毕竟集合代表了所有的排列。
答案就是
以上除了式子的判断条件,均在对 \(998,244,353\) 取模意义下进行。
时间复杂度 \(O(n\cdot 2^n)\)
Code:
#include<cstdio>
#include<cstring>
#define p 998244353
int a[20],f[1<<20],g[1<<20],sum[1<<20];
int Plus(int x,int y){return x+y>=p?x+y-p:x+y;}
int Mul(int x,int y){return 1ll*x*y%p;}
int main()
{
int n,U;
scanf("%d",&n);
U=(1<<n)-1;
for(int i=0;i<n;++i)
{
scanf("%d",&a[i]);
f[1<<i]=1;
if(a[i]<0)
g[1<<i]=1;
}
for(int i=1;i<=U;++i)
for(int j=0;j<n;++j)
if(i&(1<<j))
sum[i]+=a[j];
for(int i=1;i<=U;++i)
for(int j=0;j<n;++j)
if(!(i&(1<<j)))
{
if(sum[i]+a[j]>=a[j])
f[i|(1<<j)]=Plus(f[i|(1<<j)],f[i]);
if(sum[i]+a[j]<0)
g[i|(1<<j)]=Plus(g[i|(1<<j)],g[i]);
}
g[0]=1;
int ans=0;
for(int i=1;i<=U;++i)
ans=Plus(ans,Mul(Mul(p+sum[i],f[i]),g[U^i]));
printf("%d\n",ans);
return 0;
}