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呢? 这和一样, 如果smaxLen_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 curssStep_down之和 也就是去除Step_down[s] + 2后的Step_down[cur])
因此, 儿子smaxLen_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是叶子, 令fc的邻接点 (只有一个), 对于最初树 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为所有的叶节点集合, UL的邻接点集合, 现在不是动态的了, 一次性删除完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. 收集树中金币相关推荐

  1. 树莓派卸载腾出空间_腾出时间进行仪表和观测

    树莓派卸载腾出空间 数字看起来如何?(How are the numbers looking?) Working in tech start-ups, we are often asked about ...

  2. 第 338 场周赛 (力扣周赛)

    6354. K件物品的最大和 袋子中装有一些物品,每个物品上都标记着数字 1 .0 或 -1 . 给你四个非负整数 numOnes .numZeros .numNegOnes 和 k . 袋子最初包含 ...

  3. 【LeetCode 周赛题解】第338场周赛题解

    题目列表 6354. K 件物品的最大和(easy) 6355. 质数减法运算(medium) 6357. 使数组元素全部相等的最少操作次数(medium) 6356. 收集树中金币(hard) 63 ...

  4. 收集金币(人人网笔试)

    题目描述: 小M来到了一个迷宫中,这个迷宫可以用一个N*M的矩阵表示.在这个迷宫的某些位置中存在金币.一开始小M在迷宫的入口:矩阵的左上角,位置(1,1)处:迷宫的出口位于矩阵的右下角,位置(N,M) ...

  5. unity小球吃金币小游戏

    链接放在这里 unity小球吃金币小游戏-Unity3D文档类资源-CSDN下载这是我在学完虚拟现实技术这门课程后利用unity所做的小球吃金币小游戏,里面有源码和作品源文件,用u更多下载资源.学习资 ...

  6. 第二次 leetcode周赛总结(开心!多总结,多进步)

    T1:k件物品的最大和 袋子中装有一些物品,每个物品上都标记着数字 1 .0 或 -1 . 给你四个非负整数 numOnes .numZeros .numNegOnes 和 k . 袋子最初包含: n ...

  7. 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 = + − ...

  8. CSP-J复赛复习题目(NOIP普及组2000-2011)

    CSP-J复赛复习题目(NOIP普及组2000-2011) NOIP普及组复赛(某个不存在的比赛)2000-2011年的题面和样例 可以用来复习CSP-J 建议去OJ上查看并提交 祝大家CSP RP+ ...

  9. 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 ...

最新文章

  1. oracle rman异地备份,Rman 异地备份 - markGao的个人空间 - OSCHINA - 中文开源技术交流社区...
  2. Transformer中的位置编码(PE,position)
  3. java缓冲流,BufferedReader,BufferedWriter 详解
  4. linux xampp nginx,nginx配置教程_如何配置nginx_nginx安装与配置详解
  5. Redis源码剖析(二)io多路复用函数及事件驱动流程
  6. mysql semi-synchronous_MySQL Semisynchronous Replication介绍
  7. docker安装mysql_Docker 安装 MySQL
  8. Scrum指南这么改,我看要完蛋!
  9. ASP.NET 经典60道面试题
  10. java中的weblogic_Java访问Weblogic中的连接池
  11. 阶段1 语言基础+高级_1-3-Java语言高级_08-JDK8新特性_第1节 常用函数接口_15_常用的函数式接口_Predicate接口练习-集合信息的筛选...
  12. 【高等数学】微积分----教你如何简单地推导求导公式(二)
  13. Python之OpenCV 007 《走近混沌》分形艺术Fractal之美
  14. Flutter(十七) 实现国际化
  15. 物联网环境监测数据中心-物联网项目开发
  16. Lending Club信贷违约风险分析(R语言)
  17. c语言 自动计时的秒表,c语言实现的简单秒表计时器
  18. 腾讯AI Lab 提出「完全依存森林」,大幅缓解关系抽取中的错误传递
  19. wps插入入html,WPS文字技巧—如何在WPS文字中快速插入域
  20. windows下编程控制摄像头的详细介绍

热门文章

  1. 羊了个羊 通关代码思路
  2. 什么是智能生产线?常州智慧工厂,数字孪生,三维交互
  3. 数据导入 - Kafka 结合Doris Routine load 任务导入
  4. JAVA实现RC4加密
  5. Chrome设置PAC模式无效的解决方案
  6. 02_MySQL环境搭建
  7. SAP PLM ECN/DCN/自定义审批事件处理类(PLM常用类 函数)
  8. SAP PLM模块常用表及表关系
  9. PLM的vocab中加入自己的词汇
  10. 用好Windows命令 识别木马蛛丝马迹