本文涉及知识点
C++BFS算法
动态规划汇总
图论知识汇总
树形dp 换根法 BFS
LeetCode 2581. 统计可能的树根数目
Alice 有一棵 n 个节点的树,节点编号为 0 到 n - 1 。树用一个长度为 n - 1 的二维整数数组 edges 表示,其中 edges[i] = [ai, bi] ,表示树中节点 ai 和 bi 之间有一条边。
Alice 想要 Bob 找到这棵树的根。她允许 Bob 对这棵树进行若干次 猜测 。每一次猜测,Bob 做如下事情:
选择两个 不相等 的整数 u 和 v ,且树中必须存在边 [u, v] 。
Bob 猜测树中 u 是 v 的 父节点 。
Bob 的猜测用二维整数数组 guesses 表示,其中 guesses[j] = [uj, vj] 表示 Bob 猜 uj 是 vj 的父节点。
Alice 非常懒,她不想逐个回答 Bob 的猜测,只告诉 Bob 这些猜测里面 至少 有 k 个猜测的结果为 true 。
给你二维整数数组 edges ,Bob 的所有猜测和整数 k ,请你返回可能成为树根的 节点数目 。如果没有这样的树,则返回 0。
示例 1:
输入:edges = [[0,1],[1,2],[1,3],[4,2]], guesses = [[1,3],[0,1],[1,0],[2,4]], k = 3
输出:3
解释:
根为节点 0 ,正确的猜测为 [1,3], [0,1], [2,4]
根为节点 1 ,正确的猜测为 [1,3], [1,0], [2,4]
根为节点 2 ,正确的猜测为 [1,3], [1,0], [2,4]
根为节点 3 ,正确的猜测为 [1,0], [2,4]
根为节点 4 ,正确的猜测为 [1,3], [1,0]
节点 0 ,1 或 2 为根时,可以得到 3 个正确的猜测。
示例 2:
输入:edges = [[0,1],[1,2],[2,3],[3,4]], guesses = [[1,0],[3,4],[2,1],[3,2]], k = 1
输出:5
解释:
根为节点 0 ,正确的猜测为 [3,4]
根为节点 1 ,正确的猜测为 [1,0], [3,4]
根为节点 2 ,正确的猜测为 [1,0], [2,1], [3,4]
根为节点 3 ,正确的猜测为 [1,0], [2,1], [3,2], [3,4]
根为节点 4 ,正确的猜测为 [1,0], [2,1], [3,2]
任何节点为根,都至少有 1 个正确的猜测。
提示:
edges.length == n - 1
2 <= n <= 105
1 <= guesses.length <= 105
0 <= ai, bi, uj, vj <= n - 1
ai != bi
uj != vj
edges 表示一棵有效的树。
guesses[j] 是树中的一条边。
guesses 是唯一的。
0 <= k <= guesses.length
换根法
某棵有根树,根为root,某个儿子为child,则将根从root换成child后,除 r o o t ↔ c h i l d root\leftrightarrow child root↔child这条的边的父子关系发生变化外,其它都不边。
mGuesss[x] 记录猜测次数:x=Mask(r,v) = u*n+v
x1 = Mask(root,child)
x2 = Mask(child,root)
则 dp[child] = dp[root] - mGuesss[x1] + mGuress[x2]
分三步:
一,令root为0,计算m_dp[0]。
二,dfs各节点计算m_dp[cur]。
三,统计m_dp中为k的元素数量。
动态规划的状态表示
m_dp[cur]表示以cur为根据猜对父子关系的数量。
空间复杂度: O(n)
动态规划的转移方程
dp[child] = dp[root] - mGuesss[x1] + mGuress[x2]
单个状态的转移方程时间复杂度:O(1) 总时间复杂度:O(n)
动态规划的初始值
dp[0]先计算
动态规划的填表顺序
深度优先,广度优先也可以。
动态规划的返回值
cout(dp.being(),dp.end(),k)
代码(超时)
核心代码
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;}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;}
};class Solution {
public:int rootCount(vector<vector<int>>& edges, vector<vector<int>>& guesses, int k) {m_c = edges.size() + 1;m_dp.resize(m_c);m_vNeiBo = CNeiBo::Two(m_c, edges, false);for (const auto& v : guesses) {m_mGuess[Mask(v[0], v[1])]++;}m_dp[0] = DFS1(0, -1);DFS2(0, -1);return count_if(m_dp.begin(), m_dp.end(), [&](int i) {return i >= k; });}int DFS1(int cur, int par) {int ret = 0;if (-1 != par) { ret += m_mGuess[Mask(par, cur)];}for (const auto& next : m_vNeiBo[cur]) {if (next == par) { continue; }ret += DFS1(next, cur);}return ret;}void DFS2(int cur, int par) {if (-1 != par) {m_dp[cur] = m_dp[par];m_dp[cur] -= m_mGuess[Mask(par, cur)];m_dp[cur] += m_mGuess[Mask(cur, par)];} for (const auto& next : m_vNeiBo[cur]) {if (next == par) { continue; }DFS2(next, cur);}}long long Mask(long long par, int cur) { return m_c * par + cur; }int m_c;vector<int> m_dp;unordered_map<long long, int> m_mGuess;vector < vector <int>> m_vNeiBo;
};
单元测试
template<class T1, class T2>
void AssertEx(const T1& t1, const T2& t2)
{Assert::AreEqual(t1, t2);
}template<class T>
void AssertEx(const vector<T>& v1, const vector<T>& v2)
{Assert::AreEqual(v1.size(), v2.size());for (int i = 0; i < v1.size(); i++){Assert::AreEqual(v1[i], v2[i]);}
}template<class T>
void AssertV2(vector<vector<T>> vv1, vector<vector<T>> vv2)
{sort(vv1.begin(), vv1.end());sort(vv2.begin(), vv2.end());Assert::AreEqual(vv1.size(), vv2.size());for (int i = 0; i < vv1.size(); i++){AssertEx(vv1[i], vv2[i]);}
}namespace UnitTest
{vector<vector<int>> edges, guesses;int k;TEST_CLASS(UnitTest){public:TEST_METHOD(TestMethod0){edges = { {0,1},{1,2},{1,3},{4,2} }, guesses = { {1,3},{0,1},{1,0},{2,4} }, k = 3;auto res = Solution().rootCount(edges, guesses, k);AssertEx(3, res);}TEST_METHOD(TestMethod1){edges = { {0,1},{1,2},{2,3},{3,4} }, guesses = { {1,0},{3,4},{2,1},{3,2} }, k = 1;auto res = Solution().rootCount(edges, guesses, k);AssertEx(5, res);}TEST_METHOD(TestMethod2){edges ={ {1,0},{2,1},{2,3},{4,0},{5,2},{6,1},{0,7},{1,8},{9,6},{10,4},{11,10},{12,8},{8,13},{14,4},{15,9},{9,16},{3,17},{4,18},{6,19},{20,13},{21,20},{19,22},{23,3},{24,0},{25,14},{17,26},{27,3},{3,28},{29,3},{4,30},{31,9},{0,32},{33,12},{34,14},{27,35},{35,36},{37,33},{38,18},{6,39} };guesses ={ {13,8},{4,18},{37,33},{4,30},{1,8},{3,17},{25,14},{0,1},{27,35},{21,20},{6,1},{26,17},{1,2},{8,13},{22,19},{30,4},{4,0},{2,5},{14,4},{9,6},{19,22},{16,9},{5,2},{29,3},{34,14},{8,1},{11,10},{15,9},{10,4},{35,27},{3,27},{33,12},{14,34},{32,0},{14,25},{39,6},{7,0},{4,10},{0,32},{23,3},{20,21},{24,0},{0,7},{1,0},{3,28},{6,9},{8,12},{18,4},{1,6},{2,1},{2,3},{3,29},{9,16},{17,26},{35,36},{13,20},{10,11},{18,38},{3,23},{0,24},{33,37},{12,33},{3,2},{20,13},{17,3} };k = 29;auto res = Solution().rootCount(edges, guesses, k);AssertEx(40, res);}};
}
DFS非常容易超时
DFS稍稍复杂,leetcode就容易超时。
所以:
一,计算出临接表。
二,DFS各节点层次。
三,计算出各节点的孩子。
四,BFS各节点。由于每个节点顶多一个父亲,所以无需判断节点是否重复访问。
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;}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;}
};class CDFSLeveChild
{
public:CDFSLeveChild(const vector<vector <int>>& vNeiBo,int root=0):m_vNeiBo(vNeiBo), Leve(m_vLeve){m_vLeve.resize(m_vNeiBo.size());DFS(root, -1);};const vector<int>& Leve;vector<vector<int>> Child() const{vector<vector <int>> vChild(m_vNeiBo.size());for (int i = 0; i < m_vNeiBo.size(); i++) {for (const auto& next : m_vNeiBo[i]) {if (m_vLeve[next] < m_vLeve[i]) { continue; }vChild[i].emplace_back(next);}}return vChild;}
protected:void DFS(int cur, int par) {if (-1 != par) { m_vLeve[cur] = m_vLeve[par] + 1; }for (const auto& next : m_vNeiBo[cur]) {if (next == par) { continue; }DFS(next, cur);}}vector<int> m_vLeve;const vector<vector <int>>& m_vNeiBo;
};
class Solution {
public:int rootCount(vector<vector<int>>& edges, vector<vector<int>>& guesses, int k) {m_c = edges.size() + 1;m_dp.resize(m_c); m_vNeiBo = CNeiBo::Two(m_c, edges, false); auto vChilds = CDFSLeveChild(m_vNeiBo).Child();for (const auto& v : guesses) {m_mGuess[Mask(v[0], v[1])]++;}for (int par = 0; par < m_c; par++) {for (int& child : vChilds[par]) {m_dp[0] += m_mGuess[Mask(par, child)];}} queue<int> que; que.emplace(0);while (que.size()) {int cur = que.front();que.pop();for (const auto& child : vChilds[cur]) {m_dp[child] = m_dp[cur];m_dp[child] -= m_mGuess[Mask(cur, child)];m_dp[child] += m_mGuess[Mask(child, cur)];que.emplace(child);}}return count_if(m_dp.begin(), m_dp.end(), [&](int i) {return i >= k; });}long long Mask(long long par, int cur) { return m_c * par + cur; }int m_c;vector<int> m_dp;unordered_map<long long, int> m_mGuess;vector < vector <int>> m_vNeiBo;
};
进一步优化
可以用数组代码映射,算法方向,总共2n-2条边。假定根为0的树。
如果这条边是 子节点执行父节点,则此边数是child。如果方向相反则是n + child。
运行速度大约提高了20%。
class Solution {
public:int rootCount(vector<vector<int>>& edges, vector<vector<int>>& guesses, int k) {m_c = edges.size() + 1;m_dp.resize(m_c); vector<int> vGuess(m_c * 2);m_vNeiBo = CNeiBo::Two(m_c, edges, false); CDFSLeveChild dfs(m_vNeiBo);auto vChilds = dfs.Child();auto Mask = [&](int par, int child) {if (dfs.Leve[par] < dfs.Leve[child]) {return child;}return par + m_c;};for (const auto& v : guesses) {vGuess[Mask(v[0], v[1])]++;}for (int par = 0; par < m_c; par++) {for (int& child : vChilds[par]) {m_dp[0] += vGuess[Mask(par, child)];}} queue<int> que; que.emplace(0);while (que.size()) {int cur = que.front();que.pop();for (const auto& child : vChilds[cur]) {m_dp[child] = m_dp[cur];m_dp[child] -= vGuess[Mask(cur, child)];m_dp[child] += vGuess[Mask(child, cur)];que.emplace(child);}}return count_if(m_dp.begin(), m_dp.end(), [&](int i) {return i >= k; });}int m_c;vector<int> m_dp; vector < vector <int>> m_vNeiBo;
};
DFS序+差分数组
root和它的某个后代childchild换根。则到 c h i l d c h i l d ↔ r o o t childchild\leftrightarrow root childchild↔root这条路径上的边都反转。可以用差分数组。
childchild和它的祖先不是连续的,但他们的DFS序是连续的。
此方案不好理解,实现也不简单。备用。
扩展阅读
视频课程
先学简单的课程,请移步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++**实现。