【深度优先搜索】【树】【有向图】【推荐】685. 冗余连接 II

LeetCode685. 冗余连接 II

在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。
输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1 到 n)的树及一条附加的有向边构成。附加的边包含在 1 到 n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组 edges 。 每个元素是一对 [ui, vi],用以表示 有向 图中连接顶点 ui 和顶点 vi 的边,其中 ui 是 vi 的一个父节点。
返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。
示例 1:

输入:edges = [[1,2],[1,3],[2,3]]
输出:[2,3]
示例 2:
输入:edges = [[1,2],[2,3],[3,4],[4,1],[1,5]]
输出:[4,1]
提示:
n == edges.length
3 <= n <= 1000
edges[i].length == 2
1 <= ui, vi <= n

预备知识

有向图并集查找使用的2个前提:

前提一:所有的点出度为1或为0。
前提二:无环。
以当前节点为起点的最长路径终点,简称最长终点。如:两条边 A B → \overrightarrow{AB} AB B C → \overrightarrow{BC} BC ,A的最长终点是C。

有向树性质

性质一:任意节点都能访问自己所有后代,且不能访问自己后代之外的节点。
性质二:如果删除的边不是指向某节点的祖先,则根节点一定能访问此节点。

暴力解法

枚举删除的边,时间复杂度O(n);判断余下的边是否是有根树,时间复杂度O(n^n)。

深度优先判断有根树

前提三该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。    ⟺    \iff 只有一个节点的入度为0,此节点为根。其它节点入度为1。
前提四该树只有一个根节点,所有其他节点都是该根节点的后继。    ⟺    \iff 根节点可以访问所有的节点。通过一条边直接访问的节点是根节点的后继。通过两条边访问的节点是后继的后继 ⋯ \cdots
如果有环一定不行,n-1条边,如果有重复节点则无法访问n-1个新节点。所以访问了重复的节点直接返回。

核心代码

class Solution {
public:
	vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
		m_c = edges.size();
		for (int i = m_c-1 ; i >=0 ;i--)
		{
			vector<vector<int>> vNeiBo(m_c);
			for (int j = 0; j < m_c; j++)
			{
				if (j != i)
				{
					vNeiBo[edges[j][0] - 1].emplace_back(edges[j][1] - 1);
				}
			}
			if (Is(vNeiBo))
			{				
				return edges[i];
			}
		}
		return {};
	}
	bool Is(const vector<vector<int>>& vNeiBo)const
	{
		vector<int> vInDeg(m_c);
		for (const auto& v : vNeiBo)
		{
			for (const auto& to : v)
			{
				vInDeg[to]++;
			}			
		}
		for (int i = 0; i < m_c; i++)
		{
			if (0 == vInDeg[i])
			{
				vector<int> vVis(m_c);
				DFS(vVis, vNeiBo, i);
				return m_c == std::accumulate(vVis.begin(), vVis.end(), 0);
			}
		}
		return false;
	}
	void DFS(vector<int>& vVis, const vector<vector<int>>& vNeiBo,int cur)const
	{
		if (vVis[cur])
		{
			return;
		}
		vVis[cur] = 1;
		for (const auto& next : vNeiBo[cur])
		{
			DFS(vVis, vNeiBo, next);
		}
	}
	int m_c;

};

测试用例

template<class T,class T2>
void Assert(const T& t1, const T2& t2)
{
	assert(t1 == t2);
}

template<class T>
void Assert(const vector<T>& v1, const vector<T>& v2)
{
	if (v1.size() != v2.size())
	{
		assert(false);
		return;
	}
	for (int i = 0; i < v1.size(); i++)
	{
		Assert(v1[i], v2[i]);
	}

}

