This commit is contained in:
Zengtudor 2024-11-20 00:56:11 +08:00
parent 690a8a3848
commit f663b768a0
2 changed files with 445 additions and 78 deletions

View File

@ -1,101 +1,128 @@
#include <bits/stdc++.h>
using ll = int64_t;
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<std::pair<ll,ll>> adj[maxn];
// 常量定义
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<std::pair<ll, ll>> adj[maxn]; // 邻接表存储树结构pair形式为邻接节点, 道路长度)
/**
* @brief num下
* @param fth
* @param u 访
* @param num
*/
void dfs(ll fth, ll u, const ll& num){
// if(num==15){
// cout<<"is 15\n";
// }
std::multiset<ll> ms;
std::multiset<ll> 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);
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.empty()) return;
if(ms.size() == 1){
// 只有一个残余路径,无法与其他路径组合,直接传递
g[u] = *ms.begin();
return;
}
// auto it = ms.begin();
ll ans{-1},nans;
ll ans = -1; // 记录无法组合的路径长度
ll nans;
while(ms.size() >= 2){
nans=*ms.begin();
ms.erase(ms.begin());
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{
f[u]++;
ms.erase(fit);
}
else{
// 找到了一个可以与nans组合形成一条赛道
f[u]++; // 增加一个赛道
ms.erase(fit); // 移除该路径,避免重用
}
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<r){
// if(vec[l]+vec[r]>=num){
// f[u]++;
// l++,r--;
// }else{
// ret=vec[l];
// l++;
// }
// }
// if(l==r){
// g[u]=vec[r];
// }else if(ret!=-1){
// g[u]=ret;
// }
}
// 处理剩余的一个路径
if(ms.size() > 0){
g[u] = *std::prev(ms.end()); // 传递最大的剩余路径
}
else if(ans != -1){
g[u] = ans; // 传递无法组合的路径
}
}
/**
* @brief num的情况下m条赛道
* @param num
* @return m条赛道truefalse
*/
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<n;i++){
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;
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;
ll l = mins, r = maxs; // 初始化二分查找的左右边界
ll ans = l; // 初始答案设为最小道路长度
ll mid;
// 二分查找,寻找满足条件的最大最小赛道长度
while(l <= r){
mid = (l + r) / 2;
// cout<<"checking: "<<mid<<'\n';
if(check(mid)){
// cout<<"ok\n";
ans=mid;
ans = mid; // 如果可以满足尝试更大的mid
l = mid + 1;
}else{
// cout<<"failed\n";
r=mid-1;
}
else{
r = mid - 1; // 否则缩小mid
}
}
cout<<ans<<'\n';
cout << ans << '\n'; // 输出最终答案
}

340
src/P5021/P5021.md Normal file
View File

@ -0,0 +1,340 @@
# NOIP2018 T3 题解
>关于使用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 <bits/stdc++.h>
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<std::pair<ll, ll>> adj[maxn]; // 邻接表存储树结构pair形式为邻接节点, 道路长度)
/**
* @brief 深度优先搜索用于计算在给定的最小赛道长度num下可以形成多少条满足条件的赛道。
* @param fth 当前节点的父节点
* @param u 当前访问的节点
* @param num 当前尝试的最小赛道长度
*/
void dfs(ll fth, ll u, const ll& num){
std::multiset<ll> 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<ll> 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` 的使用次数,如通过优先队列等更高效的数据结构,进一步提升常数性能。