今日一记:五人分鱼与医院值班推理
今天来研究这两个问题:
1. 医院值班逻辑推理问题
核心问题:七位医生(A-G)需在一周内每天安排一人值班,需满足四项条件,且已知F在星期四值班
。 算法设计特点:
- 约束条件建模:需将条件转化为数学关系(如A=C+1、D=E+2、B=G-3等),并构建变量间的逻辑依赖。
- 穷举与剪枝结合:由于变量取值有限(7天),采用嵌套循环遍历所有可能排列,但通过条件筛选快速排除无效组合(如F固定为周四,B与G的差值约束)。
- 空间映射优化:将医生与日期映射为数组索引,通过位置关系直接验证条件(如F在B、C中间需满足
B < F < C
或C < F < B
)。 关键难点:条件间存在交叉依赖(如B的时间影响G,G又可能影响其他医生),需保证多条件同时满足。
/*** @file hospital_schedule.c* @brief 医院值班逻辑推理问题:满足约束条件的七医生排班求解*/#include <stdio.h> // 标准输入输出库(用于printf)
#include <stdbool.h> // 布尔类型支持(用于bool类型)#define DOCTOR_COUNT 7 // 医生总数
#define DAYS 7 // 一周天数
#define THURSDAY_INDEX 3 // 星期四对应的数组索引(0=周一,3=周四)// 医生编号枚举定义(A=0, B=1, ..., G=6)
typedef enum { A, B, C, D, E, F, G // 自动分配从0开始的整数值
} Doctor;/*** @brief 验证当前排班是否满足所有约束条件* @param pos 数组,pos[doctor]表示该医生的值班日期索引(0-6对应周一到周日)* @return bool 满足条件返回true,否则false*/
bool validate_conditions(const int pos[]) {// 约束1: A的值班日期比C晚1天(A的位置等于C的位置+1)if (pos[A] != pos[C] + 1) return false;// 约束2: D的值班日期比E晚2天(D的位置等于E的位置+2)if (pos[D] != pos[E] + 2) return false;// 约束3: B的值班日期比G早3天(B的位置等于G的位置-3)if (pos[B] != pos[G] - 3) return false;// 约束4: F在星期四值班(直接检查固定位置)if (pos[F] != THURSDAY_INDEX) return false;// 约束5: F在B和C中间(两种情况:B < F < C 或 C < F < B)if (!((pos[B] < pos[F] && pos[F] < pos[C]) || (pos[C] < pos[F] && pos[F] < pos[B]))) {return false;}return true; // 所有条件均满足
}/*** @brief 递归生成医生排列并验证条件(回溯算法)* @param doctors 当前已分配的医生数组,doctors[day]表示该天的值班医生* @param used 数组标记已使用的医生,used[doc]=true表示该医生已被安排* @param depth 当前递归深度(0-6对应7天,depth表示正在安排第几天)*/
void generate_schedule(int doctors[], bool used[], int depth) {// 递归终止条件:已安排完所有7天if (depth == DAYS) {int pos[DOCTOR_COUNT] = {0}; // 创建医生->日期的反向映射数组for (int day = 0; day < DAYS; day++) {// 将排班数据转换为pos数组:pos[医生编号] = 值班日期pos[doctors[day]] = day;}// 验证当前排班是否满足所有约束条件if (validate_conditions(pos)) {// 打印有效排班结果printf("Valid Schedule:\n");const char* days[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};for (int i = 0; i < DAYS; i++) {// 输出每天对应的医生字母(A+编号的ASCII转换)printf("%s: Doctor %c\n", days[i], 'A' + doctors[i]);}}return; // 结束当前递归分支}/*--- 剪枝策略:星期四必须安排医生F ---*/if (depth == THURSDAY_INDEX) {// 如果F尚未被安排if (!used[F]) {doctors[depth] = F; // 星期四强制安排Fused[F] = true; // 标记F为已使用generate_schedule(doctors, used, depth + 1); // 递归安排下一天used[F] = false; // 回溯:重置F的使用标记}return; // 结束当前分支(星期四只能安排F)}/*--- 生成其他日期的排列 ---*/for (int doc = 0; doc < DOCTOR_COUNT; doc++) {// 跳过已使用的医生和星期四的F(F已在剪枝策略中处理)if (!used[doc] && doc != F) {doctors[depth] = doc; // 尝试安排当前医生到depth这一天used[doc] = true; // 标记该医生为已使用generate_schedule(doctors, used, depth + 1); // 递归安排下一天used[doc] = false; // 回溯:重置该医生的使用标记}}
}int main() {int doctors[DAYS] = {0}; // 初始化每天的值班医生数组(0-6对应A-G)bool used[DOCTOR_COUNT] = {false}; // 初始化医生使用标记数组(全部未使用)generate_schedule(doctors, used, 0); // 从第0天(周一)开始生成排列return 0; // 程序正常退出
}
输出结果:
2. 五人分鱼逆向递推问题
核心问题:五人分鱼每次扔掉1条后平分,求初始最少鱼数使五次操作后剩1条
。 算法设计特点:
- 逆向数学递推:从最后一步(E分鱼后剩1条)逆推,利用公式
x_n = (x_{n+1} * 5)/4 + 1
逐层计算初始值。 - 模运算优化:通过观察发现每次分鱼后剩余数需满足
(x-1) % 5 == 0
,仅需验证特定步长(如从1开始,步长5)的数值。 - 终止条件明确:当逆推至初始值满足五次操作均为整数时终止,无需遍历全部可能性。 关键难点:正向暴力枚举效率低,逆向递推需找到递推公式的数学规律。
/*** @file fish_distribution.c* @brief 五人分鱼逆向递推问题:求解满足条件的最小初始鱼数*/#include <stdio.h> // 标准输入输出(printf)
#include <stdbool.h> // 布尔类型支持(bool, true, false)#define PEOPLE_COUNT 5 // 分鱼人数(A、B、C、D、E)/*** @brief 验证给定初始鱼数是否满足五次分鱼的条件* @param fish 待验证的初始鱼数* @return bool 满足所有分鱼条件返回true,否则false*/
bool is_valid(int fish) {int temp = fish; // 保存当前鱼数状态,避免修改原值for (int i = 0; i < PEOPLE_COUNT; i++) { // 模拟五人依次分鱼// 每次分鱼前必须满足:当前鱼数减1后能被5整除if ((temp - 1) % 5 != 0) { return false; // 不满足条件立即返回失败}// 计算分鱼后剩余量:(当前鱼数-1)后分成5份,拿走1份,剩余4份temp = (temp - 1) / 5 * 4; }// 最终检查:经过五轮分鱼后,剩余鱼数至少为1(题目隐含条件)return temp >= 1;
}/*** @brief 寻找满足条件的最小初始鱼数* @return int 返回最小鱼数,无解时返回-1(实际上此问题有解)*/
int find_min_fish() {int fish = 1; // 初始鱼数从1开始试探(最小可能值)while (true) { // 无限循环直到找到解if (is_valid(fish)) { return fish; // 找到符合条件的最小值}// 关键优化:每次分鱼前鱼数必须满足fish ≡1 mod5// 因此只需检查形如5k+1的数,步长设为5加速搜索fish += 5; }
}int main() {// 计算并打印最小初始鱼数int min_fish = find_min_fish(); if (min_fish > 0) {// 输出结果:3121(五人分鱼经典问题的解)printf("Minimum initial fish: %d\n", min_fish); } else {printf("No solution found.\n"); // 理论上不会执行此分支}return 0;
}
输出结果:
对比维度 | 医院值班问题 | 五人分鱼问题 |
---|---|---|
问题类型 | 约束满足问题(CSP) | 数论递推问题 |
核心算法 | 穷举法+条件剪枝 | 逆向数学递推+模运算优化 |
时间复杂度 | O(n^4)(多层嵌套循环) 3 | O(k)(k为满足条件的最小值) 6 |
空间复杂度 | O(1)(仅需存储当前排列) | O(1)(仅需存储递推中间值) |
条件处理方式 | 显式逻辑判断(if语句过滤) | 隐式数学公式推导(递推公式) |
适用场景 | 多变量、多交叉约束的排列问题 | 单变量、线性递推的数学问题 |
优化策略 | 固定已知变量(如F在周四)减少搜索空间 | 利用模运算缩小候选数范围 |
三、总结
-
算法设计差异:
- 医院值班问题依赖逻辑推理与排列组合,需通过显式条件判断快速剪枝,适合处理多变量交叉约束的场景。
- 五人分鱼问题依赖数学建模与递推公式,通过逆向推导将复杂操作转化为线性计算,适合单变量递推优化。
-
效率与复杂度:
- 医院值班问题的嵌套循环导致较高时间复杂度,但通过固定变量(如F)可显著减少计算量
- 五人分鱼问题的逆向递推避免无效枚举,数学优化使其效率远高于正向暴力搜索
-
方法论启示:
- 约束类问题优先考虑变量间的逻辑关系,利用剪枝或回溯减少搜索空间。
- 递推类问题优先寻找数学规律(如模运算、递推公式),将操作转化为数值计算。