一、先看原题:




二、理解题目:
想象一排 n 个小格子,每个格子里放一个数字 a[i](这是数组 A)。然后每个格子还有两个"标签":一个是 b[i],表示"这个位置的价值";另一个是 c[i],表示"如果这个位置最后保留了非零数,要把答案计上一个权重(乘积)c[i]"。
我们可以做一种"消除相邻"的操作:选择相邻的两个格子 i 和 i+1,如果左边的数不大于右边的数(a[i] ≤ a[i+1]) ,就把左边设为 0,把右边变成 a[i+1] - a[i]。也可以把这个操作重复很多次、在不同位置做很多次。最终每个格子要么是 0,要么是一个正数(剩余的值)。
把最后的这一排数字记作 D = [d1,...,dn](每个 d_i 要么 0,要么 >0)。题目定义两个值:
-
f(D):把所有d_i > 0的位置对应的b[i]加起来(简单:保留位置的 b 的总和); -
g(D):把所有d_i > 0的位置对应的c[i]相乘(简单:保留位置的 c 的乘积)。
现在问题是:对所有"可能得到的 D"(也就是从 A 经过若干次允许的操作能得到的所有最终序列),求:
-
最大的
f(D)(即找能保留最多 b 总价值的操作方案),记为max f; -
把所有能达到该最大
f的方案对应的g(D)求和,然后对1,000,000,007取模(注意max f本身不需要取模)。
我们要输出两项:max f 和 对应方案的 g 的和(模 1e9+7)。
三、直觉判断:
直接暴力枚举所有操作显然不行 ------ 状态太多。关键在于理解"经过若干次这种相邻消除的操作之后,哪些位置可能保留非零(即 d_i > 0)"以及这些保留位置之间有什么约束。下面用简单例子给出一些直觉:
-
操作只发生在相邻位置上,并且是"把左边变 0,右边变差值"。把它展开看,本质上等价于把一段连续子区间的数通过交替相减合并成一个数,或者把它们化掉(变成 0)。这让我们把问题看成"把数组 A 划分成若干段,在每段里决定一个'保留位置'或者全部变 0" 的动态规划风格问题。
-
更具体一点:当你在一段连续区间做这些交替消除时,最终能不能留下某个位置的正数,以及留下的值的符号(正还是零)取决于这段
a的交替和 (也就是a[l] - a[l+1] + a[l+2] - ...或者相反的交替)。代码中用t前缀和来记录这种"交替求和"的值。 -
另外,如果决定让一段里某个位置保留非零,那么那段里所有可能被"保留"的位置(根据奇偶)需要满足某些最小
b的代价会被扣除(大意是:保留哪个位置会影响能否把段里别的位置化掉,以及对应的 b 的贡献如何计算)。代码里通过按位置奇偶保存最小的b(mi[2][i][j])和相应的计数(ct[2][i][j])来快速获取这些信息。
这些观察看起来抽象,但程序就是把这些"局部性质"预处理好,然后用从左到右的动态规划(把前缀看成状态)来把答案拼起来。
四、算法步骤:
想象你有一排格子(1..n)。每个格子里有三样东西:
-
a[i]:代表格子里"沙包"的重量(我们可以做"相邻消除"的动作); -
b[i]:代表格子里的"奖励糖果数值"; -
c[i]:代表格子里的"旗子力量"(用于计算方案权重,最后要乘起来)。
我们要做一连串"把左边沙包放倒(变 0),右边变成差值"的消除,并在最后统计:剩下哪些格子(d_i>0)------把它们的糖果 b 加起来(这叫 f),并把它们的旗子 c 相乘(这叫 g)。我们要找能让糖果总和 f 最大的玩法数,并把这些玩法对应的 g(乘积)加起来。
1、前缀和 s 与交替前缀和 t ------ 把区间的信息"打包带走"
比喻:
-
s[i]就像你走到第 i 个格子时口袋里装的糖果总数(从格子 1 到 i 的b总和)。所以要知道区间(i+1..j)的糖果和,只要s[j] - s[i],就像从口袋里减去你之前装的糖果,剩下的就是这段的糖果。 -
t[i]是"交替的净沙包"。想像每个格子在你走过时,奇数格你拿走 (视作负),偶数格你放回 (视作正),于是t[j] - t[i]就等于区间(i+1..j)做交替加减后的"净剩余"。这个量告诉我们:如果我们对这段做交替相减的操作,最终会剩下正数(>0)、为 0,还是负数(<0)------不同结果会决定我们是否必须牺牲某个b(糖果)。
简单手算:
A = [3,5,6], B = [1,2,3]
-
s: s0=0, s1=1, s2=3, s3=6
-
t(按代码:奇数位 -a,偶数位 +a):
t0=0
t1 = 0 - 3 = -3
t2 = -3 + 5 = 2
t3 = 2 - 6 = -4
如果看区间 (1..3): t3 - t0 = -4 → 负,说明交替相减后"净剩余"是负的(会在奇数/偶哪边留下正数,后面说)。
2、预处理 v、ic、iv(与乘积有关)------把"整段旗子乘积"快速拿到
比喻:
把 c[i] 想成每个格子的"旗子力量";一整段的旗子总力是把这些小旗子都绑在一起的合力(乘起来)。我们常常需要快速得到区间 (i+1..j) 的旗子合力 c[i+1]*...*c[j]。把从头到第 k 的合力存在 v[k](前缀乘积),那区间合力就可以用 v[j] / v[i] 得到。在模运算里"除法"用逆元(iv)表示,所以区间合力就是 v[j] * iv[i]。
ic[i](Inv(c[i]))就是单个旗子的逆元,我们在统计"如果某个位置被牺牲(扣掉 b),那对应的乘积应该除以它的 c"时会用到 ic。
直观例子(数值,不取模,方便理解):
C = [2,3,5]
v1 = 2, v2 = 2*3=6, v3 = 30
区间 1..3 的乘积就是 v3 / v0 = 30。
如果要把整个区间乘积除去位置 3 的 c(也就是 prod / c3),那是 30 / 5 = 6;这也等于 v3 * ic3(把 ic3 理解为 1/5)。
3、预处理区间内按奇偶分组的最小 b 和计数 mi / ct ------ 谁是必须牺牲的小糖果?
比喻:
-
想像区间里有奇数座位和偶数座位两类,某种操作后只能在某一类留下正数。那如果该类里不全能保住糖果,最小的糖果 (最不值钱的)就是我们必须牺牲的(相当于"扣掉最小 b")。所以我们需要知道区间里奇位/偶位的最小糖果
mi[par][l][r]。 -
但如果最小糖果出现在好几格(比如两个位置都等于最小值),每一种不同位置在
g(旗子乘积)上可能对应不同的乘积(因为会去掉不同位置的 c)。于是我们要把那些"最小 b 出现的位置"对应的(把区间乘积除以该位置的 c)的和都算上,最后再乘上前缀的g[i]。程序里把计数信息存在ct[par][l][r],它本质上是这些位置的Inv(c[p])的和,配合前缀乘积可以还原出每个具体位置对应的贡献。
更简单的形象:
-
mi= 这段里的"最小糖果值"。 -
ct= 把"把整段的旗子合力除以每个最小糖果所在位置的旗子" 的这些结果合起来(在实现上用逆元做了方便的分解)。
手算举例(小区间,继续用上面例子):
区间 (1..3),奇数位是 1 和 3(位置 1、3),它们的 b 分别 1、3 → 最小是 1(在位置 1);所以 mi[奇][1..3] = 1,且出现位置只是 1,所以 ct[奇][1..3] 对应的是 inv(c1) = 1/2(在模下是 Inv(2))。
4、计算 pl 和 pr(区间有效性的左右边界)------从 i 出发能"拉"多远?
比喻(很直观):
站在格子 i,你向右连续做"相邻消除"------每次把左放倒(变 0),右变差值。你一直做,直到不能再"正数"下去(差变成 ≤0)或者到达末尾。pl[i] 就是从 i 向右这样做能到达的最远格子。向左类似,pr[i] 是从 i 向左能到的最远格子。
为什么需要它?
因为在 DP 里我们把一块 (i+1..j) 当成"整体"处理,但并不是所有的 (i+1..j) 都能通过允许的操作变成我们想的那种形态------只有满足 pr[j] <= some <= pl[i+1] 的才行。l = max(i+1, pr[j]), r = min(j, pl[i+1]) 就是判断有没有交集(有没有合法的位置来做最小 b 的选择)。如果 l>r,说明这段在操作规则下不可能达成我们要的结构,必须跳过。
具体小例子(A=[3,5,6])我们之前算过:
-
pl[1]=3,pl[2]=3,pl[3]=3;pr[1]=1,pr[2]=2,pr[3]=3。
这说明从 1 向右可以拉到 3,从 2 向右可以拉到 3;从 3 向左可以拉到 3 到 2 的时候就停止(因为差变非正)。
5、动态规划主过程(把小块拼成大块)------"积木拼楼房"的比喻
比喻:
我们从左往右搭积木(把序列分成一段段),f[i] 表示"当我把前 i 个格子做完后,能拿到的最大糖果总数是多少";g[i] 表示当拿到这个最大糖果总数时,对应的旗子乘积(所有达成该最大糖果总数的玩法的旗子乘积之和)。
转移(用更通俗的话):
-
你已经搭好了前
i个格子(这给你当前糖果f[i]和方案权重g[i])。 -
现在你试图把接下来的
i+1..j这一段当成下一块来处理(即把一段合并成"要么全消,要么留下某个位置")。 -
先看这段是否合法 (用
l,r判定)------如果不合法就不能当一块。 -
看
T = t[j] - t[i](交替和):-
如果
T == 0:这段可以全部互相抵消,不必牺牲糖果。你就可以把s[j] - s[i](这段的所有糖果)全部加到f[i]上,变成候选f[j]。- 对应的旗子乘积就是:之前的
g[i]乘上这段所有c的乘积(也就是h = g[i] * v[j]*iv[i])。
- 对应的旗子乘积就是:之前的
-
如果
T > 0(正):说明这段经过消除后会"剩下"在偶数位这一侧(原理基于交替和),因此不能同时保住这段所有的糖果。必须 在这段里的偶数位里挑一个最小的b牺牲掉(因为奇偶哪侧最后会剩数,那一侧的最小b是最不值钱、被牺牲的)。-
在
f上我们要把这段的s[j]-s[i]加上去再减去mi[0][l][r](偶位最小值)。 -
在
g上我们要把h乘以ct[0][l][r](把所有导致最小值的不同位置对应的c贡献一起加上)。
-
-
如果
T < 0(负):同理,但牺牲的是奇数位那一类(用mi[1][l][r]/ct[1][l][r])。
-
每次得到一个候选 cand(cand 是候选的 f[j]),就比较:
-
如果
cand > f[j]:更新f[j] = cand,并把g[j]重设为对应的权重(替换)。 -
如果
cand == f[j]:说明又多了一种不同的方式能达到同样的最大糖果,那就把对应的权重累加到g[j](加法,取模)。
6、把 v[j]*iv[i]*ct 的工作做个简单代数说明(为什么它等于"对区间内所有最小 b 出现位置 p,求 sum(prod_except_c_p)")
这是关键点,必须看懂 ct 为何要累加 ic[j] 才能在 DP 中直接用 v[j]*iv[i] 恢复出"去掉某个位置 c 的区间乘积"。
设区间 L..R 的总乘积 Prod = c[L]*c[L+1]*...*c[R]。
若某个位置 p 被选择为"牺牲最小 b",则该方案的旗子乘积应该是 Prod / c[p](因为最终那一位不被保留就不乘 c[p])。我们要把所有"p 属于最小 b 的位置"的 Prod / c[p] 加起来:

