F. Knapsack for All Segments
题目:
给了长度为 N 的数组 A 和一个正数 S。定义 f(L,R),1≤ L ≤ R ≤ N 为合法序列 (,
,...
) 的方案数:
L ≤ <
< ⋯ <
≤ R,
+
+ ⋯ +
= S
求 Σf(L,R) mod 998244353
数据范围:
1 ≤ N, S , ≤ 3000
思路:
简单说就是f(L,R)为 X[i] 下标区间(L,R) 中部分数求和为S的数量。因为是部分数,也就是说可以不是区间内所有数的和加起来为s。所以下面所说的满足条件的区间其实不是原本意义的区间内所有数的和为j,而是区间内的部分数的和为j,所选的部分数中最左边的数就是要累计的左端点。
根据数据范围,可以接受O(),考虑dp,最直接的就是用 f[l][r][x] 代表区间 [l,r] 和位 [x] 的方案数,需要优化。
考虑当区间 [l,r]满足时(指在区间内选择左右端点l,r,以及区间[l,r]内的部分数,满足和为s),对答案的贡献是 (n-r+1) * l,即对所有包含区间[l,r]的区间都有 1 次贡献,总共这么多区间。根据该式,我们考虑去掉一维,将答案状态空间按照要选取的正好满足条件的序列最右端点为不同的 r ,进行不重不漏的划分。状态值改为记录贡献相关数值的形式来计算答案。
对于枚举的右端点 r 的下标 1~n 确定时,要求出右端点为r的满足条件的区间的贡献时,需要找区间的左端点下标,才可以套公式。因为是按右端点枚举的,所以可能有多个左端点l满足,算贡献时就将所有满足的左端点l与右端点r的区间的贡献全算上,之后再下一个 r。于是乎,代入公式 (n-r+1) * l 时,r 还是一一枚举的 r,而 l 是所有左端点的和。
那么如何求出满足条件的左端点的和呢?点 r 之前的和为 s-a[r] 的点以及右端点 r 就构成了满足条件的区间,该区间的左端点就是和为 s-a[r] 的点中最左边的点 l。而我们要找的正是这个 l,通过dp实现,因为右端点是枚举的,r在变,s-a[r] 也在变,所以集合表示为f[i][j]——
状态表示:集合f[i][j]表示前 i 项中,和为 j 的最左端点的下标的和。要使用公式时,需要找到区间[l,r],根据枚举的每一个右端点 i,对应的左端点f[i-1][s-a[i]](其实是所有左端点的和),正是满足条件的区间,代入公式累加。所以最终答案为:
状态计算:根据第 i 项 a[i] 与 j 的大小关系,判断以第 i 项是否可以划到区间内为依据划分为3类:
1.如果a[i] < j。说明对于前 i 个数满足和为 j 的区间可以包括第 i 项,也可以不包括第 i 项:
(1)如果选上第 i 项:所有前 i-1 项中和为 j-a[i] 的区间再算上第 i 项,正好和为 j,所有这样区间的最左端点和为 f[i-1][j-a[i]];
(2)如果不选第 i 项:等价于从前 i-1 项选和为 j 的区间,所有区间的最左端点之和为 f[i-1][j]。
前 i 个数中和为 j 的区间的最左端点之和为:f[i][j] = f[i-1][j-a[i]] + f[i-1][j]。
这里用+是因为这是两种情况,也就是两类满足条件的区间,一类区间是包含第 i 项且 i 正是所有满足的区间的右端点;另一类区间是不包含第 i 项。最左端点的和是不一样的。
2.如果a[i] == j。说明前 i 项已经有一个确定的区间[i,i],算上这个最左端点 i,同时还有前 i-1 项满足和为 j 的区间的最左端点之和 f[i-1][j]。即:f[i][j] = i + f[i-1][j]
3.如果a[i] > j。说明前i项满足和为j的区间的最左端点不可能包括第 i 项,等价于直接从前 i-1 项找。即:f[i][j] = f[i-1][j]
时间复杂度:O(s * n)
Code:
#include<iostream>
#include<algorithm>
#include<vector>
#include<map>
#include<cstring>
#include<math.h>
using namespace std;
//#define x first
//#define y second
#define IOS ios::sync_with_stdio(false);cin.tie(0);
#define int long long
typedef pair<int, int>PII;
const int N = 3010, mod = 998244353;
int n, s;
int a[N];
int f[N][N];
void solve()
{
cin >> n >> s;
for (int i = 1; i <= n; i++)cin >> a[i];
int ans = 0;
for (int i = 1; i <= n; i++) //枚举右端点(不一定必选的)
{
//每次枚举到的i,作为区间右端点,找到满足条件的左端点套公式计算区间贡献
if (a[i] < s)ans = (ans + f[i - 1][s - a[i]] * (n - i + 1)) % mod; //如果a[i]<s,这里算的是右端点为i,左端点在i之前的区间贡献
else if (a[i] == s)ans = (ans + i * (n - i + 1)) % mod; //如果a[i]==s,右端点为i的有贡献的区间只有[i,i],左端点为i
//状态转移
for (int j = 0; j <= s; j++) //枚举前i项的和j(因为是二维的,从小到大/从大到小枚举都可)
{
f[i][j] = f[i - 1][j]; //根据状态转移方程,无论a[i]与k的大小关系,都包含不把第i项放在区间里的情况
if (a[i] < j) //如果a[i]<j,算上以第i项为满足条件的区间的右端点的最左端点
f[i][j] = (f[i][j] + f[i - 1][j - a[i]]) % mod;
else if (a[i] == j) //如果a[i]==j,算上区间[i,i]的最左端点i
f[i][j] = (i + f[i][j]) % mod;
}
}
cout << ans << endl;
}
signed main()
{
IOS;
int t = 1;
//int t;
//cin >> t;
while (t--)
{
solve();
}
return 0;
}
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
吐槽:这题真是我晕了,这dp真绕啊,怪不得叫非典型hh。本题注意点:
1.答案不是直接根据状态转移方程得出答案的,而是间接得出的,搞公式,搞dp,两再联合搞出答案的。所以不仅在循环里算状态转移方程,H还算区间贡献+-+。
2.状态转移那感觉也可绕,真是亿亿点细节需要考虑。