【图论】 【割点】 【双连通分类】LCP 54. 夺回据点

本文涉及知识点

图论 割点 双连通分类
割点原理及封装好的割点类

LeetCode LCP 54. 夺回据点

魔物了占领若干据点,这些据点被若干条道路相连接,roads[i] = [x, y] 表示编号 x、y 的两个据点通过一条道路连接。

现在勇者要将按照以下原则将这些据点逐一夺回:

在开始的时候,勇者可以花费资源先夺回一些据点,初始夺回第 j 个据点所需消耗的资源数量为 cost[j]

接下来,勇者在不消耗资源情况下,每次可以夺回一个和「已夺回据点」相连接的魔物据点,并对其进行夺回

注:为了防止魔物暴动,勇者在每一次夺回据点后(包括花费资源夺回据点后),需要保证剩余的所有魔物据点之间是相连通的(不经过「已夺回据点」)。

请返回勇者夺回所有据点需要消耗的最少资源数量。

注意:

输入保证初始所有据点都是连通的,且不存在重边和自环

示例 1:

输入: cost = [1,2,3,4,5,6] roads = [[0,1],[0,2],[1,3],[2,3],[1,2],[2,4],[2,5]]

输出:6

解释: 勇者消耗资源 6 夺回据点 0 和 4,魔物据点 1、2、3、5 相连通; 第一次夺回据点 1,魔物据点 2、3、5 相连通; 第二次夺回据点 3,魔物据点 2、5 相连通; 第三次夺回据点 2,剩余魔物据点 5; 第四次夺回据点 5,无剩余魔物据点; 因此最少需要消耗资源为 6,可占领所有据点。i

示例 2:

输入: cost = [3,2,1,4] roads = [[0,2],[2,3],[3,1]]

输出:2

解释: 勇者消耗资源 2 夺回据点 1,魔物据点 0、2、3 相连通; 第一次夺回据点 3,魔物据点 2、0 相连通; 第二次夺回据点 2,剩余魔物据点 0; 第三次夺回据点 0,无剩余魔物据点; 因此最少需要消耗资源为 2,可占领所有据点。image.png

提示:

1 <= roads.length, cost.length <= 105

0 <= roads[i][0], roads[i][1] < cost.length

1 <= cost[i] <= 109

预备知识

点双连通:删掉任意一个点之后及关联的边后,图仍联通。

点双连通分量的缩点

性质一 : 无向图中,要么是树边,父亲指向孩子;要么是回边,后代指向祖宗。见:割点原理及封装好的割点类
性质二 :一个环一定是点双连通。
性质三 :一个环+一个树,一定不是点双连通区域。令树的一边a,b不在环上,b不在环上。则删除a,b就成了新的连通区域。
性质四 :两个环有一个点重合,一定不是点双连通。删除重合点就分开了。
性质五 :两个环有两个或更多的顶点相同,则一定是双连通区域。令相同的顶点是a和b。删除a后,两个环余下的点,通过b连通。删除b,类似。
性质六 :设G1,G2最大点双连通图,如果G1和G2包括相同的边ab,则G1 == G2。删除a后,G1和G2所有的点都可以通过b连通,故G1和G2可以合并。
性质七:无向图的任何树边都只属于一个强连通分量。

简化情况

假定是树,且节点数大于等于3。一定会存在度数>=2的节点,以任意度数为2的根,形成树。

令叶子数是k,选择k-2个叶子结束,无法完全占据。2个叶子节点最近公共祖先无法消除。

选择k-1个叶子,一定可以占据。设没选择的节点是a,k-1个消除到和a的公共祖先,余下的树都是a的祖先,从根开始消除。

不能选择根节点,除一个子树外,可以选择排除一个叶子节 点外的叶子节点;其它子树要全部选择。显然劣于k-1叶子节点。

不能选择枝节点,选了只有两个选择:

a,本子树全选,本子树外的叶子排除一个外全选。

b,本子树外的节点全选。本子树的叶子排除一个外全选。

显然劣于选择k-1个叶子节点。

红色字体是割点,红色线段是回边。

共四个点双连通区域,红色背景(节点0)、绿色背景(3)、紫色背景(6) 只和一个割点连接,消除不会形成新的连通区域。蓝色节点(4)和两个割点连通,首先删除会,让2和5断开。

