26牛客暑假多校6
点击查看题面
H题 Hard Problem
题目大意
给定一个整数 -1。
数据范围:
(测试用例数): (排列长度): - 保证所有测试用例的
之和不超过
思路
- 天然升序与贪心判定:
- 为了获得字典序最小的排列,首选构造方案是标准升序排列
。 - 对于序列内部任意相邻两项
和 ,其差值绝对值为 。因为 既非质数也非合数,即不是质数,所以内部相邻元素均满足条件。 - 唯一需要检验的是首尾环形相邻处的差值,即
。 - 如果
不是质数,则自然升序排列即为合法且字典序严格最小的极佳解。
- 质数冲突与调整构造:
- 当
为质数时,自然升序排列会因首尾差值为质数而失效。 - 注意到当
为质数且 时, 必为奇质数,由此可推导出 必为偶数。 - 当
且 为质数时(对应 ),不存在可满足条件的合法排列,应输出 -1。 - 当
且 为质数时,可对末尾 4 个元素进行局部翻转,构造排列为:
验证此构造方案的相邻差值:
前
项与后 4 项内部的相邻差绝对值均为 (非质数)。 衔接处
与 (实际填入 )的差绝对值为 (合数,非质数)。 环形首尾衔接处
(实际填入 )与 (实际填入 )的差绝对值为 。由于 为偶数,故 必定为偶数,即必定为合数(非质数)。 该构造在前
位置上保持了最小可能数值,确保了字典序最小的要求。
复杂度分析
时间复杂度: 预处理埃氏筛判定质数的时间复杂度为
空间复杂度: 预处理质数标记数组占用的空间为
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int Prime[N];
// 预处理埃氏筛:Prime[i] == 0 表示 i 为质数,Prime[i] == 1 表示非质数
void init() {
Prime[0] = Prime[1] = 1; // 0 和 1 不是质数
for (int i = 2; i * i < N; ++i) {
if (!Prime[i]) {
for (int j = i * i; j < N; j += i) {
Prime[j] = 1;
}
}
}
}
bool is(int k) {
return Prime[k] == 0;
}
void fc() {
int n;
std::cin >> n;
if (!is(n - 1)) {
for (int i = 1; i <= n; i++) std::cout << i << ' ';
}
else if (n < 8) {
std::cout << "-1";
}
else {
for (int i = 1; i <= n - 4; i++) std::cout << i << ' ';
for (int i = n; i > n - 4; i--) std::cout << i << ' ';
}
std::cout << "\n";
}
int main() {
// 优化标准输入输出流性能
ios::sync_with_stdio(false);
cin.tie(nullptr);
init(); // 初始化质数表
int t = 1;
std::cin >> t;
while (t--) fc();
return 0;
}D题 Divisibility
题目大意
给定由
可以被 整除(即 ); - 存在一条从顶点
到顶点 长度为 的路径(路径可多次访问相同顶点或边)。
若不存在这样的
数据范围:
(测试用例数): (顶点数): (边数): (常数): - 保证所有测试用例中
的总和与 的总和均不超过
思路
- 奇偶状态 BFS 求解最短距离: 从起点
出发,使用 BFS 维护每个节点到达时的最小偶数步数 和最小奇数步数 。因为所有边权均为 ,BFS 首次扩展到状态 时的距离即为最短距离。 - 分类讨论计算最小倍数
: 对于顶点 ,需要寻找最小的非负整数 ,使得 且 的奇偶性与 一致。
- 情形一:
为偶数 由于偶数的任意整数倍 均为偶数,因此不可能产生奇数长度的路径。 只需考虑偶数路径:
对应的最小步数为
- 情形二:
为奇数 当 为奇数时, 的奇偶性与系数 的奇偶性完全一致: - 偶数路径贡献:初始系数
。若 为奇数,需调整为最近的偶数 ,候选值为 。 - 奇数路径贡献:初始系数
。若 为偶数,需调整为最近的奇数 ,候选值为 。 最终答案 为两类候选值中的最小值。若两状态均不可达,则 。
复杂度分析
时间复杂度: BFS 遍历整张图的时间复杂度为
空间复杂度: 链式前向星存储图需要
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll inf = 1e18;
const int N = 5e5 + 5;
int n, m;
ll k;
int head[N], nxt[N << 1], to[N << 1], cnt;
ll dis[N][2];
// 链式前向星建图
void add(int u, int v) {
to[++cnt] = v;
nxt[cnt] = head[u];
head[u] = cnt;
}
void fc() {
cnt = 0;
std::cin >> n >> m >> k;
// 初始化节点表头与距离数组
for (int i = 1; i <= n; i++) {
head[i] = -1;
dis[i][0] = dis[i][1] = inf;
}
for (int i = 0; i < m; i++) {
int u, v;
std::cin >> u >> v;
add(u, v);
add(v, u);
}
// 0 步到达起点 1,偶数路径长度为 0
dis[1][0] = 0;
// BFS 队列存储 {当前节点 u, 当前路径奇偶性 t}
std::queue<std::pair<int, int>> q;
q.push({1, 0});
while (q.size()) {
auto [u, t] = q.front();
q.pop();
for (int i = head[u]; i > 0; i = nxt[i]) {
int v = to[i];
// 奇偶状态转移:偶变奇,奇变偶
if (dis[v][t ^ 1] != inf) continue;
dis[v][t ^ 1] = dis[u][t] + 1;
q.push({v, t ^ 1});
}
}
// 分类讨论 k 的奇偶性
if (k & 1) {
// k 为奇数:系数 c 的奇偶性决定路径长度 d 的奇偶性
for (int i = 1; i <= n; i++) {
ll ans = inf;
// 1. 考虑偶数最短路径 dis[i][0]
if (dis[i][0] != inf) {
ll d = (dis[i][0] + k - 1) / k; // 向上取整求最小系数
if (d & 1) d++; // 若为奇数系数,调整为偶数系数
ans = std::min(ans, d * k);
}
// 2. 考虑奇数最短路径 dis[i][1]
if (dis[i][1] != inf) {
ll d = (dis[i][1] + k - 1) / k; // 向上取整求最小系数
if (d % 2 == 0) d++; // 若为偶数系数,调整为奇数系数
ans = std::min(ans, d * k);
}
std::cout << (ans == inf ? -1 : ans) << " ";
}
} else {
// k 为偶数:只能通过偶数路径到达
for (int i = 1; i <= n; i++) {
std::cout << (dis[i][0] == inf ? -1 : (dis[i][0] + k - 1) / k * k) << " ";
}
}
std::cout << "\n";
}
int main() {
// 优化 I/O 效率
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
std::cin >> t;
while (t--) fc();
return 0;
}G题 Game on a Graph
题目大意
给定一个包含
Alice 和 Bob 在图上进行如下游戏:
- Alice 初始站在一个非特殊顶点
上,Alice 先手,两人轮流操作。 - Alice 的操作:若当前处于特殊顶点则直接获胜;否则选择一条相连的边移动到相邻顶点。若无边可选则 Bob 获胜。
- Bob 的操作:选择图中的任意一条边并将其永久删除。若无边可删则什么都不做。
求出所有能让 Alice 在双方均采取最优策略下必胜的起始顶点
数据范围:
(测试用例数): (顶点数)、 (特殊点数): (边数): - 保证所有顶点度数
,且所有测试用例中 与 的总和均不超过 。
思路
- 终端必胜态定义:将题目指定的特殊顶点集合
初始化为绝对必胜态集合。 - 中间过程的安全转移:对于图中的任意非特殊顶点
,假设 Alice 在游戏过程中移动到了 。由于接下来轮到 Bob 操作且 Bob 会删去一条边,若要保证 Alice 之后仍能到达必胜态,顶点 在当前时刻必须至少连接 个位于必胜态集合 中的邻接点。这样即使 Bob 删去其中一条边,Alice 依然可以沿着另一条边进入必胜态。因此,当一个非特殊点邻接的必胜点数量达到 时,该点即可被升格为新的必胜态并加入集合 进行拓展。 - 起始点的特殊性(先手优势):游戏开始时 Alice 为先手,Bob 尚未进行任何删边操作。因此,如果起始顶点
邻接的必胜态集合 中至少存在 个顶点,Alice 即可在第一回合直接步入必胜态并赢得游戏。 - 算法实现流程:
- 初始化队列
,将 个特殊顶点加入队列并标记为必胜点。 - 使用多源 BFS 进行逆向传播,对于队头节点
,遍历其邻接点 。维护 记录 邻接的必胜点数量。当 且 还不是必胜点时,将 标记为必胜点并压入队列 。 - 拓扑传播结束后,统计所有非特殊点中满足
的顶点 ,这些顶点即为 Alice 的起始必胜点。
复杂度分析
时间复杂度: 基于队列的多源 BFS 中,每个顶点最多入队一次,每条无向边最多被遍历两次,时间复杂度为
空间复杂度: 需要邻接表存储图结构以及队列和状态标记数组,空间复杂度为
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 5;
int n, m, k;
int head[N], nxt[N << 1], to[N << 1], cnt;
int is[N], c[N];
void add(int u, int v) {
to[++cnt] = v;
nxt[cnt] = head[u];
head[u] = cnt;
}
void fc() {
cnt = 0;
std::cin >> n >> m >> k;
for (int i = 1; i <= n; i++) {
head[i] = -1;
is[i] = 0;
c[i] = 0;
}
for (int i = 0; i < m; i++) {
int u, v;
std::cin >> u >> v;
add(u, v);
add(v, u);
}
std::queue<int> q;
for (int i = 0; i < k; i++) {
int x;
std::cin >> x;
is[x] = 1;
q.push(x);
}
// 拓扑/BFS 传播必胜状态
while (q.size()) {
int u = q.front();
q.pop();
for (int i = head[u]; ~i; i = nxt[i]) {
int v = to[i];
if (is[v]) continue; // 已是特殊点/扩展必胜点,跳过
c[v]++; // 统计 v 邻接的必胜点数量
if (c[v] == 2) { // 邻接 2 个及以上必胜点即可转化为扩展必胜点
is[v] = 1;
q.push(v);
}
}
}
// c[i] >= 1 的非特殊点即为 Alice 起始必胜点
int ans = 0;
for (int i = 1; i <= n; i++) {
if (c[i]) ans++;
}
std::cout << ans << "\n";
for (int i = 1; i <= n; i++) {
if (c[i]) std::cout << i << ' ';
}
std::cout << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
std::cin >> t;
while (t--) fc();
return 0;
}F题 Full Alphabet
前置知识:z函数+状压dp
题目大意
给定一个由小写英文字母组成的字符串
数据范围:
- 字符串仅包含小写英文字母。
- 时间限制:10 秒,空间限制:1024 MB。
思路
- 无解条件判定: 根据 Lyndon 串定义,对于每个后缀
( ),必须满足 。利用 Z 函数求出 :
- 若
,说明后缀 是字符串 的真前缀。由于前缀的长度小于 ,在任何字典序下真前缀都严格小于 ,因此 必定不可能成为 Lyndon 串,直接输出 。
- 偏序关系提取:
- 若
,则 与后缀 在位置 处首次出现不同字符。 - 记
, 。为使 成立,必须满足 ,即字符 在字母表 中的位置必须早于字符 。 - 遍历所有后缀
,将提取到的所有字符偏序关系存入有向图中。
- 状压 DP 求解拓扑序数量:
- 统计
中出现的不同字符数量 ( ),并将字符映射至 。 - 设
表示已将二进制掩码 所代表的字符集合排好序的合法方案数。 - 状态转移:枚举下一个可以放入排列的字符
,当且仅当 的所有前置依赖字符均已在 中时, 。
- 未出现字符的排列乘积:
- 最终状态
给出 中出现的 个字符的合法排列数。 - 对于未在
中出现的剩余 个字符,它们可以在字母表中任意插入到已排好的 个字符之间。插入位置的选择数为 。 - 将两者相乘并利用
unsigned int自然对取模即可得到最终答案。
复杂度分析
时间复杂度
- Z 函数计算字符串前缀匹配的时间复杂度为
。 - 提取偏序关系遍历
个后缀,时间复杂度为 。 - 状压 DP 求解拓扑序数量,共有
个状态,每个状态转移为 ,时间复杂度为 。 - 整体时间复杂度为
。由于 ,算法在规定的时限内可以高效完成计算。
空间复杂度
- Z 数组与辅助数组大小为
。 - DP 数组大小为
。 - 整体空间复杂度为
。
参考代码
参考代码
#include <bits/stdc++.h>
using namespace std;
using ui = unsigned int;
// 计算字符串 s 的 Z 数组 (扩展 KMP)
vector<int> zArray(const string& s) {
int n = s.size();
vector<int> z(n, 0);
for (int i = 1, r = 0, c = 0; i < n; i++) {
if (r > i) z[i] = min(z[i - c], r - i);
while (i + z[i] < n && s[i + z[i]] == s[z[i]]) ++z[i];
if (i + z[i] > r) r = i + z[i], c = i;
}
return z;
}
void solve() {
string s;
if (!(cin >> s)) return;
int n = s.size();
auto z = zArray(s);
vector<int> id(26, -1);
int m = 0;
for (char c : s) {
if (id[c - 'a'] == -1) id[c - 'a'] = m++;
}
vector<int> pred(m, 0);
for (int i = 1; i < n; i++) {
if (i + z[i] == n) {
cout << "0\n";
return;
}
int v = id[s[z[i]] - 'a'];
int u = id[s[i + z[i]] - 'a'];
pred[v] |= (1 << u);
}
// 4. 状压 DP 计算拓扑序数量
vector<ui> dp(1 << m, 0);
dp[0] = 1;
for (int mask = 0; mask < (1 << m); mask++) {
if (!dp[mask]) continue;
for (int i = 0; i < m; i++) {
if (!(mask & (1 << i)) && ((pred[i] & mask) == pred[i])) {
dp[mask | (1 << i)] += dp[mask];
}
}
}
ui ans = dp[(1 << m) - 1];
for (int i = m + 1; i <= 26; i++) {
ans *= (ui)i;
}
cout << ans << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}