【C++图论 分支界限法】1786. 从第一个节点出发到最后一个节点的受限路径数|2078

本文涉及知识点

C++图论
分支界限法

LeetCode1786. 从第一个节点出发到最后一个节点的受限路径数

现有一个加权无向连通图。给你一个正整数 n ,表示图中有 n 个节点,并按从 1 到 n 给节点编号;另给你一个数组 edges ,其中每个 edges[i] = [ui, vi, weighti] 表示存在一条位于节点 ui 和 vi 之间的边,这条边的权重为 weighti 。
从节点 start 出发到节点 end 的路径是一个形如 [z0, z1, z2, …, zk] 的节点序列,满足 z0 = start 、zk = end 且在所有符合 0 <= i <= k-1 的节点 zi 和 zi+1 之间存在一条边。
路径的距离定义为这条路径上所有边的权重总和。用 distanceToLastNode(x) 表示节点 n 和 x 之间路径的最短距离。受限路径 为满足 distanceToLastNode(zi) > distanceToLastNode(zi+1) 的一条路径,其中 0 <= i <= k-1 。
返回从节点 1 出发到节点 n 的 受限路径数 。由于数字可能很大,请返回对 109 + 7 取余 的结果。
示例 1:
在这里插入图片描述

输入:n = 5, edges = [[1,2,3],[1,3,3],[2,3,1],[1,4,2],[5,2,2],[3,5,1],[5,4,10]]
输出:3
解释:每个圆包含黑色的节点编号和蓝色的 distanceToLastNode 值。三条受限路径分别是:

  1. 1 --> 2 --> 5
  2. 1 --> 2 --> 3 --> 5
  3. 1 --> 3 --> 5
    示例 2:
    在这里插入图片描述
    输入:n = 7, edges = [[1,3,1],[4,1,2],[7,3,4],[2,5,3],[5,6,1],[6,7,2],[7,5,3],[2,6,4]]
    输出:1
    解释:每个圆包含黑色的节点编号和蓝色的 distanceToLastNode 值。唯一一条受限路径是:1 --> 3 --> 7 。
    提示:
    1 <= n <= 2 * 104
    n - 1 <= edges.length <= 4 * 104
    edges[i].length == 3
    1 <= ui, vi <= n
    ui != vi
    1 <= weighti <= 105
    任意两个节点之间至多存在一条边
    任意两个节点之间至少存在一条路径

图论+BFS(错误解法)

本图是稀疏图,故用迪氏堆优化求各点到n的距离。节点i到n的最小距离简记为d[i]。
任意路径都是d[i]严格降序,故不会有环。
ans[i]记录1到i的方案数。
BFS的状态表示:当前节点cur,初始为1。
BFS的后续状态:next是cur的临接点,且d[next] >d[cur]。
BFS去重:vis数组。
BFS返回值:ans.back
时间复杂度:O(mlogn+m) m是边数。
错误原因:无法保证无后续性。

错误代码

class Solution {
    
    
		public:
			int countRestrictedPaths(int n, vector<vector<int>>& edges) {
    
    
				auto neiBo = CNeiBo::Three(n, edges, false, 1);
				CHeapDis hd(n);
				hd.Cal(n - 1, neiBo);
				vector<C1097Int<>> cnt(n);
				cnt[0] = 1;
				vector<bool> vis(n);
				queue<int> que;
				auto Add = [&](int cur) {
    
    
					if (vis[cur])return;
					vis[cur] = true;
					que.emplace(cur);
				};
				Add(0);
				while (que.size()) {
    
    
					const auto cur = que.front();
					que.pop();
					for (const auto& nxt : neiBo[cur]) {
    
    
						if (hd.m_vDis[nxt.first] >= hd.m_vDis[cur]) {
    
     continue; }
						cnt[nxt.first] += cnt[cur];
						Add(nxt.first);
					}
				}
				return cnt.back().ToInt();
			}
		};

分支界限法