点双连通区域占据任意点,任意剩余据点也是连通。

关于点双连通区域

性质一 :除根节点外的任何节点x都会放到符合以下条件的next所在连通区域。

next就是x或next是x的祖宗。如果多个next,取级别最高的。

断开cur后,next和cur的祖宗不连通。每个节点只会入栈出栈一次。
性质二 :next的父节点也会放到next所在的双连通区域。
性质三:除根节点外的任何节点x 都有符合性质一的next,根节点的子节点一定符合。

代码

两种特殊情况: 一个连通区域只有一个点。本类会解析错误,解析成没有点双连同区域。

没有割点,会解析成一个点双连通区域,解析正确。本题要特殊处理。

cpp 复制代码
class CNeiBo
{
public:	
	static vector<vector<int>> Two(int n, vector<vector<int>>& edges, bool bDirect, int iBase = 0) 
	{
		vector<vector<int>>  vNeiBo(n);
		for (const auto& v : edges)
		{
			vNeiBo[v[0] - iBase].emplace_back(v[1] - iBase);
			if (!bDirect)
			{
				vNeiBo[v[1] - iBase].emplace_back(v[0] - iBase);
			}
		}
		return vNeiBo;
	}	
	static vector<vector<std::pair<int, int>>> Three(int n, vector<vector<int>>& edges, bool bDirect, int iBase = 0)
	{
		vector<vector<std::pair<int, int>>> vNeiBo(n);
		for (const auto& v : edges)
		{
			vNeiBo[v[0] - iBase].emplace_back(v[1] - iBase, v[2]);
			if (!bDirect)
			{
				vNeiBo[v[1] - iBase].emplace_back(v[0] - iBase, v[2]);
			}
		}
		return vNeiBo;
	}
	static vector<vector<int>> Grid(int rCount, int cCount, std::function<bool(int, int)> funVilidCur, std::function<bool(int, int)> funVilidNext)
	{
		vector<vector<int>> vNeiBo(rCount * cCount);
		auto Move = [&](int preR, int preC, int r, int c)
		{
			if ((r < 0) || (r >= rCount))
			{
				return;
			}
			if ((c < 0) || (c >= cCount))

			{
				return;
			}
			if (funVilidCur(preR, preC) && funVilidNext(r, c))
			{
				vNeiBo[cCount * preR + preC].emplace_back(r * cCount + c);
			}
		};

		for (int r = 0; r < rCount; r++)
		{
			for (int c = 0; c < cCount; c++)
			{
				Move(r, c, r + 1, c);
				Move(r, c, r - 1, c);
				Move(r, c, r, c + 1);
				Move(r, c, r, c - 1);
			}
		}
		return vNeiBo;
	}
};

//割点
class CCutPoint
{
public:
	CCutPoint(const vector<vector<int>>& vNeiB) : m_iSize(vNeiB.size())
	{
		m_vNodeToTime.assign(m_iSize, -1);
		m_vCutNewRegion.resize(m_iSize);		
	}
	void Init(const vector<vector<int>>& vNeiB)
	{
		for (int i = 0; i < m_iSize; i++)
		{
			if (-1 == m_vNodeToTime[i])
			{
				m_vRegionFirstTime.emplace_back(m_iTime);
				dfs(vNeiB, i, -1);
			}
		}
	}	
	const int m_iSize;
	const vector<int>& Time()const { return m_vNodeToTime; }//各节点的时间戳
	const vector<int>& RegionFirstTime()const { return m_vRegionFirstTime; }//各连通区域的最小时间戳
	vector<bool> Cut()const { 
		vector<bool> ret;
		for (int i = 0; i < m_iSize; i++)
		{
			ret.emplace_back(m_vCutNewRegion[i].size());
		}
		return ret; }//
	const vector < vector<pair<int, int>>>& NewRegion()const { return m_vCutNewRegion; };
protected:
	int dfs(const vector<vector<int>>& vNeiB, const int cur, const int parent)
	{
		int iMinTime = m_vNodeToTime[cur] = m_iTime++;
		OnBeginDFS(cur);
		int iRegionCount = (-1 != parent);//根连通区域数量
		for (const auto& next : vNeiB[cur]) {
			if (-1 != m_vNodeToTime[next]) {
				iMinTime = min(iMinTime, m_vNodeToTime[next]);
				continue;
			}
			const int childMinTime = dfs(vNeiB, next, cur);
			iMinTime = min(iMinTime, childMinTime);
			if (childMinTime >= m_vNodeToTime[cur]) {
				iRegionCount++;
				m_vCutNewRegion[cur].emplace_back(m_vNodeToTime[next], m_iTime);
				OnNewRegion(cur, next);
			}
		}
		if (iRegionCount < 2)
		{
			m_vCutNewRegion[cur].clear();
		}
		return iMinTime;
	}
	virtual void OnNewRegion(int cur, int next) {};
	virtual void OnBeginDFS(int cur) {};
	vector<int> m_vNodeToTime;
	vector<int> m_vRegionFirstTime;
	vector < vector<pair<int, int>>> m_vCutNewRegion; //m_vCutNewRegion[c]如果存在[left,r) 表示割掉c后,时间戳[left,r)的节点会形成新区域
	int m_iTime = 0;
};

