`Solution` `LC` 2603. 收集树中金币
LINK: https://leetcode.cn/problems/collect-coins-in-a-tree/
;
题解
有两个方法, 第一种方法是常规的思维定式的 也比较复杂, 第二种方法要简易的多 还需要思维跳跃;
所有节点分为两种: 标记点(必须要扫描到他) 和 未标记点;
方法1: 树上DFS
很容易想到的一个方法是: 遍历每一个节点, 得到当前节点的步数, 然后和Ans
取一个最小值;
于是问题就转换为: 对于一个节点, 如果求其步数;
任意节点 他的路径上 任一经过的边, 一定是经过了2
次 (即去一次 回一次); 换句话说, 他所有经过的边 会构成一棵树; 也就是, 他是原图的一个子树(连通子图);
.
这一点非常非常重要, 这是该方法的基础; 证明一下(1 因为是一笔画问题, 所以其所涉及的所有边 构成一个连通子图 即子树) (2 对于该子树上的任一边 一定是一去一回 最优策略不会经过多次; 即总步数为: 该子树的边数 * 2
)
根据此, 将该子树(路径) 分成2个部分; 我们以0为树根, 该子树分为2个部分, 当前节点为c
其父节点为f
, 子树的一部分为c-f-...
从c到f往上的这部分 记作Step_up[]
, 另部分是c往下到各个儿子这部分 往下的这部分我们记作为Step_down[]
; (该节点的答案为: Step_up[c] + Step_down[c]
)
--
计算Step_down
令D1_down[x]
为: 以0为树根, x
的所有为标记点的子节点(包括自己)中 最深的深度depth
;
当前节点c的一个儿子s 如果c需要前往s (即c-s这条边在子树中) ⟺ \iff ⟺ D1_down[s] - depth[c] > 2
;
.
这一判定条件, 非常非常重要, 只要证明此转换公式是正确的 一切都变的简单; 否则, 如果你设置D1: 距离为1的标记点; D2: 距离为2的标记点; D3: 距离>2的标记点
这样问题会变得很复杂;
.
如果c需要前往s, 则更新Step_down[ c] += (2 + Step_down[ s])
; 这个更新公式也非常重要;
--
计算Step_up
void Dfs_up( int _cur, int _fa, int _step_up, int _maxLen_up, int _deep){
_step_up
即为答案, 即cur需要往上走的子树的步数 (答案为_step_up + Step_down[cur]
);
_maxLen_up
为: 除去cur子树(即cur及其子节点)后的子树 (也就是cur往上经过fa的所有节点)中的所有标记点 到cur的最大距离 (注意, 不包括cur
);
.
有两个取值: (1 -1
表示这个没有满足要求的标记点) (2 >=1
表示存在符合要求的标记点 表示距离cur的最大距离);
.
只有当_maxLen_up > 2
时, 此时_step_up
一定是>= 2
的;
_deep
为当前节点的深度;
这个更新也很复杂, 对于dfs_up
你的关注 重点, 要放到当cur -> s
往儿子走时 如何确保儿子s
的信息是正确的;
s
是否需要前往cur
呢? 这和一样, 如果s
的maxLen_up > 2
, 则s
必须前往cur
;
.
但更新公式不是step_up[s] += (2 + step_up[cur])
这是错误的! step_up[cur]
是cur-fa的子树, 而还有一种情况 cur-s2
cur前往其他儿子(非s
) 对于s
来说 也属于up
往上;
.
即令ss
为cur的所有除了s
的儿子, s
往上的子树 其实是分为2个部分的 (1 step_up[cur]
) (2 cur
到ss
的Step_down
之和 也就是去除Step_down[s] + 2
后的Step_down[cur]
)
因此, 儿子s
的maxLen_up
和其step_up
一样, 也是分为两个情况 (一个是cur - fa
的部分) (一个是cur - ss
的部分);
这确实比较复杂;
代码
vector< int> * AA;
int D1_down[ 30004], D2_down[ 30004];
int Step_down[ 30004];
Graph * G;
int Ans;
void Dfs_down( int _cur, int _fa, int _deep){vector< int> depth( 0);auto add_depth = [&depth]( int _a){depth.push_back( _a);if( depth.size() > 2){nth_element( depth.begin(), depth.begin() + 2, depth.end(), greater<>());depth.resize( 2);}};//--Step_down[ _cur] = 0;//--for( int nex, e = G->Head[ _cur]; ~e; e = G->Next[ e]){nex = G->Vertex[ e];if( nex == _fa){ continue;}Dfs_down( nex, _cur, _deep + 1);//--add_depth( D1_down[ nex]); // 没有D2[nex];//--if( D1_down[ nex] - _deep > 2){ Step_down[ _cur] += 2 + Step_down[ nex];}}//--if( (* AA)[ _cur] == 1){ add_depth( _deep);}while( depth.size() < 2){ add_depth( -1);}sort( depth.begin(), depth.end(), greater<>());D1_down[ _cur] = depth.front(), D2_down[ _cur] = depth.back();//--// D_( _cur S_ D1_down[ _cur] S_ D2_down[ _cur] S_ Step_down[ _cur]);
}
void Dfs_up( int _cur, int _fa, int _step_up, int _maxLen_up, int _deep){// D_( _cur S_ _step_up S_ _maxLen_up);Ans = min( Ans, _step_up + Step_down[ _cur]);ASSERT_( _maxLen_up == -1 || _maxLen_up >= 1);//--if( _maxLen_up != -1){ ++ _maxLen_up;}for( int nex, e = G->Head[ _cur]; ~e; e = G->Next[ e]){nex = G->Vertex[ e];if( nex == _fa){ continue;}//--int maxLen_down;if( D1_down[ nex] != D1_down[ _cur]){ maxLen_down = D1_down[ _cur];}else{ maxLen_down = D2_down[ _cur];}if( maxLen_down != -1){maxLen_down -= _deep;++ maxLen_down;}//--auto len = max( _maxLen_up, maxLen_down); if( len > 2){auto step_down = Step_down[ _cur];if( D1_down[ nex] - _deep > 2){ step_down -= (2 + Step_down[ nex]);}//--Dfs_up( nex, _cur, (_step_up + step_down) + 2, len, _deep + 1);}else{Dfs_up( nex, _cur, 0, len, _deep + 1);}}
}
int collectTheCoins(vector<int>& A, vector<vector<int>>& B) {AA = &A;int n = A.size();G = new Graph( n, n * 2, n);for( auto & v : B){ G->Add_edge( v[0], v[1]), G->Add_edge( v[1], v[0]);}//--Ans = 0x7F7F7F7F;Dfs_down( 0, -1, 0);Dfs_up( 0, -1, 0, -1, 0);//--return Ans;
}
方法2: 动态删除叶节点
这很需要思维跳跃性;
对于这棵树中的一个非标记点的叶节点c
, 将其和邻近边一同删除掉, 不停的重复次过程; (注意, 因为是动态的过程 我们所删去的节点 可能一开始并不是叶节点);
对于所删除的节点c
:
1 他一定可以不是答案 (即答案可以选择其他节点来获得)
.
当要删除c时 此时的子树中 c是叶子, 令f
为c
的邻接点 (只有一个), 对于最初树 f也是c邻接点 但c可能还有若干其他邻接点 (只是已经删去了) 令sub
为c的所有除了f
的子树集合 (即sub里的点和边 此时都已经删去了) ;
.
(1 如果c的答案路径为空, 则f的路径也为空)
.
(2 如果c的路径不为空 他一定不会经过sub
因为sub
都不是标记点, 即c的路径 一定是往f方向走的; 假设步数为x
, 则f的路径步数为x - 2
比c更优); 因此c一定可以不是答案;
2 答案节点的路径, 一定不经过该节点;
.
因为sub
不会是答案, 所以答案路径一定是从f
来到达c
, 而sub
里没有标记点 所以从f到c是无意义的;
--
此时删去完后, 此时的树 所有的叶节点 都是标记点;
令L
为所有的叶节点集合, U
为L
的邻接点集合, 现在不是动态的了, 一次性删除完L, U
, 剩下的树: 任一节点都是答案节点, 他们的路径都一样 都是这个剩下的树的边数 * 2
;
其实这个思路是错误的, 看个例子: X - b - a - Y, a - c - d - Z
(XYZ
为叶子 都是标记点)
.
对于Z
他的邻接点d
, 确实要删去, d
不会是答案 也不会被答案路径所经过;
.
但是, 重要的a
他是Y
的临界点, 但不可以去掉的, 因为a
距离X
为2; 换句话说, 最终的答案 是a-c
这个子树 (不管答案选a/c
, 路径都是这个子树, 即答案为: 这个子树的边数 * 2
)
正确的处理是: 分两次处理 (1 把所有叶节点给去掉) (2 再把所有叶节点给去掉)
代码
int collectTheCoins(vector<int>& A, vector<vector<int>>& B) {if( accumulate( A.begin(), A.end(), 0) <= 1){ return 0;}//--int n = A.size();Graph G( n, n * 2, n);vector< int> Deg( n, 0);for( auto & v : B){ G.Add_edge( v[0], v[1]), G.Add_edge( v[1], v[0]);Deg[ v[0]] ++, Deg[ v[1]] ++;}//--{ // 动态的 删除特定的叶子queue< int> que;for( int i = 0; i < n; ++i){if( Deg[ i] == 1 && A[ i] == 0){ que.push( i);}}while( !que.empty()){int cur = que.front(); que.pop();ASSERT_( Deg[ cur] != 0);if( Deg[ cur] == 0){ continue;}for( int nex, e = G.Head[ cur]; ~e; e = G.Next[ e]){nex = G.Vertex[ e];//---- Deg[ cur], -- Deg[ nex];if( Deg[ nex] == 1 && A[ nex] == 0){ que.push( nex);} // A[nex] == 1}}}{ // (现在叶子全是标记点) 删除所有叶子和`与叶子距离为1的新叶节点`;{ // 第一层的叶子queue< int> que;for( int i = 0; i < n; ++i){if( Deg[ i] == 1){ ASSERT_( A[ i] == 1);que.push( i);}}while( !que.empty()){int cur = que.front(); que.pop();if( Deg[ cur] == 0){ continue;}for( int nex, e = G.Head[ cur]; ~e; e = G.Next[ e]){ nex = G.Vertex[ e];//---- Deg[ cur], -- Deg[ nex];}}}{ // 第二层的叶子 (去除第一层叶子后 新的叶子)queue< int> que;for( int i = 0; i < n; ++i){if( Deg[ i] == 1){ que.push( i);}}while( !que.empty()){int cur = que.front(); que.pop();if( Deg[ cur] == 0){ continue;}for( int nex, e = G.Head[ cur]; ~e; e = G.Next[ e]){ nex = G.Vertex[ e];//---- Deg[ cur], -- Deg[ nex];}}}}int Ans = 0;for( int cur = 0; cur < n; ++cur){for( int nex, e = G.Head[ cur]; ~e; e = G.Next[ e]){nex = G.Vertex[ e];if( Deg[ cur] > 0 && Deg[ nex] > 0){++ Ans;}}}return Ans;
}
`Solution` `LC` 2603. 收集树中金币相关推荐
- 树莓派卸载腾出空间_腾出时间进行仪表和观测
树莓派卸载腾出空间 数字看起来如何?(How are the numbers looking?) Working in tech start-ups, we are often asked about ...
- 第 338 场周赛 (力扣周赛)
6354. K件物品的最大和 袋子中装有一些物品,每个物品上都标记着数字 1 .0 或 -1 . 给你四个非负整数 numOnes .numZeros .numNegOnes 和 k . 袋子最初包含 ...
- 【LeetCode 周赛题解】第338场周赛题解
题目列表 6354. K 件物品的最大和(easy) 6355. 质数减法运算(medium) 6357. 使数组元素全部相等的最少操作次数(medium) 6356. 收集树中金币(hard) 63 ...
- 收集金币(人人网笔试)
题目描述: 小M来到了一个迷宫中,这个迷宫可以用一个N*M的矩阵表示.在这个迷宫的某些位置中存在金币.一开始小M在迷宫的入口:矩阵的左上角,位置(1,1)处:迷宫的出口位于矩阵的右下角,位置(N,M) ...
- unity小球吃金币小游戏
链接放在这里 unity小球吃金币小游戏-Unity3D文档类资源-CSDN下载这是我在学完虚拟现实技术这门课程后利用unity所做的小球吃金币小游戏,里面有源码和作品源文件,用u更多下载资源.学习资 ...
- 第二次 leetcode周赛总结(开心!多总结,多进步)
T1:k件物品的最大和 袋子中装有一些物品,每个物品上都标记着数字 1 .0 或 -1 . 给你四个非负整数 numOnes .numZeros .numNegOnes 和 k . 袋子最初包含: n ...
- NOIP2009 pj
A 1.多项式输出 (poly.pas/c/cpp) [问题描述] 一元 n 次多项式可用如下的表达式表示: 1 0 1 1 f (x) a x a xn ... a x a n n n = + − ...
- CSP-J复赛复习题目(NOIP普及组2000-2011)
CSP-J复赛复习题目(NOIP普及组2000-2011) NOIP普及组复赛(某个不存在的比赛)2000-2011年的题面和样例 可以用来复习CSP-J 建议去OJ上查看并提交 祝大家CSP RP+ ...
- Learning C# by Developing Games with Unity 5.x(2nd) 学习
项目:https://pan.baidu.com/s/1o7IMcZo 1 using UnityEngine; 2 using System.Collections; 3 4 namespace V ...
最新文章
- oracle rman异地备份,Rman 异地备份 - markGao的个人空间 - OSCHINA - 中文开源技术交流社区...
- Transformer中的位置编码(PE,position)
- java缓冲流,BufferedReader,BufferedWriter 详解
- linux xampp nginx,nginx配置教程_如何配置nginx_nginx安装与配置详解
- Redis源码剖析(二)io多路复用函数及事件驱动流程
- mysql semi-synchronous_MySQL Semisynchronous Replication介绍
- docker安装mysql_Docker 安装 MySQL
- Scrum指南这么改,我看要完蛋!
- ASP.NET 经典60道面试题
- java中的weblogic_Java访问Weblogic中的连接池
- 阶段1 语言基础+高级_1-3-Java语言高级_08-JDK8新特性_第1节 常用函数接口_15_常用的函数式接口_Predicate接口练习-集合信息的筛选...
- 【高等数学】微积分----教你如何简单地推导求导公式(二)
- Python之OpenCV 007 《走近混沌》分形艺术Fractal之美
- Flutter(十七) 实现国际化
- 物联网环境监测数据中心-物联网项目开发
- Lending Club信贷违约风险分析(R语言)
- c语言 自动计时的秒表,c语言实现的简单秒表计时器
- 腾讯AI Lab 提出「完全依存森林」,大幅缓解关系抽取中的错误传递
- wps插入入html,WPS文字技巧—如何在WPS文字中快速插入域
- windows下编程控制摄像头的详细介绍