本文涉及知识点
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 --> 2 --> 5
- 1 --> 2 --> 3 --> 5
- 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++**实现。