F.小苯的糖果游戏
题目描述
小苯和格格正在玩一款名为“吃糖果”的游戏,游戏的过程是这样的:
两人面前有 n n n 堆糖果,从左到右编号从 1 到 n n n,其中第 i i i 堆糖果中有 a i a_i ai 颗糖果。初始时小苯在 0 号位置(第一堆糖果的左侧),格格在 n + 1 n+1 n+1 号位置。(第 n n n 堆糖果的右侧)
假设小苯目前位于 i i i 号糖果堆的位置,格格位于 j j j 号糖果堆( i < j − 1 i < j-1 i<j−1),则每秒钟都会发生如下过程:
- 如果 i = j − 1 i = j - 1 i=j−1(即两人已经相邻),则游戏结束。
- 否则,两人轮流操作,小苯先手:
- 轮到小苯时,小苯可以选择待在原地;或跳跃到 i + 1 i+1 i+1 号糖果堆,然后选择吃掉所有 a i + 1 a_{i+1} ai+1 颗糖果,或者不吃。
- 轮到格格时,格格可以选择待在原地;或跳跃到 j − 1 j-1 j−1 号糖果堆,然后选择吃掉所有 a j − 1 a_{j-1} aj−1 颗糖果,或者不吃。
(当然,吃掉某堆糖果的前提是此堆糖果之前没被吃掉。换句话说每堆糖果被任何人吃掉后都会消失。)
现在我们考虑所有结束的游戏,为了公平起见,小苯希望自己和格格吃掉的糖果数量相同,他想知道有多少种不同的游戏过程能满足他的要求,请你帮他算一算吧。
(游戏过程的不同性在下方备注处给出了定义。)
输入描述:
本题含有多组测试数据。
第一行一个正整数
T
(
1
≤
T
≤
100
)
T\ (1 \leq T \leq 100)
T (1≤T≤100),表示测试数据的组数。
接下来对于每组测试数据,输入包含两行。
第一行一个正整数
n
(
1
≤
n
≤
100
)
n\ (1 \leq n \leq 100)
n (1≤n≤100),表示糖果的总堆数。
第二行
n
n
n 个正整数
a
i
(
1
≤
a
i
≤
100
)
a_i\ (1 \leq a_i \leq 100)
ai (1≤ai≤100),表示每堆糖果的总个数。
输出描述:
对于每组测试数据,输出一行一个整数,表示不同的游戏过程数。
(由于结果可能很大,因此你只要输出答案对
998244353
998244353
998244353 取模的值即可。)
示例1
输入
2 3 1 2 1 6 1 3 1 1 2 1
输出
6 41
提示
说明
考虑第一组测试数据,最终结束时满足小苯要求的,不同的游戏过程有:
小苯位于 i = 0 i=0 i=0,格格位于 j = 1 j=1 j=1,两人都没有吃糖果。
小苯位于 i = 1 i=1 i=1,格格位于 j = 2 j=2 j=2,两人都没有吃糖果。
小苯位于 i = 2 i=2 i=2,格格位于 j = 3 j=3 j=3,两人都没有吃糖果。
小苯位于 i = 3 i=3 i=3,格格位于 j = 4 j=4 j=4,两人都没有吃糖果。
小苯位于 i = 1 i=1 i=1,格格位于 j = 2 j=2 j=2,小苯选择吃掉的糖果编号集合为: { 1 } \{1\} {1},格格选择吃掉的糖果编号集合为 { 3 } \{3\} {3}。
小苯位于 i = 2 i=2 i=2,格格位于 j = 3 j=3 j=3,小苯选择吃掉的糖果编号集合为: { 1 } \{1\} {1},格格选择吃掉的糖果编号集合为 { 3 } \{3\} {3}。
备注:
考虑两个游戏过程:
游戏过程 1: 结束时,小苯位于 p o s 1 pos_1 pos1,小苯选择拿走的糖果堆的编号集合为 S 1 S_1 S1,格格选择拿走的糖果堆编号集合为 S 2 S_2 S2。
游戏过程 2: 结束时,小苯位于 p o s 2 pos_2 pos2,格格位于 p o s 2 pos_2 pos2,小苯选择拿走的糖果堆的编号集合为 S 3 S_3 S3,格格选择拿走的糖果堆编号集合为 S 4 S_4 S4。
定义两个游戏过程相同,当且仅当: p o s 1 = p o s 2 pos_1=pos_2 pos1=pos2,同时 S 1 = S 3 S_1=S_3 S1=S3 且 S 2 = S 4 S_2=S4 S2=S4。
否则两个游戏过程不相同。
示例代码
const int N = 1e2 + 5;
const int NN = 1e4 + 5;
const int M = 998244353;
int a[N], f1[N][NN], f2[N][NN];
void solve()
{
int n; cin >> n;
int s = 0;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
s += a[i];
}
memset(f1, 0, sizeof(f1));
memset(f2, 0, sizeof(f2));
f1[0][0] = 1;
for (int i = 1; i <= n; i++)
{// 遍历堆
for (int j = 0; j <= s; j++)
{
f1[i][j] = f1[i - 1][j]; // 不选第 i 堆糖果
if (j >= a[i])
{
f1[i][j] = (f1[i][j] + f1[i - 1][j - a[i]]) % M;
}
}
}
// f2为后缀dp
f2[n + 1][0] = 1;
for (int i = n; i >= 1; i--)
{
for (int j = 0; j <= s; j++)
{
f2[i][j] = f2[i + 1][j];
if (j >= a[i])
{
f2[i][j] = (f2[i][j] + f2[i + 1][j - a[i]]) % M;
}
}
}
int ans = 0;
for (int i = 0; i <= n; i++)
{
for (int j = 0; j <= s; j++)
{
ans = (ans + f1[i][j] * f2[i + 1][j] % M) % M;
}
}
cout << ans << endl;
}
其实我们是能够将 f 2 f2 f2的优化为滚动数组的,因为我们在算答案的时候只需要知道当前层的信息即可
f2[0] = 1; // 注意此处数组底标的意义
for (int i = n; i >= 1; i--)
{
for (int j = s; j >= a[i]; j--)
{
f2[j] = (f2[j] + f2[j - a[i]]) % M;
}
for (int j = 0; j <= s; j++)
{
ans = (ans + f1[i - 1][j] * f2[j] % M) % M;
}
}
ans = (ans + f1[n][0]) % M; // 一定要加上这一行,上面的循环是没有处理格格在原地的情况的,实际上就是 +1