将各节点从1开始改成0从开始。
小根堆记录{d[i],i},初始{d[n-1],n-1}。 从堆顶取出元素i,枚举i的临界点j,如果d[j] <= dp[i]忽略。如果vis[j]忽略,将{d[j],j}放到堆中。按此顺序处理i,可以保证无后效性,下面来证明:
处理i时,任何d[j] < d[i]的i都已经处理。令j的最短路径是{j1,j2 ⋯ \cdots j} 这些节点的最短路径显然都小于d[i]。用数学归纳法来证明。
j1是n-1的临接点,所以j1一定在堆,由于d[j1] <d[i],故j1先于i处理。处理j1时,j2也入堆,由于d[j2] < d[i],故j2先于i处理。 ⋯ \cdots

进一步优化

vis在出堆时判断,入堆到出堆有时间差,可能这段时间vis[i]发生变化。这样优化后,就是迪氏堆优化最短路。vSort的节点,确保d[i]升序。这样可以保证无后效性。

代码

核心代码

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>> Mat(vector<vector<int>>& neiBoMat)
	{
    
    
		vector<vector<int>> neiBo(neiBoMat.size());
		for (int i = 0; i < neiBoMat.size(); i++)
		{
    
    
			for (int j = i + 1; j < neiBoMat.size(); j++)
			{
    
    
				if (neiBoMat[i][j])
				{
    
    
					neiBo[i].emplace_back(j);
					neiBo[j].emplace_back(i);
				}
			}
		}
		return neiBo;
	}
};

typedef pair<long long, int> PAIRLLI;
class  CHeapDis
{
    
    
public:
	CHeapDis(int n,long long llEmpty = LLONG_MAX/10):m_llEmpty(llEmpty)
	{
    
    
		m_vDis.assign(n, m_llEmpty);
	}
	void Cal(int start, const vector<vector<pair<int, int>>>& vNeiB)
	{
    
    
		std::priority_queue<PAIRLLI, vector<PAIRLLI>, greater<PAIRLLI>> minHeap;
		minHeap.emplace(0, start);
		while (minHeap.size())
		{
    
    
			const long long llDist = minHeap.top().first;
			const int iCur = minHeap.top().second;
			minHeap.pop();
			if (m_llEmpty != m_vDis[iCur])
			{
    
    
				continue;
			}
			m_vDis[iCur] = llDist;
			for (const auto& it : vNeiB[iCur])
			{
    
    
				minHeap.emplace(llDist + it.second, it.first);
			}
		}
	}
	vector<long long> m_vDis;
	const long long m_llEmpty;
};
template<int MOD = 1000000007>
class C1097Int
{
    
    
public:
	C1097Int(long long llData = 0) :m_iData(llData% MOD)
	{
    
    

	}
	C1097Int  operator+(const C1097Int& o)const
	{
    
    
		return C1097Int(((long long)m_iData + o.m_iData) % MOD);
	}
	C1097Int& operator+=(const C1097Int& o)
	{
    
    
		m_iData = ((long long)m_iData + o.m_iData) % MOD;
		return *this;
	}
	C1097Int& operator-=(const C1097Int& o)
	{
    
    
		m_iData = (m_iData + MOD - o.m_iData) % MOD;
		return *this;
	}
	C1097Int  operator-(const C1097Int& o)
	{
    
    
		return C1097Int((m_iData + MOD - o.m_iData) % MOD);
	}
	C1097Int  operator*(const C1097Int& o)const
	{
    
    
		return((long long)m_iData * o.m_iData) % MOD;
	}
	C1097Int& operator*=(const C1097Int& o)
	{
    
    
		m_iData = ((long long)m_iData * o.m_iData) % MOD;
		return *this;
	}
	C1097Int  operator/(const C1097Int& o)const
	{
    
    
		return *this * o.PowNegative1();
	}
	C1097Int& operator/=(const C1097Int& o)
	{
    
    
		*this /= o.PowNegative1();
		return *this;
	}
	bool operator==(const C1097Int& o)const
	{
    
    
		return m_iData == o.m_iData;
	}
	bool operator<(const C1097Int& o)const
	{
    
    
		return m_iData < o.m_iData;
	}
	C1097Int pow(long long n)const
	{
    
    
		C1097Int iRet = 1, iCur = *this;
		while (n)
		{
    
    
			if (n & 1)
			{
    
    
				iRet *= iCur;
			}
			iCur *= iCur;
			n >>= 1;
		}
		return iRet;
	}
	C1097Int PowNegative1()const
	{
    
    
		return pow(MOD - 2);
	}
	int ToInt()const
	{
    
    
		return m_iData;
	}
private:
	int m_iData = 0;;
};

