文章目录
八、区域赛算法模版第八章—贪心(Greedy)
8.1 核心思想与正确性证明
贪心的本质: 每一步都做出在当前看来最优的选择,期望通过一系列局部最优达到全局最优。
关键区别: 贪心与动态规划的核心差异在于无后效性——贪心一旦做出选择,就不会再更改;而 DP 会保留所有可能的状态。
正确性证明的三种主要方法:
-
交换论证法(Exchange Argument)
- 假设存在一个最优解,且该解与贪心解在某一步不同。
- 通过交换两个元素的顺序,证明交换后不会使解变差。
- 反复交换,最终可以将最优解“调整”为贪心解,从而证明贪心策略最优。
- 典型应用: 排序类贪心(如两地调度、国王游戏)。
-
归纳法
- 证明贪心选择是“安全”的:存在一个最优解包含贪心选择。
- 证明做出贪心选择后,剩余问题是相同性质的子问题。
- 典型应用: 活动安排问题。
-
拟阵理论(Matroid)
- 拟阵是贪心算法正确性的经典理论框架。
- 若问题能建模为拟阵,则按权值从大到小排序后贪心选择,必然得到最大权独立集。
- 典型应用: 最小生成树(Kruskal)、线性无关向量选择。
8.2 区间调度(不相交区间最多)
问题: 给定若干区间,选择尽可能多的互不重叠的区间。
贪心策略: 按右端点升序排序,每次选择结束时间最早且不与已选区间冲突的区间。
class IntervalScheduler {
public:
// 按右端点排序,依次选择不重叠的区间
int maxNonOverlap(vector<pair<int, int>>& intervals) {
sort(intervals.begin(), intervals.end(),
[](const auto& a, const auto& b) {
return a.second < b.second;
});
int count = 0;
int lastEnd = INT_MIN;
for (const auto& [start, end] : intervals) {
if (start >= lastEnd) {
count++;
lastEnd = end;
}
}
return count;
}
};
思路要点:
- 结束时间越早,为后续区间留下的时间越多
- 这是拟阵结构下的经典贪心,交换论证法可证
8.3 区间覆盖(覆盖全部的最少区间)
问题: 用尽可能少的区间覆盖一个给定范围 [0, target]。
贪心策略: 按左端点排序,每次选择能覆盖当前起点且向右延伸最远的区间。
class IntervalCover {
public:
// 覆盖 [0, target] 所需的最少区间数
int minCover(vector<pair<int, int>>& intervals, int target) {
sort(intervals.begin(), intervals.end());
int count = 0;
int current = 0; // 当前已覆盖到的位置
int i = 0;
int n = intervals.size();
while (current < target) {
int furthest = current;
// 在所有左端点 <= current 的区间中,找右端点最远的
while (i < n && intervals[i].first <= current) {
furthest = max(furthest, intervals[i].second);
i++;
}
if (furthest == current) return -1; // 无法覆盖
current = furthest;
count++;
}
return count;
}
};
8.4 区间选点(覆盖所有区间的最少点)
问题: 在数轴上选尽可能少的点,使每个区间至少包含一个选中的点。
贪心策略: 按右端点升序排序,每次选择当前区间的右端点作为选点。
class IntervalPointCover {
public:
int minPoints(vector<pair<int, int>>& intervals) {
sort(intervals.begin(), intervals.end(),
[](const auto& a, const auto& b) {
return a.second < b.second;
});
int count = 0;
int lastPoint = INT_MIN;
for (const auto& [start, end] : intervals) {
if (start > lastPoint) { // 当前区间未被覆盖
count++;
lastPoint = end; // 选择右端点
}
}
return count;
}
};
8.5 交换论证法(排序类贪心)
问题: 给定若干元素,按某种规则排序后依次处理,求最优解。
核心方法: 比较“先处理 a 再处理 b”和“先处理 b 再处理 a”的代价,推导出排序规则。
经典例题:两地调度(LeetCode 1029)
问题: 2n 个人,每人去 A 市费用 a[i],去 B 市费用 b[i]。每个城市必须恰好 n 人,求最小总费用。
贪心策略: 按 a[i] - b[i] 升序排序,前 n 人去 A,后 n 人去 B。
class TwoCityScheduler {
public:
int twoCitySchedCost(vector<vector<int>>& costs) {
int n = costs.size();
// 按 (去A的费用 - 去B的费用) 升序排序
sort(costs.begin(), costs.end(),
[](const vector<int>& a, const vector<int>& b) {
return a[0] - a[1] < b[0] - b[1];
});
int total = 0;
int half = n / 2;
// 前一半去 A,后一半去 B
for (int i = 0; i < half; i++) {
total += costs[i][0];
}
for (int i = half; i < n; i++) {
total += costs[i][1];
}
return total;
}
};
证明思路(交换论证法):
- 假设最优解中,第 i 个人去了 A,第 j 个人去了 B。
- 如果
(a[i] - b[i]) > (a[j] - b[j]),即 i 去 A 的相对成本比 j 高。 - 交换后,总费用变化 =
(a[j] + b[i]) - (a[i] + b[j]) = (a[j] - b[j]) - (a[i] - b[i]) < 0,费用降低。 - 所以按
a[i] - b[i]升序,前 n 人去 A,后 n 人去 B 是最优的。
8.6 优先级队列贪心
问题: 动态维护当前最优选择,每次取极值处理。
经典例题:合并果子(哈夫曼树)
问题: n 堆果子,每次合并两堆,代价为两堆重量之和,求合并成一堆的最小总代价。
贪心策略: 每次选择当前重量最小的两堆合并。
class HuffmanMerge {
public:
long long minMergeCost(vector<int>& fruits) {
// 小根堆
priority_queue<long long, vector<long long>, greater<long long>> pq(
fruits.begin(), fruits.end());
long long totalCost = 0;
while (pq.size() > 1) {
long long a = pq.top(); pq.pop();
long long b = pq.top(); pq.pop();
long long merged = a + b;
totalCost += merged;
pq.push(merged);
}
return totalCost;
}
};
思路要点:
- 每次合并最小的两堆,可以让较大的重量被合并更少的次数
- 这是哈夫曼编码的核心思想,正确性由归纳法证明
8.7 反悔贪心(Regret Greedy)
核心思想: 先做出当前最优选择,如果后续发现该选择导致结果变差,则“反悔”撤销它。
经典例题:建筑抢修(JSOI 2007)
问题: n 个建筑需要抢修,每个建筑有耗时 t[i] 和截止时间 d[i]。求最多能抢修多少个建筑。
贪心策略:
- 按截止时间
d[i]升序排序。 - 依次尝试抢修每个建筑,累加当前总耗时。
- 如果总耗时超过了当前建筑的截止时间,则从已选择的建筑中反悔,移除耗时最长的那个。
- 最终答案就是选择的建筑数量。
class RegretGreedy {
public:
struct Building {
int time; // 抢修耗时
int deadline; // 截止时间
};
int maxRepair(vector<Building>& buildings) {
// 按截止时间升序排序
sort(buildings.begin(), buildings.end(),
[](const Building& a, const Building& b) {
return a.deadline < b.deadline;
});
// 大根堆:存储已选择的建筑的耗时
priority_queue<int> chosen;
long long totalTime = 0;
for (const auto& b : buildings) {
totalTime += b.time;
chosen.push(b.time);
// 如果总耗时超过截止时间,反悔:移除耗时最长的
if (totalTime > b.deadline) {
totalTime -= chosen.top();
chosen.pop();
}
}
return chosen.size();
}
};
思路要点:
- 反悔贪心是“带撤销的贪心”
- 用堆维护已选择集合,便于快速找到需要反悔的元素
- 正确性证明通常需要构造性论证
8.8 中位数贪心
问题: 货仓选址,在数轴上选一个点,使得到所有点的距离之和最小。
结论: 选择中位数即可。
class MedianGreedy {
public:
long long minTotalDistance(vector<int>& positions) {
sort(positions.begin(), positions.end());
int n = positions.size();
int median = positions[n / 2];
long long total = 0;
for (int pos : positions) {
total += abs(pos - median);
}
return total;
}
};
8.9 贪心常见模型总结
| 模型 | 贪心策略 | 经典题 |
|---|---|---|
| 区间调度 | 按右端点排序 | 无重叠区间 |
| 区间覆盖 | 按左端点排序,每次选延伸最远的 | 视频拼接 |
| 区间选点 | 按右端点排序,选右端点 | 用最少箭引爆气球 |
| 交换论证 | 比较交换前后,推导排序规则 | 两地调度、国王游戏 |
| 哈夫曼合并 | 每次合并最小的两个 | 合并果子 |
| 反悔贪心 | 先选,不行再反悔 | 建筑抢修 |
| 中位数 | 选中间点 | 货仓选址 |
| 单调栈 | 维护最优单调结构 | 删数字得最小数 |
8.10 注意事项
- 贪心不是万能,使用前必须证明正确性
- 排序规则往往通过交换论证法推导
- 优先级队列常用于动态维护极值
- 反悔贪心需要设计好“反悔”的逻辑
8.11.1 区域赛题
- 贪心— 题目链接:第45届国际大学生编程设计竞赛济南站D
- 题目描述及思路:
Fight against involution(ICPC 济南 2020 D / 牛客 10662 D)
【题目描述】
有 n 个学生,每个学生需要写一篇论文,第 i 个学生的论文字数 w_i 必须满足:
L_i ≤ w_i ≤ R_i
成绩规则:第 i 个学生的成绩 g_i = n - K_i,其中 K_i 是满足 w_j > w_i 的学生数量。
也就是说,成绩 = 比我字数多的人数越少,成绩越高。
每个学生都想拿到最高成绩,所以在“最优决策”下,每个学生都会写到上限 R_i。
此时,所有学生的成绩都是 n(因为没有人字数比你多?不,如果大家都写 R_i,那么成绩取决于 R_i 的分布)。
但 MianKing 发现:如果大家都写很少的字数,成绩可能不变,但可以省下时间打游戏。
现在 MianKing 想要重新决定每个学生的 w_i,满足以下条件:
1) 每个学生的成绩不能低于原来(即大家都写 R_i 时)的成绩。
2) 最小化所有学生的字数之和 Σ w_i。
求最小的 Σ w_i。
【输入】
第一行:n(1 ≤ n ≤ 10^5)
接下来 n 行:每行两个整数 L_i, R_i(1 ≤ L_i ≤ R_i ≤ 10^9)
【输出】
一个整数,表示最小的 Σ w_i。
【样例1】
输入:
3
1 10000
1 10000
1 10000
输出:
3
【样例2】
输入:
4
1 2
2 2
2 4
3 4
输出:
10
【思路解说】
【一、原始成绩的计算】
在“最优决策”下,每个学生都写 R_i。
此时,第 i 个学生的成绩 = n - (满足 R_j > R_i 的学生数)。
也就是说,成绩只取决于 R_i 的大小关系。
如果两个学生的 R_i 相同,他们的成绩相同。
如果 R_i 不同,R_i 越大的学生,成绩越低(因为比他字数多的人少?不对,R_i 越大,比他字数多的人越少,所以成绩越高)。
实际上,成绩 = n - (R_j > R_i 的人数)。
所以 R_i 越大,成绩越高。
【二、约束条件】
我们需要给每个学生选一个新的 w_i,满足:
1) L_i ≤ w_i ≤ R_i
2) 新成绩 ≥ 原成绩
原成绩 = n - (R_j > R_i 的人数)。
新成绩 = n - (w_j > w_i 的人数)。
所以条件 2 等价于:
(w_j > w_i 的人数) ≤ (R_j > R_i 的人数)
也就是说,新方案中,比 i 字数多的人数不能超过原方案中比 i 字数多的人数。
【三、贪心策略:按 R 分组,从大到小处理】
把学生按 R_i 从大到小排序。
对于 R_i 相同的学生,他们原本的成绩相同(因为 R_j > R_i 的人数相同)。
**关键观察:**
- 对于 R_i 最大的那一组,原成绩 = n(因为没有人 R_j > R_i)。
- 所以新方案中,这一组的学生成绩也必须是 n,即没有人 w_j > w_i。
- 这意味着这一组的学生必须写得**不比任何人少**,即他们的 w_i 必须 ≥ 其他所有人的 w_i。
- 为了最小化总和,这一组的学生应该取 w_i = max(L_i, 其他组的最大的 w)。
**更精确地:**
- 从 R 最大的组开始处理。
- 设当前组的 maxL = 组内所有 L_i 的最大值。
- 设下一组的 w 值为 next_w(即下一组实际取的字数)。
- 当前组的 w 必须 ≥ next_w(因为当前组成绩不能低于下一组?实际上,当前组 R 更大,成绩更高,所以当前组的 w 必须 ≥ 下一组的 w)。
- 同时,当前组的 w 必须 ≥ maxL(满足下界)。
- 所以当前组的 w = max(maxL, next_w)。
- 当前组的总贡献 = w × 组内人数。
- 更新 next_w = w,继续处理上一组。
【四、算法流程】
1) 读入 n 和所有 (L_i, R_i)。
2) 按 R_i 从大到小排序。
3) 把 R_i 相同的学生分到同一组,记录每组的 maxL 和人数。
4) 从最后一组(R 最小)往前处理:
- 初始 next_w = 0。
- 对于当前组,w = max(组内 maxL, next_w)。
- 答案 += w × 组内人数。
- next_w = w。
5) 输出答案。
【五、样例验证】
样例1:三个学生都是 (1, 10000)
按 R 分组:一组,maxL = 1,人数 3。
从后往前:next_w = 0,w = max(1, 0) = 1。
答案 = 1 × 3 = 3 ✓
样例2:四个学生 (1,2), (2,2), (2,4), (3,4)
按 R 分组:
R=4: (2,4), (3,4) -> maxL = 3, 人数 2
R=2: (1,2), (2,2) -> maxL = 2, 人数 2
从后往前:
最后一组 R=2: next_w = 0, w = max(2, 0) = 2, 答案 += 2×2 = 4, next_w = 2
上一组 R=4: w = max(3, 2) = 3, 答案 += 3×2 = 6, 总答案 = 10 ✓
【六、复杂度】
- 排序:O(n log n)
- 分组和处理:O(n)
- 总复杂度:O(n log n),可以过。
【七、注意事项】
- 按 R 从大到小排序。
- 同一 R 的学生分到一组,取组内 maxL。
- 从最后一组往前贪心,w = max(maxL, next_w)。
- 基于以上思路,代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 学生结构体:L 是下界,R 是上界
struct Student {
int L, R;
};
int main() {
int n;
scanf("%d", &n);
vector<Student> a(n);
for (int i = 0; i < n; i++) {
scanf("%d %d", &a[i].L, &a[i].R);
}
// 按 R 从大到小排序
// 因为 R 越大,原始成绩越高,需要优先保证这些学生的成绩不降低
sort(a.begin(), a.end(), [](const Student &x, const Student &y) {
return x.R > y.R;
});
// 按 R 分组,每组记录:
// maxL:组内所有 L 的最大值(为了满足组内所有学生的下界)
// count:组内学生人数
vector<pair<int, int>> groups; // (maxL, count)
int i = 0;
while (i < n) {
int j = i;
int maxL = 0;
// 把 R 相同的所有学生分到同一组
while (j < n && a[j].R == a[i].R) {
maxL = max(maxL, a[j].L);
j++;
}
groups.push_back({maxL, j - i});
i = j;
}
// 从最后一组(R 最小)往前贪心
// 原因:R 越小的组,原始成绩越低,新方案中字数可以更少
// 但新方案中,字数必须满足:R 大的组字数 >= R 小的组字数
// (否则 R 大的组成绩会降低)
ll ans = 0;
int next_w = 0; // 下一组的实际字数,初始为 0(最后一组没有下一组)
// 从后往前处理每组
for (int k = (int)groups.size() - 1; k >= 0; k--) {
// 当前组的字数 = max(组内下界 maxL, 下一组的字数 next_w)
// 因为当前组 R 更大,成绩更高,所以字数必须 >= 下一组的字数
int w = max(groups[k].first, next_w);
// 当前组所有学生都取 w,累加贡献
ans += (ll)w * groups[k].second;
// 更新 next_w,供上一组使用
next_w = w;
}
printf("%lld\n", ans);
return 0;
}
- 建树+自下而上的贪心—题目链接:第45届国际大学生程序设计竞赛上海站M
- 题目描述和思路如下:
Gitignore(2020 ICPC 上海站 M / CF Gym 102900 M)
【题目描述】
你的 git 项目中有一些文件需要被忽略(不同步)。你需要计算 `gitignore` 文件最少需要写多少行。
形式上,你的项目是一个文件夹。文件夹可以包含文件和子文件夹。没有空文件夹(即没有不包含任何文件或子文件夹的文件夹)。初始时,git 软件会同步项目中的所有文件。你可以在 `gitignore` 中指定一些文件或文件夹来排除它们。对于 `gitignore` 中的每一行,你可以指定一个文件,或者指定一个文件夹(表示忽略该文件夹下的所有文件)。你不能忽略整个项目文件夹(即不能写空行)。
给定项目中所有文件的路径,以及它们是否应该被忽略。你的任务是计算 `gitignore` 的最少行数。
【输入】
第一行:T,测试用例个数。
对于每个测试用例:
第一行:n, m(0 ≤ n, m,且 1 ≤ n + m ≤ 100),分别表示应该被忽略的文件数量,和不能被忽略的文件数量。
接下来 n 行:应该被忽略的文件路径。
接下来 m 行:不能被忽略的文件路径。
路径仅包含小写英文字母和斜杠 '/'。路径不以斜杠开头或结尾,没有连续的斜杠。文件路径是唯一的,且保证在同一个文件夹下不会有同名文件或文件夹(即不会出现 "a/b/a" 和 "a/b/a/d" 的情况)。
保证整个输入中所有路径的总长度不超过 1000。
【输出】
对于每个测试用例,输出一行一个整数,表示最少需要的 `gitignore` 行数。
【样例1】
输入:
2
3 0
data/train
data/test
model
3 1
data/train
data/test
model
data/sample
输出:
2
3
【样例解释】
样例 1:`data/` 文件夹下所有文件都要忽略,可以写 1 行 `data/`;`model` 单独写 1 行。共 2 行。
样例 2:`data/` 下有一个不能忽略的 `data/sample`,所以不能整个忽略 `data/`,只能分别写 `data/train` 和 `data/test`;`model` 单独写 1 行。共 3 行。
【思路解说】
【一、核心建模:目录树】
我们可以把所有的路径(无论是否该忽略)按 `/` 拆开,建成一棵**多叉树**(类似于 Trie)。
- 根节点代表项目根目录。
- 中间的每个节点代表一个文件夹。
- 叶子节点代表一个具体的文件。
在建树的同时,我们需要在节点上打标记:
- `hasIgn = true`:表示该节点(如果是文件)或其子树中包含**应该被忽略**的文件。
- `hasKeep = true`:表示该节点或其子树中包含**不能被忽略**的文件。
**注意:**
- 必须先处理所有需要忽略的文件并打上 `hasIgn`,再处理不能忽略的文件并打上 `hasKeep`,防止信息覆盖。
- 路径的层级关系通过 `map<string, int>` 来存子节点(应对字符串名字)。
【二、贪心策略:自底向上合并】
建完树后,我们从根节点开始 DFS 计算答案。对于当前节点 `u`:
1. **如果 `hasIgn == false`**:说明该文件夹(或文件)及其子树下没有任何需要忽略的文件,对答案的贡献为 0。
2. **如果 `hasKeep == false`**:说明该文件夹及其子树下**所有文件都是可以忽略的**。此时最优策略是**直接忽略整个文件夹**(写 1 行)。所以返回贡献 1,不需要再往下拆分了。
3. **如果 `hasIgn == true` 且 `hasKeep == true`**:说明这个文件夹里既有要忽略的,又有不能忽略的。我们**不能忽略整个文件夹**(会误伤保留的文件),只能递归处理它的所有子节点,把子节点的贡献累加起来。
**【三、根节点的特判】**
题目规定,我们不能直接忽略整个项目(即 `gitignore` 中不能写空行)。
因此,对于根节点(项目目录),我们**绝对不能返回 1 代表整个忽略**,而是必须遍历它的所有子节点,把子节点的 `dfs()` 返回值累加起来。
【四、算法流程】
1) 为每个测试用例初始化一棵空树(只有根节点 0)。
2) 读入 n 个需要忽略的文件:
- 按 '/' 拆分路径,插入树中。
- 在最后一个节点标记 `hasIgn = true`。
3) 读入 m 个不能忽略的文件:
- 按 '/' 拆分路径,插入树中。
- 在最后一个节点标记 `hasKeep = true`。
4) 自底向上汇总标记(`collect` 函数):
- 递归遍历所有子节点,如果子节点有 `hasIgn` / `hasKeep`,则父节点也要继承对应标记。
5) 计算答案:
- 初始化 `ans = 0`。
- 遍历根节点(0 号节点)的所有子节点,把 `dfs(子节点)` 的返回值累加。
6) 输出 `ans`。
【五、复杂度】
- 建树:O(总字符数)
- 汇总标记:O(节点数)
- 计算答案:O(节点数)
- 总复杂度:O(总字符数),对于题目 ≤ 1000 的数据量,瞬间通过。
- 空间复杂度:O(总字符数),即树的节点数。
【六、注意事项】
- 根节点不能直接返回 1(不能忽略整个项目)。
- 建树时只创建实际路径中的节点,不需要补全其他。
- 用 `map<string, int>` 来存子节点,比固定数组更灵活且不会浪费空间。
- 每次 `T` 循环结束后必须 `tr.clear()` 并重建根节点,防止多组测试数据互相影响。
- 路径可能是单纯的文件夹,也可能是文件夹下的文件,按 `path` 中的 `/` 分割即可。
- 基于以上思路,代码如下:
#include <bits/stdc++.h>
using namespace std;
// 树节点:用 map 存孩子(名字 -> 编号)
struct Node {
map<string, int> ch; // 子节点映射:文件夹/文件名 -> 节点编号
bool hasIgn = false; // 标记:该子树内是否存在必须忽略的文件
bool hasKeep = false; // 标记:该子树内是否存在不能忽略的文件
};
vector<Node> tr; // 全局目录树,tr[0] 为根节点
// 创建一个新节点,返回其编号
int newNode() {
tr.push_back(Node());
return (int)tr.size() - 1;
}
// 把文件路径插入树中,返回该路径最终指向的节点编号
int insertPath(const string& path) {
int cur = 0; // 从根节点 0 开始
string seg;
// 遍历字符串直到末尾(i == path.size() 时也能触发一次处理)
for (size_t i = 0; i <= path.size(); i++) {
if (i == path.size() || path[i] == '/') {
if (!seg.empty()) { // 提取出一段路径名
auto it = tr[cur].ch.find(seg);
int nxt;
if (it == tr[cur].ch.end()) {
// 如果子节点不存在,则新建
nxt = newNode();
tr[cur].ch[seg] = nxt;
} else {
// 如果已存在,直接复用
nxt = it->second;
}
cur = nxt;
seg.clear(); // 清空当前段,准备读下一段
}
} else {
seg += path[i]; // 拼接当前段的字符
}
}
return cur; // 返回叶子节点(即文件本身)的编号
}
// 自底向上汇总标记:如果子节点有 hasIgn / hasKeep,父节点也要继承
void collect(int u) {
for (auto& kv : tr[u].ch) {
collect(kv.second); // 递归处理子节点
// 子节点有的标记,父节点也要有
tr[u].hasIgn = tr[u].hasIgn || tr[kv.second].hasIgn;
tr[u].hasKeep = tr[u].hasKeep || tr[kv.second].hasKeep;
}
}
// 核心 DFS:计算忽略 u 子树下所有该忽略文件的最少行数
int dfs(int u) {
if (!tr[u].hasIgn) return 0; // 子树里没有任何要忽略的,不需要操作
if (!tr[u].hasKeep) return 1; // 子树里全是该忽略的,直接忽略整个目录(1 行)
// 子树里既有该忽略的,又有必须保留的
// 不能整体忽略当前目录,只能拆开分别处理各个子节点
int sum = 0;
for (auto& kv : tr[u].ch) {
sum += dfs(kv.second); // 累加每个子节点需要的行数
}
return sum;
}
int main() {
int T;
if (scanf("%d", &T) != 1) return 0; // 读入测试用例数
while (T--) {
int n, m;
scanf("%d %d", &n, &m); // n: 要忽略的文件数,m: 不能忽略的文件数
tr.clear(); // 清空上一组数据的树
newNode(); // 创建根节点 0
char buf[1005]; // 读入路径用的缓冲区
// ---------- 先处理要忽略的文件 ----------
vector<int> ignNodes; // 其实不需要保存,为了和原代码一致保留
for (int i = 0; i < n; i++) {
scanf("%s", buf);
int id = insertPath(string(buf));
tr[id].hasIgn = true; // 在该文件节点标记:这是要忽略的文件
ignNodes.push_back(id);
}
// ---------- 再处理不能忽略的文件 ----------
for (int i = 0; i < m; i++) {
scanf("%s", buf);
int id = insertPath(string(buf));
tr[id].hasKeep = true; // 在该文件节点标记:这是不能忽略的文件
}
// ---------- 自底向上汇总标记 ----------
collect(0); // 从根节点开始,把子树标记传递到父节点
// ---------- 计算答案 ----------
// 根节点不能整体忽略,必须拆开,逐个处理根的子节点
int ans = 0;
for (auto& kv : tr[0].ch) {
ans += dfs(kv.second); // 累加每个顶级目录/文件所需的行数
}
printf("%d\n", ans); // 输出当前测试用例的答案
}
return 0;
}
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/xxwl585/article/details/168245245