int main()
{
	vector<vector<int>> edges;
	{
		Solution sln;
		edges = { {5,2},{5,1},{3,1},{3,4},{3,5} };
		auto res = sln.findRedundantDirectedConnection(edges);
		Assert({ 3,1 }, res);
	}
	{
		Solution sln;
		edges = { {1, 2}, { 2,3 }, { 3,1 }, { 4,1 } };
		auto res = sln.findRedundantDirectedConnection(edges);
		Assert({ 3,1 }, res);
	}
	{
		Solution sln;
		edges = { {1,2},{1,3},{2,3} };
		auto res = sln.findRedundantDirectedConnection(edges);
		Assert({ 2,3 }, res);
	}
	{
		Solution sln;
		edges = { {1,2},{2,3},{3,4},{4,1},{1,5} };
		auto res = sln.findRedundantDirectedConnection(edges);
		Assert({ 4,1 }, res);
	}
	{
		Solution sln;
		edges = { {2,1},{3,1},{4,2},{1,4} };
		auto res = sln.findRedundantDirectedConnection(edges);
		Assert({ 2,1 }, res);
	}
	{
		Solution sln;
		edges = { {4,2},{1,5},{5,2},{5,3},{2,4} }		;
		auto res = sln.findRedundantDirectedConnection(edges);
		Assert({ 4,2 }, res);
	}
}

并集查找