class CConnectAfterCutPoint 
{
public:
	CConnectAfterCutPoint(const vector<vector<int>>& vNeiB) :m_ct(vNeiB)
	{
		m_vTimeToNode.resize(m_ct.m_iSize);
		m_vNodeToRegion.resize(m_ct.m_iSize);
		for (int iNode = 0; iNode < m_ct.m_iSize; iNode++)
		{
			m_vTimeToNode[m_ct.Time()[iNode]] = iNode;
		}
		for (int iTime = 0,iRegion= 0; iTime < m_ct.m_iSize; iTime++)
		{
			if ((iRegion < m_ct.RegionFirstTime().size()) && (m_ct.RegionFirstTime()[iRegion] == iTime))
			{
				iRegion++;
			}
			m_vNodeToRegion[m_vTimeToNode[iTime]] = (iRegion - 1);
		}
	}
	bool Connect(int src, int dest, int iCut)const
	{
		if (m_vNodeToRegion[src] != m_vNodeToRegion[dest])
		{
			return false;//不在一个连通区域
		}
		if (0 == m_ct.NewRegion()[iCut].size())
		{//不是割点
			return true;
		}
		const int r1 = GetCutRegion(iCut, src);
		const int r2 = GetCutRegion(iCut, dest);
		return r1 == r2;
	}
	vector<vector<int>> GetSubRegionOfCut(const int iCut)const
	{//删除iCut及和它相连的边后,iCut所在的区域会分成几个区域:父节点一个区域、各子节点		一个区域
			//父节点所在区域可能为空,如果iCut所在的连通区域只有一个节点,则返回一个没有节点的			区域。
		const auto& v = m_ct.NewRegion()[iCut];
		vector<int> vParen;
		const int iRegion = m_vNodeToRegion[iCut];
		const int iEndTime = (iRegion + 1 == m_ct.RegionFirstTime().size()) ? m_ct.m_iSize : m_ct.RegionFirstTime()[iRegion+1];
		vector<vector<int>> vRet;	
		for (int iTime = m_ct.RegionFirstTime()[iRegion],j=-1; iTime < iEndTime; iTime++)
		{
			if (iCut == m_vTimeToNode[iTime])
			{
				continue;
			}
			if ((j + 1 < v.size()) && (v[j + 1].first == iTime))
			{
				j++;
				vRet.emplace_back();
			}
			const int iNode = m_vTimeToNode[iTime];
			if ((-1 != j ) && (iTime >= v[j].first) && (iTime < v[j].second))
			{
				vRet.back().emplace_back(iNode);
			}
			else
			{
				vParen.emplace_back(iNode);
			}			
		}
		vRet.emplace_back();
		vRet.back().swap(vParen);
		return vRet;
	}	
protected:
	int GetCutRegion(int iCut, int iNode)const
	{
		const auto& v = m_ct.NewRegion()[iCut];
		auto it = std::upper_bound(v.begin(), v.end(), m_ct.Time()[iNode], [](int time, const std::pair<int, int>& pr) {return  time < pr.first; });
		if (v.begin() == it)
		{
			return v.size();
		}
		--it;
		return (it->second > m_ct.Time()[iNode]) ? (it - v.begin()) : v.size();
	}
	vector<int> m_vTimeToNode;
	vector<int> m_vNodeToRegion;//各节点所在区域
	const CCutPoint m_ct;
};

