1617. 统计子树中城市之间最大距离


  图论啊,那我先寄为敬。今天是代码搬运工。

  题意很好理解啊,就是给你一个图,让你返回所有直径对应的子树的数量。是的没错,是所有直径,你不仅要枚举 1 − d 1-d 1−d 的所有直径,你还要枚举这些直径对应的所有子树。很好理解但一点思路都没有,逛了各个大神们题解,介绍三个比较好理解的方法。

  既然又要枚举直径又要枚举子树。那要么是直接枚举直径,对于每个直径找对应的子树的数量,但是太难了,官解第三个方法就是这个,看不懂,放弃了。那就只能枚举子树,计算其直径,然后统计了。前两个方法就是依据这个来的。

  其实说子树就复杂了,题目给的虽然是树,但是实际上还是边的集合,而子树不就是这个集合的子集吗。说到枚举子集,那必然是状态压缩了(即用二进制来表示一个子集,对于长度为 n n n 的集合,可以用十进制数的 0 − 2 n 0-2^n 0−2n 的二进制来表示一个子集,二进制对应的位为 1 1 1 表示这个元素在这个子集中,反之则表示不在)。那枚举子集的问题解决了,怎么计算一棵树的直径呢?我们这里直接说定理啊,不难理解:

  三个方法中,状态压缩是必要前提,根据直径的计算方法不同,就有了前两个方法。


树形动态规划

  根据第一个求树的直径的方法,我们每次以当前节点为根向下延伸求一个最远距离 f i r s t first first 和一个次远距离 s e c o n d second second,记录 f i r s t + s e c o n d first+second first+second 的最大值,遍历完子树之后即可得到树的直径。更多细节看代码注释。

class Solution {public:vector<int> countSubgraphsForEachDiameter(int n, vector<vector<int>>& edges) {vector<vector<int>> adj(n);for(auto edge:edges){//创建邻接表int x=edge[0]-1;int y=edge[1]-1;adj[x].push_back(y);adj[y].push_back(x);}function<int(int, int& ,int& )> dfs=[&](int root,int& mask,int& d){int first=0;int second=0;mask&=~(1<<root);//在全集中去掉根节点for(int v:adj[root]){//枚举所有与根节点直接相连的点if(mask&(1<<v)){//如果这个点在子树里,才进行下一步递归mask&=~(1<<v);//从子树中删去这个点,递归这个点的孩子int dis=1+dfs(v,mask,d);//这一步已经递归完了返回结果了,我们得到了一个距离,现在根据这个距离更新最远距离和次远距离if(dis>first){//这个距离比最远距离还要大,二者都需要更新second=first;first=dis;}else if(dis>second){//这个距离大于次远距离小于最远距离,只更新次远距离second=dis;}}}d=max(d,first+second);//更新当前最大直径//对于每一个子函数,返回最远距离,再由父函数+1,最后回到根节点就可以得到最远距离,次远距离只是顺路得到的return first;};vector<int> res(n-1);for(int i=1;i<(1<<n);i++){//状态压缩枚举子树//已知子树是一个连通图,那么,把任何一个节点当作根,都可以得到一棵树//我们知道,i的二进制表示了全集中每个元素是否在子集中,这一步其实就是找出子集中编号最大的节点作为该子树的根节点//这个函数的作用是得到一个十进制数的二进制前导0的个数,int类型四字节,32位int root=32-__builtin_clz(i)-1;int mask=i;int d=0;dfs(root,mask,d);if(mask==0&&d>0){//如果遍历过程需要不断移除当前节点,所以如果mask!=0,意味着有节点不可达,是非法子树。//d>0是为了处理res边界问题,且d=0时说明这个点时孤立的,也没有意义res[d-1]++;}}return res;}
};

BFS+DFS

  根据第二个方法,我们需要将每一个子图搜两次。第一次从根节点开始,找一个最远距离,即图的深度;第二次将这个最远的点作为根节点,再求一次深度,第二次得到的深度即为树的直径。这里有一个小技巧:如果我们在搜第一遍的时候用BFS,因为BFS的算法特性,从根节点开始走,最后走到的点一定是最远的,那我们可以在判断子图的连通性的同时得到最远的点,即第一个深度。更多代码细节就不赘述了,看一看方法一的代码注释。

class Solution {public:vector<int> countSubgraphsForEachDiameter(int n, vector<vector<int>>& edges) {vector<vector<int>> adj(n);for(auto edge:edges){int x=edge[0]-1;int y=edge[1]-1;adj[x].push_back(y);adj[y].push_back(x);}function<int(int, int ,int )> dfs=[&](int parent,int u,int mask){int depth=0;for(int v:adj[u]){if(v!=parent&&mask&(1<<v)){//判断是不是父节点,保证遍历的都是子节点,不会开倒车depth=max(depth,1+dfs(u,v,mask));//每次递归的都是同一棵子树,所以不用删去节点}}return depth;};vector<int> res(n-1);for(int i=1;i<(1<<n);i++){int x=32-__builtin_clz(i)-1;int mask=i;int y=-1;//第一次的最远点queue<int> q;q.push(x);mask&=~(1<<x);while(!q.empty()){y=q.front();q.pop();for(int v:adj[y]){if(mask&(1<<v)){mask&=~(1<<v);q.push(v);}}}if(mask==0){//合法子树int d=dfs(-1,y,i);if(d>0){res[d-1]++;}}}return res;}
};

最短路径+动态规划

