🔍 引言
想象一下:一群数字手拉手围成圆圈跳华尔兹,只有‘质数和’的舞伴才能赢得掌声;另一群数字在循环跑道上赛跑,裁判GCD举着‘最大公约数’的标尺,寻找最均衡的冠军……
这不是童话,而是蓝桥杯算法题中的真实剧情!今天,让我们用数学的望远镜和算法的指挥棒,揭开这两场数字盛宴的终极秘密!
📖 正文
Part 1 | 蓝桥杯的数学乐园:当数字变成舞者与运动员
蓝桥杯的题目总能把冰冷的数字变成鲜活的角色。今天的两个问题,一个让数字在圆周上寻找“质数舞伴”,另一个让数字在循环跑道上挑战GCD极限——看似毫无关联,却共享数学规律与指针操作的双重基因。
Part 2 | 题目一:圆周质数对(数学+双指针)
💡 问题核心
将数组首尾相连成环,找到一种排列方式,使得相邻两数之和为质数的对数最多。例如 [2,3,4]
排列为 3-2-4
,有两对质数和(3+2=5,4+3=7)。
🔑 算法思路解析
-
质数的魔法筛选:
-
预处理质数表,快速判断任意两数之和是否为质数。
-
小技巧:若两数之和为2,则必为1+1,但1非质数,可直接排除。
-
-
排列的舞蹈编排:
-
图论视角:将每个数字视为节点,若两数之和为质数则连边,问题转化为寻找包含最多边的哈密顿回路。
-
贪心试探:用双指针从两端向中心逼近,优先连接质数和对,动态调整排列。
-
-
复杂度妥协:
-
暴力回溯法时间复杂度为O(n!),需用剪枝或动态规划优化。
-
⚡ 思维亮点
-
数学与图论的跨界融合:用质数判定为数字建立“社交关系”,问题秒变“朋友圈最大闭环”。
-
双指针的灵动性:像导演调整舞者位置,局部优化带动全局最优。
#include <stdio.h> // 标准输入输出库,提供printf等函数
#include <stdbool.h> // 布尔类型支持库,定义bool、true、false
#include <stdlib.h> // 标准库,包含动态内存管理、退出函数等
#include <string.h> // 字符串操作库,提供memcpy等函数
// 全局状态记录
int max_pairs = 0; // 记录找到的最大质数对数量
int* best_perm = NULL; // 指向最佳排列数组的指针
bool* is_prime = NULL; // 质数筛表指针(标记数字是否为质数)
int max_sum = 0; // 数组元素两两之和的最大值
/**
* 埃拉托斯特尼筛法生成质数表
* @param max_num 筛表上限(生成0~max_num的质数标记)
*/
void sieve(int max_num) {
// 分配质数筛表内存(包含0到max_num共max_num+1个元素)
is_prime = (bool*)malloc((max_num + 1) * sizeof(bool));
// 初始化所有元素为true(假设都是质数)
memset(is_prime, true, (max_num + 1) * sizeof(bool));
// 手动标记0和1为非质数
is_prime[0] = is_prime[1] = false;
// 筛法核心:从2开始遍历到sqrt(max_num)
for (int i = 2; i * i <= max_num; ++i) {
if (is_prime[i]) { // 如果i是质数,标记其倍数为非质数
// 从i²开始,以i为步长标记倍数
for (int j = i * i; j <= max_num; j += i) {
is_prime[j] = false;
}
}
}
}
/**
* 计算环形排列的质数对数量
* @param arr 当前排列数组指针
* @param n 数组长度
* @return 质数对数量
*/
int count_pairs(int* arr, int n) {
int cnt = 0; // 质数对计数器
for (int i = 0; i < n; ++i) { // 遍历所有相邻对
// 计算当前元素与下一个元素的环形和
int sum = arr[i] + arr[(i + 1) % n];
// 检查是否在筛表范围内且为质数
if (sum <= max_sum && is_prime[sum]) ++cnt;
}
return cnt;
}
/**
* 递归生成全排列并更新最优解
* @param arr 当前排列数组指针
* @param start 当前处理的起始位置
* @param n 数组长度
*/
void permute(int* arr, int start, int n) {
// 递归终止条件:完成一个排列
if (start == n) {
// 计算当前排列的质数对数
int current = count_pairs(arr, n);
// 如果优于当前最优解则更新
if (current > max_pairs) {
max_pairs = current; // 更新最大对数
memcpy(best_perm, arr, n * sizeof(int)); // 保存排列
}
return;
}
// 全排列生成(带剪枝优化)
for (int i = start; i < n; ++i) {
// 交换当前元素与后续元素生成新排列
int tmp = arr[start];
arr[start] = arr[i];
arr[i] = tmp;
// 剪枝优化:检查前一对是否有效(start>0时)
if (start > 0) {
// 计算前一个元素与当前元素的组合
int sum = arr[start - 1] + arr[start];
// 如果和超过筛表范围或非质数,则跳过该分支
if (sum > max_sum || !is_prime[sum]) {
// 回溯交换(恢复数组状态)
arr[i] = arr[start];
arr[start] = tmp;
continue; // 跳过后续递归
}
}
// 递归处理下一个位置
permute(arr, start + 1, n);
// 回溯恢复数组原始状态
tmp = arr[start];
arr[start] = arr[i];
arr[i] = tmp;
}
}
int main() {
// 输入测试数据
int arr[] = {2, 3, 4};
int n = sizeof(arr) / sizeof(arr[0]); // 计算数组长度
// 打印原始数组
int length = sizeof(arr) / sizeof(arr[0]); // 获取数组长度
printf("["); // 输出左括号
for (int i = 0; i < length; i++) {
// 控制逗号格式:第一个元素前不加逗号
printf(i == 0 ? "%d" : ", %d", arr[i]);
}
printf("]\n"); // 输出右括号和换行
max_sum = 0;
for (int i = 0; i < n; ++i) { // 外层遍历元素
for (int j = 0; j < n; ++j) { // 内层遍历元素
if (i != j) { // 排除相同元素组合
int s = arr[i] + arr[j];
if (s > max_sum) max_sum = s; // 更新最大值
}
}
}
// 初始化质数筛表(覆盖所有可能的和)
sieve(max_sum);
// 初始化最佳排列数组(分配内存并拷贝初始数组)
best_perm = (int*)malloc(n * sizeof(int));
memcpy(best_perm, arr, n * sizeof(int));
// 创建排列生成缓冲区(避免修改原数组)
int* buffer = (int*)malloc(n * sizeof(int));
memcpy(buffer, arr, n * sizeof(int));
// 递归生成所有排列并寻找最优解
permute(buffer, 0, n);
// 输出结果
printf("Maximum Prime Logarithm: %d\n Arrangement Scheme:", max_pairs);
for (int i = 0; i < n; ++i) { // 遍历最佳排列
printf("%d ", best_perm[i]);
}
// 输出相邻和验证信息
printf("\n Adjacency and Verification:\n");
for (int i = 0; i < n; ++i) {
int j = (i + 1) % n; // 环形下一个位置
int sum = best_perm[i] + best_perm[j];
printf("%d+%d=%d %s\n", best_perm[i], best_perm[j], sum,
is_prime[sum] ? "prime" : "non-prime"); // 三目运算符选择输出
}
// 释放动态分配的内存
free(is_prime); // 释放质数筛表
free(best_perm); // 释放最佳排列数组
free(buffer); // 释放排列缓冲区
return 0; // 程序正常退出
}
输出结果:
Part 3 | 题目二:循环移位GCD极值(数学+滑动窗口)
💡 问题核心
对数组循环右移k次后,计算所有前缀GCD的最大值,最终取所有k中该最大值的最小可能值。例如 [8,6,12]
在k=1时前缀GCD最大值为12,需找到所有k对应结果中的最小值。
🔑 算法思路解析
-
GCD的接力赛规则:
-
前缀GCD具有单调不增性:一旦GCD变小,后续前缀无法再变大。
-
-
滑动窗口的智慧:
-
将循环数组展开为双倍长度(如
[8,6,12,8,6,12]
),用滑动窗口遍历所有可能的循环起点。 -
动态维护当前窗口的前缀GCD,记录最小值。
-
-
数学性质加速:
-
若某前缀GCD降为1,可直接终止当前窗口计算(因1是最小可能值)。
-
⚡ 思维亮点
-
循环变线性的魔术:通过数组翻倍将环形问题“拉直”,滑动窗口化身“时空管理者”。
-
GCD的单调性:利用数学性质大幅减少无效计算,效率提升如“快进键”。
#include <stdio.h> // 标准输入输出库,提供printf函数
#include <limits.h> // 定义整数类型极限值,如INT_MAX
/**
* 寻找数组最小值
* @param arr 数组指针(需检查非空)
* @param n 数组长度(需大于0)
* @return 数组中的最小值,空数组返回INT_MAX
*/
int findMin(int* arr, int n) {
int min_val = INT_MAX; // 初始化为最大整数值(2147483647)
for (int i = 0; i < n; ++i) { // 遍历数组每个元素
if (arr[i] < min_val) { // 发现更小值时触发更新
min_val = arr[i]; // 更新当前最小值记录
}
}
return min_val; // 返回遍历后得到的最小值
}
int main() {
// 测试用例数组(原始数据)
int arr[] = {8, 6, 12};
// 计算数组长度:总字节数 / 单个元素字节数
int n = sizeof(arr) / sizeof(arr[0]);
// 打印原始数组(新增代码段)
int length = sizeof(arr) / sizeof(arr[0]); // 获取数组长度(等同于n)
printf("["); // 输出左方括号
for (int i = 0; i < length; i++) { // 遍历数组元素
// 条件运算符控制输出格式:首元素无前导逗号
printf(i == 0 ? "%d" : ", %d", arr[i]);
}
printf("]\n"); // 输出右方括号和换行符
// 调用函数获取结果
int result = findMin(arr, n);
// 格式化输出最终结果(修改后的输出语句)
printf("The minimum value of the maximum GCD in all cyclic permutations: %d\n", result); // 输出结果
return 0;
}
输出结果:
Part 4 | 双题对比:数学的浪漫与算法的冷峻
对比维度 | 圆周质数对(数学+双指针) | 循环移位GCD极值(数学+滑动窗口) |
---|---|---|
核心目标 | 最大化浪漫的“质数舞伴”对数 | 最小化冷酷的“GCD最大值” |
算法武器 | 图论建模+贪心试探 | 滑动窗口+GCD单调性 |
时间复杂度 | O(n²) ~ O(n!)(依赖优化) | O(n²) → O(n)(优化后) |
关键挑战 | 哈密顿回路的边数最大化 | 循环窗口的前缀GCD快速计算 |
思维美学 | 艺术性编排(如舞蹈设计) | 工程性优化(如机械传动) |
Part 5 | 总结:数学是诗,算法是剑
两道题宛如算法世界的“冰与火之歌”:
-
圆周质数对是数学的浪漫诗篇,用质数为数字赋予情感,双指针是舞步的指挥家;
-
循环GCD极值是算法的冷酷利剑,用滑动窗口切割问题,GCD单调性是斩断冗余的刃光。
蓝桥杯的江湖中,唯有同时掌握“诗人的灵感”与“剑客的精准”,才能笑傲题海!
📝 结语
数字的圆周上,质数对在跳着永恒的华尔兹;循环的赛道中,GCD在追逐极限的平衡点。算法之美,正在于将数学的抽象化为逻辑的具象,让无序变为有序。
下一次,当你看到一串数字时,愿你能听见质数的圆舞曲,看见GCD的抛物线——因为每个数字,都是算法宇宙中等待被点亮的星辰。