核心总结
编程题分为五类,A/B/C类各四道变种,D/E类各一道核心题。
能源传输线路 与才艺表演排列 两类题目为偏易,分别聚焦滑动窗口+双端队列、全排列枚举验证逻辑,侧重基础算法应用与边界处理;
工匠帝国 以动态规划为核心,需掌握01/完全背包组合应用,难度中等;
圆环馆连通性判断 与队列链判断 综合数学映射/并查集、LCA/线段树等,涉及多算法融合与大数据优化,难度中等偏上。
整体适配算法入门至进阶阶段学习者
文章目录
- 核心总结
- [A - 能源传输线路](#A - 能源传输线路)
-
- [A1 - 能源传输线路(能量阈值600含,输出YES/NO)](#A1 - 能源传输线路(能量阈值600含,输出YES/NO))
- [A2 - 能源传输线路(能量阈值500含,输出OK/-1)](#A2 - 能源传输线路(能量阈值500含,输出OK/-1))
- [A3 - 能源传输线路(能量阈值500含,输出YES/NO)](#A3 - 能源传输线路(能量阈值500含,输出YES/NO))
- [A4 - 能源传输线路(能量阈值600含,输出OK/-1)](#A4 - 能源传输线路(能量阈值600含,输出OK/-1))
- [B - 工匠帝国](#B - 工匠帝国)
-
- [B1 - 工匠帝国(价格阈值5000含,输出YES/NO)](#B1 - 工匠帝国(价格阈值5000含,输出YES/NO))
- [B2 - 工匠帝国(价格阈值6000含,输出OK/-1)](#B2 - 工匠帝国(价格阈值6000含,输出OK/-1))
- [B3 - 工匠帝国(价格阈值6000含,输出YES/NO)](#B3 - 工匠帝国(价格阈值6000含,输出YES/NO))
- [B4 - 工匠帝国(价格阈值5000含,输出OK/-1)](#B4 - 工匠帝国(价格阈值5000含,输出OK/-1))
- [C - 才艺表演排列](#C - 才艺表演排列)
-
- [C1 - 才艺表演排列(剔除类型N,输出YES/NO)](#C1 - 才艺表演排列(剔除类型N,输出YES/NO))
- [C2 - 才艺表演排列(剔除类型P,输出OK/-1)](#C2 - 才艺表演排列(剔除类型P,输出OK/-1))
- [C3 - 才艺表演排列(剔除类型P,输出YES/NO)](#C3 - 才艺表演排列(剔除类型P,输出YES/NO))
- [C4 - 才艺表演排列(剔除类型N,输出OK/-1)](#C4 - 才艺表演排列(剔除类型N,输出OK/-1))
- [D - 圆环馆连通性判断](#D - 圆环馆连通性判断)
- [E - 队列链判断(树结构队列操作)](#E - 队列链判断(树结构队列操作))
A - 能源传输线路
相似题目差异总结:
- 核心差异:能量筛选阈值不同(600/500),输出标识不同(YES/NO / OK/-1),其余场景逻辑一致。
- 核心算法:一致采用「筛选有效节点→滑动窗口+双端队列维护区间极值→收缩窗口保证差值合规→统计最长有效区间」的线性时间策略。
- 优化点:vector预分配空间、关闭IO同步、双端队列O(1)维护区间最大/最小值,规避暴力枚举区间的O(n²)复杂度。
A1 - 能源传输线路(能量阈值600含,输出YES/NO)
题目描述:在银河系边缘的殖民星球上,科学家们建立了一个复杂的能源传输线路。网络由n个能源节点组成,每个节点有一定的初始能量值。由于星际风暴的影响,网络中的能量分布不均匀。为了优化能源分配,工程师希望找到一段最长的连续区间(至少包含两个能源节点),使得区间内能量最大值与最小值的差不超过阈值K,若存在这个区间,则首先输出一行"YES",若不存在这段区间,输出"NO"。因当前的技术限制,科学家们无法对能量超过600(含)的节点进行操作,因此计算线路时需暂时忽略这些节点。
运行条件:
- 总时限:5000毫秒
- 单组时限:1000毫秒
- 总内存:320 MB
- 单组内存:64 MB
输入格式:
- 第一行包含两个整数n和k,表示节点数量和能量差阈值
- 第二行包含n个整数a1,a2,...,an,表示每个节点的初始能量值
输出格式:输出包含一或两行,若所求区间存在,则首先输出一行"YES",之后第二行输出一个整数,表示满足条件的最长连续区间长度。若不存在这段区间,输出"NO"。
输入输出样例:
-
样例1:
输入 :plaintext8 3 1 601 5 602 6 655 7 8输出 :
plaintextYES 4 -
样例2:
输入 :plaintext7 1 1 10 20 30 40 566 543输出 :
plaintextNO
代码:
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
// 关闭同步加速输入输出
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int node_count, diff_threshold;
cin >> node_count >> diff_threshold;
// 存储筛选后的有效节点(能量<600)
vector<int> valid_nodes;
valid_nodes.reserve(node_count); // 预分配空间提升效率
// 筛选符合条件的节点
for (int i = 0; i < node_count; ++i) {
int energy;
cin >> energy;
if (energy < 600) { // 忽略能量≥600的节点
valid_nodes.push_back(energy);
}
}
int valid_count = valid_nodes.size();
int left_ptr = 0; // 滑动窗口左边界
int max_length = 0; // 记录最长有效区间长度
// 双端队列:分别维护区间内最大值和最小值的索引
deque<int> max_idx_deque;
deque<int> min_idx_deque;
// 滑动窗口右边界遍历
for (int right_ptr = 0; right_ptr < valid_count; ++right_ptr) {
// 维护最大值队列:队尾元素≤当前元素则弹出,保证队首是最大值索引
while (!max_idx_deque.empty() && valid_nodes[max_idx_deque.back()] <= valid_nodes[right_ptr]) {
max_idx_deque.pop_back();
}
max_idx_deque.push_back(right_ptr);
// 维护最小值队列:队尾元素≥当前元素则弹出,保证队首是最小值索引
while (!min_idx_deque.empty() && valid_nodes[min_idx_deque.back()] >= valid_nodes[right_ptr]) {
min_idx_deque.pop_back();
}
min_idx_deque.push_back(right_ptr);
// 若区间最大最小差值超过阈值,移动左边界收缩窗口
while (!max_idx_deque.empty() && !min_idx_deque.empty()) {
int current_max = valid_nodes[max_idx_deque.front()];
int current_min = valid_nodes[min_idx_deque.front()];
if (current_max - current_min > diff_threshold) {
// 左边界移动时,移除队列中超出窗口的索引
if (max_idx_deque.front() == left_ptr) {
max_idx_deque.pop_front();
}
if (min_idx_deque.front() == left_ptr) {
min_idx_deque.pop_front();
}
left_ptr++;
} else {
break; // 差值符合条件,无需收缩
}
}
// 更新最长区间长度
int current_window_len = right_ptr - left_ptr + 1;
if (current_window_len > max_length) {
max_length = current_window_len;
}
}
// 输出结果:至少包含两个节点才有效
if (max_length >= 2) {
cout << "YES\n" << max_length;
} else {
cout << "NO";
}
return 0;
}
A2 - 能源传输线路(能量阈值500含,输出OK/-1)
题目描述:在银河系边缘的殖民星球上,科学家们建立了一个复杂的能源传输线路。网络由n个能源节点组成,每个节点有一定的初始能量值。由于星际风暴的影响,网络中的能量分布不均匀。为了优化能源分配,工程师希望找到一段最长的连续区间(至少包含两个能源节点),使得区间内能量最大值与最小值的差不超过给定的阈值k,若存在这个区间,则首先输出一行"OK",若不存在这段区间,输出"-1"。因当前的技术限制,科学家们无法对能量超过500(含)的节点进行操作,因此计算线路时需暂时忽略这些节点。
运行条件:
- 总时限:5000毫秒
- 单组时限:1000毫秒
- 总内存:320 MB
- 单组内存:64 MB
输入格式:
- 第一行包含两个整数n和k,表示节点数量和能量差阈值
- 第二行包含n个整数a1,a2,...,an,表示每个节点的初始能量值
输出格式:输出包含一或两行,若所求区间存在,则首先输出一行"OK",之后第二行输出一个整数,表示满足条件的最长连续区间长度。若不存在这段区间,输出"-1"。
输入输出样例:
-
样例1:
输入 :plaintext8 3 1 501 5 502 6 555 7 8输出 :
plaintextOK 4 -
样例2:
输入 :plaintext7 1 1 10 20 30 40 566 543输出 :
plaintext-1
代码:
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
// 加速输入输出效率
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n_total, k_limit;
cin >> n_total >> k_limit;
// 存储能量<500的有效节点
vector<int> qualified_nodes;
qualified_nodes.reserve(n_total);
// 筛选有效节点,排除能量≥500的节点
for (int i = 0; i < n_total; ++i) {
int val;
cin >> val;
if (val < 500) {
qualified_nodes.emplace_back(val);
}
}
int m = qualified_nodes.size();
int left = 0;
int longest_len = 0;
// 双端队列维护区间最大、最小值索引
deque<int> max_deque;
deque<int> min_deque;
// 扩展窗口右边界
for (int right = 0; right < m; ++right) {
// 维护最大值队列:保持队首为当前区间最大值
while (!max_deque.empty() && qualified_nodes[max_deque.back()] <= qualified_nodes[right]) {
max_deque.pop_back();
}
max_deque.push_back(right);
// 维护最小值队列:保持队首为当前区间最小值
while (!min_deque.empty() && qualified_nodes[min_deque.back()] >= qualified_nodes[right]) {
min_deque.pop_back();
}
min_deque.push_back(right);
// 收缩窗口:当最大最小差值超过阈值时,移动左边界
while (true) {
if (max_deque.empty() || min_deque.empty()) break;
int range_max = qualified_nodes[max_deque.front()];
int range_min = qualified_nodes[min_deque.front()];
if (range_max - range_min > k_limit) {
if (max_deque.front() == left) max_deque.pop_front();
if (min_deque.front() == left) min_deque.pop_front();
left++;
} else {
break;
}
}
// 更新最长区间长度
int curr_len = right - left + 1;
if (curr_len > longest_len) {
longest_len = curr_len;
}
}
// 输出结果:区间长度至少为2才符合要求
if (longest_len >= 2) {
cout << "OK\n" << longest_len;
} else {
cout << "-1";
}
return 0;
}
A3 - 能源传输线路(能量阈值500含,输出YES/NO)
题目描述:在银河系边缘的殖民星球上,科学家们建立了一个复杂的能源传输线路。网络由n个能源节点组成,每个节点有一定的初始能量值。由于星际风暴的影响,网络中的能量分布不均匀。为了优化能源分配,工程师希望找到一段最长的连续区间(至少包含两个能源节点),使得区间内能量最大值与最小值的差不超过阈值K,若存在这个区间,则首先输出一行"YES",若不存在这段区间,输出"NO"。因当前的技术限制,科学家们无法对能量超过500(含)的节点进行操作,因此计算线路时需暂时忽略这些节点。
运行条件:
- 总时限:5000毫秒
- 单组时限:1000毫秒
- 总内存:320 MB
- 单组内存:64 MB
输入格式:
- 第一行包含两个整数n和k,表示节点数量和能量差阈值
- 第二行包含n个整数a1,a2,...,an,表示每个节点的初始能量值
输出格式:输出包含一或两行,若所求区间存在,则首先输出一行"YES",之后第二行输出一个整数,表示满足条件的最长连续区间长度。若不存在这段区间,输出"NO"。
输入输出样例:
-
样例1:
输入 :plaintext8 3 1 501 5 502 6 555 7 8输出 :
plaintextYES 4 -
样例2:
输入 :plaintext7 1 1 10 20 30 40 566 543输出 :
plaintextNO
代码:
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int node_num, threshold;
cin >> node_num >> threshold;
// 收集有效节点(能量小于500)
vector<int> valid_energy;
for (int i = 0; i < node_num; ++i) {
int e;
cin >> e;
if (e < 500) {
valid_energy.push_back(e);
}
}
int m = valid_energy.size();
int left = 0;
int max_valid_len = 0;
// 双端队列:存储索引,用于快速获取区间最大、最小值
deque<int> max_queue;
deque<int> min_queue;
// 滑动窗口遍历所有可能的右边界
for (int right = 0; right < m; ++right) {
// 维护最大值队列:移除所有小于当前元素的队尾元素
while (!max_queue.empty() && valid_energy[max_queue.back()] <= valid_energy[right]) {
max_queue.pop_back();
}
max_queue.push_back(right);
// 维护最小值队列:移除所有大于当前元素的队尾元素
while (!min_queue.empty() && valid_energy[min_queue.back()] >= valid_energy[right]) {
min_queue.pop_back();
}
min_queue.push_back(right);
// 检查当前区间是否符合条件,不符合则移动左边界
while (!max_queue.empty() && !min_queue.empty()) {
int current_max = valid_energy[max_queue.front()];
int current_min = valid_energy[min_queue.front()];
if (current_max - current_min > threshold) {
// 移除超出窗口的索引
if (max_queue.front() == left) max_queue.pop_front();
if (min_queue.front() == left) min_queue.pop_front();
left++;
} else {
break;
}
}
// 记录当前窗口的最大长度
int current_len = right - left + 1;
if (current_len > max_valid_len) {
max_valid_len = current_len;
}
}
// 判断是否存在至少两个节点的有效区间
if (max_valid_len >= 2) {
cout << "YES\n" << max_valid_len;
} else {
cout << "NO";
}
return 0;
}
A4 - 能源传输线路(能量阈值600含,输出OK/-1)
题目描述:在银河系边缘的殖民星球上,科学家们建立了一个复杂的能源传输线路。网络由n个能源节点组成,每个节点有一定的初始能量值。由于星际风暴的影响,网络中的能量分布不均匀。为了优化能源分配,工程师希望找到一段最长的连续区间(至少包含两个能源节点),使得区间内能量最大值与最小值的差不超过给定的阈值k,若存在这个区间,则首先输出一行"OK",若不存在这段区间,输出"-1"。因当前的技术限制,科学家们无法对能量超过600(含)的节点进行操作,因此计算线路时需暂时忽略这些节点。
运行条件:
- 总时限:5000毫秒
- 单组时限:1000毫秒
- 总内存:320 MB
- 单组内存:64 MB
输入格式:
- 第一行包含两个整数n和k,表示节点数量和能量差阈值
- 第二行包含n个整数a1,a2,...,an,表示每个节点的初始能量值
输出格式:输出包含一或两行,若所求区间存在,则首先输出一行"OK",之后第二行输出一个整数,表示满足条件的最长连续区间长度。若不存在这段区间,输出"-1"。
输入输出样例:
-
样例1:
输入 :plaintext8 3 1 601 5 602 6 655 7 8输出 :
plaintextOK 4 -
样例2:
输入 :plaintext7 1 1 10 20 30 40 566 543输出 :
plaintext-1
代码:
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int total_nodes, diff_limit;
cin >> total_nodes >> diff_limit;
// 筛选能量小于600的节点
vector<int> filtered_nodes;
filtered_nodes.reserve(total_nodes);
for (int i = 0; i < total_nodes; ++i) {
int energy_val;
cin >> energy_val;
if (energy_val < 600) {
filtered_nodes.push_back(energy_val);
}
}
int m = filtered_nodes.size();
int left_bound = 0;
int max_len = 0;
// 双端队列维护区间最大、最小值索引
deque<int> max_idx_q;
deque<int> min_idx_q;
// 遍历右边界,扩展窗口
for (int right_bound = 0; right_bound < m; ++right_bound) {
// 维护最大值队列:保证队首是当前区间最大值
while (!max_idx_q.empty() && filtered_nodes[max_idx_q.back()] <= filtered_nodes[right_bound]) {
max_idx_q.pop_back();
}
max_idx_q.push_back(right_bound);
// 维护最小值队列:保证队首是当前区间最小值
while (!min_idx_q.empty() && filtered_nodes[min_idx_q.back()] >= filtered_nodes[right_bound]) {
min_idx_q.pop_back();
}
min_idx_q.push_back(right_bound);
// 收缩窗口:当差值超过阈值时移动左边界
while (!max_idx_q.empty() && !min_idx_q.empty()) {
int range_max = filtered_nodes[max_idx_q.front()];
int range_min = filtered_nodes[min_idx_q.front()];
if (range_max - range_min > diff_limit) {
if (max_idx_q.front() == left_bound) max_idx_q.pop_front();
if (min_idx_q.front() == left_bound) min_idx_q.pop_front();
left_bound++;
} else {
break;
}
}
// 更新最长区间长度
int window_len = right_bound - left_bound + 1;
if (window_len > max_len) {
max_len = window_len;
}
}
// 输出结果
if (max_len >= 2) {
cout << "OK\n" << max_len;
} else {
cout << "-1";
}
return 0;
}
B - 工匠帝国
相似题目差异总结:
- 核心差异:价格筛选阈值不同(5000/6000),输出标识不同(YES/NO / OK/-1),其余问题模型一致。
- 核心算法:一致采用「筛选有效增益包→01背包处理首次购买+完全背包处理重复购买→DP数组维护最小花费→查询超额声望的最小花费」的动态规划策略。
- 优化点:DP数组预留超额声望空间、INF值标记无效状态、逆序/顺序遍历区分背包类型,保证大数据量下的时间效率。
B1 - 工匠帝国(价格阈值5000含,输出YES/NO)
题目描述:在一个工匠帝国,每年都会举行盛大的工艺盛典。来自各地的工匠们会在数日内展示他们的技艺,以获得评审团的声望值。经过多日竞争,各位工匠已经累积了一定的声望。盛典的最后一天,工匠们可以购买各种增益包来提升自己的声望值。每种增益包都可以无限次购买,且有两条特别规则:每种增益包第一次购买时,会额外获得一次性的首购奖励;之后再次购买同一种包,只能获得其基础声望。最终目标是:在最后一天获得至少S点额外声望,以争夺最终冠军。然而,增益包是有价格的,每位工匠都希望在达到至少S声望的前提下,花费最少的金钱,且若某增益包的价格超过5000(含),则认为该增益包为假货,不能够购买。现在请你帮某位工匠计算:若要达到至少S声望,最少需要花费多少钱?若能够通过购买增益包达到S点声望,则首先输出一行"YES",若无论如何都达不到,输出"NO"。
运行条件:
- 总时限:5000毫秒
- 单组时限:1000毫秒
- 总内存:320 MB
- 单组内存:64 MB
输入格式:
- 第一行输入两个整数n和S,表示增益包数量和目标声望值。
- 接下来n行,每行包含3个整数cost_i、base_value_i和bonus_i,分别代表第i种增益包的价格、该包每次购买基础获得的声望和该包首次购买额外获得的声望。
输出格式:输出包含一或两行,若能够通过购买增益包达到S点声望,则首先输出一行"YES",接下来第二行输出一个整数,表示达到至少S声望所需的最小花费。若无论如何都达不到,输出"NO"。
输入输出样例:
-
样例1:
输入 :plaintext1 100 10 0 5输出 :
plaintextNO -
样例2:
输入 :plaintext3 9 3 4 1 4 6 0 5100 10000 5000输出 :
plaintextYES 6
代码:
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const long long INF = 1e18; // 无穷大表示不可达
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int pack_num, target_rep;
if (!(cin >> pack_num >> target_rep)) {
cout << "NO" << endl;
return 0;
}
// dp[rep] 表示获得rep点声望的最小花费
int dp_size = target_rep + 20005; // 预留额外空间处理超额声望
vector<long long> dp(dp_size, INF);
dp[0] = 0; // 初始状态:0声望花费0元
// 遍历每个增益包
for (int i = 0; i < pack_num; ++i) {
int price, base_rep, extra_rep;
cin >> price >> base_rep >> extra_rep;
// 跳过价格≥5000的假货
if (price >= 5000) {
continue;
}
// 首次购买的总声望(基础+额外)
int first_buy_rep = base_rep + extra_rep;
// 后续购买的声望(仅基础)
int later_buy_rep = base_rep;
// 处理首次购买:01背包(每个包仅能首次购买一次)
for (int j = dp_size - 1; j >= first_buy_rep; --j) {
if (dp[j - first_buy_rep] != INF) {
dp[j] = min(dp[j], dp[j - first_buy_rep] + price);
}
}
// 处理后续购买:完全背包(可无限次购买)
if (later_buy_rep > 0) { // 基础声望为0时后续购买无意义
for (int j = later_buy_rep; j < dp_size; ++j) {
if (dp[j - later_buy_rep] != INF) {
dp[j] = min(dp[j], dp[j - later_buy_rep] + price);
}
}
}
}
// 查找达到目标声望的最小花费(包括超额情况)
long long min_cost = INF;
for (int j = target_rep; j < dp_size; ++j) {
if (dp[j] < min_cost) {
min_cost = dp[j];
}
}
// 输出结果
if (min_cost == INF) {
cout << "NO" << endl;
} else {
cout << "YES" << endl;
cout << min_cost << endl;
}
return 0;
}
B2 - 工匠帝国(价格阈值6000含,输出OK/-1)
题目描述:在一个工匠帝国,每年都会举行盛大的工艺盛典。来自各地的工匠们会在数日内展示他们的技艺,以获得评审团的声望值。经过多日竞争,各位工匠已经累积了一定的声望。盛典的最后一天,工匠们可以购买各种增益包来提升自己的声望值。每种增益包都可以无限次购买,且有两条特别规则:每种增益包第一次购买时,会额外获得一次性的首购奖励;之后再次购买同一种包,只能获得其基础声望。最终目标是:在最后一天获得至少S点额外声望,以争夺最终冠军。然而,增益包是有价格的,每位工匠都希望在达到至少S声望的前提下,花费最少的金钱,且若某增益包的价格超过6000(含),则认为该增益包为假货,不能够购买。现在请你帮某位工匠计算:若要达到至少S声望,最少需要花费多少钱?若能够通过购买增益包达到S点声望,则首先输出一行"OK",若无论如何都达不到,输出"-1"。
运行条件:
- 总时限:5000毫秒
- 单组时限:1000毫秒
- 总内存:320 MB
- 单组内存:64 MB
输入格式:
- 第一行输入两个整数n和S,表示增益包数量和目标声望值。
- 接下来输入n行,每行包含3个整数cost_i、base_value_i和bonus_i,分别代表第i种增益包的价格、该包每次购买基础获得的声望和该包首次购买额外获得的声望。
输出格式:输出包含一或两行,若能够通过购买增益包达到S点声望,则首先输出一行"OK",接下来第二行输出一个整数,表示达到至少S声望所需的最小花费。若无论如何都达不到,输出"-1"。
输入输出样例:
-
样例1:
输入 :plaintext1 100 10 0 5输出 :
plaintext-1 -
样例2:
输入 :plaintext3 9 3 4 1 4 6 0 6100 10000 5000输出 :
plaintextOK 6
代码:
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const long long INF = 1e18; // 定义无穷大,代表不可达状态
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n_packs, target_s;
if (!(cin >> n_packs >> target_s)) {
cout << "-1" << endl;
return 0;
}
// dp数组:dp[r]表示获得r点声望的最小花费
int dp_max = target_s + 20005; // 预留20005的超额空间
vector<long long> dp(dp_max, INF);
dp[0] = 0; // 初始状态:0声望花费0元
// 遍历每个增益包
for (int i = 0; i < n_packs; ++i) {
int cost, base, bonus;
cin >> cost >> base >> bonus;
// 跳过价格≥6000的假货
if (cost >= 6000) {
continue;
}
int first_rep = base + bonus; // 首次购买获得的总声望
int repeat_rep = base; // 重复购买获得的声望
// 处理首次购买(01背包问题,逆序遍历)
for (int r = dp_max - 1; r >= first_rep; --r) {
if (dp[r - first_rep] != INF) {
dp[r] = min(dp[r], dp[r - first_rep] + cost);
}
}
// 处理重复购买(完全背包问题,顺序遍历)
if (repeat_rep > 0) {
for (int r = repeat_rep; r < dp_max; ++r) {
if (dp[r - repeat_rep] != INF) {
dp[r] = min(dp[r], dp[r - repeat_rep] + cost);
}
}
}
}
// 查找达到目标声望的最小花费
long long min_expense = INF;
for (int r = target_s; r < dp_max; ++r) {
if (dp[r] < min_expense) {
min_expense = dp[r];
}
}
// 输出结果
if (min_expense == INF) {
cout << "-1" << endl;
} else {
cout << "OK" << endl;
cout << min_expense << endl;
}
return 0;
}
B3 - 工匠帝国(价格阈值6000含,输出YES/NO)
题目描述:在一个工匠帝国,每年都会举行盛大的工艺盛典。来自各地的工匠们会在数日内展示他们的技艺,以获得评审团的声望值。经过多日竞争,各位工匠已经累积了一定的声望。盛典的最后一天,工匠们可以购买各种增益包来提升自己的声望值。每种增益包都可以无限次购买,且有两条特别规则:每种增益包第一次购买时,会额外获得一次性的首购奖励;之后再次购买同一种包,只能获得其基础声望。最终目标是:在最后一天获得至少S点额外声望,以争夺最终冠军。然而,增益包是有价格的,每位工匠都希望在达到至少S声望的前提下,花费最少的金钱,且若某增益包的价格超过6000(含),则认为该增益包为假货,不能够购买。现在请你帮某位工匠计算:若要达到至少S声望,最少需要花费多少钱?若能够通过购买增益包达到S点声望,则首先输出一行"YES",若无论如何都达不到,输出"NO"。
运行条件:
- 总时限:5000毫秒
- 单组时限:1000毫秒
- 总内存:320 MB
- 单组内存:64 MB
输入格式:
- 第一行输入两个整数n和S,表示增益包数量和目标声望值。
- 接下来n行,每行包含3个整数cost_i、base_value_i和bonus_i,分别代表第i种增益包的价格、该包每次购买基础获得的声望和该包首次购买额外获得的声望。
输出格式:输出包含一或两行,若能够通过购买增益包达到S点声望,则首先输出一行"YES",接下来第二行输出一个整数,表示达到至少S声望所需的最小花费。若无论如何都达不到,输出"NO"。
输入输出样例:
-
样例1:
输入 :plaintext1 100 10 0 5输出 :
plaintextNO -
样例2:
输入 :plaintext3 9 3 4 1 4 6 0 6100 10000 5000输出 :
plaintextYES 6
代码:
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const long long INF = 1e18; // 无穷大标记
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int pack_count, target_rep;
cin >> pack_count >> target_rep;
// dp数组:存储获得对应声望的最小花费
int dp_len = target_rep + 20005;
vector<long long> min_cost(dp_len, INF);
min_cost[0] = 0; // 初始状态
// 处理每个增益包
for (int i = 0; i < pack_count; ++i) {
int c, b, bo;
cin >> c >> b >> bo;
// 排除价格≥6000的假货
if (c >= 6000) {
continue;
}
int first_get = b + bo; // 首次购买总声望
int more_get = b; // 后续购买声望
// 首次购买:01背包,逆序遍历避免重复
for (int j = dp_len - 1; j >= first_get; --j) {
if (min_cost[j - first_get] != INF) {
min_cost[j] = min(min_cost[j], min_cost[j - first_get] + c);
}
}
// 后续购买:完全背包,顺序遍历允许重复
if (more_get > 0) {
for (int j = more_get; j < dp_len; ++j) {
if (min_cost[j - more_get] != INF) {
min_cost[j] = min(min_cost[j], min_cost[j - more_get] + c);
}
}
}
}
// 查找最小花费(包括超额声望)
long long result = INF;
for (int j = target_rep; j < dp_len; ++j) {
if (min_cost[j] < result) {
result = min_cost[j];
}
}
// 输出结果
if (result == INF) {
cout << "NO" << endl;
} else {
cout << "YES" << endl;
cout << result << endl;
}
return 0;
}
B4 - 工匠帝国(价格阈值5000含,输出OK/-1)
题目描述:在一个工匠帝国,每年都会举行盛大的工艺盛典。来自各地的工匠们会在数日内展示他们的技艺,以获得评审团的声望值。经过多日竞争,各位工匠已经累积了一定的声望。盛典的最后一天,工匠们可以购买各种增益包来提升自己的声望值。每种增益包都可以无限次购买,且有两条特别规则:每种增益包第一次购买时,会额外获得一次性的首购奖励;之后再次购买同一种包,只能获得其基础声望。最终目标是:在最后一天获得至少S点额外声望,以争夺最终冠军。然而,增益包是有价格的,每位工匠都希望在达到至少S声望的前提下,花费最少的金钱,且若某增益包的价格超过5000(含),则认为该增益包为假货,不能够购买。现在请你帮某位工匠计算:若要达到至少S声望,最少需要花费多少钱?若能够通过购买增益包达到S点声望,则首先输出一行"OK",若无论如何都达不到,输出"-1"。
运行条件:
- 总时限:5000毫秒
- 单组时限:1000毫秒
- 总内存:320 MB
- 单组内存:64 MB
输入格式:
- 第一行输入两个整数n和S,表示增益包数量和目标声望值。
- 接下来输入n行,每行包含3个整数cost_i、base_value_i和bonus_i,分别代表第i种增益包的价格、该包每次购买基础获得的声望和该包首次购买额外获得的声望。
输出格式:输出包含一或两行,若能够通过购买增益包达到S点声望,则首先输出一行"OK",接下来第二行输出一个整数,表示达到至少S声望所需的最小花费。若无论如何都达不到,输出"-1"。
输入输出样例:
-
样例1:
输入 :plaintext1 100 10 0 5输出 :
plaintext-1 -
样例2:
输入 :plaintext3 9 3 4 1 4 6 0 5100 10000 5000输出 :
plaintextOK 6
代码:
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const long long INF = 1e18; // 定义无穷大
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, s_target;
if (!(cin >> n >> s_target)) {
cout << "-1" << endl;
return 0;
}
// dp[r] 表示获得r点声望的最小花费
int dp_cap = s_target + 20005;
vector<long long> dp(dp_cap, INF);
dp[0] = 0; // 初始状态
// 遍历所有增益包
for (int i = 0; i < n; ++i) {
int cost, base_rep, bonus_rep;
cin >> cost >> base_rep >> bonus_rep;
// 跳过价格≥5000的假货
if (cost >= 5000) {
continue;
}
int first_total = base_rep + bonus_rep; // 首次购买总声望
int repeat_total = base_rep; // 重复购买声望
// 处理首次购买(01背包)
for (int r = dp_cap - 1; r >= first_total; --r) {
if (dp[r - first_total] != INF) {
dp[r] = min(dp[r], dp[r - first_total] + cost);
}
}
// 处理重复购买(完全背包)
if (repeat_total > 0) {
for (int r = repeat_total; r < dp_cap; ++r) {
if (dp[r - repeat_total] != INF) {
dp[r] = min(dp[r], dp[r - repeat_total] + cost);
}
}
}
}
// 查找最小花费
long long min_money = INF;
for (int r = s_target; r < dp_cap; ++r) {
if (dp[r] < min_money) {
min_money = dp[r];
}
}
// 输出结果
if (min_money == INF) {
cout << "-1" << endl;
} else {
cout << "OK" << endl;
cout << min_money << endl;
}
return 0;
}
C - 才艺表演排列
相似题目差异总结:
- 核心差异:剔除的才艺类型不同(N/P),输出标识不同(YES/NO / OK/-1),排列验证逻辑一致。
- 核心算法:一致采用「筛选有效学生→next_permutation生成全排列→验证相邻才艺类型→收集并输出有效排列」的枚举验证策略。
- 优化点:利用STL内置排列函数生成字典序序列、vector存储有效结果,避免手动排列生成的复杂逻辑,保证代码简洁性。
C1 - 才艺表演排列(剔除类型N,输出YES/NO)
题目描述:学校要举办一场才艺表演,有n个学生报名参加。每个学生有一个编号(从1到n)和一个才艺类型(用大写字母表示,例如,A代表唱歌,B代表跳舞,C代表乐器等)。老师发现有些才艺类型如果连续表演会影响效果,所以规定相同才艺类型的学生不能连续出场。另外,因表演主题限制,类型为"N"(小品)的才艺不能出现在本次表演中,排列顺序前需剔除才艺类型为"N"的同学,剔除后需对剩余的学生重新编号。请你帮助老师生成所有满足条件的出场顺序,给定一个整数n和一个长度为n的字符串,表示n个学生的才艺类型。要求生成所有可能的出场顺序,并且满足相邻位置的学生才艺类型不能相同的条件,按字典序输出所有满足条件的序列。如果能够输出若干的出场顺序,则先输出一行"YES",如果没有满足条件的序列,则输出"NO"。
运行条件:
- 总时限:5000毫秒
- 单组时限:1000毫秒
- 总内存:320 MB
- 单组内存:64 MB
输入格式:
- 第一行是一个整数n。
- 第二行是一个长度为n的字符串,由大写字母组成,表示第1到第n个学生的才艺类型。
输出格式:输出包含若干行,若能够输出若干的出场顺序,则先输出一行"YES",接下来若干行输出所有满足条件的出场顺序,每行一个序列。否则只输出一个"NO"。
输入输出样例:
-
样例1:
输入 :plaintext5 ANBNC输出 :
plaintextYES 123 132 213 231 312 321 -
样例2:
输入 :plaintext5 ANBNA输出 :
plaintextYES 123 321
代码:
cpp
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
using namespace std;
int main() {
int student_count;
if (!(cin >> student_count)) {
return 0;
}
string talent_str;
cin >> talent_str;
// 筛选出才艺类型不是'N'的学生
vector<char> valid_talents;
for (char t : talent_str) {
if (t != 'N') {
valid_talents.push_back(t);
}
}
int m = valid_talents.size();
// 无有效学生时直接输出YES
if (m == 0) {
cout << "YES" << endl;
return 0;
}
// 生成初始编号序列(1-based)
vector<int> order(m);
iota(order.begin(), order.end(), 1);
vector<vector<int>> valid_orders; // 存储所有有效排列
// 遍历所有全排列
do {
bool is_valid = true;
// 检查相邻学生才艺类型是否不同
for (int i = 0; i < m - 1; ++i) {
// 当前位置和下一个位置的才艺类型
char curr_t = valid_talents[order[i] - 1];
char next_t = valid_talents[order[i + 1] - 1];
if (curr_t == next_t) {
is_valid = false;
break;
}
}
// 有效排列加入结果集
if (is_valid) {
valid_orders.push_back(order);
}
} while (next_permutation(order.begin(), order.end()));
// 输出结果
if (valid_orders.empty()) {
cout << "NO" << endl;
} else {
cout << "YES" << endl;
for (auto &seq : valid_orders) {
for (int num : seq) {
cout << num;
}
cout << endl;
}
}
return 0;
}
C2 - 才艺表演排列(剔除类型P,输出OK/-1)
题目描述:学校要举办一场才艺表演,有n个学生报名参加。每个学生有一个编号(从1到n)和一个才艺类型(用大写字母表示,例如,A代表唱歌,B代表跳舞,C代表乐器等)。老师发现有些才艺类型如果连续表演会影响效果,所以规定相同才艺类型的学生不能连续出场。另外,因表演主题限制,类型为"P"(小品)的才艺不能出现在本次表演中,排列顺序前需剔除才艺类型为"P"的同学,剔除后需对剩余的学生重新编号。请你帮助老师生成所有满足条件的出场顺序,给定一个整数n和一个长度为n的字符串,表示n个学生的才艺类型。要求生成所有可能的出场顺序,并且满足相邻位置的学生才艺类型不能相同的条件,按字典序输出所有满足条件的序列。如果能够输出若干的出场顺序,则先输出一行"OK",如果没有满足条件的序列,则输出"-1"。
运行条件:
- 总时限:5000毫秒
- 单组时限:1000毫秒
- 总内存:320 MB
- 单组内存:64 MB
输入格式:
- 第一行是一个整数n。
- 第二行是一个长度为n的字符串,由大写字母组成,表示第1到第n个学生的才艺类型。
输出格式:输出包含若干行,若能够输出若干的出场顺序,则先输出一行"OK",接下来若干行输出所有满足条件的出场顺序,每行一个序列。否则只输出一个"-1"。
输入输出样例:
-
样例1:
输入 :plaintext5 APBPC输出 :
plaintextOK 123 132 213 231 312 321 -
样例2:
输入 :plaintext5 APBAP输出 :
plaintextOK 123 321
代码:
cpp
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
using namespace std;
int main() {
int n;
if (!(cin >> n)) {
return 0;
}
string talents;
cin >> talents;
// 筛选剔除类型为'P'的学生
vector<char> filtered_talents;
for (char c : talents) {
if (c != 'P') {
filtered_talents.push_back(c);
}
}
int m = filtered_talents.size();
// 无有效学生时输出OK
if (m == 0) {
cout << "OK" << endl;
return 0;
}
// 生成1~m的编号序列
vector<int> perm(m);
iota(perm.begin(), perm.end(), 1);
vector<vector<int>> valid_perms; // 存储有效排列
// 遍历所有全排列
do {
bool valid = true;
// 检查相邻才艺是否相同
for (int i = 0; i < m - 1; ++i) {
int idx1 = perm[i] - 1;
int idx2 = perm[i + 1] - 1;
if (filtered_talents[idx1] == filtered_talents[idx2]) {
valid = false;
break;
}
}
if (valid) {
valid_perms.push_back(perm);
}
} while (next_permutation(perm.begin(), perm.end()));
// 输出结果
if (valid_perms.empty()) {
cout << "-1" << endl;
} else {
cout << "OK" << endl;
for (auto &p : valid_perms) {
for (int num : p) {
cout << num;
}
cout << endl;
}
}
return 0;
}
C3 - 才艺表演排列(剔除类型P,输出YES/NO)
题目描述:学校要举办一场才艺表演,有n个学生报名参加。每个学生有一个编号(从1到n)和一个才艺类型(用大写字母表示,例如,A代表唱歌,B代表跳舞,C代表乐器等)。老师发现有些才艺类型如果连续表演会影响效果,所以规定相同才艺类型的学生不能连续出场。另外,因表演主题限制,类型为"P"(小品)的才艺不能出现在本次表演中,排列顺序前需剔除才艺类型为"P"的同学,剔除后需对剩余的学生重新编号。请你帮助老师生成所有满足条件的出场顺序,给定一个整数n和一个长度为n的字符串,表示n个学生的才艺类型。要求生成所有可能的出场顺序,并且满足相邻位置的学生才艺类型不能相同的条件,按字典序输出所有满足条件的序列。如果能够输出若干的出场顺序,则先输出一行"YES",如果没有满足条件的序列,则输出"NO"。
运行条件:
- 总时限:5000毫秒
- 单组时限:1000毫秒
- 总内存:320 MB
- 单组内存:64 MB
输入格式:
- 第一行是一个整数n。
- 第二行是一个长度为n的字符串,由大写字母组成,表示第1到第n个学生的才艺类型。
输出格式:输出包含若干行,若能够输出若干的出场顺序,则先输出一行"YES",接下来若干行输出所有满足条件的出场顺序,每行一个序列。否则只输出一个"NO"。
输入输出样例:
-
样例1:
输入 :plaintext5 APBPC输出 :
plaintextYES 123 132 213 231 312 321 -
样例2:
输入 :plaintext5 APBAP输出 :
plaintextYES 123 321
代码:
cpp
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
using namespace std;
int main() {
int total_students;
if (!(cin >> total_students)) {
return 0;
}
string talent_types;
cin >> talent_types;
// 筛选有效学生(剔除类型'P')
vector<char> valid_types;
for (char t : talent_types) {
if (t != 'P') {
valid_types.push_back(t);
}
}
int m = valid_types.size();
if (m == 0) {
cout << "YES" << endl;
return 0;
}
// 生成编号序列
vector<int> num_seq(m);
iota(num_seq.begin(), num_seq.end(), 1);
vector<vector<int>> valid_sequences; // 存储有效排列
// 遍历所有全排列
do {
bool is_ok = true;
// 检查相邻才艺是否不同
for (int i = 0; i < m - 1; ++i) {
char t1 = valid_types[num_seq[i] - 1];
char t2 = valid_types[num_seq[i + 1] - 1];
if (t1 == t2) {
is_ok = false;
break;
}
}
if (is_ok) {
valid_sequences.push_back(num_seq);
}
} while (next_permutation(num_seq.begin(), num_seq.end()));
// 输出结果
if (valid_sequences.empty()) {
cout << "NO" << endl;
} else {
cout << "YES" << endl;
for (auto &seq : valid_sequences) {
for (int num : seq) {
cout << num;
}
cout << endl;
}
}
return 0;
}
C4 - 才艺表演排列(剔除类型N,输出OK/-1)
题目描述:学校要举办一场才艺表演,有n个学生报名参加。每个学生有一个编号(从1到n)和一个才艺类型(用大写字母表示,例如,A代表唱歌,B代表跳舞,C代表乐器等)。老师发现有些才艺类型如果连续表演会影响效果,所以规定相同才艺类型的学生不能连续出场。另外,因表演主题限制,类型为"N"(小品)的才艺不能出现在本次表演中,排列顺序前需剔除才艺类型为"N"的同学,剔除后需对剩余的学生重新编号。请你帮助老师生成所有满足条件的出场顺序,给定一个整数n和一个长度为n的字符串,表示n个学生的才艺类型。要求生成所有可能的出场顺序,并且满足相邻位置的学生才艺类型不能相同的条件,按字典序输出所有满足条件的序列。如果能够输出若干的出场顺序,则先输出一行"OK",如果没有满足条件的序列,则输出"-1"。
运行条件:
- 总时限:5000毫秒
- 单组时限:1000毫秒
- 总内存:320 MB
- 单组内存:64 MB
输入格式:
- 第一行是一个整数n。
- 第二行是一个长度为n的字符串,由大写字母组成,表示第1到第n个学生的才艺类型。
输出格式:输出包含若干行,若能够输出若干的出场顺序,则先输出一行"OK",接下来若干行输出所有满足条件的出场顺序,每行一个序列。否则只输出一个"-1"。
输入输出样例:
-
样例1:
输入 :plaintext5 ANBNC输出 :
plaintextOK 123 132 213 231 312 321 -
样例2:
输入 :plaintext5 ANBNA输出 :
plaintextOK 123 321
代码:
cpp
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
using namespace std;
int main() {
int n;
if (!(cin >> n)) {
return 0;
}
string s;
cin >> s;
// 剔除类型为'N'的学生
vector<char> talents;
for (char c : s) {
if (c != 'N') {
talents.push_back(c);
}
}
int m = talents.size();
if (m == 0) {
cout << "OK" << endl;
return 0;
}
// 生成1~m的编号
vector<int> perm(m);
iota(perm.begin(), perm.end(), 1);
vector<vector<int>> valid_perms; // 存储有效排列
// 遍历所有全排列
do {
bool valid = true;
// 检查相邻才艺是否相同
for (int i = 0; i < m - 1; ++i) {
int pos1 = perm[i] - 1;
int pos2 = perm[i + 1] - 1;
if (talents[pos1] == talents[pos2]) {
valid = false;
break;
}
}
if (valid) {
valid_perms.push_back(perm);
}
} while (next_permutation(perm.begin(), perm.end()));
// 输出结果
if (valid_perms.empty()) {
cout << "-1" << endl;
} else {
cout << "OK" << endl;
for (auto &p : valid_perms) {
for (int num : p) {
cout << num;
}
cout << endl;
}
}
return 0;
}
D - 圆环馆连通性判断
- 核心算法:采用「前缀和映射空间编号→并查集初始化→层间空间连通性数学计算→合并连通空间→查询根节点一致性」的并查集策略。
- 优化点:并查集路径压缩、前缀和快速计算空间编号,保证1e6级查询的O(α(n))时间复杂度,适配大规模查询场景。
题目描述:Nike的家是圆环馆,其地图可以用一个圆环从外向内分为k层圆环表示(最外圈是第1层圆环,依次向内增加圆环)。第k层圆环上建p_i面墙把这层圆环等分。方便起见,保证各层圆环均在12点钟方向开始建墙。第i层圆环从12点钟方向顺时针开始数的第j个空间编号为 (i,j)(i,j从1开始)。内外层圆环段之间若有公共边,则互相连通。同一层的圆环段若被墙隔离,则互相无法连通。现在给出q个询问,每个询问给出4个整数i,j,k,l,你需要判断(i,j)和(k,l)是否存在一条连通的路径。
运行条件:
- 总时限:5000毫秒
- 单组时限:1000毫秒
- 总内存:320 MB
- 单组内存:64 MB
输入格式:
- 第一行有两个整数 k 和 q。
- 接下来一行,有k个整数用空格隔开,表示从外到内每个圆环等分成多少个圆环段。
- 接下来q行,每行有4个整数i,j,k,l,如题目所述。
输出格式:输出一个长度为q的01串,其中若第i个询问答案为连通,则该01串第i个字符为1,否则为0。
输入输出样例:
-
样例1:
输入 :plaintext1 3 3 1 1 1 2 1 2 1 3 1 1 1 3输出 :
plaintext000 -
样例2:
输入 :plaintext5 7 2 4 6 8 10 1 1 1 2 1 1 2 1 1 1 2 2 1 1 2 3 1 1 2 4 1 1 5 5 1 1 5 6输出 :
plaintext0110010
代码:
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAX_LAYERS = 1005; // 最大层数
const int MAX_SPACES = 2000005; // 最大空间总数
int parent[MAX_SPACES]; // 并查集父节点数组
int layer_segments[MAX_LAYERS]; // 每层的分段数
int prefix_sum[MAX_LAYERS]; // 前缀和,用于计算空间编号
// 并查集查找函数(路径压缩)
int find_root(int x) {
if (parent[x] == x) {
return x;
}
return parent[x] = find_root(parent[x]);
}
// 并查集合并函数
void union_sets(int x, int y) {
int root_x = find_root(x);
int root_y = find_root(y);
if (root_x != root_y) {
parent[root_x] = root_y;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int layer_count, query_count;
cin >> layer_count >> query_count;
// 读取每层的分段数
for (int i = 1; i <= layer_count; ++i) {
cin >> layer_segments[i];
}
// 计算前缀和,确定每层空间的起始编号
prefix_sum[0] = 0;
for (int i = 1; i <= layer_count; ++i) {
prefix_sum[i] = prefix_sum[i - 1] + layer_segments[i];
}
// 初始化并查集:每个空间的父节点是自身
for (int i = 1; i <= prefix_sum[layer_count]; ++i) {
parent[i] = i;
}
// 合并相邻层的连通空间
for (int i = 1; i < layer_count; ++i) {
long long curr_layer_segs = layer_segments[i]; // 当前层分段数
long long next_layer_segs = layer_segments[i + 1];// 下一层分段数
// 遍历当前层的每个空间
for (int j = 1; j <= curr_layer_segs; ++j) {
// 计算当前空间在下层对应的连通区间
long long left = (1LL * (j - 1) * next_layer_segs) / curr_layer_segs + 1;
long long right = (1LL * j * next_layer_segs - 1) / curr_layer_segs + 1;
// 当前空间的编号
int curr_space = prefix_sum[i - 1] + j;
// 合并当前空间与下层对应的所有空间
for (long long x = left; x <= right; ++x) {
int next_space = prefix_sum[i] + x;
union_sets(curr_space, next_space);
}
}
}
// 处理每个查询
string result;
for (int q = 0; q < query_count; ++q) {
int layer1, seg1, layer2, seg2;
cin >> layer1 >> seg1 >> layer2 >> seg2;
// 计算两个空间的编号
int space1 = prefix_sum[layer1 - 1] + seg1;
int space2 = prefix_sum[layer2 - 1] + seg2;
// 判断是否连通
if (find_root(space1) == find_root(space2)) {
result += '1';
} else {
result += '0';
}
}
// 输出结果
cout << result << endl;
return 0;
}
E - 队列链判断(树结构队列操作)
- 核心算法:采用「DFS生成欧拉序→ST表预处理LCA→线段树维护队列元素的最小覆盖链→合并链信息验证是否共链」的树链查询策略。
- 优化点:关闭IO同步、线段树4倍空间预分配、deque维护队列避免数组移动,适配1e6级操作量的线性时间复杂度。
题目描述:对于n个顶点和n-1条边组成的无向连通图,给q个操作。每个操作格式如下:假设有1个队列,初始为空。1 x 在队列的队尾加入一个点x(队中可以有重复元素);2 删除队首点。如果当前队为空,不执行操作;3 询问图中是否存在一条链,使得队列中所有点均在这一条链上。注:此处的1、2、3为每种操作的类型标识。
运行条件:
- 总时限:5000毫秒
- 单组时限:1000毫秒
- 总内存:320 MB
- 单组内存:64 MB
输入格式:
- 第一行包含2个正整数 n,q (1<=n,q<=1e6)。
- 接下来的 n-1 行中,每行给出两个用空格分隔的正整数,表示一条连接2个点的编号(从 1 到 n 编号)。
- 接下来的 q 行中,每行给出1个操作,格式如前所述。
输出格式:输出一行01序列,长度为3类型操作的个数(只有3类型操作才进行询问)。若第i个3类型询问答案为是在一条链上,则第i位的序列为1,否则为0。
输入输出样例:
-
样例1:
输入 :plaintext7 7 1 2 1 3 2 4 2 5 3 6 3 7 1 1 1 2 1 3 1 4 3 1 5 3输出 :
plaintext10 -
样例2:
输入 :plaintext4 11 1 2 1 3 1 4 1 1 1 2 1 3 1 4 3 2 3 2 3 2 3输出 :
plaintext0011
代码:
cpp
#include <iostream>
#include <vector>
#include <deque>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;
// ===================== 常量定义 =====================
const int MAX_NODE = 1e6 + 10; // 最大节点数
const int MAX_OP = 1e6 + 10; // 最大操作数
const int LOG = 22; // 稀疏表对数上限(2^22 > 2e6)
const int EULER_SIZE = 2 * MAX_NODE; // 欧拉序最大长度
// ===================== 树结构 & LCA 相关 =====================
// 邻接表存储树的边
struct Edge {
int to; // 边的终点
int next; // 下一条边的索引
} edges[2 * MAX_NODE]; // 无向图,边数翻倍
int head[MAX_NODE]; // 每个节点的邻接表头
int edge_cnt = 0; // 边的计数器
// 欧拉序相关变量
int euler[EULER_SIZE]; // 存储欧拉序列
int first_pos[MAX_NODE];// 每个节点在欧拉序中第一次出现的位置
int depth[MAX_NODE]; // 每个节点的深度
int euler_cnt = 0; // 欧拉序计数器
// ST表(稀疏表):用于快速查询LCA
int st_table[LOG][EULER_SIZE];
int log_table[EULER_SIZE]; // 预处理log2值,加速查询
// 添加无向边
inline void add_edge(int u, int v) {
edges[++edge_cnt].to = v;
edges[edge_cnt].next = head[u];
head[u] = edge_cnt;
edges[++edge_cnt].to = u;
edges[edge_cnt].next = head[v];
head[v] = edge_cnt;
}
// DFS生成欧拉序,同时记录节点深度和首次出现位置
void dfs(int u, int fa, int d) {
depth[u] = d; // 记录当前节点深度
euler[++euler_cnt] = u; // 欧拉序入队
first_pos[u] = euler_cnt; // 记录首次出现位置
// 遍历所有邻边
for (int i = head[u]; i; i = edges[i].next) {
int v = edges[i].to;
if (v != fa) { // 避免回边到父节点
dfs(v, u, d + 1);
euler[++euler_cnt] = u;// 回溯时再次入队
}
}
}
// 比较两个节点在欧拉序中的深度,返回深度更小的(用于ST表)
inline int min_depth(int a, int b) {
return depth[a] < depth[b] ? a : b;
}
// 预处理ST表(用于LCA查询)
void build_st_table() {
// 初始化ST表第一层(k=0)
for (int i = 1; i <= euler_cnt; ++i) {
st_table[0][i] = euler[i];
}
// 递推构建ST表
for (int k = 1; k < LOG; ++k) {
for (int i = 1; i + (1 << k) - 1 <= euler_cnt; ++i) {
st_table[k][i] = min_depth(
st_table[k-1][i],
st_table[k-1][i + (1 << (k-1))]
);
}
}
// 预处理log_table:log_table[i] = floor(log2(i))
log_table[1] = 0;
for (int i = 2; i <= euler_cnt; ++i) {
log_table[i] = log_table[i >> 1] + 1;
}
}
// 查询u和v的最近公共祖先(LCA)
inline int query_lca(int u, int v) {
int l = first_pos[u], r = first_pos[v];
if (l > r) swap(l, r); // 保证l <= r
int k = log_table[r - l + 1]; // 计算区间长度的log2值
return min_depth(
st_table[k][l],
st_table[k][r - (1 << k) + 1]
);
}
// 计算两个节点之间的距离(基于LCA)
inline int get_dist(int u, int v) {
int lca = query_lca(u, v);
return depth[u] + depth[v] - 2 * depth[lca];
}
// 判断点x是否在u-v的链上
inline bool is_on_chain(int x, int u, int v) {
int d1 = get_dist(x, u);
int d2 = get_dist(x, v);
int d3 = get_dist(u, v);
return (d1 + d2) == d3; // 距离和等于u-v总距离则在链上
}
// ===================== 线段树(维护队列的链信息) =====================
// 线段树节点:维护区间内所有点的最小覆盖链
struct SegNode {
int u; // 链的一个端点
int v; // 链的另一个端点
bool is_valid; // 区间内所有点是否在同一条链上
} seg_tree[4 * MAX_OP]; // 线段树数组(4倍空间)
deque<int> que; // 维护操作的队列
int op_idx = 0; // 队列元素的操作索引(用于线段树的位置映射)
// 合并两个子节点的链信息,得到父节点的信息
SegNode merge(const SegNode& left, const SegNode& right) {
SegNode res;
// 情况1:左区间无效,直接返回右区间
if (!left.is_valid) {
res = right;
return res;
}
// 情况2:右区间无效,直接返回左区间
if (!right.is_valid) {
res = left;
return res;
}
// 情况3:左右区间都有效,尝试合并链
int u1 = left.u, v1 = left.v;
int u2 = right.u, v2 = right.v;
// 候选链端点:u1-v1, u1-v2, u2-v1, u2-v2 中找覆盖所有点的链
// 先验证原左区间的链是否能覆盖右区间的所有点
if (is_on_chain(u2, u1, v1) && is_on_chain(v2, u1, v1)) {
res.u = u1;
res.v = v1;
res.is_valid = true;
return res;
}
// 验证u1-v2是否能覆盖所有点
if (is_on_chain(v1, u1, v2) && is_on_chain(u2, u1, v2)) {
res.u = u1;
res.v = v2;
res.is_valid = true;
return res;
}
// 验证u2-v1是否能覆盖所有点
if (is_on_chain(u1, u2, v1) && is_on_chain(v2, u2, v1)) {
res.u = u2;
res.v = v1;
res.is_valid = true;
return res;
}
// 验证u2-v2是否能覆盖所有点
if (is_on_chain(u1, u2, v2) && is_on_chain(v1, u2, v2)) {
res.u = u2;
res.v = v2;
res.is_valid = true;
return res;
}
// 所有候选链都无法覆盖,标记为无效
res.is_valid = false;
return res;
}
// 初始化线段树
void init_seg_tree(int node, int l, int r) {
seg_tree[node].is_valid = false;
if (l == r) return;
int mid = (l + r) / 2;
init_seg_tree(node << 1, l, mid);
init_seg_tree(node << 1 | 1, mid + 1, r);
}
// 更新线段树的某个位置(对应队列中的一个元素)
void update_seg_tree(int node, int l, int r, int pos, int val, bool is_add) {
if (l == r) {
if (is_add) { // 添加元素:链端点就是自己,有效
seg_tree[node].u = val;
seg_tree[node].v = val;
seg_tree[node].is_valid = true;
} else { // 删除元素:标记为无效
seg_tree[node].is_valid = false;
}
return;
}
int mid = (l + r) / 2;
if (pos <= mid) {
update_seg_tree(node << 1, l, mid, pos, val, is_add);
} else {
update_seg_tree(node << 1 | 1, mid + 1, r, pos, val, is_add);
}
// 合并左右子节点的信息
seg_tree[node] = merge(seg_tree[node << 1], seg_tree[node << 1 | 1]);
}
// ===================== 主函数 =====================
int main() {
// 优化cin/cout速度,适配1e6级数据量
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
// 初始化邻接表
memset(head, 0, sizeof(head));
edge_cnt = 0;
// 输入n和q
int n, q;
cin >> n >> q;
// 输入n-1条边,构建树
for (int i = 1; i <= n - 1; ++i) {
int u, v;
cin >> u >> v;
add_edge(u, v);
}
// DFS生成欧拉序(根节点选1,深度从1开始)
dfs(1, 0, 1);
// 构建ST表,预处理LCA查询
build_st_table();
// 初始化线段树(操作数最大为1e6)
init_seg_tree(1, 1, MAX_OP);
// 维护队列的操作索引(队首、队尾)
int que_front = 1;
int que_back = 0;
// 存储查询结果的字符串
string ans = "";
// 处理q个操作
while (q--) {
int op_type;
cin >> op_type;
if (op_type == 1) { // 操作1:入队
int x;
cin >> x;
que.push_back(x);
que_back++;
// 更新线段树:添加位置que_back的元素x
update_seg_tree(1, 1, MAX_OP, que_back, x, true);
} else if (op_type == 2) { // 操作2:出队
if (!que.empty()) {
// 更新线段树:删除位置que_front的元素
update_seg_tree(1, 1, MAX_OP, que_front, 0, false);
que.pop_front();
que_front++;
}
} else if (op_type == 3) { // 操作3:查询
if (que.empty()) { // 队列为空,默认在一条链上
ans += '1';
} else {
// 查询线段树根节点的有效性
ans += seg_tree[1].is_valid ? '1' : '0';
}
}
}
// 输出查询结果
cout << ans << endl;
return 0;
}