class Solution {
    
    
		public:
			int countRestrictedPaths(int n, vector<vector<int>>& edges) {
    
    
				auto neiBo = CNeiBo::Three(n, edges, false, 1);						
				vector<int> vSortNode,dis(n,-1);
				priority_queue<pair<int,int>,vector<pair<int, int>>,greater<>> heap;
				heap.emplace(0, n - 1);
				while (heap.size()) {
    
    
					const auto [d, cur] = heap.top();
					heap.pop();
					if (-1 != dis[cur]) {
    
     continue; }
					dis[cur] =d ;
					vSortNode.emplace_back(cur);
					for (const auto& pre : neiBo[cur]) {
    
    
						heap.emplace(d + pre.second, pre.first);
					}
				}
				vector<C1097Int<>> cnt(n);
				cnt[n - 1] = 1;
				for (int i : vSortNode) {
    
    
					for (const auto& pre : neiBo[i]) {
    
    
						if (dis[pre.first] > dis[i]) {
    
    
							cnt[pre.first] += cnt[i];
						}
					}
				}
				return cnt[0].ToInt();
			}
		};

单元测试

int n;
		vector<vector<int>> edges;
		TEST_METHOD(TestMethod1)
		{
    
    
			n = 1, edges = {
    
      };
			auto res = Solution().countRestrictedPaths(n, edges);
			AssertEx(1, res);
		}
		TEST_METHOD(TestMethod2)
		{
    
    
			n = 2, edges = {
    
     {
    
    1,2,3} };
			auto res = Solution().countRestrictedPaths(n, edges);
			AssertEx(1, res);
		}
		TEST_METHOD(TestMethod11)
		{
    
    
			n = 5, edges = {
    
     {
    
    1,2,3},{
    
    1,3,3},{
    
    2,3,1},{
    
    1,4,2},{
    
    5,2,2},{
    
    3,5,1},{
    
    5,4,10} };
			auto res = Solution().countRestrictedPaths(n, edges);
			AssertEx(3, res);
		}
		TEST_METHOD(TestMethod12)
		{
    
    
			n = 7, edges = {
    
     {
    
    1,3,1},{
    
    4,1,2},{
    
    7,3,4},{
    
    2,5,3},{
    
    5,6,1},{
    
    6,7,2},{
    
    7,5,3},{
    
    2,6,4} };
			auto res = Solution().countRestrictedPaths(n, edges);
			AssertEx(1, res);
		}
		TEST_METHOD(TestMethod13)
		{
    
    
			n = 9, edges = {
    
     {
    
    6,2,35129},{
    
    3,4,99499},{
    
    2,7,43547},{
    
    8,1,78671},{
    
    2,1,66308},{
    
    9,6,33462},{
    
    5,1,48249},{
    
    2,3,44414},{
    
    6,7,44602},{
    
    1,7,14931},{
    
    8,9,38171},{
    
    4,5,30827},{
    
    3,9,79166},{
    
    4,8,93731},{
    
    5,9,64068},{
    
    7,5,17741},{
    
    6,3,76017},{
    
    9,4,72244} };
			auto res = Solution().countRestrictedPaths(n, edges);
			AssertEx(6, res);
		}

扩展阅读

我想对大家说的话
工作中遇到的问题,可以按类别查阅鄙人的算法文章,请点击《算法与数据汇总》。
学习算法:按章节学习《喜缺全书算法册》,大量的题目和测试用例,打包下载。重视操作
有效学习:明确的目标 及时的反馈 拉伸区(难度合适) 专注
闻缺陷则喜(喜缺)是一个美好的愿望,早发现问题,早修改问题,给老板节约钱。
子墨子言之:事无终始,无务多业。也就是我们常说的专业的人做专业的事。
如果程序是一条龙,那算法就是他的是睛
失败+反思=成功 成功+反思=成功

视频课程

先学简单的课程,请移步CSDN学院,听白银讲师(也就是鄙人)的讲解。
https://edu.csdn.net/course/detail/38771
如何你想快速形成战斗了,为老板分忧,请学习C#入职培训、C++入职培训等课程
https://edu.csdn.net/lecturer/6176

测试环境

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