class CPointBitConnect :public CCutPoint
{//点双连通区域
public:
	CPointBitConnect(const vector<vector<int>>& vNeiB):CCutPoint(vNeiB)
	{
		
	}
	stack<int> m_staNodes;
	vector<vector<int>> m_vBitConnect;
protected:
	virtual void OnNewRegion(int cur, int next) override {
		m_vBitConnect.emplace_back();
		while (true)
		{
			const int t = m_staNodes.top();
			m_staNodes.pop();
			m_vBitConnect.back().emplace_back(t);
			if (t == next)
			{
				break;
			}
		}
		m_vBitConnect.back().emplace_back(cur);
	};
	virtual void OnBeginDFS(int cur)override {
		m_staNodes.emplace(cur);
	};
};

class Solution {
public:
    long long minimumCost(vector<int>& cost, vector<vector<int>>& roads) {
        m_c = cost.size();
        if (1 == m_c)
        {
            return cost[0];
        }
        auto neiBo = CNeiBo::Two(m_c, roads, false);
        CPointBitConnect pbc(neiBo);
        pbc.Init(neiBo);
        if (1 == pbc.m_vBitConnect.size())
        {
            return *std::min_element(cost.begin(), cost.end());
        }
        long long ans = 0;
        int iMax = 0;
        vector<bool> vCut = pbc.Cut();
        for (auto& v : pbc.m_vBitConnect)
        {
            int iCutCount = 0;
            int iCurMin = INT_MAX;
            for (const auto& iNode : v)
            {
                iCutCount += vCut[iNode];
                if (!vCut[iNode])
                {
                    iCurMin = min(iCurMin, cost[iNode]);
                }
            }
            if (1 == iCutCount)
            {
                iMax = max(iMax, iCurMin);
                ans += iCurMin;
            }
        }
        return ans - iMax;
    }
    int m_c;
};

测试用例

cpp 复制代码
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<int> cost;
    vector<vector<int>> roads;

    {
        Solution sln;
        cost = { 3,2,1,4 }, roads = { {0,2},{2,3},{3,1} };
        auto res = sln.minimumCost(cost, roads);
        Assert(2, res);
    }
    {
        Solution sln;
        cost = { 1,2,3,4,5,6 }, roads = { {0,1},{0,2},{1,3},{2,3},{1,2},{2,4},{2,5} };
        auto res = sln.minimumCost(cost, roads);
        Assert(6, res);
    }
  
}

扩展阅读

视频课程

有效学习:明确的目标 及时的反馈 拉伸区(难度合适),可以先学简单的课程,请移步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++**实现。

相关推荐
杨连江3 分钟前
原子级平面限域协同晶核诱导定向生长单层鳞片石墨的研究
算法
MATLAB代码顾问9 分钟前
混合粒子群-模拟退火算法(HPSO-SA)求解作业车间调度问题——附MATLAB代码
算法·matlab·模拟退火算法
Felven13 分钟前
C. Prefix Min and Suffix Max
算法
加农炮手Jinx14 分钟前
LeetCode 26. Remove Duplicates from Sorted Array 题解
算法·leetcode·力扣
加农炮手Jinx14 分钟前
LeetCode 88. Merge Sorted Array 题解
算法·leetcode·力扣
格林威14 分钟前
线阵工业相机:如何计算线阵相机的行频(Line Rate)?公式+实例
开发语言·人工智能·数码相机·算法·计算机视觉·工业相机·线阵相机
yueyue54317 分钟前
透过现象看本质:以fast_lio架构的整套算法的局部避障改为TEB算法为例深度探讨——如何成为一个合格的算法架构师?
算法·架构
梨花爱跨境17 分钟前
红人视频×A10算法:亚马逊转化率与流量闭环实战
算法
近津薪荼18 分钟前
C++ vector容器底层深度剖析与模拟实现
开发语言·c++
广州山泉婚姻21 分钟前
C++ STL Vector 入门与实战全攻略
c语言·c++