7、把整个过程再用一个小例子完整走一遍(包含 g 的数值,用普通分数代替模运算,便于理解)
还是例子:
A = [3,5,6], B = [1,2,3], C = [2,3,5]。我们算一遍主要的 DP 路径。
前缀:
-
s = [0,1,3,6]
-
t = [0,-3,2,-4]
-
v = [1,2,6,30](v[0]=1)
-
ic = [ -, 1/2, 1/3, 1/5 ](每个 c 的倒数)
pl/pr 如前: pl=[,3,3,3], pr=[,1,2,3]
初始化:
- f0=0, g0=1
转移重要一步:i=0 → j=3(把整个 1..3 当作一块)
-
l = max(1, pr[3]=3) = 3
-
r = min(3, pl[1]=3) = 3 → 合法
-
T = t3 - t0 = -4 → 负,说明牺牲奇位上的最小 b(奇位在 [3..3] 是 b3=3,S={3})
-
cand f = f0 + (s3-s0) - mi_奇 = 0 + 6 - 3 = 3 → 候选 f3 = 3
-
h = g0 * Prod(c1..3) = 1 * 30 = 30
-
ct_奇 = sum Inv(c[p] for p in S) = Inv(c3) = 1/5
-
对 g 的贡献 = h * ct_奇 = 30 * 1/5 = 6 → 这等价于把整段乘积除去 c3(即 2*3 = 6)
-
因此 g3(此路径带来的贡献) = 6
(解释:这个 6 表示:如果我们选择把 1..3 合并并牺牲第 3 号位置来换取最大糖果,那么最终该种方案对应的旗子乘积是 6。)
最后你会比较所有从不同 i 得到的 f3 值,取最大的(是 3),并把所有产生 f3=3 的路径对应的 g 的权重相加(就是程序最后输出的 g[n])。
五、参考程序:
cpp
#include <bits/stdc++.h>
#define FL(i, a, b) for (int i = (a); i <= (b); ++i) // 从 a 到 b 的 for 循环简写;
#define FR(i, a, b) for (int i = (a); i >= (b); --i) // 从 a 递减到 b 的 for 循环简写;
using namespace std;
typedef long long ll; // 长整型别名
const int N = 5e3 + 10; // 数组最大长度(根据题目约束);
const int MOD = 1e9 + 7; // 取模常数;
const ll INFLL = 1e18; // 表示一个很大的长整型(作为负无穷的对照使用);
// 全局变量(输入与预处理用)
int n, a[N], b[N], c[N], ic[N]; // n 和三个数组 a,b,c;ic 存 c 的逆元;
int pl[N], pr[N], v[N], iv[N]; // pl/pr 用于区间合法性边界;v 为 c 的前缀乘积;iv 为 v 的逆元;
ll mi[2][N][N], ct[2][N][N]; // mi 按奇偶保存区间最小 b,ct 保存对应的 c 的贡献(用于方案计数);
ll s[N], t[N], f[N], g[N]; // s: b 的前缀和;t: a 的交替前缀和;f/g 为 DP 数组;
// 快速幂,用于计算模的幂(求逆元时会用到)
int Qpow(int a, int b) {
int ret = 1; // 结果初值 1;
for (; b; b >>= 1) {
if (b & 1)
ret = (ll)ret * a % MOD; // 当当前位为 1 时乘入基数并取模;
a = (ll)a * a % MOD; // 基数平方并取模;
}
return ret; // 返回 a^b % MOD;
}
// 计算模逆(模 MOD 下的乘法逆元)
int Inv(int x) {
return Qpow(x, MOD - 2); // 费马小定理求逆元(MOD 为质数);
}
void Solve() {
scanf("%d", &n); // 读取单个测试点的 n;
// 读入 a,并构造交替前缀和 t
FL(i, 1, n) {
scanf("%d", &a[i]);
t[i] = t[i - 1] + (i & 1? -a[i] : a[i]);
// t[i] 为 a 的交替前缀和,代码中奇数位减,偶数位加;
}
// 读入 b,并构造普通前缀和 s
FL(i, 1, n) {
scanf("%d", &b[i]);
s[i] = s[i - 1] + b[i]; // s[i] 为 b 的前缀和,方便 O(1) 计算区间和;
}
// 读入 c,同时构造前缀乘积 v,及需要的逆元 ic, iv
v[0] = iv[0] = 1; // 空前缀乘积为 1;
FL(i, 1, n) {
scanf("%d", &c[i]);
v[i] = (ll)v[i - 1] * c[i] % MOD; // 前缀乘积 v[i] = product c[1..i] % MOD;
ic[i] = Inv(c[i]); // 每个 c 的模逆(用于 ct 的构造);
iv[i] = Inv(v[i]); // 前缀乘积 v[i] 的模逆(用于区间乘积的快速计算);
}
// 预处理区间信息:对每个起点 i,计算到 j 的区间内按奇偶分类的最小 b(mi)及其贡献计数(ct)
FL(i, 1, n) {
mi[0][i][i - 1] = mi[1][i][i - 1] = INFLL; // 初始化空区间的最小值为很大(便于取 min);
ct[0][i][i - 1] = ct[1][i][i - 1] = 0; // 空区间贡献为 0;
FL(j, i, n) {
FL(k, 0, 1) {
mi[k][i][j] = mi[k][i][j - 1];
ct[k][i][j] = ct[k][i][j - 1];
// 先继承前一个 j-1 的值(滚动更新);
}
mi[j & 1][i][j] = min(mi[j & 1][i][j], (ll)b[j]);
// 根据下标奇偶更新该类的最小 b 值;
ct[j & 1][i][j] = (ct[j & 1][i][j] + ic[j]) % MOD;
// 把该位置的 ic(c 的逆元)累加到对应的 ct 上;
// 说明:这里把 ic 累加起来,配合后面用 v[j]*iv[i] 可以得到区间内这些位置的 c 的乘积之和(在模意义下)
}
}
// 计算 pl/pr:每个位置 i 向右/向左交替相减能扩展到的边界
FL(i, 1, n) {
int d = a[i];
FL(j, i, n) {
d = a[j + 1] - d; // 模拟从 i 开始向右交替相减的过程(注意访问 a[j+1]);
if (j == n || d <= 0) {
pl[i] = j; // 当越过末尾或差变非正时停止,记录右边界 pl[i]
break;
}
}
d = a[i];
FR(j, i, 1) {
d = a[j - 1] - d; // 向左模拟交替相减
if (j == 1 || d <= 0) {
pr[i] = (j > 1 && !d? j - 1 : j);
// 注意当刚好为 0 时的特殊处理(pr 的定义在代码里作了调整)
break;
}
}
}
// 初始化 DP 数组 f 和 g;f 为极小值,g 为 0
fill(f, f + n + 1, -INFLL); // f 用来记录最大 b 和,先填为负无穷
fill(g, g + n + 1, 0); // g 用来记录达到该 f 时对应的 c 乘积之和(模 MOD)
f[0] = 0, g[0] = 1; // 空前缀的值为 0,方案权重为 1(乘法中性元)
// 主 DP:枚举前缀 i,然后枚举下一块末端 j
FL(i, 0, n) {
FL(j, i + 1, n) {
int l = max(i + 1, pr[j]), r = min(j, pl[i + 1]);
// l,r 用于判断区间 [i+1..j] 是否在 pl/pr 限制下合法
if (l > r){
continue; // 不合法区间跳过
}
// 计算把前缀 i 的方案 g[i] 与区间 c[i+1..j] 的乘积合并后的基数 h
int h = (ll)g[i] * v[j] % MOD * iv[i] % MOD;
// h = g[i] * product(c[i+1..j]) % MOD,作为新方案的基础权重
if (t[j] - t[i] == 0) {
// 若交替和为 0,则区间内无需扣除最小 b,全部 b 都能加上
f[j] = max(f[j], f[i] + (s[j] - s[i]));
g[j] = (g[j] + h) % MOD; // 所有这样的方案将其 g 权重累加
} else if (t[j] - t[i] > 0) {
// 若交替和为正,则需扣除区间 [l..r] 中偶数下标的最小 b
f[j] = max(f[j], f[i] + (s[j] - s[i]) - mi[0][l][r]);
// 对应的方案权重按该最小 b 出现位置的 c 的贡献累加
g[j] = (g[j] + (ll)h * ct[0][l][r]) % MOD;
} else {
// 若交替和为负,则需扣除区间 [l..r] 中奇数下标的最小 b
f[j] = max(f[j], f[i] + (s[j] - s[i]) - mi[1][l][r]);
g[j] = (g[j] + (ll)h * ct[1][l][r]) % MOD;
}
}
}
// 输出:最大 f[n](原值),以及对应的 g[n](已模)
printf("%lld %lld\n", f[n], g[n]);
}
int main() {
int T;
scanf("%*d %d", &T); // 输入文件格式中第一项为样例编号,这里跳过它,读取 T(测试点个数)
while (T--) {
Solve();
}
return 0;
}
六、小结:
-
s 是普通糖果前缀和;t 是交替前缀和(记得奇数位减、偶数位加)。
-
v 是
c的前缀乘积;iv 是它的逆;ic 是每个c的逆(用于"除去某个位置的 c")。 -
mi[par][l][r]:在区间里按奇/偶找最小糖果;ct[par][l][r]:把"把区间乘积除去最小位置的 c"这类贡献预先整理成能快速相乘的形式(用逆元)。
-
pl/pr:判断从某点向左右扩展交替减能走多远(保证区间合法)。
-
DP:把区间当积木拼接;看交替和决定是否需要牺牲哪一类;把 f 用最大化,g 按权重累加(等于用前缀乘积乘以那些 inv(c) 的和)。