所有边反向,即:根节出度为0,其它节点出度为1。所有节点的最长终点是都是根节点。即判定一:所有节点的最终节点相同。
有向树是判断一,无需证明。下面来证明判断一一定是有向树,判定一符合前提四无需证明,只需证明判定一符合前提三。
{ n − 1 个非根节点最终目的地是根节点,说明这些定点的出度至少为 1 。 总共 n − 1 条边。 → { 根节点出度为 0 其它节点出度为 1 \begin{cases} n-1个非根节点最终目的地是根节点,说明这些定点的出度至少为1。\\ 总共n-1条边。\\ \end{cases}\rightarrow \begin{cases}根节点出度为0\\ 其它节点出度为1 \end{cases} {n1个非根节点最终目的地是根节点,说明这些定点的出度至少为1总共n1条边。{根节点出度为0其它节点出度为1

简化

n-1条边,如果无环,则说明访问了n-1个新节点。且只有一个连通区域,否则顶点更多。故:
无环没有出度为2的定点    ⟺    \iff 有向树

代码

class CUnionFindDirect
{
public:
	CUnionFindDirect(int iSize)
	{
		m_vRoot.resize(iSize);
		iota(m_vRoot.begin(), m_vRoot.end(), 0);
	}
	void Connect(bool& bConflic, bool& bCyc, int iFrom, int iTo)
	{
		bConflic = bCyc = false;
		if (iFrom != m_vRoot[iFrom])
		{
			bConflic = true;
		}

		Fresh(iTo);
		if (m_vRoot[iTo] == iFrom)
		{
			bCyc = true;
		}
		if (bConflic || bCyc)
		{
			return;
		}

		m_vRoot[iFrom] = m_vRoot[iTo];
	}
private:
	int Fresh(int iNode)
	{
		if (m_vRoot[iNode] == iNode)
		{
			return iNode;
		}
		return m_vRoot[iNode] = Fresh(m_vRoot[iNode]);
	}
	vector<int> m_vRoot;
};

class Solution {
public:
	vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
		m_c = edges.size();
		for (int i = m_c-1 ; i >=0 ;i--)
		{			
			if (Is(edges,i))
			{				
				return edges[i];
			}
		}
		return {};
	}
	bool Is(const vector<vector<int>>& edges,int i )const
	{
		CUnionFindDirect uf(m_c);
		for (int j = 0; j < m_c; j++)
		{
			if (j != i)
			{
				bool b2 = false, bCyc = false;
				uf.Connect(b2,bCyc,edges[j][1] - 1, edges[j][0] - 1);
				if (b2 || bCyc)
				{
					return false;
				}
			}
		}
		return true;
	}

	int m_c;

};

特殊情况

如果有环,计算最长终点,可能会死循环。所以要提前判断。有环    ⟺    \iff B的最长终点是A,发现新边 A B → \overrightarrow{AB} AB
必要条件无需证明环是: B → A → B 。 必要条件无需证明环是:B \rightarrow A \rightarrow B 。 必要条件无需证明环是:BAB
现在证明充分条件:
由于是环,所以B一定能到A,假定增加新边 A B → \overrightarrow{AB} AB 前,B的最长终点是C,那么A能访问C,也就是A出度至少为1。加上 A B → \overrightarrow{AB} AB 出度就成了2。和前提三矛盾。

优化

有向森林B的父节点是C,增加的边 A B → \overrightarrow{AB} AB 。有以下两种情况:
一,无环。如果B不是A的祖先,则B无法访问A,增加新边后,就无法形成环。存在入度为2的顶点
1.1,和森林的某边重合。删除任意一边。
在这里插入图片描述

1.2,前向边,A是祖先,B是后代(A能访问B)。
在这里插入图片描述
如果先发现AB,后发现CB则是横叉边。
1.3,横叉变。A和B都无法到达他们的公共祖先,所以不会有环。
在这里插入图片描述
如果先发现AB,后发现CB则是前向边。
总结: 根据性质二,B不会是A祖先,所以1.2和1.3删除CB不会影响根节点访问A。

二,后向边,B是祖先,A是后代,B能访问A。增加新边后,A能访问B。形成了环。
2.1,如果B是根节点,则这个环上所有节点都能访问B,也就是能访问所有节点。删除任意一边后,会形成新树。不存在入度为2的顶点
在这里插入图片描述

,2.2 如果B不是根节点。不包括 C B → \overrightarrow{CB} CB A B → \overrightarrow{AB} AB 如果C是根节点的后代,则删除 A B → \overrightarrow{AB} AB 可以形成新树 。如果A是根节点的后代,则删除 C B → \overrightarrow{CB} CB ,可形成新树 。存在入度为2的顶点

在这里插入图片描述

无论是发现环,还是发现入度为2,还是同时发现环和入度为2,都只能删除折线。
处理方法一: { 删除 C B → 不通过 C B → ,根节点能到达 A 。 删除 A B → 不通过 A B → ,根节点能到达 C 。 处理方法一:\begin{cases} 删除\overrightarrow{CB} & 不通过\overrightarrow{CB},根节点能到达A。\\ 删除\overrightarrow{AB} &不通过\overrightarrow{AB},根节点能到达C。\\ \end{cases} 处理方法一:{删除CB 删除AB 不通过CB ,根节点能到达A不通过AB ,根节点能到达C

总结,5种情况合并两种:
{ 删除环中任意一边(最后一边) 不存在入度为 2 的定点 处理方法一 o t h e r \begin{cases} 删除环中任意一边(最后一边) & 不存在入度为2的定点 \\ 处理方法一 & other \\ \end{cases} {删除环中任意一边(最后一边)处理方法一不存在入度为2的定点other

代码

class CUnionFindDirect
{
public:
	CUnionFindDirect(int iSize)
	{
		m_vRoot.resize(iSize);
		iota(m_vRoot.begin(), m_vRoot.end(), 0);
	}
	void Connect(bool& bConflic, bool& bCyc, int iFrom, int iTo)
	{
		bConflic = bCyc = false;
		if (iFrom != m_vRoot[iFrom])
		{
			bConflic = true;
		}

		Fresh(iTo);
		if (m_vRoot[iTo] == iFrom)
		{
			bCyc = true;
		}
		if (bConflic || bCyc)
		{
			return;
		}

		m_vRoot[iFrom] = m_vRoot[iTo];
	}
	int GetMaxDest(int iFrom)
	{
		Fresh(iFrom);
		return m_vRoot[iFrom];
	}	
private:
	int Fresh(int iNode)
	{
		if (m_vRoot[iNode] == iNode)
		{
			return iNode;
		}
		return m_vRoot[iNode] = Fresh(m_vRoot[iNode]);
	}
	vector<int> m_vRoot;
};

class Solution {
public:
	vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
		m_c = edges.size();
		vector<int> vParent(m_c, -1);
		int iParentOld = -1;
		for (int j = 0; j < m_c; j++)
		{
			if (vParent[edges[j][1] - 1] >= 0)
			{
				iParentOld = vParent[edges[j][1] - 1];
			}
			vParent[edges[j][1] - 1] = edges[j][0] - 1;
		}
		const int root = std::find(vParent.begin(), vParent.end(), -1) - vParent.begin();
		CUnionFindDirect uf(m_c);		
		int iCyc = -1;
		int i2 = -1;
		for (int j = 0; j < m_c; j++)
		{
			bool b2 = false, bCyc = false;
			uf.Connect(b2, bCyc, edges[j][1] - 1, edges[j][0] - 1);
			if (b2)
			{
				i2 = j;
			}
			if (bCyc)
			{
				iCyc = j ;
			}
		}
		if (-1 != i2)
		{
			const int dest = uf.GetMaxDest(edges[i2][1] - 1);
			return (root == dest) ? edges[i2] : vector<int>{ iParentOld + 1, edges[i2][1] };
		}

		return edges[iCyc];
	}
	int m_c;
};

注意

一,所有边都加到并集查找才判断最长终点。
二,由于冲突边和环边会忽略。所以可能出现 忽略环边时,让冲突边也消失。碰巧可以按环处理。
在这里插入图片描述
BA和CD已经发现,CB未发现。发现AB是环边,忽略。由于AB被忽略,所以CB不是冲突边。

娱乐性优化

情况2.2进一步分析,无论何种发现顺序都是删除环冲突边:
2.2.1,同时发现环边和冲突边。环冲突边最后发现。删除冲突边。
2.2.2 先发现环边,环边是冲突边。无法发先冲突边。删除环边。
2.2.3先发现环边,环边不是冲突边。能发现冲突边。删除另外一条冲突边。
2.2.4 先发现冲突边,忽略冲突环边,无法发现环。删除冲突边。
2.2.5 先发现冲突边,忽略非环冲突边,能发现环。删除另外一条冲突边。
总结: { 删除冲突边 发现冲突边未发现环边 1 , 2.2.4 删除环边 发现环边未发现冲突边 2.1 , 2.22 删除冲突边(环边 ) 冲突边就是环边 2.2.1 删除另外一条冲突边 发现冲突边和环边两者不等 2.2.3 , 2.25 总结:\begin{cases} 删除冲突边 & 发现冲突边未发现环边 & 1,2.2.4 \\ 删除环边 & 发现环边未发现冲突边 &2.1, 2.22 \\ 删除冲突边(环边) & 冲突边就是环边 & 2.2.1 \\ 删除另外一条冲突边 & 发现冲突边和环边两者不等 & 2.2.3,2.25\\ \end{cases} 总结: 删除冲突边删除环边删除冲突边(环边)删除另外一条冲突边发现冲突边未发现环边发现环边未发现冲突边冲突边就是环边发现冲突边和环边两者不等1,2.2.42.1,2.222.2.12.2.3,2.25

扩展阅读

视频课程

有效学习:明确的目标 及时的反馈 拉伸区(难度合适),可以先学简单的课程,请移步CSDN学院,听白银讲师(也就是鄙人)的讲解。
https://edu.csdn.net/course/detail/38771

如何你想快

速形成战斗了,为老板分忧,请学习C#入职培训、C++入职培训等课程
https://edu.csdn.net/lecturer/6176

相关下载

想高屋建瓴的学习算法,请下载《喜缺全书算法册》doc版
https://download.csdn.net/download/he_zhidan/88348653

我想对大家说的话
闻缺陷则喜是一个美好的愿望,早发现问题,早修改问题,给老板节约钱。
子墨子言之:事无终始,无务多业。也就是我们常说的专业的人做专业的事。
如果程序是一条龙,那算法就是他的是睛

测试环境

操作系统:win7 开发环境: VS2019 C++17
或者 操作系统:win10 开发环境: VS2022 C++17
如无特殊说明,本算法用**C++**实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/394763.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C#上位机与三菱PLC的通信07--使用第3方通讯库读写数据

1、通讯库介绍 mcprotocol 是一个基于 Node.js 的三菱 PLC MC 协议通信库&#xff0c;具有以下特点&#xff1a; 支持多种三菱 PLC MC 协议的设备&#xff0c;如 FX3U、Q03UDECPU、QJ71E71 等。 支持多种功能码和数据类型&#xff0c;如读取线圈&#xff08;M&#xff09;、…

工具:clang-format使用

Visual Studio 在扩展-》管理扩展-》联机-》搜索Format on Save (for VS2022)插件&#xff0c;并安装 安装需要关闭VS&#xff0c;安装后重新打开vs。 这样每次保存源文件时就会自动调用vs默认的clang-format.exe对源码进行缩进&#xff0c;空格等进行格式化 如果想使用自定…

MT8781核心板_MTK8781安卓核心板规格参数

MT8781安卓核心板以其强大的性能和高效的能耐备受瞩目。其八核CPU架构包括(2x Cortex-A76 2.2GHz 6x Cortex-A55 2.0GHz)&#xff0c;以及高性能的Arm Mali G57级GPU。同时&#xff0c;配备高达2,133MHz的LPDDR4X内存和快速的UFS 2.2级存储&#xff0c;大大加速了数据访问速…

定制红酒:为客户创造与众不同的品鉴体验

品鉴红酒不仅仅是为了享受美酒&#xff0c;更是一种生活方式的体现。云仓酒庄洒派深知这一点&#xff0c;并致力于为客户创造与众不同的品鉴体验。 1. 专业品鉴指导 云仓酒庄洒派拥有一支专业的品鉴团队&#xff0c;为客户提供详细的品鉴指导。无论是对于红酒的基本知识、品鉴…

qlv转mp4怎么操作?打破格式限制,转换技巧大揭秘

在数字媒体的世界里&#xff0c;视频格式的转换已成为我们日常生活中的常见需求。其中&#xff0c;将腾 讯视频特有的qlv格式转换为更通用的mp4格式&#xff0c;是许多用户面临的挑战。如何快速、高效地完成qlv转mp4&#xff1f;本文将为你揭开转换技巧的神秘面纱&#xff0c;让…

PMP和软件高项哪个含金量高?

PMP认证和高项二者可以说有相似之处又有不同之处&#xff0c;在知识点的方面他们的接触面非常广泛&#xff0c;多至80%&#xff0c;而在作用和适用方向上二者就有比较大些的区别&#xff0c;至于考哪个不考哪个的问题我身为过来人还是不建议去多想的&#xff0c;两个都照杀&…

【Linux】进程间通信——共享内存

文章目录 共享内存的概要创建共享内存shmget()参数keyshmget()参数sizeshmget()参数shmflg 删除共享内存挂载共享内存去关联 共享内存的概要 共享内存允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间传递数据的一种非常有效的方式。不同进程之间…

多维时序 | Matlab实现基于VMD-DBO-LSTM、VMD-LSTM、LSTM的多变量时间序列预测

多维时序 | Matlab实现基于VMD-DBO-LSTM、VMD-LSTM、LSTM的多变量时间序列预测 目录 多维时序 | Matlab实现基于VMD-DBO-LSTM、VMD-LSTM、LSTM的多变量时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Matlab实现基于VMD-DBO-LSTM、VMD-LSTM、LSTM的多变量时间…

Eclipse - Formatter

Eclipse - Formatter References Window -> Preferences -> C/C -> Code Style -> Formatter BSD/Allman [built-in] or K& R [built-in] References [1] Yongqiang Cheng, https://yongqiang.blog.csdn.net/

电脑卡住不动了怎么办?三招解救你的电脑!

电脑卡住不动是一种常见的故障&#xff0c;可能会给用户带来困扰。这种情况可能由于多种原因引起&#xff0c;包括软件问题、硬件故障或系统错误。那么&#xff0c;电脑卡住不动了怎么办呢&#xff1f;在本文中&#xff0c;我们将介绍电脑卡住不动的可能原因&#xff0c;并提供…

Itext生成pdf文件,html转pdf时中文一直显示不出来

之前使用freemark模板渲染ftl页面,转出的pdf中&#xff0c;css2的很多样式好像不支持 改造成使用html页面来转pdf&#xff0c;css2的样式可以生效,itext是不支持css3的弹性布局的ITextRenderer pdfRenderer new ITextRenderer();// 添加字体设置ITextFontResolver fontResolve…

微信小程序 点击右上角三个点 当前页面未设置分享

js文件中添加 //用户点击右上角分享朋友圈 onShareTimeline () { },

RSA加密解密(二)——用shell加密后java无法解密的问题

我们需要用shell生成加密原文数据&#xff0c;存入mysql中&#xff0c;然后用java取出mysql中的加密数据并解密出原文。 这个任务中遇到的最大问题是&#xff0c;用shell进行base64编码后的加密数据&#xff0c;无法被java的解密程序解密&#xff0c;会报错。 Exception in t…

访问学者感谢信|人文社科工作者赴北欧访学

编者按&#xff1a;这位访问学者从委托我们申请到获得邀请函只用了一个月时间。为了表达感激之情&#xff0c;其当时就写了这封感谢信&#xff0c;但依据我们的惯例&#xff0c;一般是待申请者出国&#xff0c;一切安排妥当后再发成功案例。所以时至今日&#xff0c;才将该申请…

统计图柱形图绘制方法

统计图柱形图绘制方法 常用的统计图有条形图、柱形图、折线图、曲线图、饼图、环形图、扇形图。 前几类图比较容易绘制&#xff0c;饼图环形图绘制较难。 今提供条形图和柱形图的绘制方法&#xff0c;附加环形图和艺术环图的绘制方法供参考。 本方法采用C语言的最基本功能&…

PCL 计算点云AABB包围盒的体积

目录 一、AABB包围盒二、代码实现三、结果展示四、相关链接本文由CSDN点云侠原创,原文链接。爬虫自重,把自己当个人。 一、AABB包围盒 AABB包围盒又称了 轴对齐包围盒,是点云包围盒里最简单的一种,其计算方法也极其简单。获取包围盒之后,根据包围盒的长宽高进行体积计算即…

电销机器人是如何进行工作的

ai电销机器人&#xff0c;主要是解决电销企业筛选客户问题。 我们会帮你搭建好电销后台&#xff0c;你使用的时候只需要导入客户手机号&#xff0c;设置外呼时间&#xff0c;系统就可以帮你自动拨打电话&#xff0c;并把打出来的意向客户自动推送到你的微信上面。非常方便快捷…

在C++使用OpenCV库创建一个带滚动条的窗口,用于动态调整图像的亮度

QuickDemo.cpp #include<quick_opencv.h>//创建一个带滚动条的窗口&#xff0c;用于动态调整图像的亮度 static void on_track(int position, void* userdata) //回调函数:当滚动条的位置改变时被自动调用的回调函数 //position:滚动条的当前位置; userdata:用户提供的数…

Eclipse 创建 Hello World 工程

Eclipse 创建 Hello World 工程 1. Hello WorldReferences Download and install the Eclipse IDE. 1. Hello World Eclipse -> double click -> Launch 单击蓝色方框 (右上角) 最大化 IDE File -> New -> C Project -> Finish Project name&#xff1a;工程名…

办公记事常用工具:桌面记事本助你高效记事

在繁忙的办公环境中&#xff0c;我常常感到自己的大脑像是一个不停旋转的陀螺&#xff0c;各种待办事项、会议安排和灵感想法在脑海中交织&#xff0c;仿佛随时都要迸发出来。然而&#xff0c;只靠人脑记忆&#xff0c;不仅辛苦&#xff0c;而且容易出错。在这样的背景下&#…