当前位置:首页 > 技术 > 正文内容

AtCoder Beginner Contest 456 (ABC456) 题目解析

访客 技术 2026年8月2日 1

本篇将对 AtCoder Beginner Contest 456 (ABC456) 中的多个题目进行分析与解答,涵盖了从基础的条件判断到高级的图论和数据结构应用。

A - 掷骰子问题

思路分析

题目询问一个给定的整数 n 是否可能由掷三个标准六面骰子(每个骰子点数为 1 到 6)所得的总和。要解决这个问题,我们首先需要确定三个骰子点数之和的最小值和最大值。

  • 最小值:当三个骰子都掷出 1 时,总和为 1 + 1 + 1 = 3。
  • 最大值:当三个骰子都掷出 6 时,总和为 6 + 6 + 6 = 18。

进一步思考可知,从 3 到 18 之间的所有整数和都是可以通过调整三个骰子的点数来达成的。例如,要得到 4,可以是 (1, 1, 2);要得到 5,可以是 (1, 1, 3) 或 (1, 2, 2);以此类推。因此,我们只需要判断给定的 n 是否在 [3, 18] 这个闭区间内即可。

代码实现

#include <iostream> // 引入标准输入输出库

// 函数用于判断给定的目标和是否可能由三个标准骰子掷出
void evaluateDiceRollSum() {
    int target_sum;
    std::cin >> target_sum; // 从标准输入读取目标和

    // 三个标准六面骰子的点数和范围是 [1+1+1, 6+6+6],即 [3, 18]。
    // 范围内的所有整数和都是可达的。
    if (target_sum >= 3 && target_sum <= 18) {
        std::cout << "Yes\n"; // 如果在范围内,输出 "Yes"
    } else {
        std::cout << "No\n";  // 否则,输出 "No"
    }
}

int main() {
    // 优化 C++ 标准流的输入/输出性能
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    evaluateDiceRollSum(); // 调用求解函数

    return 0;
}

B - 456 问题

思路分析

此题是一个概率问题,涉及三个独立的"骰子"(或者说,三组不同的随机结果序列)。每个"骰子"有 6 种可能的结果,这些结果由输入数据给出。我们需要计算三个"骰子"分别掷出数字 4、5、6 的所有排列组合的总概率。

首先,我们需要计算每个"骰子"掷出 4、5、6 的各自概率。对于第 i 个"骰子",其 6 种结果是已知的。我们可以统计其中出现 4、5、6 的次数,然后除以 6 得到相应的概率。

假设我们有三个"骰子" D0, D1, D2,并且知道它们掷出 4、5、6 的概率:

  • P(Di=4):骰子 i 掷出 4 的概率
  • P(Di=5):骰子 i 掷出 5 的概率
  • P(Di=6):骰子 i 掷出 6 的概率

题目要求的是 D0, D1, D2 掷出的结果是 4, 5, 6 的某个排列。由于三个"骰子"是独立的,我们可以将所有 3! = 6 种排列的概率相加:

  1. (D0=4, D1=5, D2=6):P(D0=4) * P(D1=5) * P(D2=6)
  2. (D0=4, D1=6, D2=5):P(D0=4) * P(D1=6) * P(D2=5)
  3. (D0=5, D1=4, D2=6):P(D0=5) * P(D1=4) * P(D2=6)
  4. (D0=5, D1=6, D2=4):P(D0=5) * P(D1=6) * P(D2=4)
  5. (D0=6, D1=4, D2=5):P(D0=6) * P(D1=4) * P(D2=5)
  6. (D0=6, D1=5, D2=4):P(D0=6) * P(D1=5) * P(D2=4)

将这六种情况的概率相加即可得到最终答案。

代码实现

#include <iostream>
#include <vector>
#include <iomanip> // 用于设置浮点数输出精度

