在算法世界中,极值问题如同硬币的两面——一面追求平衡与公平,另一面追求差异与突破。今天,我们通过两个经典的极值问题,探讨如何在约束条件下寻找最优解,并揭示优化理论中的深层智慧。无论你使用的是 C++、JavaScript、TypeScript、Java 还是 Go,这些思想都能助你写出更高效的代码。
极值问题的哲学与数学本质
极值问题贯穿计算机科学的发展史。从狄利克雷原理到庞特里亚金极大值原理,数学家们始终在探索“在约束下如何最优”。本文聚焦的两个问题,正是这一思想的算法体现:
- 最小化数对的最大差值:属于组合优化中的分配问题,核心是在约束下寻找最均衡的配对方案。在调度、资源分配等领域广泛应用。
- 改变整数能得到的最大差值:属于数字变换优化问题,要求通过有限操作实现数值差异的最大化。
这两个问题看似对立,实则共享“在约束中寻优”的数学内核。理解它们,能帮你掌握算法设计中的平衡与突破之道。
⚖️ 最小化数对的最大差值:算法深度剖析
问题描述:给定整数数组 nums 和整数 p,从 nums 中选出 p 个下标对(每个下标最多出现一次),使得所有配对差值的最大值最小化。
数学建模
设配对集合 S,满足:
- |S| = p
- 每个下标最多出现在一个配对中
- 目标:min{ max{|nums[i]-nums[j]| for (i,j)∈S } }
算法策略
排序预处理:首先对数组排序。排序后,相邻元素间的差值成为候选最小差值,这符合局部最优性原理。在 Java 或 C++ 中,使用 Arrays.sort() 或 std::sort() 即可高效完成。
二分搜索:在 [0, max(nums)-min(nums)] 范围内二分查找最优差值,时间复杂度 O(log(max_val)),体现对数搜索的高效性。
贪心可行性判断:对于候选差值 mid,遍历排序数组,若相邻元素差值 ≤ mid,则选择该配对并跳过下一个元素,统计配对数。若配对数 ≥ p,则 mid 可行。
复杂度分析
- 排序:O(n log n)
- 二分搜索:O(log(max_val))
- 可行性判断:O(n)
- 总复杂度:O(n log n + n log(max_val))
✅ 示例验证
示例1:nums = [10,1,2,7,1,3], p = 2,输出 1。解释:选择下标对 (1,4) 和 (2,5),差值分别为 0 和 1,最大差值为 1。
示例2:nums = [4,2,1,2], p = 1,输出 0。解释:选择下标对 (1,3),差值为 0。
以下是本题的参考实现(TypeScript 风格):
#include // 包含标准输入输出头文件,用于printf等函数
#include // 包含标准库头文件,用于qsort、abs等函数
// 比较函数,用于qsort排序,按升序排列
int compare(const void* a, const void* b) {
// 将void指针转换为int指针,然后解引用获取值
int num1 = *(const int*)a; // 获取第一个整数的值
int num2 = *(const int*)b; // 获取第二个整数的值
// 返回两数的差值,正数表示num1>num2,负数表示num1= p) {
return 1; // 返回1表示可以形成至少p个配对
}
}
return 0; // 返回0表示无法形成至少p个配对
}
// 主函数:寻找p个配对的最大差值的最小值
int minimizeMax(int* nums, int numsSize, int p) {
// 如果不需要配对,直接返回0
if (p == 0) {
return 0; // 空集的最大差值为0
}
// 创建数组副本用于排序(不修改原数组)
int* sortedNums = (int*)malloc(numsSize * sizeof(int)); // 分配内存空间
// 复制原数组到新数组
for (int i = 0; i < numsSize; i++) {
sortedNums[i] = nums[i]; // 逐个元素复制
}
// 对数组进行排序
qsort(sortedNums, numsSize, sizeof(int), compare);
// 初始化二分搜索的边界
int left = 0; // 最小可能差值
int right = sortedNums[numsSize - 1] - sortedNums[0]; // 最大可能差值
int result = right; // 初始化结果为最大差值
// 二分搜索寻找最小可能的最大差值
while (left <= right) {
int mid = left + (right - left) / 2; // 计算中间值,避免整数溢出
// 检查是否能在当前mid值下形成至少p个配对
if (canFormPairs(sortedNums, numsSize, p, mid)) {
result = mid; // 更新结果为当前mid值
right = mid - 1; // 尝试更小的差值
} else {
left = mid + 1; // 需要更大的差值
}
}
// 释放动态分配的内存
free(sortedNums);
return result; // 返回找到的最小最大差值
}
// 主函数,程序入口点
int main() {
// 示例1测试数据
int nums1[] = {10, 1, 2, 7, 1, 3}; // 定义第一个测试数组
int p1 = 2; // 第一个测试需要的配对数
int size1 = sizeof(nums1) / sizeof(nums1[0]); // 计算第一个数组的长度
// 示例2测试数据
int nums2[] = {4, 2, 1, 2}; // 定义第二个测试数组
int p2 = 1; // 第二个测试需要的配对数
int size2 = sizeof(nums2) / sizeof(nums2[0]); // 计算第二个数组的长度
// 示例3测试数据(边界情况:p=0)
int nums3[] = {1, 2, 3, 4}; // 定义第三个测试数组
int p3 = 0; // 第三个测试需要的配对数
int size3 = sizeof(nums3) / sizeof(nums3[0]); // 计算第三个数组的长度
// 示例4测试数据(所有元素相同)
int nums4[] = {5, 5, 5, 5}; // 定义第四个测试数组
int p4 = 2; // 第四个测试需要的配对数
int size4 = sizeof(nums4) / sizeof(nums4[0]); // 计算第四个数组的长度
// 调用函数计算结果
int result1 = minimizeMax(nums1, size1, p1); // 计算第一个测试的结果
int result2 = minimizeMax(nums2, size2, p2); // 计算第二个测试的结果
int result3 = minimizeMax(nums3, size3, p3); // 计算第三个测试的结果
int result4 = minimizeMax(nums4, size4, p4); // 计算第四个测试的结果
// 输出结果
printf("示例1 - 数组: [10,1,2,7,1,3], p = %d\n", p1); // 打印第一个测试的输入
printf("结果: %d\n\n", result1); // 打印第一个测试的结果
printf("示例2 - 数组: [4,2,1,2], p = %d\n", p2); // 打印第二个测试的输入
printf("结果: %d\n\n", result2); // 打印第二个测试的结果
printf("示例3 - 数组: [1,2,3,4], p = %d\n", p3); // 打印第三个测试的输入
printf("结果: %d\n\n", result3); // 打印第三个测试的结果
printf("示例4 - 数组: [5,5,5,5], p = %d\n", p4); // 打印第四个测试的输入
printf("结果: %d\n\n", result4); // 打印第四个测试的结果
return 0; // 程序正常结束
}
运行结果如下:

