把两种颜色的历史压成另一色最后值 DP,并用最大/次大状态 O(1) 查询排除当前值后的最优转移。
OJ: luogu
题目 ID: P11233
难度:提高+/省选-
标签:动态规划状态压缩最大次大值
日期: 2026-06-22 18:36
题意
给定数组 A,要把每个位置染成红色或蓝色。
对位置 i,只看它左边最近的同色位置 j:
- 如果不存在这样的
j,贡献0; - 如果存在且
A_i = A_j,贡献A_i; - 否则贡献
0。
求最大总贡献。
思路
先看一个直接暴力:枚举每个位置染红还是染蓝,然后维护两种颜色的最后值,逐个计算贡献。
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
int T, n;
int a[MAXN];
long long calc_score(int mask) {
int last[2];
last[0] = last[1] = 0;
long long score = 0;
for (int i = 1; i <= n; i++) {
int color = (mask >> (i - 1)) & 1;
if (last[color] == a[i]) {
score += a[i];
}
last[color] = a[i];
}
return score;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> T;
while (T--) {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
long long ans = 0;
int total = 1 << n;
for (int mask = 0; mask < total; mask++) {
ans = max(ans, calc_score(mask));
}
cout << ans << '\n';
}
return 0;
}暴力有 2^n 种染色方案,不能处理 n = 2 * 10^5。
关键观察是:处理到某个位置时,一种颜色对后续的影响,只剩下“这种颜色最后一次出现的值”。更早的位置不会再直接影响贡献。
如果直接记录红色最后值和蓝色最后值,会有二维状态。我们可以利用“当前处理完的位置一定属于某一种颜色”来压缩状态。
设处理到上一个位置,last_value = A_{i-1}。令:
| 符号 | 含义 |
|---|---|
last_value |
上一个位置所在颜色的最后值 |
dp[x] |
另一种颜色最后值为 x 时的最大得分 |
lazy_add |
所有状态共同增加的得分 |
现在处理 cur = A_i,有两种选择。
第一种:把 cur 染成和上一个位置相同的颜色。若 cur == last_value,那么每个状态都会增加 cur 分;否则不增加。这个转移不会改变另一种颜色的最后值,所以可以用 lazy_add 统一维护。
第二种:把 cur 染成另一种颜色。若这个状态中另一种颜色最后值 x == cur,就增加 cur 分;否则不增加。染完后,刚使用的颜色变成当前颜色,而“另一种颜色”的最后值变成原来的 last_value,所以要更新状态 dp[last_value]。
也就是:
dp[last_value] = max(
所有 x != cur 的 dp[x],
dp[cur] + cur
)注意这里的 dp 说的是实际得分。代码中为了支持统一加分,实际保存的是 dp[x] - lazy_add。
用最大、次大替代 multiset
转移时需要快速求:
所有 x != cur 的最大 dp[x]其实不需要 multiset。因为每个状态 dp[x] 只会被更新成更大的值,而 lazy_add 是所有状态一起增加,不会改变状态之间的大小关系。
所以只维护两个状态即可:
| 变量 | 含义 |
|---|---|
best_key, best_value |
当前 dp[x] - lazy_add 最大的状态 |
second_key, second_value |
key 不同于 best_key 的次大状态 |
查询“排除 cur 后的最大值”时:
- 如果
best_key != cur,答案就是best_value + lazy_add; - 如果
best_key == cur,答案就是second_value + lazy_add。
当某个 dp[x] 变大时,只要用它更新最大、次大两个记录即可。
样例 1 的 DP 状态表
下面用样例第一组 1 2 1 展示状态如何转移。表里的 dp[x] 写的是实际得分,x = 0 表示另一种颜色还没有出现过。
| 处理位置 | 处理前 last_value |
当前 cur |
处理前有效状态 | max_except(cur) |
dp[cur] + cur |
本轮更新 | 处理后有效状态 |
|---|---|---|---|---|---|---|---|
| 初始 | - | 1 |
- | - | - | 放入空颜色状态 dp[0]=0 |
dp[0]=0 |
i=2 |
1 |
2 |
dp[0]=0 |
0 |
不存在 | 更新 dp[1]=0 |
dp[0]=0, dp[1]=0 |
i=3 |
2 |
1 |
dp[0]=0, dp[1]=0 |
0 |
dp[1]+1=1 |
更新 dp[2]=1 |
dp[0]=0, dp[1]=0, dp[2]=1 |
第 3 个数为 1 时,状态 dp[1] 表示另一种颜色最后值是 1,如果把当前 1 染到那种颜色上,就能得到贡献 1。于是 dp[2] 被更新成 1,最终答案也是 1。
如果出现 cur == last_value,说明把当前数继续染成和上一个数相同的颜色时,所有状态都会增加 cur。代码不用逐个修改 dp[x],只需要令 lazy_add += cur。
最后答案就是所有状态的最大值加上 lazy_add。
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
const int MAXV = 1000005;
const long long NEG = -(long long)4e18;
int T, n;
int a[MAXN];
bool active_state[MAXV];
long long dp[MAXV]; // dp[x] 表示另一种颜色最后一个数为 x 时的最优得分,统一减去 lazy
long long lazy_add;
vector<int> touched;
int best_key, second_key; // dp 最大、次大的状态编号
long long best_value, second_value; // 对应的 dp[x],不包含 lazy_add
long long get_actual(int x) {
if (!active_state[x]) {
return NEG;
}
return dp[x] + lazy_add;
}
long long get_max_except(int x) {
if (best_key == -1) {
return NEG;
}
if (best_key != x) {
return best_value + lazy_add;
}
return second_value + lazy_add;
}
void swap_best() {
swap(best_key, second_key);
swap(best_value, second_value);
}
// 某个状态的 dp[x] 只会变大,用最大、次大两个状态就能回答“排除 x 的最大值”。
void update_best(int x) {
long long value = dp[x];
if (best_key == x) {
best_value = value;
return;
}
if (second_key == x) {
second_value = value;
if (second_value > best_value) {
swap_best();
}
return;
}
if (value > best_value) {
second_key = best_key;
second_value = best_value;
best_key = x;
best_value = value;
} else if (value > second_value) {
second_key = x;
second_value = value;
}
}
void set_state(int x, long long actual_value) {
if (active_state[x] && actual_value <= get_actual(x)) {
return;
}
if (active_state[x]) {
dp[x] = actual_value - lazy_add;
} else {
active_state[x] = true;
touched.push_back(x);
dp[x] = actual_value - lazy_add;
}
update_best(x);
}
void clear_case() {
for (int i = 0; i < (int)touched.size(); i++) {
active_state[touched[i]] = false;
dp[touched[i]] = 0;
}
touched.clear();
lazy_add = 0;
best_key = -1;
second_key = -1;
best_value = NEG;
second_value = NEG;
}
void solve_one() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
clear_case();
int last_value = a[1];
set_state(0, 0);
for (int i = 2; i <= n; i++) {
int x = a[i];
// 当前数染到“另一种颜色”时,新的另一色最后值会变成 last_value。
long long candidate = get_max_except(x);
if (active_state[x]) {
candidate = max(candidate, get_actual(x) + x);
}
// 当前数染到和上一个数相同的颜色,所有状态都会得到这一段相邻相同的贡献。
if (x == last_value) {
lazy_add += x;
}
long long current = get_actual(last_value);
if (candidate > current) {
set_state(last_value, candidate);
}
last_value = x;
}
long long ans = NEG;
if (best_key != -1) {
ans = best_value + lazy_add;
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> T;
while (T--) {
solve_one();
}
return 0;
}复杂度
每个位置只进行常数次数组操作和最大、次大状态维护,时间复杂度为
空间复杂度为 V = 10^6 是值域上界。
总结
这题的核心是把“两个颜色的历史”压成“两个颜色的最后值”。进一步利用当前颜色的最后值固定为上一个数,把二维状态压成一维的 dp[x]。
连续染同色带来的统一加分用懒标记维护。改染另一色时需要查询“排除当前值后的最大状态”,由于状态值只会变大,维护最大和次大两个状态就足够了,不需要 multiset。