// 函数用于计算三个"骰子"掷出 4, 5, 6 任意排列的总概率
void calculate_target_probability() {
    // input_outcomes[i] 存储第 i 个"骰子"的 6 种可能结果
    std::vector<std::vector<int>> input_outcomes(3, std::vector<int>(6));
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 6; ++j) {
            std::cin >> input_outcomes[i][j];
        }
    }

    // event_probabilities[i][k] 存储第 i 个"骰子"掷出值 (4+k) 的概率
    // k=0 对应 4,k=1 对应 5,k=2 对应 6。
    std::vector<std::vector<double>> event_probabilities(3, std::vector<double>(3));

    for (int i = 0; i < 3; ++i) { // 遍历每个"骰子"
        std::vector<int> counts_of_456(3, 0); // 统计 4, 5, 6 的出现次数
        for (int outcome_val : input_outcomes[i]) {
            if (outcome_val == 4) {
                counts_of_456[0]++;
            } else if (outcome_val == 5) {
                counts_of_456[1]++;
            } else if (outcome_val == 6) {
                counts_of_456[2]++;
            }
        }
        // 计算每个目标值 (4, 5, 6) 的概率
        for (int k = 0; k < 3; ++k) {
            event_probabilities[i][k] = static_cast<double>(counts_of_456[k]) / 6.0;
        }
    }

    // 计算所有 3! = 6 种 (4,5,6) 排列组合的总概率
    double total_probability = 0.0;

    // 0:4, 1:5, 2:6
    // Permutation 1: (D0=4, D1=5, D2=6)
    total_probability += event_probabilities[0][0] * event_probabilities[1][1] * event_probabilities[2][2];
    // Permutation 2: (D0=4, D1=6, D2=5)
    total_probability += event_probabilities[0][0] * event_probabilities[1][2] * event_probabilities[2][1];

    // Permutation 3: (D0=5, D1=4, D2=6)
    total_probability += event_probabilities[0][1] * event_probabilities[1][0] * event_probabilities[2][2];
    // Permutation 4: (D0=5, D1=6, D2=4)
    total_probability += event_probabilities[0][1] * event_probabilities[1][2] * event_probabilities[2][0];

    // Permutation 5: (D0=6, D1=4, D2=5)
    total_probability += event_probabilities[0][2] * event_probabilities[1][0] * event_probabilities[2][1];
    // Permutation 6: (D0=6, D1=5, D2=4)
    total_probability += event_probabilities[0][2] * event_probabilities[1][1] * event_probabilities[2][0];
    
    // 输出结果,保留 10 位小数
    std::cout << std::fixed << std::setprecision(10) << total_probability << '\n';
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL); // 解除 cin 和 cout 的绑定,提高效率

    calculate_target_probability(); // 调用求解函数

    return 0;
}

C - 非相邻子序列

思路分析

问题要求计算一个给定字符串中所有"非相邻"连续子序列的数量。这里的"非相邻"指的是子序列内部的字符,不存在相邻的两个字符相同的情况。例如,对于字符串 "abacaba","aba" 是一个有效的非相邻子序列,而 "aa" 则不是。

我们关注的是连续子序列。如果字符串中的两个相邻字符 s[i]s[i+1] 相同,那么所有跨越这两个位置的连续子序列都将包含相邻的相同字符,因此它们是无效的。这实际上将字符串分割成多个"有效"片段。

例如,对于字符串 "aabccdeff":

  • 'a' == 'a',所以 "aa" 处的相邻字符相同,在此处断开。
  • 'c' == 'c',所以 "cc" 处的相邻字符相同,在此处断开。
  • 'f' == 'f',所以 "ff" 处的相邻字符相同,在此处断开。

字符串被分割为片段 "a", "b", "c", "d", "e", "f"。注意 "aabccdeff" 实际可划分为: "aa" (断开) "b" "cc" (断开) "d" "e" "ff" (断开) 有效片段是: "a" (长度1) -> 对应原始字符串中第一个'a' "b" (长度1) "c" (长度1) -> 对应原始字符串中第一个'c' "d" (长度1) "e" (长度1) "f" (长度1) -> 对应原始字符串中第一个'f' 这与原始思路的解释有出入。我们应理解为:对于一个连续的字符序列,例如 "abcde",其所有连续子序列都是有效的("a", "ab", "abc", ..., "e", "de", ...)。一旦出现 s[i] == s[i+1],比如 "abaac",那么 "baa", "aa", "abaa" 等子序列都是无效的。我们只计算那些不包含相邻相同字符的连续子序列

