题解:洛谷 P1460 健康的荷斯坦奶牛

【题目来源】

洛谷:P1460 [USACO2.1] 健康的荷斯坦奶牛 Healthy Holsteins - 洛谷

【题目描述】

农民 John 以拥有世界上最健康的奶牛为傲。他知道每种饲料中所包含的牛所需的最低的维他命量是多少。请你帮助农夫喂养他的牛,以保持它们的健康,使喂给牛的饲料的种数最少。

给出牛所需的最低的维他命量,输出喂给牛需要哪些种类的饲料,且所需的饲料剂量最少。

维他命量以整数表示,每种饲料最多只能对牛使用一次,数据保证存在解。

【输入】

第一行一个整数 v,表示需要的维他命的种类数。第二行 v 个整数,表示牛每天需要的每种维他命的最小量。

第三行一个整数 g,表示可用来喂牛的饲料的种数。下面 g 行,第 n 行表示编号为 n 饲料包含的各种维他命的量的多少。

【输出】

输出文件只有一行,包括牛必需的最小的饲料种数 p;后面有 p 个数,表示所选择的饲料编号(按从小到大排列)。

如果有多个解,输出饲料序号最小的(即字典序最小)。

【输入样例】

4
100 200 300 400
3
50  50  50  50
200 300 200 300
900 150 389 399

【输出样例】

2 1 3

【核心思想】

  1. 问题分析:给定 \(v\) 种维他命的最小需求量,以及 \(g\) 种饲料各自含有的维他命量。要求选择最少数量的饲料,使得每种维他命的总量均满足最小需求。若有多解,输出饲料编号字典序最小的方案。这是一个子集枚举 + 可行性验证问题。

  2. 算法选择

    • DFS 枚举子集:对每个饲料编号 \(pos\),决策"选"或"不选",递归搜索所有可能的子集
    • 剪枝优化:当已选数量 \(cnt \ge minSelCnt\) 时提前返回(当前已不可能更优)
    • 可行性验证:对选定的饲料子集,累加每种维他命的总量,判断是否全部满足需求
  3. 关键步骤

    • 读入数据\(v\)(维他命种类数)、\(v[1..v]\)(最小需求量)、\(g\)(饲料种数)、\(f[i][j]\)(饲料 \(i\) 中维他命 \(j\) 的含量)
    • DFS 函数 dfs(pos, cnt)
      • pos:当前考虑的饲料编号
      • cnt:已选择的饲料数量
      • 终止条件:若 \(pos > g\),调用 check(cnt) 验证当前选择,若满足需求且 \(cnt < minSelCnt\),更新最优解
      • 选择当前饲料sel[cnt+1] = pos,递归 dfs(pos+1, cnt+1)
      • 不选当前饲料:递归 dfs(pos+1, cnt)
    • 验证函数 check(selCnt)
      • 对每种维他命 \(i\),累加所选饲料中该维他命的含量 sum
      • 若存在 sum < v[i],返回 false
      • 全部满足返回 true
    • 输出:最小选择数及对应的饲料编号
  4. 时间/空间复杂度

    • 时间复杂度:\(O(2^g \cdot v \cdot g)\),枚举 \(2^g\) 个子集,每个验证 \(O(v \cdot g)\)
    • 空间复杂度:\(O(g)\),递归深度和选择数组
  5. DFS 子集枚举的核心思想

    • 二进制决策模型:每个饲料只有"选"或"不选"两种状态,DFS 按编号顺序枚举形成一棵决策树
    • 最优性剪枝:维护当前最优解 \(minSelCnt\),当 \(cnt \ge minSelCnt\) 时无需继续搜索该分支
    • 字典序自然保证:按编号 \(1 \sim g\) 顺序决策,先选后递归,首次找到的最小数量解即为字典序最小
    • 可行性优先于最优性:先找到任意可行解获得 \(minSelCnt\) 初值,后续搜索以此剪枝
    • 适用于子集选择、组合优化、最小覆盖类问题

【解题思路】

【算法标签】

普及- #DFS-一维

【代码详解】

#include <bits/stdc++.h>
using namespace std;
int vType, fType, p;
int v[30], f[20][30], sel[20], ans[20];
int minSelCnt = 20;
bool check(int selCnt)  // 定义维他命检查函数
{
    for (int i=1; i<=vType; i++) {  // 依次遍历所有维他命种类
        int sum = 0;  // 定义每种维他命的总数,每次循环初始为0
        for (int j=1; j<=selCnt; j++) {  // 依次遍历选择的那些饲料
            sum += f[sel[j]][i];  // sum加上选择的饲料编号的维他命值
        }
        if (sum<v[i]) {  // 如果总和
            return false;
        }
    }
    return true;
}
void dfs(int pos, int cnt)  // 定义dfs函数,pos为饲料编号,cnt为饲料选择的数量
{
    if (pos > fType) {  // dfs退出条件,直到选到最后一种饲料编号
        if (check(cnt) && cnt < minSelCnt) {  // 如果此时选择的饲料的每种维他命之和大于最小维他命需要,且选择数量小于最小选择数量
            minSelCnt = cnt;  // 更新最小选择数量
            for (int i=1; i<=cnt; i++) {  // 更新结果数组,即更新为选择的饲料种类
                ans[i] = sel[i];
            }
        }
        return;  // 一定要写!
    }
    sel[cnt+1] = pos;  // 选择编号为pos的饲料
    dfs(pos+1, cnt+1);  // 继续选择pos+1编号的饲料,cnt自增1
    sel[cnt+1] = 0;  // 不选编号为pos的饲料,不选就用0占位(这句可以忽略,因为下一句dfs中cnt不变,下次执行后会被修改)
    dfs(pos+1, cnt);  // 继续dfs搜索pos+1编号的饲料,但cnt不增加
}
int main()
{
    cin >> vType;  // 输入需要的维他命种类数
    for (int i=1; i<=vType; i++) {  // 遍历维他命种类数
        cin >> v[i];  // 输入每种维他命的最小量
    }
    cin >> fType;  // 输入饲料种数
    for (int i=1; i<=fType; i++) {  // for循环遍历饲料种数
        for (int j=1; j<=vType; j++) {  // for循环遍历维他命种类数
            cin >> f[i][j];  // 输入这种饲料各种维他命的值
        }
    }
    dfs(1, 0);  // 使用dfs搜索,起始饲料编号为1,0种选择
    cout << minSelCnt << " ";
    for (int i=1; i<=minSelCnt; i++) {
        cout << ans[i] << " ";
    }
    return 0;
}

【运行结果】

4
100 200 300 400
3
50  50  50  50
200 300 200 300
900 150 389 399
2 1 3
posted @ 2026-08-03 08:47  团爸讲算法  阅读(14)  评论(0)    收藏  举报