  这个做法真的是另辟蹊径啊,太妙了。这个做法是构建新的子树,有点类似于官解第三个方法。这个做法基于题设的一个前提:保证了给的是一棵树。换句话说,这个拓扑里面不可能有环。进一步想,对于一棵子树,如果我们往这个子树里面加一个与子树里面的某一节点有邻接边的节点,即新加入的这个点在加入子图后是可达的。再结合题设,就可以推断出,新加入的这个点一定是一个叶子节点。可以反证,如果这个点不是叶子节点,他有两个邻接边,那就只有两个情况:第一,新加入这个节点导致出现了环,这与题设相悖;第二,之前的子树是不合法子树,有节点不可达。第一种情况是不允许出现的,而第二种情况如下图左所示:一个新节点的加入联通了子树和一个不可达的节点。这种情况我们可以直接把它排除掉,因为这棵子树可以通过右边这种情况实现,不会存在漏答案。

  既然新加入的点一定是一个叶子节点,那就好说了。因为直径可以描述为,一个叶子节点到所有叶子节点的最大值。因此我们枚举合法的子树,试图往里加一个新的节点,并计算这棵新树直径,一样可以得到所有子树的直径,最后统一以下即可得到最终答案。

class Solution {public:vector<int> countSubgraphsForEachDiameter(int n, vector<vector<int>>& edges) {vector<vector<int>> dis(n,vector<int>(n,INT_MAX));//初始化个点之间的距离为节点数目,即可能距离的最大值,方便后面更新最短距离vector<int> dp(1<<n,0);//子树的直径for(int i=0;i<n;i++) dis[i][i]=0;//初始化同一节点之间距离为0for(auto edge:edges){//相邻节点间距离为1int x=edge[0]-1;int y=edge[1]-1;dis[x][y]=1;dis[y][x]=1;dp[(1<<x)+(1<<y)]=1;//所有由两个节点组成的子树,直径都为1//弗洛伊夫算最短路径for(int k=0;k<n;k++){for(int i=0;i<n;i++){for(int j=0;j<n;j++){if(dis[i][k]!=INT_MAX&&dis[k][j]!=INT_MAX){dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);}}}}for(int j=1;j<(1<<n);j++){if(dp[j]==0) continue;//该子树本身不合法,直接跳过for(int i=0;i<n;i++){//遍历节点找符合条件的新节点//当前节点已经在子树中了或者该子树已经计算过了,跳过。如101+010=011+100=111,是同一棵子树,只是节点添加顺序不同if(((1<<i)&j)!=0||dp[j+(1<<i)!=0]){continue;}for(int k=0;k<n;k++){//找一个与需要加入的节点直连的点,即新节点加入子树的连接点if(((1<<k)&j)!=0&&dis[i][k]==1){//这个点在子树集合里,且与需要新加入的点之间有边dp[j+(1<<i)]=dp[j];//新子树的直径不会小于旧子树的直径,先赋个值break;}}//如果这个新节点没找到符合条件的节点来加入旧子树,即dp[i+(1<<j)]没被赋值,直接跳过if(dp[j+(1<<i)]==0) continue;for(int k=0;k<n;k++){//寻找这个新节点与旧子树中所有节点距离的最大值,即为新子树的直径if(((1<<k)&j)!=0){dp[j+(1<<i)]=max(dp[j+(1<<i)],dis[i][k]);}}}}}vector<int> res(n-1,0);for(int j=0;j<(1<<n);j++){if(dp[j]!=0){res[dp[j]-1]++;// cout<<dp[j]<<endl;}}return res;}
};

【LeetCode 每日一题】1617. 统计子树中城市之间最大距离(hard)相关推荐

  1. LeetCode 1617. 统计子树中城市之间最大距离(枚举所有可能+图的最大直径)

    文章目录 1. 题目 2. 解题 1. 题目 给你 n 个城市,编号为从 1 到 n .同时给你一个大小为 n-1 的数组 edges ,其中 edges[i] = [ui, vi] 表示城市 ui ...

  2. LeetCode每日一题——1684. 统计一致字符串的数目