因此,我们可以遍历字符串,每当遇到 s[i] == s[i+1] 时,说明从 s[0]s[i] 形成了一个"有效连续片段"。这个片段内部的所有连续子序列都是非相邻的。一个长度为 L 的连续片段,其包含的连续子序列数量为 L * (L + 1) / 2。我们将每个这样的有效片段的连续子序列数量累加起来即可。最后,对于字符串末尾的剩余部分,也需要单独计算其贡献。

代码实现

#include <iostream>
#include <string>
#include <vector> // 在此题中非必需,但通常用于竞赛编程

// 定义长整型别名和模数常量
typedef long long llong;
const llong MOD_VALUE = 998244353; // 题目要求的模数

// 函数用于计算字符串中非相邻连续子序列的数量
void calculate_non_adjacent_subsequences() {
    std::string input_string;
    std::cin >> input_string;

    // 特殊处理单字符字符串的情况,只有一个子序列,即自身,它是有效的。
    if (input_string.length() == 1) {
        std::cout << 1 << '\n';
        return;
    }

    llong current_segment_length = 0; // 当前有效连续片段的长度
    llong total_valid_subsequences = 0; // 累计总的有效连续子序列数量

    // 遍历字符串以识别有效连续片段
    for (int i = 0; i < input_string.length(); ++i) {
        current_segment_length++; // 延长当前片段

        // 如果到达字符串末尾,或者当前字符与下一个字符相同,则当前有效片段结束。
        if (i == input_string.length() - 1 || input_string[i] == input_string[i + 1]) {
            // 一个长度为 L 的连续片段,其包含 L * (L + 1) / 2 个连续子序列。
            // 例如,"abc" 长度为 3,子序列有 "a","b","c","ab","bc","abc",共 6 个。
            // 3 * (3 + 1) / 2 = 6。
            llong subsequences_in_segment = (current_segment_length * (current_segment_length + 1) / 2) % MOD_VALUE;
            total_valid_subsequences = (total_valid_subsequences + subsequences_in_segment) % MOD_VALUE;
            current_segment_length = 0; // 重置片段长度,开始统计下一个片段
        }
    }

    std::cout << total_valid_subsequences << '\n';
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
    calculate_non_adjacent_subsequences(); // 调用求解函数
    return 0;
}

D - 非相邻子序列 2

思路分析

"非相邻子序列 2"通常指在子序列中,不允许出现两个相邻的字符相同。例如,"aba" 是有效子序列,而 "aa" 或 "abbc" (其中的 "bb") 则不是。这与 C 题的"连续子序列"含义有所不同,这里指的可以是非连续的子序列

这是一个经典的动态规划问题。我们可以定义 dp[k] 为以字符 ('a' + k) 结尾的、所有有效非相邻子序列的数量。遍历输入字符串中的每个字符 current_char

  1. 确定 current_char 对应的索引 char_idx (例如 'a' -> 0, 'b' -> 1, 'c' -> 2)。
  2. 要形成一个以 current_char 结尾的有效子序列,有两种方式:
    • 单独的 current_char 自身,这算作一个长度为 1 的子序列。
    • current_char 附加到任何一个以不同字符结尾的有效子序列之后。例如,如果 current_char 是 'a',它可以附加到任何以 'b' 或 'c' 结尾的子序列。
  3. 因此,新的 dp[char_idx] 值将是 (1 + (dp[0] + dp[1] + dp[2] - dp[char_idx])) % MOD_VALUE。这里的 (dp[0] + dp[1] + dp[2] - dp[char_idx]) 代表了所有以非 char_idx 结尾的子序列数量。
  4. 其他 dp[k] (其中 k != char_idx) 的值在本步骤保持不变,因为 current_char 不会创建以其他字符结尾的新有效子序列。

最终答案是所有 dp[0] + dp[1] + dp[2] 的总和。

代码实现

#include <iostream>
#include <string>
#include <vector>

typedef long long llong;
const llong MOD_VAL = 998244353; // 题目要求的模数

