【PKUSC2018】【状压dp】【计数dp】最大前缀和

6 篇文章 0 订阅
5 篇文章 0 订阅

【描述】
小 C 是一个算法竞赛爱好者,有一天小 C 遇到了一个非常难的问题:求一个序列的最大子段和。
但是小 C 并不会做这个题,于是小 C 决定把序列随机打乱,然后取序列的最大前缀和作为答案。
小 C 是一个非常有自知之明的人,他知道自己的算法完全不对,所以并不关心正确率,他只关心求出的解的期望值,现在请你帮他解决这个问题,由于答案可能非常复杂,所以你只需要输出答案乘上 n! 后对 998244353 取模的值,显然这是个整数。
注:最大前缀和的定义: ∀ i ∈ [ 1 , n ] , ∑ j = 1 i a j ∀i∈[1,n],∑^i_{j=1}a_j i[1,n]j=1iaj的最大值。
数据规模: n < = 20 , ∑ i = 1 n ∣ a i ∣ < 1 e 9 n<=20,\sum_{i=1}^n |a_i|<1e9 n<=20,i=1nai<1e9

【思路】

首先,我们先考虑一个最大前缀和的必要条件:设最大的位置在i,那么 ∀ k ∈ [ i + 1 , n ] , ∑ j = i + 1 k a j < 0 总 是 成 立 ∀k∈[i+1,n],∑^k_{j=i+1}a_j<0总是成立 k[i+1,n]j=i+1kaj<0。这给我们提供了一个思路,如果我们保证i处的前缀和是前i个位置里最大的,将序列从i,i+1处断开,后面部分的序列的前缀和总是小于等于0,即可保证i是最大的前缀和。剩下的便是一个计数问题,题目所求即所有排列最大前缀和之和。由于我们把序列分为了两部分,转移时我们需要知道某个数是否在其中一个序列中,所以考虑状压dp。记 f [ s ] f[s] f[s]表示 i ∈ s , a i i∈s,a_i is,ai构成的序列且最大前缀和在第 ∣ s ∣ |s| s位的方案数,即前半部分序列的信息。 g [ s ] g[s] g[s]表示 i ∈ s , a i i∈s,a_i is,ai构成的序列且每个位置的前缀和均小于等于0。为方便转移,记 s u m [ s ] sum[s] sum[s] ∑ i ∈ s a i \sum_{i∈s} a_i isai。g的转移很显然,我们考虑从前到后依次填数,保证每次填以后的状态小于等于0即可,即:
g [ s ∪ i ] = ∑ s u m s ∪ i ≤ 0 g [ s ] g[s \cup i]= \sum_{sum_{s \cup i}\le0} g[s] g[si]=sumsi0g[s]
f的转移很头疼,因为我们要确保当前位置的前缀和最大。我们考虑倒着填数:
f [ s ∪ i ] = ∑ s u m s > 0 f [ s ] f[s\cup i]=\sum_{sum_s>0}f[s] f[si]=sums>0f[s]
只要每次填之前的sum大于0,不管这一位填什么,后面的sum加上这个数构成的前缀和一定是最大的。注意,由于一个种排列可能有多个位置满足前缀和最大,所以f和g的关于sum的转移条件只能有一个取等,否则会重复计算贡献。答案由我们最开始的思路可得: a n s = ∑ s ∈ U s u m [ s ] ∗ f [ s ] ∗ g [ ans=\sum_{s∈U}sum[s]*f[s]*g[ ans=sUsum[s]f[s]g[U^s ] ] ]
代码:

#include<bits/stdc++.h>
#define re register
using namespace std;
const int N=1.1e6+5,mod=998244353;
inline int red(){
    int data=0;bool w=0; char ch=getchar();
    while(ch!='-' && (ch<'0' || ch>'9')) ch=getchar();
    if(ch=='-') w=1,ch=getchar();
    while(ch>='0' && ch<='9') data=(data<<3)+(data<<1)+ch-'0',ch=getchar();
    return w?-data:data;
}
int f[N]={1},sum[N],g[N]={1},U,ans=0,n;
inline void Add(int&a,const int&b){((a+=b)>=mod)&&(a-=mod);}
inline int dec(const int&a,const int&b){return a<b?a-b+mod:a-b;}
inline int mul(const int&a,const int&b){return 1ll*a*b%mod;}
int main(){n=red();U=1<<n;
	for(int re i=0;i^n;++i)sum[1<<i]=red(),f[1<<i]=1;
	for(int re i=1;i^U;++i)sum[i]=sum[i^(i&(-i))]+sum[i&(-i)];
	for(int re i=1;i^U;++i)if(sum[i]>0)
		for(int re j=0;j^n;j++)
			if(!(i&(1<<j)))Add(f[i|(1<<j)],f[i]);
	for(int re i=1;i^U;++i)if(sum[i]<=0)
		for(int re j=0;j^n;++j)
			if((i&(1<<j)))Add(g[i],g[i^(1<<j)]);
	for(int re i=1;i^U;++i)
		Add(ans,mul(dec(sum[i]%mod,0),mul(f[i],g[(U-1)^i])));
	cout<<ans<<"\n";
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值