[CSP-S 2024] 染色

GitHub跳转原题关系图返回列表

把两种颜色的历史压成另一色最后值 DP,并用最大/次大状态 O(1) 查询排除当前值后的最优转移。

OJ: luogu

题目 ID: P11233

难度:提高+/省选-

标签:动态规划状态压缩最大次大值

日期: 2026-06-22 18:36

题意

给定数组 A,要把每个位置染成红色或蓝色。

对位置 i,只看它左边最近的同色位置 j

  • 如果不存在这样的 j,贡献 0
  • 如果存在且 A_i = A_j,贡献 A_i
  • 否则贡献 0

求最大总贡献。

思路

先看一个直接暴力:枚举每个位置染红还是染蓝,然后维护两种颜色的最后值,逐个计算贡献。

cpp
// 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]

也就是:

text
dp[last_value] = max(
  所有 x != cur 的 dp[x],
  dp[cur] + cur
)

注意这里的 dp 说的是实际得分。代码中为了支持统一加分,实际保存的是 dp[x] - lazy_add

用最大、次大替代 multiset

转移时需要快速求:

text
所有 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

代码

cpp
#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;
}

复杂度

每个位置只进行常数次数组操作和最大、次大状态维护,时间复杂度为 O(n)O(n)

空间复杂度为 O(n+V)O(n + V),其中 V = 10^6 是值域上界。

总结

这题的核心是把“两个颜色的历史”压成“两个颜色的最后值”。进一步利用当前颜色的最后值固定为上一个数,把二维状态压成一维的 dp[x]

连续染同色带来的统一加分用懒标记维护。改染另一色时需要查询“排除当前值后的最大状态”,由于状态值只会变大,维护最大和次大两个状态就足够了,不需要 multiset