// 函数用于计算字符串中有效非相邻子序列的数量
// 有效非相邻子序列指:子序列中任何两个相邻字符都不相同。
void count_valid_subsequences_no_adjacent_dupes() {
    std::string input_str;
    std::cin >> input_str;

    // dp_counts[0] 存储以 'a' 结尾的有效子序列数量
    // dp_counts[1] 存储以 'b' 结尾的有效子序列数量
    // dp_counts[2] 存储以 'c' 结尾的有效子序列数量
    llong dp_counts[3] = {0, 0, 0};

    // 遍历输入字符串中的每个字符
    for (char current_char : input_str) {
        int char_idx = current_char - 'a'; // 将字符 'a', 'b', 'c' 映射到索引 0, 1, 2

        llong sum_of_other_chars_counts = 0;
        // 计算所有以与 current_char 不同字符结尾的有效子序列数量
        for (int k = 0; k < 3; ++k) {
            if (k != char_idx) {
                sum_of_other_chars_counts = (sum_of_other_chars_counts + dp_counts[k]) % MOD_VAL;
            }
        }
        
        // 更新以 current_char 结尾的有效子序列数量:
        // 1. current_char 自身作为一个长度为 1 的子序列 (+1)。
        // 2. 将 current_char 附加到所有以其他字符结尾的有效子序列之后。
        dp_counts[char_idx] = (sum_of_other_chars_counts + 1) % MOD_VAL;
        // 注意:其他 dp_counts[k] (k != char_idx) 的值保持不变,
        // 因为 current_char 不会生成以它们结尾的新子序列。
    }

    // 最终答案是所有以 'a', 'b', 'c' 结尾的有效子序列的总和。
    llong total_valid_subsequences = 0;
    for (int k = 0; k < 3; ++k) {
        total_valid_subsequences = (total_valid_subsequences + dp_counts[k]) % MOD_VAL;
    }
    std::cout << total_valid_subsequences << '\n';
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
    count_valid_subsequences_no_adjacent_dupes(); // 调用求解函数
    return 0;
}

E - 无尽假期

思路分析

题目要求判断是否存在一条路径,使得一个人可以无限期地处于假期状态。这可以被建模为一个图论问题,目标是判断图中是否存在一个满足特定条件的环。核心挑战在于假期状态与日期有关,需要在图结构中考虑时间维度。

问题转化:状态压缩与图的扩展

将原始图中的每个城市 u 与一周中的每一天 d 组合成一个"状态节点" (u, d)。如果一周有 W 天,那么总共有 N * W 个这样的状态节点。从状态节点 (u, d)(v, (d+1)%W) 存在一条有向边,当前提是:

  1. 城市 u 在第 d 天是假期('o')。
  2. 城市 v 在第 (d+1)%W 天是假期('o')。
  3. 原始图中的城市 uv 之间存在道路(包括 u == v 的自环,表示可以停留在原地)。

这样,问题就转化成了:在这个扩展的有向图中是否存在环?如果存在环,则说明可以无限期地在假期状态中循环,即存在无尽假期;否则不存在。

解决方案一:深度优先搜索 (DFS) 检测环

DFS 是一种常见的检测有向图中环的方法。我们可以维护两个布尔数组:

  • recursion_stack_visited[node_idx]:表示节点当前是否在 DFS 递归栈中(即当前路径上)。
  • fully_processed_no_cycle[node_idx]:表示节点及其所有可达路径都已被探索过,且没有发现环。

DFS 遍历图。如果遇到一个节点 v

  • 如果 fully_processed_no_cycle[v] 为真,说明从 v 出发无环,无需再次探索。
  • 如果 recursion_stack_visited[v] 为真,说明 v 已经在当前递归路径上,检测到了一条反向边,因此存在环。
  • 否则,将 v 标记为 recursion_stack_visited[v] = true,并递归对其邻居进行 DFS。如果递归调用返回真,则说明发现了环。
  • DFS 返回后,将 v 标记为 recursion_stack_visited[v] = false,并设置 fully_processed_no_cycle[v] = true

从每个未完全处理的节点开始 DFS,只要发现一个环,就可以立即判断为 "Yes"。

代码实现一:扩展图 + DFS 检测环

#include <iostream>
#include <vector>
#include <string>
#include <utility> // For std::pair