    LeetCode每日一题系列 题目:1684. 统计一致字符串的数目 难度:简单 文章目录 LeetCode每日一题系列 题目 示例 思路 题解 题目 给你一个由不同字符组成的字符串 allowed ...

  3. LeetCode 每日一题——1684. 统计一致字符串的数目

    1.题目描述 1684. 统计一致字符串的数目 给你一个由不同字符组成的字符串 allowed 和一个字符串数组 words .如果一个字符串的每一个字符都在 allowed 中,就称这个字符串是 一 ...

  4. LeetCode每日一题(持续更新中~~~)

    文章目录 2432. 处理用时最长的那个任务的员工5.5 1419. 数青蛙5.6 1010. 总持续时间可被 60 整除的歌曲5.7 2432. 处理用时最长的那个任务的员工5.5 共有 n 位员工 ...

  5. Leetcode每日一题:56. I. 数组中数字出现的次数

    本题想到了用异或去解 但是中间步骤没想到: 参照大佬解法: 相同的数异或为0,不同的异或为1.0和任何数异或等于这个数本身. nums = [1,2,10,4,1,4,3,3] a^a=0 a^0=a ...

  6. LeetCode每日一题——1812. 判断国际象棋棋盘中一个格子的颜色

    LeetCode每日一题系列 题目:1812. 判断国际象棋棋盘中一个格子的颜色 难度:简单 文章目录 LeetCode每日一题系列 题目 示例 思路 题解 题目 给你一个坐标 coordinates ...

  7. 【LeetCode每日一题】——109.有序链表转换二叉搜索树

    文章目录 一[题目类别] 二[题目难度] 三[题目编号] 四[题目描述] 五[题目示例] 六[题目提示] 七[解题思路] 八[时间频度] 九[代码实现] 十[提交结果] 一[题目类别] 二叉树 二[题 ...

  8. leetcode每日一题--前缀树;前缀哈希;深搜;面试题 08.04. 幂集;648. 单词替换面试题 01.09. 字符串轮转;剑指 Offer II 062. 实现前缀树

    leetcode每日一题 ps:今天的每日一题没意思,简单的模拟,自己换一道 面试题 08.04. 幂集 幂集.编写一种方法,返回某集合的所有子集.集合中不包含重复的元素. 说明:解集不能包含重复的子 ...

  9. Leetcode每日一题——思路小记

    文章目录 LeetCode每日一题 golang T15 2020.6.12 三数之和,双指针的运用 T70 2020.6.13 斐波那契数列 T1014 2020.6.17 最佳观光:双指针,计算公 ...

最新文章

  1. System.currentTimeMillis()竟然存在性能问题,这我能信?
  2. HDU2642(二维的树状数组)
  3. LNMP之 nginx 启动脚本和配置文件
  4. 同一个Spring-AOP的坑,我一天踩了两次,深坑啊
  5. python软件界面-用Python写一个语音播放软件
  6. 教你怎么使用Jmail发送匿名的邮件(不要身份认证)
  7. 阿里云服务器买完不知道如何使用(新手入门教程)
  8. background属性总结
  9. Cocos--开启物理
  10. 制作Win10安装U盘(量产PE+UEFI)双引导
  11. Django操作数据库
  12. SQL Server 2000管理专家系列课程之二:如何让数据库中的数据更有条理性? – 规范SQL Server 2000数据...
  13. 模拟登陆手机版新浪微博
  14. 手把手教学使用图床,再也不需要重复上传照片到博客啦
  15. python蚂蚁森林自动偷能量_蚂蚁森林自动偷能量app
  16. 中国电竞20年:从小众娱乐到新兴体育产业
  17. 2022茶艺师(中级)复训题库及模拟考试
  18. 项目开发中的时区问题汇总
  19. 使用mybatis框架分页插件报错### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax;
  20. 练字格子纸模板pdf_a4田字格练字纸打印版-练字标准田字格模板-a4打印版下载最新免费excel版-西西软件下载...

热门文章

  1. DEDECMS5.7 用星星图标表示软件等级
  2. 【每日新闻】华为推出方舟编译器称可提升安卓系统效率
  3. 微服务架构的环境搭建及简单测试
  4. adb shell top命令详解
  5. Spring IoC 原理及实现
  6. python实现人脸识别并且将识别到的人脸保存到本地
  7. 全固态钙钛矿敏化ZnO-TiO2/TiO2/SrTiO3染料敏化太阳能电池DSSCs/碱金属钛酸盐Li4Ti5O(12)尖晶石结构/SrTiO3薄膜电极材料
  8. 什么是 photoshop 中最重要、最不可缺少的功能?我认为是“通道”吧!
  9. excel之自动套用格式
  10. python判断list是否为空_Python - 判断list是否为空