From f663b768a0dedf99d266a0f5854f6a624a7bd19d Mon Sep 17 00:00:00 2001 From: Zengtudor Date: Wed, 20 Nov 2024 00:56:11 +0800 Subject: [PATCH] update --- src/P5021/P5021.cpp | 183 ++++++++++++++---------- src/P5021/P5021.md | 340 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 445 insertions(+), 78 deletions(-) create mode 100644 src/P5021/P5021.md diff --git a/src/P5021/P5021.cpp b/src/P5021/P5021.cpp index 7107bc8..50ad7e9 100644 --- a/src/P5021/P5021.cpp +++ b/src/P5021/P5021.cpp @@ -1,101 +1,128 @@ #include -using ll = int64_t; -using std::cin,std::cout; +using ll = int64_t; // 定义长整型 +using std::cin, std::cout; -const ll maxn=5e4+5, inf=(ll(1)<<60); -ll n,m,mins{inf},maxs,g[maxn],f[maxn]; -std::vector> adj[maxn]; +// 常量定义 +const ll maxn = 5e4 + 5; // 最大节点数 +const ll inf = (ll(1) << 60); // 无穷大,用于初始最小值 -void dfs(ll fth,ll u,const ll &num){ - // if(num==15){ - // cout<<"is 15\n"; - // } - std::multiset ms; - for(auto [v,w]:adj[u]){ - if(fth==v)continue; - dfs(u,v,num); - f[u]+=f[v]; - if(ll ff=g[v]+w;ff>=num){ - ++f[u]; - }else{ - // vec.emplace_back(ff); - ms.emplace(ff); +ll n, m; // 节点数和赛道数 +ll mins = inf; // 道路的最小长度初始化为无穷大 +ll maxs = 0; // 道路总长度初始化为0 +ll g[maxn], f[maxn]; // g[u]:当前节点u向上传递的最长路径长度 + // f[u]:当前节点u子树中形成的赛道数 +std::vector> adj[maxn]; // 邻接表存储树结构,pair形式为(邻接节点, 道路长度) + +/** + * @brief 深度优先搜索,用于计算在给定的最小赛道长度num下,可以形成多少条满足条件的赛道。 + * @param fth 当前节点的父节点 + * @param u 当前访问的节点 + * @param num 当前尝试的最小赛道长度 + */ +void dfs(ll fth, ll u, const ll& num){ + std::multiset ms; // 存储当前节点所有子路径的部分长度 + + // 遍历当前节点的所有邻接节点 + for(auto [v, w] : adj[u]){ + if(fth == v) continue; // 避免回到父节点 + + dfs(u, v, num); // 递归遍历子节点 + f[u] += f[v]; // 累加子节点已经形成的赛道数 + + ll ff = g[v] + w; // 计算从子节点v向上传递的路径长度 + if(ff >= num){ + ++f[u]; // 该路径长度已满足至少为num,可以形成一条赛道 + } + else{ + ms.emplace(ff); // 不足,则记录下来,尝试与其他路径组合 } } - if(ms.size()==0)return; - if(ms.size()==1){ - g[u]=*ms.begin(); + + // 如果没有残余的部分路径,不需要处理 + if(ms.empty()) return; + + if(ms.size() == 1){ + // 只有一个残余路径,无法与其他路径组合,直接传递 + g[u] = *ms.begin(); return; } - // auto it = ms.begin(); - ll ans{-1},nans; - while(ms.size()>=2){ - nans=*ms.begin(); - ms.erase(ms.begin()); - auto fit = ms.lower_bound(num-nans); - if(fit==ms.end()){ - ans=nans; - }else{ - f[u]++; - ms.erase(fit); + + ll ans = -1; // 记录无法组合的路径长度 + ll nans; + + while(ms.size() >= 2){ + nans = *ms.begin(); // 取出最小的部分路径 + ms.erase(ms.begin()); // 移除该部分 + + // 尝试在剩余的部分路径中找到一个,使得两者之和 >= num + // 即寻找 >= (num - nans) 的最小值 + auto fit = ms.lower_bound(num - nans); + if(fit == ms.end()){ + // 没有找到满足条件的路径,记录当前的nans + ans = nans; + } + else{ + // 找到了一个可以与nans组合形成一条赛道 + f[u]++; // 增加一个赛道 + ms.erase(fit); // 移除该路径,避免重用 } } - if(ms.size()>0){ - g[u]=*std::prev(ms.end()); - }else if(ans!=-1){ - g[u]=ans; + + // 处理剩余的一个路径 + if(ms.size() > 0){ + g[u] = *std::prev(ms.end()); // 传递最大的剩余路径 + } + else if(ans != -1){ + g[u] = ans; // 传递无法组合的路径 } - // std::sort(vec.begin(),vec.end()); - // ll l{0},r{ll(vec.size())-1},ret{-1}; - // while(l=num){ - // f[u]++; - // l++,r--; - // }else{ - // ret=vec[l]; - // l++; - // } - // } - // if(l==r){ - // g[u]=vec[r]; - // }else if(ret!=-1){ - // g[u]=ret; - // } } +/** + * @brief 检查在最小赛道长度为num的情况下,是否可以形成至少m条赛道。 + * @param num 当前尝试的最小赛道长度 + * @return 如果可以形成至少m条赛道,则返回true;否则返回false + */ bool check(ll num){ - std::fill(f,f+1+n,0); - std::fill(g,g+1+n,0); - dfs(0,1,num); - return f[1]>=m; + std::fill(f, f + 1 + n, 0); // 初始化赛道计数 + std::fill(g, g + 1 + n, 0); // 初始化路径长度 + dfs(0, 1, num); // 从根节点1开始DFS + return f[1] >= m; // 判断总赛道数是否满足 } int main(){ - std::iostream::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr); + // 优化输入输出速度 + std::iostream::sync_with_stdio(false); + cin.tie(nullptr); + cout.tie(nullptr); - cin>>n>>m; - for(ll i{1};i>u>>v>>w; - mins=std::min(mins,w); - maxs+=w; - adj[u].emplace_back(v,w); - adj[v].emplace_back(u,w); + cin >> n >> m; // 读取节点数和赛道数 + + // 读取n-1条道路,并构建邻接表 + for(ll i = 1; i < n; i++){ + ll u, v, w; + cin >> u >> v >> w; + mins = std::min(mins, w); // 更新道路最小长度 + maxs += w; // 计算所有道路的总长度 + adj[u].emplace_back(v, w); + adj[v].emplace_back(u, w); } - ll l{mins},r{maxs},ans{l},mid; - while(l<=r){ - mid=(l+r)/2; - // cout<<"checking: "<关于使用AI +> +>1. 代码由我书写,注释和解释由ai书写 +>2. 文章经过了我的审核 +>3. [博客食用更佳](https://blog.zziyu.cn/archives/noip2018-t3-ti-jie) + +## 问题描述 + +C 城即将举办一系列赛车比赛。比赛前需要在城内修建 $m$ 条赛道。城内有 $n$ 个路口,编号为 $1, 2, \dots, n$,这些路口通过 $n-1$ 条双向道路相连,形成一棵树结构。每条道路连接两个路口,并有一个长度 $l_i$。 + +**赛道的定义与要求如下:** + +- 每条赛道是一组互不相同的道路,形成一条从某个路口出发,经过一系列道路到达另一个路口的路径。 +- 每条道路在所有赛道中至多被一条赛道使用(即赛道之间不允许有道路重叠)。 +- 赛道的长度为其包含的所有道路长度之和。 + +**目标:** +设计一种赛道修建方案,使得在修建的 $m$ 条赛道中,长度最小的赛道的长度尽可能大。 + +简而言之,我们需要从树中选择 $m$ 条互不重叠的路径,使得这些路径中的最短路径尽可能长。 + +## 算法思路 + +为了找到满足条件的最大最小赛道长度,我们可以使用**二分查找**结合**深度优先搜索(DFS)** 的方法来解决问题。 + +### 步骤如下: + +1. **二分查找范围确定:** + - 最小长度 `L` 为所有道路的最小长度。 + - 最大长度 `R` 为所有道路长度之和。 + +2. **中点尝试:** + - 取中点 `mid = (L + R) / 2`,尝试判断是否可以构造出至少 `m` 条路径,每条路径的长度都至少为 `mid`。 + +3. **DFS 检查:** + - 从树的根节点开始进行 DFS。 + - 在每个节点,尝试向下枚举可能的路径长度,并统计可以形成满足条件的赛道数。 + - 利用贪心策略尽可能组合子路径,尝试达到或超过 `mid`。 + +4. **更新二分查找范围:** + - 如果可以构造出至少 `m` 条满足条件的赛道,则尝试增大 `mid`(即 `L = mid + 1`)。 + - 否则,减小 `mid`(即 `R = mid - 1`)。 + +5. **最终答案:** + - 当二分查找结束时,`R` 即为所求的最大最小赛道长度。 + +## 代码详解与注释 + +以下是经过详细注释的代码版本,结合了上述算法思路进行解释。 + +```cpp +#include + +using ll = int64_t; // 定义长整型 +using std::cin, std::cout; + +// 常量定义 +const ll maxn = 5e4 + 5; // 最大节点数 +const ll inf = (ll(1) << 60); // 无穷大,用于初始最小值 + +ll n, m; // 节点数和赛道数 +ll mins = inf; // 道路的最小长度初始化为无穷大 +ll maxs = 0; // 道路总长度初始化为0 +ll g[maxn], f[maxn]; // g[u]:当前节点u向上传递的最长路径长度 + // f[u]:当前节点u子树中形成的赛道数 +std::vector> adj[maxn]; // 邻接表存储树结构,pair形式为(邻接节点, 道路长度) + +/** + * @brief 深度优先搜索,用于计算在给定的最小赛道长度num下,可以形成多少条满足条件的赛道。 + * @param fth 当前节点的父节点 + * @param u 当前访问的节点 + * @param num 当前尝试的最小赛道长度 + */ +void dfs(ll fth, ll u, const ll& num){ + std::multiset ms; // 存储当前节点所有子路径的部分长度 + + // 遍历当前节点的所有邻接节点 + for(auto [v, w] : adj[u]){ + if(fth == v) continue; // 避免回到父节点 + + dfs(u, v, num); // 递归遍历子节点 + f[u] += f[v]; // 累加子节点已经形成的赛道数 + + ll ff = g[v] + w; // 计算从子节点v向上传递的路径长度 + if(ff >= num){ + ++f[u]; // 该路径长度已满足至少为num,可以形成一条赛道 + } + else{ + ms.emplace(ff); // 不足,则记录下来,尝试与其他路径组合 + } + } + + // 如果没有残余的部分路径,不需要处理 + if(ms.empty()) return; + + if(ms.size() == 1){ + // 只有一个残余路径,无法与其他路径组合,直接传递 + g[u] = *ms.begin(); + return; + } + + ll ans = -1; // 记录无法组合的路径长度 + ll nans; + + while(ms.size() >= 2){ + nans = *ms.begin(); // 取出最小的部分路径 + ms.erase(ms.begin()); // 移除该部分 + + // 尝试在剩余的部分路径中找到一个,使得两者之和 >= num + // 即寻找 >= (num - nans) 的最小值 + auto fit = ms.lower_bound(num - nans); + if(fit == ms.end()){ + // 没有找到满足条件的路径,记录当前的nans + ans = nans; + } + else{ + // 找到了一个可以与nans组合形成一条赛道 + f[u]++; // 增加一个赛道 + ms.erase(fit); // 移除该路径,避免重用 + } + } + + // 处理剩余的一个路径 + if(ms.size() > 0){ + g[u] = *std::prev(ms.end()); // 传递最大的剩余路径 + } + else if(ans != -1){ + g[u] = ans; // 传递无法组合的路径 + } +} + +/** + * @brief 检查在最小赛道长度为num的情况下,是否可以形成至少m条赛道。 + * @param num 当前尝试的最小赛道长度 + * @return 如果可以形成至少m条赛道,则返回true;否则返回false + */ +bool check(ll num){ + std::fill(f, f + 1 + n, 0); // 初始化赛道计数 + std::fill(g, g + 1 + n, 0); // 初始化路径长度 + dfs(0, 1, num); // 从根节点1开始DFS + return f[1] >= m; // 判断总赛道数是否满足 +} + +int main(){ + // 优化输入输出速度 + std::iostream::sync_with_stdio(false); + cin.tie(nullptr); + cout.tie(nullptr); + + cin >> n >> m; // 读取节点数和赛道数 + + // 读取n-1条道路,并构建邻接表 + for(ll i = 1; i < n; i++){ + ll u, v, w; + cin >> u >> v >> w; + mins = std::min(mins, w); // 更新道路最小长度 + maxs += w; // 计算所有道路的总长度 + adj[u].emplace_back(v, w); + adj[v].emplace_back(u, w); + } + + ll l = mins, r = maxs; // 初始化二分查找的左右边界 + ll ans = l; // 初始答案设为最小道路长度 + ll mid; + + // 二分查找,寻找满足条件的最大最小赛道长度 + while(l <= r){ + mid = (l + r) / 2; + if(check(mid)){ + ans = mid; // 如果可以满足,尝试更大的mid + l = mid + 1; + } + else{ + r = mid - 1; // 否则,缩小mid + } + } + + cout << ans << '\n'; // 输出最终答案 +} +``` + +### 代码详解 + +1. **输入与邻接表构建:** + + ```cpp + cin >> n >> m; // 读取节点数和赛道数 + + // 读取n-1条道路,并构建邻接表 + for(ll i = 1; i < n; i++){ + ll u, v, w; + cin >> u >> v >> w; + mins = std::min(mins, w); // 更新道路最小长度 + maxs += w; // 计算所有道路的总长度 + adj[u].emplace_back(v, w); + adj[v].emplace_back(u, w); + } + ``` + + 这里,我们读取了树的结构,并通过邻接表 `adj` 存储每个节点的邻接节点及其道路长度。同时,计算了所有道路的最小长度 `mins` 和总长度 `maxs`,用于二分查找的初始范围。 + +2. **二分查找部分:** + + ```cpp + ll l = mins, r = maxs; // 初始化二分查找的左右边界 + ll ans = l; // 初始答案设为最小道路长度 + ll mid; + + // 二分查找,寻找满足条件的最大最小赛道长度 + while(l <= r){ + mid = (l + r) / 2; + if(check(mid)){ + ans = mid; // 如果可以满足,尝试更大的mid + l = mid + 1; + } + else{ + r = mid - 1; // 否则,缩小mid + } + } + + cout << ans << '\n'; // 输出最终答案 + ``` + + 通过二分查找,我们在 `mins` 和 `maxs` 之间寻找最大的 `mid`,使得至少可以形成 `m` 条长度不小于 `mid` 的赛道。`check(mid)` 函数用于判断是否满足条件。 + +3. **检查函数 `check(ll num)`:** + + ```cpp + bool check(ll num){ + std::fill(f, f + 1 + n, 0); // 初始化赛道计数 + std::fill(g, g + 1 + n, 0); // 初始化路径长度 + dfs(0, 1, num); // 从根节点1开始DFS + return f[1] >= m; // 判断总赛道数是否满足 + } + ``` + + 这个函数通过调用 `dfs`,计算在给定的最小赛道长度 `num` 下,能够形成的赛道数。若满足至少 `m` 条,则返回 `true`。 + +4. **深度优先搜索函数 `dfs(ll fth, ll u, const ll& num)`:** + + ```cpp + void dfs(ll fth, ll u, const ll& num){ + std::multiset ms; // 存储当前节点所有子路径的部分长度 + + // 遍历当前节点的所有邻接节点 + for(auto [v, w] : adj[u]){ + if(fth == v) continue; // 避免回到父节点 + + dfs(u, v, num); // 递归遍历子节点 + f[u] += f[v]; // 累加子节点已经形成的赛道数 + + ll ff = g[v] + w; // 计算从子节点v向上传递的路径长度 + if(ff >= num){ + ++f[u]; // 该路径长度已满足至少为num,可以形成一条赛道 + } + else{ + ms.emplace(ff); // 不足,则记录下来,尝试与其他路径组合 + } + } + + // 如果没有残余的部分路径,不需要处理 + if(ms.empty()) return; + + if(ms.size() == 1){ + // 只有一个残余路径,无法与其他路径组合,直接传递 + g[u] = *ms.begin(); + return; + } + + ll ans = -1; // 记录无法组合的路径长度 + ll nans; + + while(ms.size() >= 2){ + nans = *ms.begin(); // 取出最小的部分路径 + ms.erase(ms.begin()); // 移除该部分 + + // 尝试在剩余的部分路径中找到一个,使得两者之和 >= num + // 即寻找 >= (num - nans) 的最小值 + auto fit = ms.lower_bound(num - nans); + if(fit == ms.end()){ + // 没有找到满足条件的路径,记录当前的nans + ans = nans; + } + else{ + // 找到了一个可以与nans组合形成一条赛道 + f[u]++; // 增加一个赛道 + ms.erase(fit); // 移除该路径,避免重用 + } + } + + // 处理剩余的一个路径 + if(ms.size() > 0){ + g[u] = *std::prev(ms.end()); // 传递最大的剩余路径 + } + else if(ans != -1){ + g[u] = ans; // 传递无法组合的路径 + } + } + ``` + + **核心逻辑解释:** + + - **目的:** 在给定最小赛道长度 `num` 的条件下,统计当前子树能够形成的赛道数。 + + - **工作原理:** + 1. 对于每个子节点,递归获取其向上传递的最长路径长度 `g[v]`。 + 2. 若 `g[v] + w >= num`,则可以形成一条赛道,赛道数 `f[u]` 增加。 + 3. 否则,将 `g[v] + w` 放入多重集合 `ms` 中,尝试与其他部分路径组合以达到 `num`。 + 4. 通过贪心的方法,尽可能地将最小的部分路径与合适的另一部分路径组合,形成尽量多的赛道。 + 5. 最终,未能组合的最大部分路径长度被传递给父节点,供上层继续组合使用。 + + - **为什么使用 `multiset`:** + 使用 `multiset` 是因为我们需要频繁地进行插入、删除、查找操作,并且需要保持有序,以便快速找到能够与当前部分路径组合达到 `num` 的另一部分路径。 + +## 总结 + +本题的目标是从一棵树中选择 $m$ 条互不重叠的路径,使得这些路径中的最短路径尽可能长。为了高效地找到这个最大最小值,我们采用了二分查找结合深度优先搜索的策略。 + +**关键点总结:** + +1. **二分查找:** 用于在可能的最小赛道长度范围内高效定位目标值。 + +2. **深度优先搜索(DFS):** 在每次二分查找的尝试中,DFS 用于遍历树并统计符合当前最小赛道长度条件的赛道数。 + +3. **贪心策略:** 在 DFS 中,通过贪心地组合子路径,最大化赛道数,确保路径不重叠且尽可能利用已有的部分路径。 + +4. **数据结构选择:** 使用 `multiset` 来高效处理部分路径的管理和组合,确保算法的效率和正确性。 + +通过这种方法,我们能够在较大的数据规模下(如 $n \leq 5 \times 10^4$)高效地解决问题,满足题目的时间和空间要求。 + +--- + +**进一步优化与思考:** + +- **时间复杂度:** 主体部分的时间复杂度取决于二分查找的次数(约 $O(\log S)$,$S$ 为总长度)以及每次 `check` 操作的复杂度(线性遍历所有节点和部分集合)。总体上,适用于题目给定的数据规模。 + +- **空间复杂度:** 主要由邻接表和辅助数组 `f`、`g` 组成,符合题目限制。 + +- **可能的优化:** 在实现中,可以通过一些技巧减少 `multiset` 的使用次数,如通过优先队列等更高效的数据结构,进一步提升常数性能。 \ No newline at end of file