// 全局变量,简化 DFS 参数传递(在竞赛编程中常见)
int num_cities_global, num_days_in_week_global;
std::vector<std::string> city_holiday_schedule_global; // 'o' 表示假期,'x' 表示工作

// 扩展图:节点索引 = 城市ID * 一周天数 + 当天日期
std::vector<std::vector<int>> expanded_time_graph_global;
// recursion_stack_visited[node_idx] 为真表示节点当前在 DFS 递归栈中
std::vector<bool> recursion_stack_visited_global;
// fully_processed_no_cycle[node_idx] 为真表示节点及所有可达路径已探索且无环
std::vector<bool> fully_processed_no_cycle_global;

// DFS 函数用于检测有向图中的环
bool detect_cycle_dfs(int current_node_idx) {
    if (fully_processed_no_cycle_global[current_node_idx]) {
        return false; // 该节点已确认无环
    }
    if (recursion_stack_visited_global[current_node_idx]) {
        return true; // 发现反向边,存在环
    }

    recursion_stack_visited_global[current_node_idx] = true; // 标记当前节点在递归栈中

    for (int neighbor_node_idx : expanded_time_graph_global[current_node_idx]) {
        if (detect_cycle_dfs(neighbor_node_idx)) {
            return true; // 子递归发现环
        }
    }

    recursion_stack_visited_global[current_node_idx] = false; // 回溯:从递归栈中移除
    fully_processed_no_cycle_global[current_node_idx] = true; // 标记该节点已处理且无环
    return false;
}

void solve_endless_holidays_dfs() {
    int m_num_roads;
    std::cin >> num_cities_global >> m_num_roads;

    std::vector<std::pair<int, int>> road_connections(m_num_roads);
    for (int i = 0; i < m_num_roads; ++i) {
        std::cin >> road_connections[i].first >> road_connections[i].second;
        road_connections[i].first--;  // 调整为 0-indexed
        road_connections[i].second--; // 调整为 0-indexed
    }

    // 允许停留在同一城市 (自环)
    for (int i = 0; i < num_cities_global; ++i) {
        road_connections.push_back({i, i});
    }

    std::cin >> num_days_in_week_global;
    city_holiday_schedule_global.resize(num_cities_global);
    for (int i = 0; i < num_cities_global; ++i) {
        std::cin >> city_holiday_schedule_global[i];
    }

    int total_expanded_nodes = num_cities_global * num_days_in_week_global;
    expanded_time_graph_global.assign(total_expanded_nodes, std::vector<int>());

    // 构建扩展图
    for (const auto& edge_pair : road_connections) {
        int city_u = edge_pair.first;
        int city_v = edge_pair.second;

        for (int current_day = 0; current_day < num_days_in_week_global; ++current_day) {
            int next_day = (current_day + 1) % num_days_in_week_global;

            // 考虑从 city_u 移动到 city_v
            if (city_holiday_schedule_global[city_u][current_day] == 'o' &&
                city_holiday_schedule_global[city_v][next_day] == 'o') {
                int node_from = city_u * num_days_in_week_global + current_day;
                int node_to = city_v * num_days_in_week_global + next_day;
                expanded_time_graph_global[node_from].push_back(node_to);
            }

            // 考虑从 city_v 移动到 city_u (因为道路是双向的)
            if (city_holiday_schedule_global[city_v][current_day] == 'o' &&
                city_holiday_schedule_global[city_u][next_day] == 'o') {
                int node_from = city_v * num_days_in_week_global + current_day;
                int node_to = city_u * num_days_in_week_global + next_day;
                expanded_time_graph_global[node_from].push_back(node_to);
            }
        }
    }
    
    recursion_stack_visited_global.assign(total_expanded_nodes, false);
    fully_processed_no_cycle_global.assign(total_expanded_nodes, false);

    bool found_endless_holiday = false;
    for (int i = 0; i < total_expanded_nodes; ++i) {
        if (!fully_processed_no_cycle_global[i]) { // 只从未完全处理的节点开始 DFS
            if (detect_cycle_dfs(i)) {
                found_endless_holiday = true;
                break;
            }
        }
    }

    if (found_endless_holiday) {
        std::cout << "Yes\n";
    } else {
        std::cout << "No\n";
    }
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int test_cases;
    std::cin >> test_cases;
    while (test_cases--) {
        solve_endless_holidays_dfs();
    }
    return 0;
}

