【题解】P5369 [PKUSC2018]最大前缀和(状压 DP)

【题解】P5369 [PKUSC2018]最大前缀和

我是个因为题意挂了一天的傻逼……

实际上这是道很水的状压 DP(???)


题目链接

P5369 [PKUSC2018]最大前缀和 - 洛谷

题意概述

给定一个长度为 \(n\) 的序列 \(a\),求这个序列的所有排列的最大前缀和之和 \(\bmod~ 998244353\) 的结果。

注意:是最大前缀和之和而不是方案数之和。

我当时因为题目问的特别绕于是成功理解错题意 \(5h\)

思路分析

首先可以发现一个显然但是很关键的结论:

如果一个位置 \(k\) 可以成为最大前缀和(若有多个,则取最右端的位置),当且仅当 \(\sum \limits_{i=1}^k a_i >=0\)\(\sum \limits_{i=k+1}^n a_i<0\)

也就是说,我们现在只需要确定位置 \(\le k\) 有哪些数(前缀),\(>k\) 有哪些数(后缀)即可。

然后我们观察数据范围:\(1 \le n \le 20\)。自然而然可以考虑到状压。

定义 \(dp1_{sta}\) 表示选了 \(sta\) 这个状态的数作为前缀,剩下的数作为后缀的方案数;

定义 \(dp2_{sta}\) 表示选了 \(sta\) 这个状态的数作为后缀,剩下的数作为前缀的方案数。

其中 \(sta\) 表示每个数选了没选,若选,则二进制下这个数对应的这一位为 \(1\),反之为 \(0\)

那么对于每一位 \(k\) 作为最大前缀和的位置时,每一种选择 \(sta|(1<<k)\) 作为前缀的情况(至于这里为什么是 \(sta|(1<<k)\) 而非 \(sta\),后面会有解释),对答案总的贡献就为:

\[dp1_{sta}\times dp2_{sta|(1<<k) \oplus ((1<<n)-1)} \times sum_{sta|(1<<k)} \]

解释:

  1. 我们在枚举 \(sta\) 时,要保证 \(sta\) 二进制下第 \(k\) 位为 \(0\)

    之所以这样枚举,是因为我们每次枚举最大前缀和位置时,这一位已经确定了,所以如果我们直接让 \(sta\) 二进制下第 \(k\) 位为 \(1\),而此时的 \(dp1_{sta}\) 表示的状态中 \(k\) 这一位并不确定,所以可能会算重,所以我们要钦定 \(sta\) 二进制下第 \(k\) 位为 \(0\)

  2. 一个数异或 \((1<<n)-1\) 的结果是将这个数二进制下 \(\le n\) 的位取反,而对于此题中的 \(sta|(1<<k)\) 一定是小于 \((1<<n)\) 的,所以就相当于将 \(sta\) 二进制下所有位取反,而取反后的结果就是对于每一个前缀 \(sta\) 作为后缀的状态。

  3. 根据乘法原理\(dp1_{sta} \times dp2_{sta|(1<<k)}\) 的结果就是以 \(sta|(1<<k)\) 为前缀的方案数。那么乘上 \(sum_{sta|(1<<k)}\) 得到的结果就是对答案的贡献。

现在我们考虑如何转移。

假设一个状态 \(sta\) 二进制下有 \(t\)\(1\),那么它一定可以由有 \(t-1\)\(1\) 的状态转移得到。如果写成正推,那么有:

\[dp1_{sta|(1<<i)}=dp1_{sta|(1<<i)}+dp1_{sta};\\ dp2_{sta|(1<<i)}=dp2_{sta|(1<<i)}+dp2_{sta}; \]

最后我们要预处理出来每一个状态作为前缀时最大前缀和,具体做法:

对于每一个 \(sta\) 二进制下为 \(1\) 的位 \(i\) 都给 \(sum_{sta}\) 加上 \(a_i\) 即可。

然后我们就可以愉快的转移啦。

易错点

由于 \(a_i\) 可能为负,所以 \(sum\) 有可能为负,所以最后要对答案进行负数保护,即:(+mod)%mod。

代码实现

//luoguP5369
#include<cstdio>
#include<iostream>
#define int long long
using namespace std;
const int maxn=25;
const int mod=998244353;
int a[maxn],s[1<<maxn],dp1[1<<maxn],dp2[1<<maxn];
int n,ans;

inline int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}

signed main()
{
	n=read();
	for(int i=0;i<n;i++)a[i]=read();
	dp1[0]=dp2[0]=1;
	for(int sta=0;sta<(1<<n);sta++)
	{
		for(int i=0;i<n;i++)
		{
			if((sta>>i)&1)(s[sta]+=a[i])%=mod;
		}
	}
	for(int sta=0;sta<(1<<n);sta++)
	{
		for(int i=0;i<n;i++)
		{
			if((sta>>i)&1)continue;
			if(s[sta]+a[i]>=0)(dp1[sta|(1<<i)]+=dp1[sta])%=mod;
			else (dp2[sta|(1<<i)]+=dp2[sta])%=mod;
		}
	}
	for(int i=0;i<n;i++)//枚举前缀最大值的位置。 
	{
		for(int sta=0;sta<(1<<n);sta++)
		{
			if((sta>>i)&1)continue;
			int nxt=((1<<n)-1)^(sta|(1<<i));
			(ans+=dp1[sta]*dp2[nxt]%mod*s[sta|(1<<i)]%mod)%=mod;
		}
	}
	cout<<(ans+mod)%mod<<'\n';
	return 0;
}

大概算是,复习(重开)状压 DP 后的第一题?

感谢阅读。

posted @ 2022-07-30 17:42  向日葵Reta  阅读(96)  评论(0)    收藏  举报