核心启示:排序+二分+贪心的组合,是解决“最小化最大值”问题的经典范式,在 Go 中同样适用。
改变整数能得到的最大差值:数字变换的艺术
问题描述:给定整数 num,你可以进行两次操作:每次选择一个数字 x (0-9) 替换为另一个数字 y (0-9),y 可以等于 x。将两次操作得到的新数字记为 a 和 b,求 |a-b| 的最大值。注意:a 和 b 不能有前导 0,且不能为 0。
最大化 a 的策略
- 高位优先:从最高位开始,找到第一个非 9 的数字,将其所有出现替换为 9。
- 越早替换,对数值影响越大。这基于数字系统的位权原理。
最小化 b 的策略
- 首位特殊处理:若替换涉及首位,只能替换为 1(避免前导 0)。
- 非首位数字可替换为 0,但需注意有效性。
- 选择对数值减小影响最大的数字进行替换。
组合协同
最优解通常通过以下组合实现:
- a:将某个非 9 数字全部替换为 9
- b:将某个合适数字替换为 0 或 1(视位置而定)
✅ 示例验证
- 示例1:num=555 → a=999, b=111 → 差值为 888
- 示例2:num=9 → a=9, b=1 → 差值为 8
- 示例3:num=123456 → 输出 820000
- 示例4:num=10000 → 输出 80000
- 示例5:num=9288 → 输出 8700
以下是本题的参考实现(JavaScript 风格):
#include // 包含标准输入输出头文件,用于printf函数
#include // 包含标准库头文件,用于malloc、free函数
#include // 包含字符串处理头文件,用于strlen函数
// 函数:将整数转换为字符串
// 参数:num - 要转换的整数
// 返回值:动态分配的字符串指针,需要调用者释放内存
char* intToString(int num) {
// 计算数字的位数,最大支持10位整数
int length = 0; // 数字位数计数器
int temp = num; // 临时变量用于计算位数
// 计算数字的位数(包括0的情况)
if (temp == 0) {
length = 1; // 数字0有1位
} else {
while (temp > 0) { // 循环计算位数
length++; // 位数加1
temp /= 10; // 去掉最后一位
}
}
// 分配内存存储字符串(位数 + 1个字符用于字符串结束符'\0')
char* str = (char*)malloc((length + 1) * sizeof(char));
// 从最后一位开始填充字符串
str[length] = '\0'; // 字符串结束符
temp = num; // 重新初始化临时变量
// 处理数字0的特殊情况
if (temp == 0) {
str[0] = '0'; // 直接设置为'0'
} else {
// 从字符串末尾开始填充数字字符
for (int i = length - 1; i >= 0; i--) {
str[i] = '0' + (temp % 10); // 获取最后一位数字并转换为字符
temp /= 10; // 去掉最后一位
}
}
return str; // 返回生成的字符串
}
// 函数:将字符串转换为整数
// 参数:str - 要转换的字符串
// 返回值:转换后的整数值
int stringToInt(const char* str) {
int result = 0; // 结果变量
int i = 0; // 字符串索引
// 遍历字符串的每个字符
while (str[i] != '\0') {
result = result * 10 + (str[i] - '0'); // 将字符转换为数字并累加
i++; // 移动到下一个字符
}
return result; // 返回转换结果
}
// 函数:替换字符串中所有指定的字符
// 参数:str - 原始字符串,oldChar - 要替换的字符,newChar - 替换为的字符
// 返回值:新的字符串指针,需要调用者释放内存
char* replaceAll(const char* str, char oldChar, char newChar) {
int length = strlen(str); // 获取字符串长度
char* newStr = (char*)malloc(length + 1); // 分配新字符串内存
// 复制原始字符串到新字符串
for (int i = 0; i < length; i++) {
// 如果字符匹配,则替换;否则保持原字符
if (str[i] == oldChar) {
newStr[i] = newChar; // 替换字符
} else {
newStr[i] = str[i]; // 保持原字符
}
}
newStr[length] = '\0'; // 添加字符串结束符
return newStr; // 返回新字符串
}
// 函数:检查字符串是否包含前导零
// 参数:str - 要检查的字符串
// 返回值:1表示有前导零,0表示没有前导零
int hasLeadingZero(const char* str) {
// 检查字符串长度大于1且第一个字符是'0'
return (strlen(str) > 1 && str[0] == '0');
}
// 函数:检查字符串是否表示零
// 参数:str - 要检查的字符串
// 返回值:1表示是零,0表示不是零
int isZero(const char* str) {
// 检查字符串是否为"0"
return (strlen(str) == 1 && str[0] == '0');
}
// 函数:寻找最大化a值的替换策略
// 参数:str - 原始数字字符串
// 返回值:最大化后的a值字符串,需要调用者释放内存
char* findMaxA(const char* str) {
int length = strlen(str); // 获取字符串长度
char* maxA = (char*)malloc(length + 1); // 分配结果字符串内存
strcpy(maxA, str); // 复制原始字符串
// 从高位到低位寻找第一个不是9的数字
for (int i = 0; i < length; i++) {
// 如果当前数字不是9,则替换所有该数字为9
if (maxA[i] != '9') {
char digitToReplace = maxA[i]; // 记录要替换的数字
// 替换所有该数字为9
for (int j = 0; j < length; j++) {
if (maxA[j] == digitToReplace) {
maxA[j] = '9'; // 替换为9
}
}
break; // 只替换第一个非9数字
}
}
return maxA; // 返回最大化后的字符串
}
// 函数:寻找最小化b值的替换策略
// 参数:str - 原始数字字符串
// 返回值:最小化后的b值字符串,需要调用者释放内存
char* findMinB(const char* str) {
int length = strlen(str); // 获取字符串长度
char* minB = (char*)malloc(length + 1); // 分配结果字符串内存
strcpy(minB, str); // 复制原始字符串
// 情况1:如果第一位数字大于1,将其替换为1
if (minB[0] > '1') {
char firstDigit = minB[0]; // 记录第一位数字
// 替换所有该数字为1
for (int i = 0; i < length; i++) {
if (minB[i] == firstDigit) {
minB[i] = '1'; // 替换为1
}
}
}
// 情况2:如果第一位是1,寻找其他可以替换为0的数字
else {
// 从高位到低位寻找第一个可以替换为0的数字
int replaced = 0; // 替换标志
for (int i = 1; i < length && !replaced; i++) {
// 如果当前数字不是0且不是第一位,可以替换为0
if (minB[i] != '0' && minB[i] != minB[0]) {
char digitToReplace = minB[i]; // 记录要替换的数字
// 替换所有该数字为0
for (int j = 0; j < length; j++) {
if (minB[j] == digitToReplace) {
minB[j] = '0'; // 替换为0
}
}
replaced = 1; // 标记已替换
}
}
// 情况3:如果没有找到可以替换的数字,尝试其他策略
if (!replaced) {
// 寻找第一个不是1的数字替换为0
for (int i = 1; i < length && !replaced; i++) {
if (minB[i] != '1') {
char digitToReplace = minB[i]; // 记录要替换的数字
// 替换所有该数字为0
for (int j = 0; j < length; j++) {
if (minB[j] == digitToReplace) {
minB[j] = '0'; // 替换为0
}
}
replaced = 1; // 标记已替换
}
}
}
}
return minB; // 返回最小化后的字符串
}
// 主函数:计算最大差值
// 参数:num - 输入的整数
// 返回值:a和b的最大差值
int maximizeDifference(int num) {
// 将整数转换为字符串
char* numStr = intToString(num); // 转换整数为字符串
int result = 0; // 结果变量
// 寻找最大化a值的策略
char* maxAStr = findMaxA(numStr); // 获取最大化a的字符串
// 检查a是否合法(无前导零且不为零)
if (hasLeadingZero(maxAStr) || isZero(maxAStr)) {
free(maxAStr); // 释放内存
free(numStr); // 释放内存
return 0; // 返回0表示无效
}
// 寻找最小化b值的策略
char* minBStr = findMinB(numStr); // 获取最小化b的字符串
// 检查b是否合法(无前导零且不为零)
if (hasLeadingZero(minBStr) || isZero(minBStr)) {
free(maxAStr); // 释放内存
free(minBStr); // 释放内存
free(numStr); // 释放内存
return 0; // 返回0表示无效
}
// 将字符串转换为整数
int a = stringToInt(maxAStr); // 转换a字符串为整数
int b = stringToInt(minBStr); // 转换b字符串为整数
// 计算最大差值
result = a - b; // 计算a和b的差值
// 释放所有动态分配的内存
free(maxAStr); // 释放a字符串内存
free(minBStr); // 释放b字符串内存
free(numStr); // 释放原始字符串内存
return result; // 返回最终结果
}
// 主函数:程序入口点
int main() {
// 测试用例1:示例1
int num1 = 555; // 定义测试用例1
int result1 = maximizeDifference(num1); // 计算结果1
printf("测试用例1 - 输入: %d\n", num1); // 打印输入1
printf("输出: %d\n", result1); // 打印输出1
printf("解释: a=999, b=111, 差值=888\n\n"); // 打印解释1
// 测试用例2:示例2
int num2 = 9; // 定义测试用例2
int result2 = maximizeDifference(num2); // 计算结果2
printf("测试用例2 - 输入: %d\n", num2); // 打印输入2
printf("输出: %d\n", result2); // 打印输出2
printf("解释: a=9, b=1, 差值=8\n\n"); // 打印解释2
// 测试用例3:示例3
int num3 = 123456; // 定义测试用例3
int result3 = maximizeDifference(num3); // 计算结果3
printf("测试用例3 - 输入: %d\n", num3); // 打印输入3
printf("输出: %d\n", result3); // 打印输出3
printf("解释: a=923456, b=103456, 差值=820000\n\n"); // 打印解释3
// 测试用例4:示例4
int num4 = 10000; // 定义测试用例4
int result4 = maximizeDifference(num4); // 计算结果4
printf("测试用例4 - 输入: %d\n", num4); // 打印输入4
printf("输出: %d\n", result4); // 打印输出4
printf("解释: a=90000, b=10000, 差值=80000\n\n"); // 打印解释4
// 测试用例5:示例5
int num5 = 9288; // 定义测试用例5
int result5 = maximizeDifference(num5); // 计算结果5
printf("测试用例5 - 输入: %d\n", num5); // 打印输入5
printf("输出: %d\n", result5); // 打印输出5
printf("解释: a=9988, b=1288, 差值=8700\n\n"); // 打印解释5
return 0; // 程序正常结束
}
运行结果如下:

核心启示:数字变换问题考验的是对位权与约束的全局把控,在 Java 或 C++ 中,字符串操作与整数转换是常用技巧。
算法思想对比:两种极值问题的深层对话
| 维度 | 最小化最大差值 | 最大化数字差异 |
|---|---|---|
| 目标 | 均衡分配 | 创造最大差异 |
| 核心策略 | 排序+二分+贪心 | 高位优先+约束处理 |
| 约束类型 | 配对不相交 | 无前导零、非零 |
| 时间复杂度 | O(n log n + n log max) | O(d),d为数字位数 |
| 维度 | 最小化数对最大差值 | 改变整数最大差值 |
|---|---|---|
| 问题类型 | 组合优化问题 | 数字变换优化问题 |
| 搜索空间 | 连续或离散的数值范围 | 离散的数字替换可能性 |
| 约束条件 | 下标不相交、配对数量固定 | 数字合法性、操作次数限制 |
| 最优性准则 | 最小化最大值(min-max) | 最大化差值(max-diff) |
| 算法范式 | 二分搜索+贪心验证 | 策略分析+情况枚举 |
⚖️ 平衡与极致
第一个问题追求系统内部的平衡与稳定,类似罗尔斯的正义理论——关注最不利情况的改善。第二个问题则追求差异与突破,反映市场竞争中的优胜劣汰思想。两者看似对立,实则是算法智慧的一体两面。
局部与全局
在最小化最大差值问题中,排序保证了局部最优性(相邻元素配对必然包含在全局最优中)。而在数字变换中,策略必须考虑全局影响——一次替换会改变整个数值。
约束处理艺术
第一个问题通过不相交配对约束保证可行性;第二个问题通过数字合法性约束确保实用性。这体现了算法设计中规则与自由的辩证关系。
实际应用与扩展思考
最小化最大差值的现实映射:
- 任务调度中的负载均衡
- 教育资源分配的公平性优化
- 医疗资源的时间窗口安排
数字变换优化的应用场景:
- 数据加密中的数值变换
- 游戏设计中的分数系统
- 金融工程中的价格调整策略
[AFFILIATE_SLOT_1] 如果你对算法优化感兴趣,推荐阅读《算法导论》或 LeetCode 专项练习,在 Go 和 TypeScript 中实践这些思想。
总结
在这场算法的两极对话中,我们见证了最小化与最大化的深刻智慧。最小化数对的最大差值教会我们在约束中寻找平衡;改变整数的最大差值则启示我们勇于突破,善于创造差异。这两种思想,实则是算法智慧的一体两面。正如数学家哈代所言:“美是第一个检验标准。”希望本文能激发你对算法世界更多的好奇与思考。
[AFFILIATE_SLOT_2] 持续关注我们的技术博客,获取更多关于 C++、JavaScript、Java 等语言的算法实战解析。
浙公网安备 33010602011771号