解决方案二:拓扑排序检测环

另一个检测有向图环的常用方法是拓扑排序(Kahn's algorithm)。如果一个有向无环图 (DAG) 可以被拓扑排序,那么所有节点都可以被排入一个线性序列。如果一个有向图包含环,那么它不能被完全拓扑排序,即拓扑排序后剩余的节点数量不等于总节点数量。

算法步骤:

  1. 计算图中每个节点的入度 (in-degree)。
  2. 将所有入度为 0 的节点加入队列。
  3. 当队列非空时,取出一个节点 u,并将其从图中"移除"(即增加已处理节点计数)。 对于 u 的每一个邻居 v
    • v 的入度减 1。
    • 如果 v 的入度变为 0,将其加入队列。
  4. 如果最终处理的节点数量等于总节点数量,则图是 DAG (无环);否则,图中存在环。

代码实现二:扩展图 + 拓扑排序检测环

#include <iostream>
#include <vector>
#include <string>
#include <utility> // For std::pair
#include <queue>   // For std::queue

void solve_endless_holidays_topo() {
    int num_cities_val, num_roads_val;
    std::cin >> num_cities_val >> num_roads_val;

    std::vector<std::pair<int, int>> raw_road_edges;
    for (int i = 0; i < num_roads_val; ++i) {
        int u, v;
        std::cin >> u >> v;
        raw_road_edges.push_back({u - 1, v - 1}); // 调整为 0-indexed
    }

    // 允许停留在同一城市 (自环)
    for (int i = 0; i < num_cities_val; ++i) {
        raw_road_edges.push_back({i, i});
    }

    int weekly_cycle_length;
    std::cin >> weekly_cycle_length;
    std::vector<std::string> city_holiday_status(num_cities_val);
    for (int i = 0; i < num_cities_val; ++i) {
        std::cin >> city_holiday_status[i];
    }

    // 扩展图节点索引 = 城市ID * 一周天数 + 当天日期
    int total_expanded_nodes = num_cities_val * weekly_cycle_length;
    std::vector<std::vector<int>> adjacency_list(total_expanded_nodes);
    std::vector<int> in_degree(total_expanded_nodes, 0);

    // 构建扩展图并计算入度
    for (const auto& edge_pair : raw_road_edges) {
        int city1 = edge_pair.first;
        int city2 = edge_pair.second;

        for (int current_day = 0; current_day < weekly_cycle_length; ++current_day) {
            int next_day = (current_day + 1) % weekly_cycle_length;

            // 从 city1 移动到 city2
            if (city_holiday_status[city1][current_day] == 'o' &&
                city_holiday_status[city2][next_day] == 'o') {
                int node_from = city1 * weekly_cycle_length + current_day;
                int node_to = city2 * weekly_cycle_length + next_day;
                adjacency_list[node_from].push_back(node_to);
                in_degree[node_to]++;
            }

            // 从 city2 移动到 city1 (因为道路是双向的)
            if (city_holiday_status[city2][current_day] == 'o' &&
                city_holiday_status[city1][next_day] == 'o') {
                int node_from = city2 * weekly_cycle_length + current_day;
                int node_to = city1 * weekly_cycle_length + next_day;
                adjacency_list[node_from].push_back(node_to);
                in_degree[node_to]++;
            }
        }
    }

    // 执行 Kahn's 算法 (拓扑排序)
    std::queue<int> q;
    for (int i = 0; i < total_expanded_nodes; ++i) {
        if (in_degree[i] == 0) {
            q.push(i);
        }
    }

    int nodes_processed_count = 0;
    while (!q.empty()) {
        int current_node = q.front();
        q.pop();
        nodes_processed_count++;

        for (int neighbor : adjacency_list[current_node]) {
            in_degree[neighbor]--;
            if (in_degree[neighbor] == 0) {
                q.push(neighbor);
            }
        }
    }

    // 如果未处理所有节点,则存在环,表示可能无尽假期。
    if (nodes_processed_count == total_expanded_nodes) {
        std::cout << "No\n"; // 所有节点均可被拓扑排序,无环。
    } else {
        std::cout << "Yes\n"; // 检测到环。
    }
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int test_cases;
    std::cin >> test_cases;
    while (test_cases--) {
        solve_endless_holidays_topo();
    }
    return 0;
}

F - 规划假期

思路分析

"规划假期"通常涉及在一个序列中选择一个最优的连续子序列,以最小化或最大化某个目标函数。此题采用了一种高效的方法:使用线段树结合矩阵乘法(min-plus 代数)来解决。这种技术常用于处理带有状态转换和范围查询的动态规划问题。

核心思想是将每个位置 i 的信息编码为一个 2x2 的状态转换矩阵 M_i。线段树的每个节点存储一个矩阵,该矩阵是其子节点矩阵的乘积。对于一个查询区间 [L, R),线段树可以快速计算出 M_R-1 * ... * M_L 的乘积矩阵。这个乘积矩阵包含了从 LR-1 整个区间的累计状态转换成本。

矩阵结构与含义

我们定义一个 2x2 的矩阵 StateMatrix,其四个元素分别代表:

  • StateMatrix[0] (即 M[0][0]): 从左边界的"状态0"到右边界的"状态0"的最小成本。
  • StateMatrix[1] (即 M[0][1]): 从左边界的"状态0"到右边界的"状态1"的最小成本。
  • StateMatrix[2] (即 M[1][0]): 从左边界的"状态1"到右边界的"状态0"的最小成本。
  • StateMatrix[3] (即 M[1][1]): 从左边界的"状态1"到右边界的"状态1"的最小成本。

在这里,"状态0"和"状态1"的具体含义取决于问题定义。通常可能表示"未选择"/"已选择"、"非活跃"/"活跃"等。

对于单个元素 A[i] (表示 daily_costs[i]),其对应的初始矩阵 M_i 定义为 {INF, 0, A[i], A[i]}。根据这个定义,我们可以推断:

  • M_i[0][0] = INF:从状态0到状态0的成本是无穷大。这通常意味着这种状态转换被禁止或不适用于累计。
  • M_i[0][1] = 0:从状态0到状态1的成本是 0。
  • M_i[1][0] = A[i]:从状态1到状态0的成本是 A[i]
  • M_i[1][1] = A[i]:从状态1到状态1的成本是 A[i]

具体的语义需要结合原始问题描述才能完全确定,但这种矩阵通常用于处理选择和不选择元素,或者在特定约束下进行状态转移的动态规划问题。

矩阵乘法 (Min-Plus 代数)

在线段树中合并两个矩阵 LR (分别代表左右子区间的状态)时,使用的是 min-plus 矩阵乘法。其定义为:

C[i][j] = min_k (L[i][k] + R[k][j])

在这个问题中,线段树的合并操作 op(l, r) 实际上计算的是 matrix_multiply(r, l)。这意味着线段树在查询区间 [L, R) 时,它将从右到左地将矩阵相乘:M_{R-1} * ... * M_L

查询与答案计算

题目要求找到一个长度为 K 的连续子区间 [l, l+K),使得某个成本最小。通过遍历所有可能的起始位置 l,并对每个区间 [l, l+K) 进行线段树查询,得到一个合并后的矩阵 P

答案的计算逻辑如下:

  • overall_min_cost = min(overall_min_cost, P[2])P[2] 对应 P[1][0],表示从区间左边界的状态1到右边界的状态0的最小成本。
  • if (l > 0) overall_min_cost = min(overall_min_cost, daily_costs[l-1] + P[3]):如果区间不是从字符串最左端开始,还会考虑区间前一个元素的成本 daily_costs[l-1] 加上 P[3] (对应 P[1][1],表示从左边界的状态1到右边界的状态1的最小成本)。这暗示了区间外的元素可能也会影响总成本。

这是一种灵活且强大的解决区间 DP 问题的技巧,特别适合于需要快速查询不同长度或位置子区间最优解的场景。

代码实现

#include <iostream>
#include <vector>
#include <array>
#include <algorithm> // For std::min

// AtCoder 库中的线段树实现
// 在实际编译时,需要确保 atcoder/segtree.hpp 文件可用
#include <atcoder/segtree>

const long long INF_COST = 1e18; // 定义一个足够大的值作为无穷大成本

// 使用 std::array<long long, 4> 表示一个 2x2 的矩阵
// 对应顺序:M[0][0], M[0][1], M[1][0], M[1][1]
using StateMatrix = std::array<long long, 4>;

// 实现 min-plus 矩阵乘法
// C[i][j] = min_k (A[i][k] + B[k][j])
StateMatrix matrix_multiply(StateMatrix left_mat, StateMatrix right_mat) {
    StateMatrix result_mat{INF_COST, INF_COST, INF_COST, INF_COST};

    // 计算 result_mat[0] (C[0][0])
    result_mat[0] = std::min(left_mat[0] + right_mat[0], left_mat[1] + right_mat[2]);
    // 计算 result_mat[1] (C[0][1])
    result_mat[1] = std::min(left_mat[0] + right_mat[1], left_mat[1] + right_mat[3]);
    // 计算 result_mat[2] (C[1][0])
    result_mat[2] = std::min(left_mat[2] + right_mat[0], left_mat[3] + right_mat[2]);
    // 计算 result_mat[3] (C[1][1])
    result_mat[3] = std::min(left_mat[2] + right_mat[1], left_mat[3] + right_mat[3]);

    return result_mat;
}

// 线段树的合并操作:在此题中是 `matrix_multiply(右子节点矩阵, 左子节点矩阵)`
// 这意味着线段树的 prod(l, r) 操作会计算 data[r-1] * ... * data[l]
StateMatrix segment_tree_merge_operation(StateMatrix right_segment_mat, StateMatrix left_segment_mat) {
    return matrix_multiply(right_segment_mat, left_segment_mat);
}

// 矩阵乘法的单位元(对应 min-plus 代数的单位矩阵)
// 对于 2x2 矩阵,单位元为 {{0, INF}, {INF, 0}}
StateMatrix identity_element() {
    return StateMatrix{0, INF_COST, INF_COST, 0};
}

void solve_plan_holidays() {
    int total_days_N, window_length_K;
    std::cin >> total_days_N >> window_length_K;

    std::vector<long long> daily_costs(total_days_N);
    for (int i = 0; i < total_days_N; ++i) {
        std::cin >> daily_costs[i];
    }

    std::vector<StateMatrix> segment_tree_initial_data(total_days_N);
    // 为每个日期 i 构建其对应的 2x2 状态转换矩阵
    // M = {{INF, 0}, {A[i], A[i]}}
    for (int i = 0; i < total_days_N; ++i) {
        segment_tree_initial_data[i] = StateMatrix{INF_COST, 0, daily_costs[i], daily_costs[i]};
    }

    // 初始化线段树
    atcoder::segtree<StateMatrix, segment_tree_merge_operation, identity_element> seg_tree(segment_tree_initial_data);

    long long overall_min_cost = INF_COST; // 存储全局最小成本

    // 遍历所有可能的长度为 K 的连续窗口 [left_idx, left_idx + window_length_K)
    for (int left_idx = 0; left_idx + window_length_K <= total_days_N; ++left_idx) {
        // 查询当前窗口的合并矩阵
        StateMatrix query_matrix = seg_tree.prod(left_idx, left_idx + window_length_K);

        // 根据问题定义,更新最小成本
        // query_matrix[2] 对应合并矩阵的 M[1][0] 元素
        overall_min_cost = std::min(overall_min_cost, query_matrix[2]);

        // 如果当前窗口不是从最左侧开始的,则考虑窗口前一个日的成本
        // query_matrix[3] 对应合并矩阵的 M[1][1] 元素
        if (left_idx > 0) {
            overall_min_cost = std::min(overall_min_cost, daily_costs[left_idx - 1] + query_matrix[3]);
        }
    }

    std::cout << overall_min_cost << "\n";
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int test_cases_count;
    std::cin >> test_cases_count;
    while (test_cases_count--) {
        solve_plan_holidays();
    }
    return 0;
}

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。