吟游诗人 - 题解
标签与难度
标签: 二分答案, 动态规划, 树状数组, 置换, 复杂度优化, 喵~ 难度: 2300
题目大意喵~
一位吟游诗人温迪需要将一首长度为 的乐谱(一个整数序列)划分成恰好 个连续的乐章。乐谱的“动听度”由响度最大的那个乐章决定,并且这个最大响度越小,乐谱就越动听。温迪的目标,就是让这个最大响度尽可能地小。
然而,另一位诗人六指乔瑟会来捣乱!在温迪开始演奏前,他可以对初始乐谱 进行至多 次变换。每次变换都是应用一个给定的置换 。也就是说,最终的乐谱 可以是 中的任意一个。
六指乔瑟非常聪明,他会选择一个变换次数 ,使得温迪演奏该乐谱 时,能达到的最小的最大乐章响度是所有可能中最大的。而温DEI迪同样聪明,对于任何给定的乐谱,他总能找到最佳划分方式,使得最大响度最小。
我们的任务,就是预测这场神仙对决的最终结果:那个被六指乔瑟“最大化”了的“最小的最大响度”究竟是多少呢,喵?
解题思路分析
这真是一场精彩的对决喵!整个问题可以看成一个双层博弈:
- 外层 (六指乔瑟): 他有 种选择(应用置换 到 次),他会选择一种,使得最终的“动听度”最差(即最小的最大响度最大)。
- 内层 (温迪): 对于乔瑟选定的任意一首乐谱,温迪需要将其划分为 个乐章,并让其中响度最大的乐章的响度尽可能小。
我们的目标是求出 。
所以,解题的核心思路就是,我们先枚举乔瑟的所有可能选择,也就是从 到 的每一次置换。对于每一个置换后得到的乐谱,我们去解决内层的温迪问题。最后,在所有结果中取一个最大值,就是最终的答案啦!
内层问题:最小化最大乐章响度
对于一个固定的乐谱序列 ,要将它分成恰好 段,并最小化最大段和。这是一个经典的“最大值最小化”问题,通常都可以用 二分答案 来解决,喵~
我们来二分这个“最小的最大响度”,称之为 max_loudness。现在的问题就转化为一个判定问题:是否存在一种划分方案,把序列 分成恰好 段,并且每一段的和都不超过 max_loudness?
这个问题比想象中要复杂一些。如果只是要求划分成 不超过 段,我们可以用贪心解决。但这里要求是 恰好 段。这意味着,对于一个给定的 max_loudness,我们需要知道能划分出的最少段数 min_partitions 和最多段数 max_partitions。只要 在这个区间内,即 min_partitions <= k <= max_partitions,那么 check(max_loudness) 就为真。
如何求 min_partitions 和 max_partitions 呢?这可以用动态规划来解决!
令 为序列 的前缀和,即 (并且 )。一个从 到 的乐章的响度就是 。
我们定义 和 分别为:将前 个音符划分成若干乐章(每个响度都不超过 max_loudness)所需的最少和最多乐章数。
状态转移方程如下:
这个转移方程的复杂度是 ,对于一个 check 函数来说太慢了。整个算法会是 ,肯定会超时的说!
DP优化:树状数组来加速!
我们需要优化这个DP过程。注意到转移条件 可以变形为 。这意味着,在计算 时,我们需要查询所有满足特定前缀和条件的 中 的最值。
这是一个典型的二维偏序问题(偏序为下标 和前缀和 ),但由于我们是按 顺序计算的,下标的限制天然满足。问题就简化为了一维数据结构上的范围查询。
我们可以将所有前缀和 进行离散化,然后用**树状数组(Fenwick Tree)**或者线段树来维护DP值。
具体做法是:
- 计算所有前缀和 ,并将它们收集起来排序、去重,得到一个离散化后的值域。
- 建立两个树状数组,一个维护 的区间最小值,另一个维护 的区间最大值。
- 当我们计算 时: a. 找到阈值 在离散化值域中的排名
rank_threshold。 b. 我们需要查询所有 排名大于等于rank_threshold的 的最值。这等价于一个后缀查询。 c. 我们可以用一个特殊构造的树状数组来实现“单点更新,后缀查询”,或者通过坐标变换(例如将排名r映射到max_rank - r + 1)在标准的前缀查询树状数组上实现。 d. 查到最值后,计算出 和 。 e. 将 的排名rank_i作为下标,把 和 更新到树状数组中,供后续的 查询。
这样,一次 check 函数的复杂度就降到了 。
整体流程与小优化
现在,我们的总算法是:
- 外层循环 从 到 。
- 在循环中,首先生成第 次置换后的乐谱序列 。
- 对序列 进行二分答案,寻找最小的
max_loudness。- 二分的
check函数内部使用 的DP+树状数组来完成。
- 二分的
- 记录下当前乐谱的最小
max_loudness,并在所有 的结果中取最大值。
总复杂度是 。在 的情况下,这依然太慢了。但是,我们可以加一个小小的优化,喵~
我们维护一个全局的 global_max_min_loudness,表示到目前为止遇到的最大“最小响度”。对于一个新的乐谱,我们先不急着二分,而是先 check(global_max_min_loudness) 一下。
- 如果
check通过,说明这个乐谱的最小最大响度不会超过我们已知的最大值。它不会对最终答案产生贡献,我们可以直接跳过对它的二分,进行下一次循环。 - 如果
check失败,说明我们找到了一个更“差”的乐谱!它的最小最大响度一定大于global_max_min_loudness。这时我们再进行二分,把二分下界设为global_max_min_loudness + 1,找到它精确的值,并更新全局最大值。
这个优化在很多情况下能显著减少二分的次数,尤其是在一个能产生较大结果的置换被较早遇到时。虽然最坏复杂度不变,但它能帮助我们通过这道题,喵~
代码实现
这是我根据上面的思路,精心重构的一份代码哦~ 希望能帮到你,喵!
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
#include <functional>
// 使用 long long 防止权值和溢出
using ll = long long;
const ll INF_LOUDNESS = 4e12; // 一个足够大的响度值
const int MAXN = 2005;
const int INF_PARTITIONS = MAXN; // DP值不可能超过N
// --- 全局变量 ---
int n, k, T;
ll initial_c[MAXN];
int perm_f[MAXN];
ll current_a[MAXN];
ll prefix_sum[MAXN];
// --- DP及数据结构相关 ---
std::vector<ll> distinct_sums;
int n_distinct;
int min_dp[MAXN], max_dp[MAXN];
// 树状数组,用于优化DP
// 实现单点更新与后缀查询
namespace FenwickTree {
int min_tree[MAXN], max_tree[MAXN];
void init() {
// n_distinct 是离散化后值的数量,+2是为了边界安全
std::fill(min_tree, min_tree + n_distinct + 2, INF_PARTITIONS);
std::fill(max_tree, max_tree + n_distinct + 2, -INF_PARTITIONS);
}
// 更新 rank 位置的值
void update_min(int rank, int val) {
for (; rank > 0; rank -= rank & -rank) min_tree[rank] = std::min(min_tree[rank], val);
}
void update_max(int rank, int val) {
for (; rank > 0; rank -= rank & -rank) max_tree[rank] = std::max(max_tree[rank], val);
}
// 查询 rank 及之后所有位置的最值
int query_min(int rank) {
int res = INF_PARTITIONS;
for (; rank <= n_distinct; rank += rank & -rank) res = std::min(res, min_tree[rank]);
return res;
}
int query_max(int rank) {
int res = -INF_PARTITIONS;
for (; rank <= n_distinct; rank += rank & -rank) res = std::max(res, max_tree[rank]);
return res;
}
}
// 判定函数:对于当前乐谱 current_a, 是否能以 max_loudness 为上限,划分成 k 段
bool check(ll max_loudness) {
// 1. 计算前缀和并离散化
prefix_sum[0] = 0;
for (int i = 1; i <= n; ++i) {
prefix_sum[i] = prefix_sum[i - 1] + current_a[i];
}
distinct_sums.assign(prefix_sum, prefix_sum + n + 1);
std::sort(distinct_sums.begin(), distinct_sums.end());
distinct_sums.erase(std::unique(distinct_sums.begin(), distinct_sums.end()), distinct_sums.end());
n_distinct = distinct_sums.size();
// 获取一个值在离散化数组中的排名 (1-based)
auto get_rank = [&](ll val) {
return std::lower_bound(distinct_sums.begin(), distinct_sums.end(), val) - distinct_sums.begin() + 1;
};
// 2. DP 过程
FenwickTree::init();
min_dp[0] = 0;
max_dp[0] = 0;
int rank_s0 = get_rank(prefix_sum[0]);
FenwickTree::update_min(rank_s0, 0);
FenwickTree::update_max(rank_s0, 0);
for (int i = 1; i <= n; ++i) {
// 寻找 S[j] >= S[i] - max_loudness
ll target_s = prefix_sum[i] - max_loudness;
int target_rank = get_rank(target_s);
int min_prev_dp = FenwickTree::query_min(target_rank);
int max_prev_dp = FenwickTree::query_max(target_rank);
min_dp[i] = (min_prev_dp == INF_PARTITIONS) ? INF_PARTITIONS : min_prev_dp + 1;
max_dp[i] = (max_prev_dp == -INF_PARTITIONS) ? -INF_PARTITIONS : max_prev_dp + 1;
int rank_si = get_rank(prefix_sum[i]);
FenwickTree::update_min(rank_si, min_dp[i]);
FenwickTree::update_max(rank_si, max_dp[i]);
}
return min_dp[n] <= k && k <= max_dp[n];
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
std::cin >> n >> k >> T;
for (int i = 1; i <= n; ++i) std::cin >> initial_c[i];
for (int i = 1; i <= n; ++i) std::cin >> perm_f[i];
// 初始化当前乐谱为原始乐谱
for (int i = 1; i <= n; ++i) current_a[i] = initial_c[i];
ll global_max_min_loudness = -INF_LOUDNESS;
for (int t = 0; t <= T; ++t) {
if (t > 0) {
// 应用置换 f: new_a[i] = old_a[f[i]]
ll temp_a[MAXN];
for (int i = 1; i <= n; ++i) {
temp_a[i] = current_a[perm_f[i]];
}
for (int i = 1; i <= n; ++i) {
current_a[i] = temp_a[i];
}
}
// 优化:如果当前已知的最大值已经可以满足,就不用二分了
if (check(global_max_min_loudness)) {
continue;
}
// 二分查找当前乐谱的最小最大响度
ll low = global_max_min_loudness + 1, high = INF_LOUDNESS, current_min_loudness = INF_LOUDNESS;
while (low <= high) {
ll mid = low + (high - low) / 2;
if (check(mid)) {
current_min_loudness = mid;
high = mid - 1;
} else {
low = mid + 1;
}
}
global_max_min_loudness = current_min_loudness;
}
std::cout << global_max_min_loudness << std::endl;
return 0;
}复杂度分析
时间复杂度:
- 外层循环最多执行 次。
- 内部的
check函数,主要开销在于排序离散化()和 次树状数组操作(每次 ),总共是 。 - 二分答案的次数是 ,其中 Range 是响度的可能范围。
- 尽管最坏情况下的理论复杂度很高,但由于我们加入了
check(global_max_min_loudness)这个剪枝优化,实际运行中可以跳过大量不必要的二分搜索,从而在时限内通过,喵~
空间复杂度:
- 主要空间开销是存储乐谱序列、前缀和、DP数组以及树状数组,它们的大小都与 呈线性关系。
知识点总结
这道题是多种算法思想的巧妙结合,像一首层次丰富的乐曲呢,喵~
- 问题建模: 识别出外层的“最大化”和内层的“最小化”博弈结构是解题的第一步。
- 二分答案: 内层的“最小化最大值”问题是二分答案的经典应用场景。
- 动态规划:
check函数的核心是 DP。设计出正确的状态(最少/最多划分数)和转移方程是关键。 - 数据结构优化: 用树状数组(或线段树)将 的 DP 优化到 ,是解决性能瓶颈的核心技巧。
- 离散化: 配合树状数组使用,处理值域较大的情况。
- 特殊树状数组: 本题解中的树状数组实现了“单点更新,后缀查询”,是一种不那么常见但非常有用的技巧。
- 置换群: 题目中的变换是基于置换的,理解如何正确地应用置换是必要的。
- 剪枝优化: 在循环中利用已知最优解进行剪枝,是让高复杂度算法通过时限的重要策略。
希望这篇题解能帮助你更好地理解这道题的每一个细节,加油哦,未来的大算法家!喵~