C 语言贪心算法实战:解决经典活动选择问题
活动选择问题是贪心算法的典型应用场景,核心目标是在一组互斥的活动中选择最多的互不冲突活动。本文通过完整的 C 语言代码实现,详解 "按结束时间排序 + 优先选结束最早活动" 的贪心策略,拆解排序、冲突判断、结果输出的全流程,帮助掌握贪心算法的核心思想与工程实现技巧。
一、活动选择问题核心规则
1. 问题描述
给定n个活动,每个活动有唯一的开始时间s[i]和结束时间f[i],活动在同一资源(如会议室)进行,要求选择最多数量的互不冲突活动(即任意两个选中活动的时间区间无重叠)。
2. 贪心策略(最优解核心)
- 核心思想:优先选择结束时间最早的活动,为后续活动留出更多可安排时间;
- 策略依据:结束时间越早,剩余可安排时间越长,能选择的活动数量越多(该策略满足 "贪心选择性质" 和 "最优子结构",可推导出全局最优解);
- 关键前提:先将所有活动按结束时间从小到大排序。
3. 冲突判断规则
若当前活动的开始时间 ≥ 上一个选中活动的结束时间 → 两个活动无冲突,可选中。
二、完整代码实现与解析
c
运行
markdown
/******************************
*文件名称:Activity_Arrangements.c
*作者:czy
*邮箱:caozhiyang_0613@163.com
*创建日期:2025/12/25
*修改日期:2025/12/26
2025/12/28
*文件功能:贪心算法解决活动选择问题
*核心思路:
* 1. 排序:用冒泡排序将活动按结束时间从小到大排序(贪心前提);
* 2. 贪心选择:优先选结束最早的活动,最大化兼容活动数量;
* 3. 核心逻辑:选中的活动开始时间 ≥ 上一个活动结束时间 → 无冲突。
*****************************/
#include<stdio.h>
#define MAX 1000
/************************************************
*函数名称:swap
*函数功能: 交换两个整数的值(指针实现)
*输入参数:
* *a - 第一个整数的指针
* *b - 第二个整数的指针
*返回参数: 无
*创建时间:2025/12/25
*修改时间:2025/12/26
*函数作者: czy
*注意事项:用于交换活动的开始/结束时间,保证排序时活动的时间对应
**************************************************/
void swap(int *a,int *b)
{
int temp = *a; // 临时变量存储a的值,避免覆盖
*a = *b; // 将b的值赋给a
*b = temp; // 将临时变量的值赋给b
}
/************************************************
*函数名称:bubblesort
*函数功能: 冒泡排序 - 按活动结束时间从小到大排序
*输入参数:
* a[2][1000] - 二维数组:a[0][i]=第i个活动开始时间,a[1][i]=第i个活动结束时间
* n - 活动总数
*返回参数: 无
*创建时间:2025/12/25
*修改时间:1-----2025/12/26
2----2025/12/28
*函数作者: czy
*核心逻辑:比较相邻活动的结束时间,交换位置(同时交换开始时间,保证时间对应)
**************************************************/
void bubblesort(int a[2][MAX],int index[MAX],int n)
{
// 外层循环:控制排序轮数(共n轮)
for(int i=0; i<n; i++)
{
// 内层循环:每轮比较到n-i-1(后i个元素已排序)
for(int j=0; j<n-i-1; j++)
{
// 若前一个活动结束时间 > 后一个 → 需要交换
if(a[1][j] > a[1][j+1])
{
swap(&a[1][j], &a[1][j+1]); // 交换结束时间
swap(&a[0][j], &a[0][j+1]); // 同步交换开始时间(关键!保证时间对应)
swap(&index[j],&index[j+1]);// 同步交换活动编号(关键)
}
}
}
}
/************************************************
*函数名称:select_activity
*函数功能: 贪心算法选择最多的互不冲突活动
*输入参数:
* a[2][MAX] - 已按结束时间排序的活动数组(a[0]=开始,a[1]=结束)
* n - 活动总数
*返回参数: 最多能选择的兼容活动数量(n≤0时返回0)
*创建时间:2025/12/25
*修改时间:2025/12/26
2025/12/28
*函数作者: czy
*贪心策略:优先选结束最早的活动,为后续活动留出更多时间
**************************************************/
int select_activity(int a[2][MAX],int index[MAX],int n,int selected_idx[MAX])
{
// 边界条件:活动数量≤0,提示错误并返回0
if(n <= 0)
{
printf("错误:活动个数必须为正整数!\n");
return 0;
}
int count = 1; // 至少选1个活动(结束最早的第一个活动)
int lastfinish = a[1][0]; // 记录上一个选中活动的结束时间(初始为第一个活动)
selected_idx[0]=index[0];
// 遍历剩余活动(从第2个开始)
for(int i=1; i<n; i++)
{
// 核心判断:当前活动开始时间 ≥ 上一个活动结束时间 → 无冲突,可选
if(a[0][i] >= lastfinish)
{
selected_idx[count]=index[i];
count++; // 选中活动,计数+1
lastfinish = a[1][i]; // 更新上一个活动的结束时间
}
}
return count; // 返回最多可选择的活动数
}
/*************************************************
*函数名称:main
*函数功能: 主函数 - 输入活动数据、调用排序+贪心函数、输出结果
*输入参数: 无
*返回参数:
* 0 - 程序正常退出
* 1 - 程序异常退出(输入无效时)
*创建时间:2025/12/26
*修改时间:2025/12/26
*函数作者:czy
**************************************************/
int main()
{
int n; // 活动个数
int a[2][MAX]; // 存储活动时间:a[0][i]=开始,a[1][i]=结束
int index[MAX]; // 活动原始编号(1~n
int selected_idx[MAX];//存储选中的活动原始编号
// 提示用户输入活动个数
printf("===== 活动选择问题(贪心算法)=====\n");
printf("请输入活动个数:\n");
scanf("%d", &n);
// 输入校验:活动个数必须为正整数
if(n <= 0 || n > MAX)
{
printf("错误:活动个数必须为1~%d之间的正整数!\n",MAX);
return 1; // 非0返回值表示程序异常退出
}
// 提示用户输入每个活动的开始时间和结束时间
printf("请依次输入每个活动的「开始时间 结束时间」(空格分隔):\n");
for(int i=0; i<n; i++)
{
index[i]=i+1;
printf("活动%d:", i+1); // 提示当前输入的是第几个活动,更友好
scanf("%d %d", &a[0][i], &a[1][i]);
}
// 步骤1:按结束时间排序(贪心算法的前提)
bubblesort(a, index , n);
// 步骤2:贪心选择最多的兼容活动
int result = select_activity(a, index,n,selected_idx);
// 输出结果(补充说明,更易理解)
printf("\n====最终选择结果===\n");
printf("\n最多能选择的互不冲突活动数量:%d\n", result);
printf("选中的活动为:\n");
// 遍历所有选中的活动(result是选中的活动总数)
for(int i=0;i<result;i++)
{
int pos=0; // 初始化pos为0:用于存储"选中编号"在排序后index数组中的位置
// 遍历排序后的index数组(找选中编号对应的位置)
for(int j =0;j<n;j++)
{
// 核心判断:找到"排序后index数组中 = 选中活动编号"的位置
if(index[j] == selected_idx[i])
{
pos = j;// 记录该位置
break; // 找到后立即退出循环,提升效率
}
}
printf("活动%d:开始时间=%d , 结束时间=%d\n",selected_idx[i],a[0][pos],a[1][pos]);
}
return 0; // 程序正常退出
}
三、核心模块拆解
1. 工具函数:swap(指针交换)
c
运行
ini
void swap(int *a,int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
- 作用:交换两个整数的值,用于排序时同步交换活动的开始时间、结束时间、原始编号;
- 核心:通过指针操作直接修改原变量值,避免值传递导致的无效交换。
2. 排序模块:bubblesort(按结束时间排序)
c
运行
css
void bubblesort(int a[2][MAX],int index[MAX],int n)
{
for(int i=0; i<n; i++)
{
for(int j=0; j<n-i-1; j++)
{
if(a[1][j] > a[1][j+1])
{
swap(&a[1][j], &a[1][j+1]); // 交换结束时间
swap(&a[0][j], &a[0][j+1]); // 同步交换开始时间
swap(&index[j],&index[j+1]);// 同步交换活动编号
}
}
}
}
- 核心逻辑 :冒泡排序的核心是 "相邻比较、逆序交换",此处按结束时间升序排序(贪心算法的前提);
- 关键细节:交换结束时间时,必须同步交换开始时间和活动原始编号,否则会出现 "时间与编号不匹配" 的错误。
3. 贪心核心:select_activity(选择最多兼容活动)
c
运行
ini
int select_activity(int a[2][MAX],int index[MAX],int n,int selected_idx[MAX])
{
if(n <= 0) { printf("错误:活动个数必须为正整数!\n"); return 0; }
int count = 1; // 初始选中第一个(结束最早的)活动
int lastfinish = a[1][0]; // 记录上一个活动的结束时间
selected_idx[0]=index[0];
for(int i=1; i<n; i++)
{
if(a[0][i] >= lastfinish) // 无冲突判断
{
selected_idx[count]=index[i];
count++;
lastfinish = a[1][i];
}
}
return count;
}
-
贪心策略落地:
- 初始选中结束时间最早的活动(排序后的第一个);
- 遍历剩余活动,仅选择 "开始时间 ≥ 上一个活动结束时间" 的活动;
- 每选中一个活动,更新 "上一个结束时间",保证后续判断的准确性;
-
返回值:最多可选择的互不冲突活动数量。
4. 主函数:流程整合与结果输出
(1)输入与校验
c
运行
perl
scanf("%d", &n);
if(n <= 0 || n > MAX)
{
printf("错误:活动个数必须为1~%d之间的正整数!\n",MAX);
return 1;
}
- 校验活动数量的合法性,避免数组越界或无效计算。
(2)数据输入
c
运行
css
for(int i=0; i<n; i++)
{
index[i]=i+1;
printf("活动%d:", i+1);
scanf("%d %d", &a[0][i], &a[1][i]);
}
- 为每个活动分配原始编号(1~n),方便后续输出时识别活动;
- 提示式输入提升用户体验。
(3)结果输出
c
运行
ini
for(int i=0;i<result;i++)
{
int pos=0;
for(int j =0;j<n;j++)
{
if(index[j] == selected_idx[i])
{
pos = j;
break;
}
}
printf("活动%d:开始时间=%d , 结束时间=%d\n",selected_idx[i],a[0][pos],a[1][pos]);
}
- 核心:通过原始编号匹配排序后的活动时间,保证输出的是活动的原始信息;
- 细节:找到匹配位置后立即
break,避免无效循环。
四、运行结果示例
输入示例
plaintext
diff
===== 活动选择问题(贪心算法)=====
请输入活动个数:
5
请依次输入每个活动的「开始时间 结束时间」(空格分隔):
活动1:1 4
活动2:3 5
活动3:0 6
活动4:5 7
活动5:8 9
输出示例
plaintext
makefile
====最终选择结果===
最多能选择的互不冲突活动数量:3
选中的活动为:
活动1:开始时间=1 , 结束时间=4
活动4:开始时间=5 , 结束时间=7
活动5:开始时间=8 , 结束时间=9
结果验证
-
排序后活动按结束时间为:活动 1(1,4)、活动 2(3,5)、活动 3(0,6)、活动 4(5,7)、活动 5(8,9);
-
贪心选择:
- 选活动 1(结束 4);
- 跳过活动 2(开始 3<4)、活动 3(开始 0<4);
- 选活动 4(开始 5≥4);
- 选活动 5(开始 8≥7);
-
总计 3 个,符合最优解。
五、贪心算法的核心要点与扩展
1. 活动选择问题的贪心正确性
- 贪心选择性质:选择结束最早的活动,剩余的时间区间最大,能容纳更多活动;
- 最优子结构:若 A 是最优解,那么 A 中第一个活动一定是结束最早的活动,剩余部分也是子问题的最优解。
2. 扩展优化思路
(1)输入合法性校验(时间维度)
c
运行
css
scanf("%d %d", &a[0][i], &a[1][i]);
if(a[0][i] < 0 || a[1][i] <= a[0][i])
{
printf("错误:活动%d的开始时间需≥0,且结束时间>开始时间!\n", i+1);
i--; // 重新输入当前活动
continue;
}
- 避免输入 "开始时间为负""结束时间≤开始时间" 的无效活动。
(2)替换更高效的排序算法
冒泡排序的时间复杂度为 O (n²),可替换为快速排序(O (nlogn)),提升大数量活动的排序效率:
c
运行
css
// 快速排序核心(按结束时间排序)
void quicksort(int a[2][MAX], int index[MAX], int left, int right)
{
if(left >= right) return;
int pivot = a[1][right]; // 基准为最右侧结束时间
int i = left - 1;
for(int j=left; j<right; j++)
{
if(a[1][j] <= pivot)
{
i++;
swap(&a[1][i], &a[1][j]);
swap(&a[0][i], &a[0][j]);
swap(&index[i], &index[j]);
}
}
i++;
swap(&a[1][i], &a[1][right]);
swap(&a[0][i], &a[0][right]);
swap(&index[i], &index[right]);
quicksort(a, index, left, i-1);
quicksort(a, index, i+1, right);
}
六、新手避坑指南
- 排序时未同步交换数据:仅交换结束时间,导致开始时间 / 编号与结束时间不匹配,输出结果混乱;
- 忽略活动原始编号:排序后直接输出数组下标,用户无法识别原始活动;
- 边界条件缺失 :未校验
n≤0或n>MAX,导致数组越界或程序崩溃; - 冲突判断逻辑错误 :误写为
a[0][i] > lastfinish,漏掉 "等于" 的情况(如活动开始时间等于上一个结束时间,实际无冲突); - 输出时未匹配位置 :直接用
selected_idx[i]作为数组下标,忽略排序后下标已变化的问题。