【树上倍增】【割点】 【换根法】3067. 在带权树网络中统计可连接服务器对 ...

打印 上一主题 下一主题

主题 542|帖子 542|积分 1626

作者保举

视频算法专题
本文涉及知识点

树上倍增 树 图论 并集查找 换根法 深度优先
割点原理及封装好的割点类(预计2024年3月11号左右发布)
LeetCode3067. 在带权树网络中统计可连接服务器对数量

给你一棵无根带权树,树中总共有 n 个节点,分别表现 n 个服务器,服务器从 0 到 n - 1 编号。同时给你一个数组 edges ,其中 edges = [ai, bi, weighti] 表现节点 ai 和 bi 之间有一条双向边,边的权值为 weighti 。再给你一个整数 signalSpeed 。
如果两个服务器 a ,b 和 c 满意以下条件,那么我们称服务器 a 和 b 是通过服务器 c 可连接的 :
a < b ,a != c 且 b != c 。
从 c 到 a 的距离是可以被 signalSpeed 整除的。
从 c 到 b 的距离是可以被 signalSpeed 整除的。
从 c 到 b 的路径与从 c 到 a 的路径没有任何公共边。
请你返回一个长度为 n 的整数数组 count ,其中 count 表现通过服务器 i 可连接 的服务器对的 数量 。
示例 1:

输入:edges = [[0,1,1],[1,2,5],[2,3,13],[3,4,9],[4,5,2]], signalSpeed = 1
输出:[0,4,6,6,4,0]
表明:由于 signalSpeed 等于 1 ,count[c] 等于所有从 c 开始且没有公共边的路径对数量。
在输入图中,count[c] 等于服务器 c 左边服务器数量乘以右边服务器数量。
示例 2:

输入:edges = [[0,6,3],[6,5,3],[0,3,1],[3,2,7],[3,1,6],[3,4,2]], signalSpeed = 3
输出:[2,0,0,0,0,0,2]
表明:通过服务器 0 ,有 2 个可连接服务器对(4, 5) 和 (4, 6) 。
通过服务器 6 ,有 2 个可连接服务器对 (4, 5) 和 (0, 5) 。
所有服务器对都必须通过服务器 0 或 6 才可连接,所以其他服务器对应的可连接服务器对数量都为 0 。
提示:
2 <= n <= 1000
edges.length == n - 1
edges.length == 3
0 <= ai, bi < n
edges = [ai, bi, weighti]
1 <= weighti <= 106
1 <= signalSpeed <= 106
输入保证 edges 构成一棵正当的树。
树上倍增

本题有三个考点:
一,怎样计算树上两个节点x1,x2的距离。
假定这两个节点的最早公共先人是pub。以恣意节点root为根,f(x)表现节点x到root的距离。
x1到x2的距离:f(x1)+f(x2)-2*f(pub)。
二,怎样找到最早公共先人:树上倍增。
记录各节点的1级先人(父节点)、2级先人、4级先人…
三,如果判定ac和bc有公共边。
树是连通无向无环图,因为无环,所以两个节点的路径唯一。
假设公共边是x3x4。则x3到c的路径唯一,假定x3到c的倒数第二个端点是x5,则ab和bc的末了一条边都是                                                        x                               3                               c                                      →                                       \overrightarrow{x3c}                  x3c            。断开所以和c相连的边,如果a和b在同一个连通区域,则有公共边。用并查集看是否在同一个连通区域。
时间复杂度: O(nnlogn)。 枚举c,时间复杂度O(n);枚举ab,时间复杂度O(n)。查公共路径O(logn)。
并集查找

  1. class CNeiBo
  2. {
  3. public:       
  4.         static vector<vector<int>> Two(int n, vector<vector<int>>& edges, bool bDirect, int iBase = 0)
  5.         {
  6.                 vector<vector<int>>  vNeiBo(n);
  7.                 for (const auto& v : edges)
  8.                 {
  9.                         vNeiBo[v[0] - iBase].emplace_back(v[1] - iBase);
  10.                         if (!bDirect)
  11.                         {
  12.                                 vNeiBo[v[1] - iBase].emplace_back(v[0] - iBase);
  13.                         }
  14.                 }
  15.                 return vNeiBo;
  16.         }       
  17.         static vector<vector<std::pair<int, int>>> Three(int n, vector<vector<int>>& edges, bool bDirect, int iBase = 0)
  18.         {
  19.                 vector<vector<std::pair<int, int>>> vNeiBo(n);
  20.                 for (const auto& v : edges)
  21.                 {
  22.                         vNeiBo[v[0] - iBase].emplace_back(v[1] - iBase, v[2]);
  23.                         if (!bDirect)
  24.                         {
  25.                                 vNeiBo[v[1] - iBase].emplace_back(v[0] - iBase, v[2]);
  26.                         }
  27.                 }
  28.                 return vNeiBo;
  29.         }
  30. };
  31. class CUnionFind
  32. {
  33. public:
  34.         CUnionFind(int iSize) :m_vNodeToRegion(iSize)
  35.         {
  36.                 for (int i = 0; i < iSize; i++)
  37.                 {
  38.                         m_vNodeToRegion[i] = i;
  39.                 }
  40.                 m_iConnetRegionCount = iSize;
  41.         }       
  42.         CUnionFind(vector<vector<int>>& vNeiBo):CUnionFind(vNeiBo.size())
  43.         {
  44.                 for (int i = 0; i < vNeiBo.size(); i++) {
  45.                         for (const auto& n : vNeiBo[i]) {
  46.                                 Union(i, n);
  47.                         }
  48.                 }
  49.         }
  50.         int GetConnectRegionIndex(int iNode)
  51.         {
  52.                 int& iConnectNO = m_vNodeToRegion[iNode];
  53.                 if (iNode == iConnectNO)
  54.                 {
  55.                         return iNode;
  56.                 }
  57.                 return iConnectNO = GetConnectRegionIndex(iConnectNO);
  58.         }
  59.         void Union(int iNode1, int iNode2)
  60.         {
  61.                 const int iConnectNO1 = GetConnectRegionIndex(iNode1);
  62.                 const int iConnectNO2 = GetConnectRegionIndex(iNode2);
  63.                 if (iConnectNO1 == iConnectNO2)
  64.                 {
  65.                         return;
  66.                 }
  67.                 m_iConnetRegionCount--;
  68.                 if (iConnectNO1 > iConnectNO2)
  69.                 {
  70.                         UnionConnect(iConnectNO1, iConnectNO2);
  71.                 }
  72.                 else
  73.                 {
  74.                         UnionConnect(iConnectNO2, iConnectNO1);
  75.                 }
  76.         }
  77.         bool IsConnect(int iNode1, int iNode2)
  78.         {
  79.                 return GetConnectRegionIndex(iNode1) == GetConnectRegionIndex(iNode2);
  80.         }
  81.         int GetConnetRegionCount()const
  82.         {
  83.                 return m_iConnetRegionCount;
  84.         }
  85.         vector<int> GetNodeCountOfRegion()//各联通区域的节点数量
  86.         {
  87.                 const int iNodeSize = m_vNodeToRegion.size();
  88.                 vector<int> vRet(iNodeSize);
  89.                 for (int i = 0; i < iNodeSize; i++)
  90.                 {
  91.                         vRet[GetConnectRegionIndex(i)]++;
  92.                 }
  93.                 return vRet;
  94.         }
  95.         std::unordered_map<int, vector<int>> GetNodeOfRegion()
  96.         {
  97.                 std::unordered_map<int, vector<int>> ret;
  98.                 const int iNodeSize = m_vNodeToRegion.size();
  99.                 for (int i = 0; i < iNodeSize; i++)
  100.                 {
  101.                         ret[GetConnectRegionIndex(i)].emplace_back(i);
  102.                 }
  103.                 return ret;
  104.         }
  105. private:
  106.         void UnionConnect(int iFrom, int iTo)
  107.         {
  108.                 m_vNodeToRegion[iFrom] = iTo;
  109.         }
  110.         vector<int> m_vNodeToRegion;//各点所在联通区域的索引,本联通区域任意一点的索引,为了增加可理解性,用最小索引
  111.         int m_iConnetRegionCount;
  112. };
  113. class CParents
  114. {
  115. public:
  116.         CParents(vector<int>& vParent, const vector<int>& vLeve):m_vLeve(vLeve)
  117.         {
  118.                 const int iMaxLeve = *std::max_element(vLeve.begin(), vLeve.end());
  119.                 int iBitNum = 0;
  120.                 for (; (1 << iBitNum) < iMaxLeve; iBitNum++);
  121.                 const int n = vParent.size();
  122.                 m_vParents.assign(iBitNum+1, vector<int>(n, -1));
  123.                 m_vParents[0] = vParent;
  124.                 //树上倍增
  125.                 for (int i = 1; i < m_vParents.size(); i++)
  126.                 {
  127.                         for (int j = 0; j < n; j++)
  128.                         {
  129.                                 const int iPre = m_vParents[i - 1][j];
  130.                                 if (-1 != iPre)
  131.                                 {
  132.                                         m_vParents[i][j] = m_vParents[i - 1][iPre];
  133.                                 }
  134.                         }
  135.                 }
  136.         }
  137.         int GetParent(int iNode, int iLeve)const
  138.         {
  139.                 int iParent = iNode;
  140.                 for (int iBit = 0; iBit < m_vParents.size(); iBit++)
  141.                 {
  142.                         if (-1 == iParent)
  143.                         {
  144.                                 return iParent;
  145.                         }
  146.                         if (iLeve & (1 << iBit))
  147.                         {
  148.                                 iParent = m_vParents[iBit][iParent];
  149.                         }
  150.                 }
  151.                 return iParent;
  152.         }
  153.         int GetPublicParent(int iNode1, int iNode2)const
  154.         {
  155.                 int leve0 = m_vLeve[iNode1];
  156.                 int leve1 = m_vLeve[iNode2];
  157.                 if (leve0 < leve1)
  158.                 {
  159.                         iNode2 = GetParent(iNode2, leve1 - leve0);
  160.                         leve1 = leve0;
  161.                 }
  162.                 else
  163.                 {
  164.                         iNode1 = GetParent(iNode1, leve0 - leve1);
  165.                         leve0 = leve1;
  166.                 }
  167.                 //二分查找
  168.                 int left = -1, r = leve0;
  169.                 while (r - left > 1)
  170.                 {
  171.                         const auto mid = left + (r - left) / 2;
  172.                         const int iParent0 = GetParent(iNode1, mid);
  173.                         const int iParent1 = GetParent(iNode2, mid);
  174.                         if (iParent0 == iParent1)
  175.                         {
  176.                                 r = mid;
  177.                         }
  178.                         else
  179.                         {
  180.                                 left = mid;
  181.                         }
  182.                 }
  183.                 return GetParent(iNode1, r);
  184.         }
  185. protected:
  186.         vector<vector<int>> m_vParents;
  187.         const vector<int> m_vLeve;
  188. };
  189. class Solution {
  190. public:
  191.         vector<int> countPairsOfConnectableServers(vector<vector<int>>& edges, int signalSpeed) {
  192.                 m_c = edges.size() + 1;
  193.                 m_vDisToRoot.resize(m_c);
  194.                 m_vParent.resize(m_c);
  195.                 m_vLeve.resize(m_c);
  196.                 auto neiBo = CNeiBo::Three(m_c, edges, false, 0);
  197.                 DFS(neiBo, 0, -1, 0,0);       
  198.                 CParents par(m_vParent,m_vLeve);
  199.                 vector<int> vRet(m_c);
  200.                 for (int c = 0; c < m_c; c++)
  201.                 {
  202.                         CUnionFind uf(m_c);
  203.                         for (const auto& v : edges)
  204.                         {
  205.                                 if ((v[0] == c) || (v[1] == c))
  206.                                 {
  207.                                         continue;
  208.                                 }
  209.                                 uf.Union(v[0], v[1]);
  210.                         }
  211.                         vector<int> vRegionCnt(m_c);
  212.                         for (int ab = 0; ab < m_c; ab++)
  213.                         {
  214.                                 if (ab == c )
  215.                                 {
  216.                                         continue;
  217.                                 }
  218.                                 const int pub = par.GetPublicParent(ab, c);
  219.                                 const int len = m_vDisToRoot[ab] + m_vDisToRoot[c] - 2 * m_vDisToRoot[pub];
  220.                                 if (0 != len % signalSpeed)
  221.                                 {
  222.                                         continue;
  223.                                 }
  224.                                 vRegionCnt[uf.GetConnectRegionIndex(ab)]++;
  225.                         }
  226.                         int&iRet = vRet[c];
  227.                         const int total = std::accumulate(vRegionCnt.begin(), vRegionCnt.end(), 0);
  228.                         for (int c1 = 0; c1 < m_c; c1++)
  229.                         {
  230.                                 iRet += vRegionCnt[c1] * (total - vRegionCnt[c1]);
  231.                         }       
  232.                         iRet /= 2;
  233.                 }
  234.                 return vRet;
  235.         }
  236.         void DFS(vector<vector<std::pair<int, int>>>& neiBo, int cur, int par,int leve,int dis)
  237.         {
  238.                 m_vDisToRoot[cur] =dis;
  239.                 m_vParent[cur] = par;
  240.                 m_vLeve[cur] = leve;
  241.                 for (const auto& [next,len] : neiBo[cur])
  242.                 {
  243.                         if (next == par)
  244.                         {
  245.                                 continue;
  246.                         }
  247.                         DFS(neiBo, next, cur,leve+1,dis+len);
  248.                 }
  249.         }
  250.         vector<int> m_vDisToRoot,m_vParent,m_vLeve;
  251.         int m_c;
  252. };
复制代码
测试用例

  1. template<class T,class T2>
  2. void Assert(const T& t1, const T2& t2)
  3. {
  4.         assert(t1 == t2);
  5. }
  6. template<class T>
  7. void Assert(const vector<T>& v1, const vector<T>& v2)
  8. {
  9.         if (v1.size() != v2.size())
  10.         {
  11.                 assert(false);
  12.                 return;
  13.         }
  14.         for (int i = 0; i < v1.size(); i++)
  15.         {
  16.                 Assert(v1[i], v2[i]);
  17.         }
  18. }
  19. int main()
  20. {
  21.         vector<vector<int>> edges;
  22.         int signalSpeed;
  23.         {
  24.                 Solution sln;
  25.                 edges = {  }, signalSpeed = 1;
  26.                 auto res = sln.countPairsOfConnectableServers(edges, signalSpeed);
  27.                 Assert({ 0 }, res);
  28.         }
  29.         {
  30.                 Solution sln;
  31.                 edges = { {0,1,1} }, signalSpeed = 1;
  32.                 auto res = sln.countPairsOfConnectableServers(edges, signalSpeed);
  33.                 Assert({ 0,0 }, res);
  34.         }
  35.         {
  36.                 Solution sln;
  37.                 edges = { {0,1,1},{1,2,1} }, signalSpeed = 1;
  38.                 auto res = sln.countPairsOfConnectableServers(edges, signalSpeed);
  39.                 Assert({ 0,1,0 }, res);
  40.         }
  41.         {
  42.                 Solution sln;
  43.                 edges = { {0,1,1},{1,2,5},{2,3,13},{3,4,9},{4,5,2} }, signalSpeed = 1;
  44.                 auto res = sln.countPairsOfConnectableServers(edges, signalSpeed);
  45.                 Assert({ 0,4,6,6,4,0 } , res);
  46.         }
  47.         {
  48.                 Solution sln;
  49.                 edges = { {0,6,3},{6,5,3},{0,3,1},{3,2,7},{3,1,6},{3,4,2} }, signalSpeed = 3;
  50.                 auto res = sln.countPairsOfConnectableServers(edges, signalSpeed);
  51.                 Assert({ 2,0,0,0,0,0,2 }, res);
  52.         }
  53. }
复制代码
换根法DFS

树中删除一个节点,则各孩子各一个连通区域,除本身及子女外一个区域。如果这个节点是根,则简朴得多。各孩子一个连通区域。
DSF(cur) 返回本身及子孙到当前根节点距离是signalSpeed 倍的节点数量。令当前根节点各孩子的返回值是{i1,i2,                                   ⋯                              \cdots                  ⋯,im} 。i1*i2+(i1+i2)*i3                                    ⋯                              \cdots                  ⋯ +(I1+i2+                                   …                              \dots                  … + i                                                                 m                               −                               1                                                 _{m-1}                  m−1​)*im。如许不必除以二。
a<b ,表现a !=b ,(a,b)和(b,a)只取一个。
  1. class CNeiBo
  2. {
  3. public:       
  4.         static vector<vector<int>> Two(int n, vector<vector<int>>& edges, bool bDirect, int iBase = 0)
  5.         {
  6.                 vector<vector<int>>  vNeiBo(n);
  7.                 for (const auto& v : edges)
  8.                 {
  9.                         vNeiBo[v[0] - iBase].emplace_back(v[1] - iBase);
  10.                         if (!bDirect)
  11.                         {
  12.                                 vNeiBo[v[1] - iBase].emplace_back(v[0] - iBase);
  13.                         }
  14.                 }
  15.                 return vNeiBo;
  16.         }       
  17.         static vector<vector<std::pair<int, int>>> Three(int n, vector<vector<int>>& edges, bool bDirect, int iBase = 0)
  18.         {
  19.                 vector<vector<std::pair<int, int>>> vNeiBo(n);
  20.                 for (const auto& v : edges)
  21.                 {
  22.                         vNeiBo[v[0] - iBase].emplace_back(v[1] - iBase, v[2]);
  23.                         if (!bDirect)
  24.                         {
  25.                                 vNeiBo[v[1] - iBase].emplace_back(v[0] - iBase, v[2]);
  26.                         }
  27.                 }
  28.                 return vNeiBo;
  29.         }
  30. };
  31. class Solution {
  32. public:
  33.         vector<int> countPairsOfConnectableServers(vector<vector<int>>& edges, int signalSpeed) {
  34.                 m_c = edges.size() + 1;
  35.                 m_iSignalSpeed = signalSpeed;               
  36.                 auto neiBo = CNeiBo::Three(m_c, edges, false, 0);               
  37.                 vector<int> vRet(m_c);
  38.                 for (int c = 0; c < m_c; c++)
  39.                 {
  40.                         int& iRet = vRet[c];
  41.                         int left = 0;
  42.                         for (const auto& [next, len] : neiBo[c])
  43.                         {
  44.                                 int cur = DFS(neiBo, next, c, len);
  45.                                 iRet += left * cur;
  46.                                 left += cur;
  47.                         }
  48.                 }
  49.                 return vRet;
  50.         }
  51.         int DFS(vector<vector<std::pair<int, int>>>& neiBo, int cur, int par,int dis)
  52.         {
  53.                 int iRet = (0 ==dis % m_iSignalSpeed);
  54.                 for (const auto& [next,len] : neiBo[cur])
  55.                 {
  56.                         if (next == par)
  57.                         {
  58.                                 continue;
  59.                         }
  60.                         iRet +=DFS(neiBo, next, cur,dis+len);
  61.                 }
  62.                 return iRet;
  63.         }
  64.         int m_iSignalSpeed;
  65.         int m_c;
  66. };
复制代码
割点

本解法过于复杂,除非用了提前封装好的割点扩展类,否则被使用。
  1. class CNeiBo
  2. {
  3. public:       
  4.         static vector<vector<int>> Two(int n, vector<vector<int>>& edges, bool bDirect, int iBase = 0)
  5.         {
  6.                 vector<vector<int>>  vNeiBo(n);
  7.                 for (const auto& v : edges)
  8.                 {
  9.                         vNeiBo[v[0] - iBase].emplace_back(v[1] - iBase);
  10.                         if (!bDirect)
  11.                         {
  12.                                 vNeiBo[v[1] - iBase].emplace_back(v[0] - iBase);
  13.                         }
  14.                 }
  15.                 return vNeiBo;
  16.         }       
  17.         static vector<vector<std::pair<int, int>>> Three(int n, vector<vector<int>>& edges, bool bDirect, int iBase = 0)
  18.         {
  19.                 vector<vector<std::pair<int, int>>> vNeiBo(n);
  20.                 for (const auto& v : edges)
  21.                 {
  22.                         vNeiBo[v[0] - iBase].emplace_back(v[1] - iBase, v[2]);
  23.                         if (!bDirect)
  24.                         {
  25.                                 vNeiBo[v[1] - iBase].emplace_back(v[0] - iBase, v[2]);
  26.                         }
  27.                 }
  28.                 return vNeiBo;
  29.         }
  30. };
  31. class CUnionFind
  32. {
  33. public:
  34.         CUnionFind(int iSize) :m_vNodeToRegion(iSize)
  35.         {
  36.                 for (int i = 0; i < iSize; i++)
  37.                 {
  38.                         m_vNodeToRegion[i] = i;
  39.                 }
  40.                 m_iConnetRegionCount = iSize;
  41.         }       
  42.         CUnionFind(vector<vector<int>>& vNeiBo):CUnionFind(vNeiBo.size())
  43.         {
  44.                 for (int i = 0; i < vNeiBo.size(); i++) {
  45.                         for (const auto& n : vNeiBo[i]) {
  46.                                 Union(i, n);
  47.                         }
  48.                 }
  49.         }
  50.         int GetConnectRegionIndex(int iNode)
  51.         {
  52.                 int& iConnectNO = m_vNodeToRegion[iNode];
  53.                 if (iNode == iConnectNO)
  54.                 {
  55.                         return iNode;
  56.                 }
  57.                 return iConnectNO = GetConnectRegionIndex(iConnectNO);
  58.         }
  59.         void Union(int iNode1, int iNode2)
  60.         {
  61.                 const int iConnectNO1 = GetConnectRegionIndex(iNode1);
  62.                 const int iConnectNO2 = GetConnectRegionIndex(iNode2);
  63.                 if (iConnectNO1 == iConnectNO2)
  64.                 {
  65.                         return;
  66.                 }
  67.                 m_iConnetRegionCount--;
  68.                 if (iConnectNO1 > iConnectNO2)
  69.                 {
  70.                         UnionConnect(iConnectNO1, iConnectNO2);
  71.                 }
  72.                 else
  73.                 {
  74.                         UnionConnect(iConnectNO2, iConnectNO1);
  75.                 }
  76.         }
  77.         bool IsConnect(int iNode1, int iNode2)
  78.         {
  79.                 return GetConnectRegionIndex(iNode1) == GetConnectRegionIndex(iNode2);
  80.         }
  81.         int GetConnetRegionCount()const
  82.         {
  83.                 return m_iConnetRegionCount;
  84.         }
  85.         vector<int> GetNodeCountOfRegion()//各联通区域的节点数量
  86.         {
  87.                 const int iNodeSize = m_vNodeToRegion.size();
  88.                 vector<int> vRet(iNodeSize);
  89.                 for (int i = 0; i < iNodeSize; i++)
  90.                 {
  91.                         vRet[GetConnectRegionIndex(i)]++;
  92.                 }
  93.                 return vRet;
  94.         }
  95.         std::unordered_map<int, vector<int>> GetNodeOfRegion()
  96.         {
  97.                 std::unordered_map<int, vector<int>> ret;
  98.                 const int iNodeSize = m_vNodeToRegion.size();
  99.                 for (int i = 0; i < iNodeSize; i++)
  100.                 {
  101.                         ret[GetConnectRegionIndex(i)].emplace_back(i);
  102.                 }
  103.                 return ret;
  104.         }
  105. private:
  106.         void UnionConnect(int iFrom, int iTo)
  107.         {
  108.                 m_vNodeToRegion[iFrom] = iTo;
  109.         }
  110.         vector<int> m_vNodeToRegion;//各点所在联通区域的索引,本联通区域任意一点的索引,为了增加可理解性,用最小索引
  111.         int m_iConnetRegionCount;
  112. };
  113. class CParents
  114. {
  115. public:
  116.         CParents(vector<int>& vParent, const int iMaxLeve)
  117.         {       
  118.                 int iBitNum = 0;
  119.                 for (; (1 << iBitNum) < iMaxLeve; iBitNum++);
  120.                 const int n = vParent.size();
  121.                 m_vParents.assign(iBitNum+1, vector<int>(n, -1));
  122.                 m_vParents[0] = vParent;
  123.                 //树上倍增
  124.                 for (int i = 1; i < m_vParents.size(); i++)
  125.                 {
  126.                         for (int j = 0; j < n; j++)
  127.                         {
  128.                                 const int iPre = m_vParents[i - 1][j];
  129.                                 if (-1 != iPre)
  130.                                 {
  131.                                         m_vParents[i][j] = m_vParents[i - 1][iPre];
  132.                                 }
  133.                         }
  134.                 }
  135.         }
  136.         int GetParent(int iNode, int iLeve)const
  137.         {
  138.                 int iParent = iNode;
  139.                 for (int iBit = 0; iBit < m_vParents.size(); iBit++)
  140.                 {
  141.                         if (-1 == iParent)
  142.                         {
  143.                                 return iParent;
  144.                         }
  145.                         if (iLeve & (1 << iBit))
  146.                         {
  147.                                 iParent = m_vParents[iBit][iParent];
  148.                         }
  149.                 }
  150.                 return iParent;
  151.         }       
  152. protected:
  153.         vector<vector<int>> m_vParents;
  154. };
  155. class C2Parents : CParents
  156. {
  157. public:
  158.         C2Parents(vector<int>& vParent, const vector<int>& vLeve) :m_vLeve(vLeve)
  159.                 , CParents(vParent,*std::max_element(vLeve.begin(), vLeve.end()))
  160.         {               
  161.         }       
  162.         int GetPublicParent(int iNode1, int iNode2)const
  163.         {
  164.                 int leve0 = m_vLeve[iNode1];
  165.                 int leve1 = m_vLeve[iNode2];
  166.                 if (leve0 < leve1)
  167.                 {
  168.                         iNode2 = GetParent(iNode2, leve1 - leve0);
  169.                         leve1 = leve0;
  170.                 }
  171.                 else
  172.                 {
  173.                         iNode1 = GetParent(iNode1, leve0 - leve1);
  174.                         leve0 = leve1;
  175.                 }
  176.                 //二分查找
  177.                 int left = -1, r = leve0;
  178.                 while (r - left > 1)
  179.                 {
  180.                         const auto mid = left + (r - left) / 2;
  181.                         const int iParent0 = GetParent(iNode1, mid);
  182.                         const int iParent1 = GetParent(iNode2, mid);
  183.                         if (iParent0 == iParent1)
  184.                         {
  185.                                 r = mid;
  186.                         }
  187.                         else
  188.                         {
  189.                                 left = mid;
  190.                         }
  191.                 }
  192.                 return GetParent(iNode1, r);
  193.         }
  194. protected:
  195.         vector<vector<int>> m_vParents;
  196.         const vector<int> m_vLeve;
  197. };
  198. class CCutPointEx
  199. {
  200. public:
  201.         CCutPointEx(const vector<vector<int>>& vNeiB) : m_iSize(vNeiB.size())
  202.         {
  203.                 m_vNodeToTime.assign(m_iSize, -1);
  204.                 m_vChildFirstEnd.resize(m_iSize);
  205.                 m_vNodeToRegion.assign(m_iSize, -1);
  206.                 m_vCut.assign(m_iSize, false);
  207.                 for (int i = 0; i < m_iSize; i++)
  208.                 {
  209.                         if (-1 != m_vNodeToTime[i])
  210.                         {
  211.                                 continue;
  212.                         }
  213.                         dfs(i, -1, vNeiB);
  214.                         m_iRegionCount++;
  215.                 }
  216.                 m_vTimeToNode.resize(m_iSize);
  217.                 for (int i = 0; i < m_iSize; i++)
  218.                 {
  219.                         m_vTimeToNode[m_vNodeToTime[i]] = i;;
  220.                 }
  221.         }
  222.         bool Visit(int src, int dest, int iCut)const
  223.         {
  224.                 if (m_vNodeToRegion[src] != m_vNodeToRegion[dest])
  225.                 {
  226.                         return false;//不在一个连通区域
  227.                 }
  228.                 if (!m_vCut[iCut])
  229.                 {
  230.                         return true;
  231.                 }
  232.                 const int r1 = GetCutRegion(iCut, src);
  233.                 const int r2 = GetCutRegion(iCut, dest);
  234.                 return r1 == r2;
  235.         }
  236.         vector<vector<int>> GetSubRegionOfCut(const int iCut)const
  237.         {//删除iCut及和它相连的边后,iCut所在的区域会分成几个区域:父节点一个区域、各子节点一个区域
  238.          //父节点所在区域可能为空,如果iCut所在的连通区域只有一个节点,则返回一个没有节点的区域。
  239.                 const auto& v = m_vChildFirstEnd[iCut];
  240.                 vector<vector<int>> vRet(1);                       
  241.                 int j = 0;
  242.                 for (int iTime=0;iTime < m_iSize; iTime++ )
  243.                 {
  244.                         const int iNode        = m_vTimeToNode[iTime];
  245.                         if ((j < v.size()) && ( iTime  >= v[j].first ))
  246.                         {
  247.                                 j++;
  248.                                 vRet.emplace_back();
  249.                         }
  250.                         if ((iCut != iNode) && (m_vNodeToRegion[iNode] == m_vNodeToRegion[iCut]))
  251.                         {
  252.                                 if (v.size()&&(iTime >= v.back().second))
  253.                                 {
  254.                                         vRet[0].emplace_back(iNode);
  255.                                 }
  256.                                 else
  257.                                 {
  258.                                         vRet.back().emplace_back(iNode);
  259.                                 }
  260.                         }
  261.                 }
  262.                 return vRet;
  263.         }
  264. protected:
  265.         int dfs(int cur, int parent, const vector<vector<int>>& vNeiB)
  266.         {
  267.                 auto& curTime = m_vNodeToTime[cur];
  268.                 m_vNodeToRegion[cur] = m_iRegionCount;
  269.                 curTime = m_iTime++;
  270.                 int iCutChild = 0;
  271.                 int iMinTime = curTime;
  272.                 for (const auto& next : vNeiB[cur])
  273.                 {
  274.                         if (-1 != m_vNodeToTime[next])
  275.                         {
  276.                                 iMinTime = min(iMinTime, m_vNodeToTime[next]);
  277.                                 continue;
  278.                         }
  279.                         int iChildBeginTime = m_iTime;
  280.                         const int iChildMinTime = dfs(next, cur, vNeiB);
  281.                         iMinTime = min(iMinTime, iChildMinTime);
  282.                         if (iChildMinTime >= curTime)
  283.                         {
  284.                                 iCutChild++;
  285.                                 m_vChildFirstEnd[cur].push_back({ iChildBeginTime,m_iTime });
  286.                         };
  287.                 }
  288.                 m_vCut[cur] = (iCutChild + (-1 != parent)) >= 2;
  289.                 return iMinTime;
  290.         }
  291.         int GetCutRegion(int iCut, int iNode)const
  292.         {
  293.                 const auto& v = m_vChildFirstEnd[iCut];
  294.                 auto it = std::upper_bound(v.begin(), v.end(), m_vNodeToTime[iNode], [](int time, const std::pair<int, int>& pr) {return time < pr.first; });
  295.                 if (v.begin() == it)
  296.                 {
  297.                         return v.size();
  298.                 }
  299.                 --it;
  300.                 return (it->second > m_vNodeToTime[iNode]) ? (it - v.begin()) : v.size();
  301.         }
  302.         int m_iTime = 0;
  303.         const int m_iSize;//时间戳
  304.         int m_iRegionCount = 0;
  305.         vector<int> m_vNodeToTime;//各节点到达时间,从0开始。 -1表示未处理
  306.         vector<bool> m_vCut;//是否是割点
  307.         vector<int> m_vNodeToRegion;//各节点所在区域
  308.         vector<vector<pair<int, int>>> m_vChildFirstEnd;//左闭右开空间[0,m_vChildFirstEnd[0].first)和[m_vChildFirstEnd.back().second,iSize)是一个区域
  309.         vector<int> m_vTimeToNode;
  310. };
复制代码
2024年3月9号 新封装

  1. class CNeiBo
  2. {
  3. public:       
  4.         static vector<vector<int>> Two(int n, vector<vector<int>>& edges, bool bDirect, int iBase = 0)
  5.         {
  6.                 vector<vector<int>>  vNeiBo(n);
  7.                 for (const auto& v : edges)
  8.                 {
  9.                         vNeiBo[v[0] - iBase].emplace_back(v[1] - iBase);
  10.                         if (!bDirect)
  11.                         {
  12.                                 vNeiBo[v[1] - iBase].emplace_back(v[0] - iBase);
  13.                         }
  14.                 }
  15.                 return vNeiBo;
  16.         }       
  17.         static vector<vector<std::pair<int, int>>> Three(int n, vector<vector<int>>& edges, bool bDirect, int iBase = 0)
  18.         {
  19.                 vector<vector<std::pair<int, int>>> vNeiBo(n);
  20.                 for (const auto& v : edges)
  21.                 {
  22.                         vNeiBo[v[0] - iBase].emplace_back(v[1] - iBase, v[2]);
  23.                         if (!bDirect)
  24.                         {
  25.                                 vNeiBo[v[1] - iBase].emplace_back(v[0] - iBase, v[2]);
  26.                         }
  27.                 }
  28.                 return vNeiBo;
  29.         }
  30. };
  31. class CUnionFind
  32. {
  33. public:
  34.         CUnionFind(int iSize) :m_vNodeToRegion(iSize)
  35.         {
  36.                 for (int i = 0; i < iSize; i++)
  37.                 {
  38.                         m_vNodeToRegion[i] = i;
  39.                 }
  40.                 m_iConnetRegionCount = iSize;
  41.         }       
  42.         CUnionFind(vector<vector<int>>& vNeiBo):CUnionFind(vNeiBo.size())
  43.         {
  44.                 for (int i = 0; i < vNeiBo.size(); i++) {
  45.                         for (const auto& n : vNeiBo[i]) {
  46.                                 Union(i, n);
  47.                         }
  48.                 }
  49.         }
  50.         int GetConnectRegionIndex(int iNode)
  51.         {
  52.                 int& iConnectNO = m_vNodeToRegion[iNode];
  53.                 if (iNode == iConnectNO)
  54.                 {
  55.                         return iNode;
  56.                 }
  57.                 return iConnectNO = GetConnectRegionIndex(iConnectNO);
  58.         }
  59.         void Union(int iNode1, int iNode2)
  60.         {
  61.                 const int iConnectNO1 = GetConnectRegionIndex(iNode1);
  62.                 const int iConnectNO2 = GetConnectRegionIndex(iNode2);
  63.                 if (iConnectNO1 == iConnectNO2)
  64.                 {
  65.                         return;
  66.                 }
  67.                 m_iConnetRegionCount--;
  68.                 if (iConnectNO1 > iConnectNO2)
  69.                 {
  70.                         UnionConnect(iConnectNO1, iConnectNO2);
  71.                 }
  72.                 else
  73.                 {
  74.                         UnionConnect(iConnectNO2, iConnectNO1);
  75.                 }
  76.         }
  77.         bool IsConnect(int iNode1, int iNode2)
  78.         {
  79.                 return GetConnectRegionIndex(iNode1) == GetConnectRegionIndex(iNode2);
  80.         }
  81.         int GetConnetRegionCount()const
  82.         {
  83.                 return m_iConnetRegionCount;
  84.         }
  85.         vector<int> GetNodeCountOfRegion()//各联通区域的节点数量
  86.         {
  87.                 const int iNodeSize = m_vNodeToRegion.size();
  88.                 vector<int> vRet(iNodeSize);
  89.                 for (int i = 0; i < iNodeSize; i++)
  90.                 {
  91.                         vRet[GetConnectRegionIndex(i)]++;
  92.                 }
  93.                 return vRet;
  94.         }
  95.         std::unordered_map<int, vector<int>> GetNodeOfRegion()
  96.         {
  97.                 std::unordered_map<int, vector<int>> ret;
  98.                 const int iNodeSize = m_vNodeToRegion.size();
  99.                 for (int i = 0; i < iNodeSize; i++)
  100.                 {
  101.                         ret[GetConnectRegionIndex(i)].emplace_back(i);
  102.                 }
  103.                 return ret;
  104.         }
  105. private:
  106.         void UnionConnect(int iFrom, int iTo)
  107.         {
  108.                 m_vNodeToRegion[iFrom] = iTo;
  109.         }
  110.         vector<int> m_vNodeToRegion;//各点所在联通区域的索引,本联通区域任意一点的索引,为了增加可理解性,用最小索引
  111.         int m_iConnetRegionCount;
  112. };
  113. class CParents
  114. {
  115. public:
  116.         CParents(vector<int>& vParent, const int iMaxLeve)
  117.         {       
  118.                 int iBitNum = 0;
  119.                 for (; (1 << iBitNum) < iMaxLeve; iBitNum++);
  120.                 const int n = vParent.size();
  121.                 m_vParents.assign(iBitNum+1, vector<int>(n, -1));
  122.                 m_vParents[0] = vParent;
  123.                 //树上倍增
  124.                 for (int i = 1; i < m_vParents.size(); i++)
  125.                 {
  126.                         for (int j = 0; j < n; j++)
  127.                         {
  128.                                 const int iPre = m_vParents[i - 1][j];
  129.                                 if (-1 != iPre)
  130.                                 {
  131.                                         m_vParents[i][j] = m_vParents[i - 1][iPre];
  132.                                 }
  133.                         }
  134.                 }
  135.         }
  136.         int GetParent(int iNode, int iLeve)const
  137.         {
  138.                 int iParent = iNode;
  139.                 for (int iBit = 0; iBit < m_vParents.size(); iBit++)
  140.                 {
  141.                         if (-1 == iParent)
  142.                         {
  143.                                 return iParent;
  144.                         }
  145.                         if (iLeve & (1 << iBit))
  146.                         {
  147.                                 iParent = m_vParents[iBit][iParent];
  148.                         }
  149.                 }
  150.                 return iParent;
  151.         }       
  152. protected:
  153.         vector<vector<int>> m_vParents;
  154. };
  155. class C2Parents : CParents
  156. {
  157. public:
  158.         C2Parents(vector<int>& vParent, const vector<int>& vLeve) :m_vLeve(vLeve)
  159.                 , CParents(vParent,*std::max_element(vLeve.begin(), vLeve.end()))
  160.         {               
  161.         }       
  162.         int GetPublicParent(int iNode1, int iNode2)const
  163.         {
  164.                 int leve0 = m_vLeve[iNode1];
  165.                 int leve1 = m_vLeve[iNode2];
  166.                 if (leve0 < leve1)
  167.                 {
  168.                         iNode2 = GetParent(iNode2, leve1 - leve0);
  169.                         leve1 = leve0;
  170.                 }
  171.                 else
  172.                 {
  173.                         iNode1 = GetParent(iNode1, leve0 - leve1);
  174.                         leve0 = leve1;
  175.                 }
  176.                 //二分查找
  177.                 int left = -1, r = leve0;
  178.                 while (r - left > 1)
  179.                 {
  180.                         const auto mid = left + (r - left) / 2;
  181.                         const int iParent0 = GetParent(iNode1, mid);
  182.                         const int iParent1 = GetParent(iNode2, mid);
  183.                         if (iParent0 == iParent1)
  184.                         {
  185.                                 r = mid;
  186.                         }
  187.                         else
  188.                         {
  189.                                 left = mid;
  190.                         }
  191.                 }
  192.                 return GetParent(iNode1, r);
  193.         }
  194. protected:
  195.         vector<vector<int>> m_vParents;
  196.         const vector<int> m_vLeve;
  197. };
  198. //割点
  199. class CCutPoint
  200. {
  201. public:
  202.         CCutPoint(const vector<vector<int>>& vNeiB) : m_iSize(vNeiB.size())
  203.         {
  204.                 m_vNodeToTime.assign(m_iSize, -1);
  205.                 m_vCutNewRegion.resize(m_iSize);
  206.                 for (int i = 0; i < m_iSize; i++)
  207.                 {
  208.                         if (-1 == m_vNodeToTime[i])
  209.                         {
  210.                                 m_vRegionFirstTime.emplace_back(m_iTime);
  211.                                 dfs(vNeiB, i, -1);
  212.                         }
  213.                 }       
  214.         }
  215.         int dfs(const vector<vector<int>>& vNeiB,const int cur, const int parent)
  216.         {
  217.                 int iMinTime = m_vNodeToTime[cur] = m_iTime++;
  218.                 int iRegionCount = (-1 != parent);//根连通区域数量
  219.                 for (const auto& next : vNeiB[cur])                {
  220.                         if (-1  != m_vNodeToTime[next])                        {
  221.                                 iMinTime = min(iMinTime, m_vNodeToTime[next]);
  222.                                 continue;
  223.                         }
  224.                         const int childMinTime = dfs(vNeiB, next, cur);
  225.                         iMinTime = min(iMinTime, childMinTime);
  226.                         if (childMinTime >= m_vNodeToTime[cur])                        {
  227.                                 iRegionCount++;
  228.                                 m_vCutNewRegion[cur].emplace_back(m_vNodeToTime[next], m_iTime);
  229.                         }
  230.                 }
  231.                 if (iRegionCount < 2)
  232.                 {
  233.                         m_vCutNewRegion[cur].clear();
  234.                 }
  235.                 return iMinTime;
  236.         }
  237.         const int m_iSize;
  238.         const vector<int>& Time()const { return m_vNodeToTime; }//各节点的时间戳
  239.         const vector<int>& RegionFirstTime()const { return m_vRegionFirstTime; }//各连通区域的最小时间戳
  240.         vector<bool> Cut()const {
  241.                 vector<bool> ret;
  242.                 for (int i = 0; i < m_iSize; i++)
  243.                 {
  244.                         ret.emplace_back(m_vCutNewRegion[i].size());
  245.                 }
  246.                 return ret; }//
  247.         const vector < vector<pair<int, int>>>& NewRegion()const { return m_vCutNewRegion; };
  248. protected:
  249.         vector<int> m_vNodeToTime;
  250.         vector<int> m_vRegionFirstTime;
  251.         vector < vector<pair<int, int>>> m_vCutNewRegion; //m_vCutNewRegion[c]如果存在[left,r) 表示割掉c后,时间戳[left,r)的节点会形成新区域
  252.         int m_iTime = 0;
  253. };
  254. class CConnectAfterCutPoint
  255. {
  256. public:
  257.         CConnectAfterCutPoint(const vector<vector<int>>& vNeiB) :m_ct(vNeiB)
  258.         {
  259.                 m_vTimeToNode.resize(m_ct.m_iSize);
  260.                 m_vNodeToRegion.resize(m_ct.m_iSize);
  261.                 for (int iNode = 0; iNode < m_ct.m_iSize; iNode++)
  262.                 {
  263.                         m_vTimeToNode[m_ct.Time()[iNode]] = iNode;
  264.                 }
  265.                 for (int iTime = 0,iRegion= 0; iTime < m_ct.m_iSize; iTime++)
  266.                 {
  267.                         if ((iRegion < m_ct.RegionFirstTime().size()) && (m_ct.RegionFirstTime()[iRegion] == iTime))
  268.                         {
  269.                                 iRegion++;
  270.                         }
  271.                         m_vNodeToRegion[m_vTimeToNode[iTime]] = (iRegion - 1);
  272.                 }
  273.         }
  274.         bool Connect(int src, int dest, int iCut)const
  275.         {
  276.                 if (m_vNodeToRegion[src] != m_vNodeToRegion[dest])
  277.                 {
  278.                         return false;//不在一个连通区域
  279.                 }
  280.                 if (0 == m_ct.NewRegion()[iCut].size())
  281.                 {//不是割点
  282.                         return true;
  283.                 }
  284.                 const int r1 = GetCutRegion(iCut, src);
  285.                 const int r2 = GetCutRegion(iCut, dest);
  286.                 return r1 == r2;
  287.         }
  288.         vector<vector<int>> GetSubRegionOfCut(const int iCut)const
  289.         {//删除iCut及和它相连的边后,iCut所在的区域会分成几个区域:父节点一个区域、各子节点                一个区域
  290.                         //父节点所在区域可能为空,如果iCut所在的连通区域只有一个节点,则返回一个没有节点的                        区域。
  291.                 const auto& v = m_ct.NewRegion()[iCut];
  292.                 vector<int> vParen;
  293.                 const int iRegion = m_vNodeToRegion[iCut];
  294.                 const int iEndTime = (iRegion + 1 == m_ct.RegionFirstTime().size()) ? m_ct.m_iSize : m_ct.RegionFirstTime()[iRegion+1];
  295.                 vector<vector<int>> vRet;       
  296.                 for (int iTime = m_ct.RegionFirstTime()[iRegion],j=-1; iTime < iEndTime; iTime++)
  297.                 {
  298.                         if (iCut == m_vTimeToNode[iTime])
  299.                         {
  300.                                 continue;
  301.                         }
  302.                         if ((j + 1 < v.size()) && (v[j + 1].first == iTime))
  303.                         {
  304.                                 j++;
  305.                                 vRet.emplace_back();
  306.                         }
  307.                         const int iNode = m_vTimeToNode[iTime];
  308.                         if ((-1 != j ) && (iTime >= v[j].first) && (iTime < v[j].second))
  309.                         {
  310.                                 vRet.back().emplace_back(iNode);
  311.                         }
  312.                         else
  313.                         {
  314.                                 vParen.emplace_back(iNode);
  315.                         }                       
  316.                 }
  317.                 vRet.emplace_back();
  318.                 vRet.back().swap(vParen);
  319.                 return vRet;
  320.         }       
  321. protected:
  322.         int GetCutRegion(int iCut, int iNode)const
  323.         {
  324.                 const auto& v = m_ct.NewRegion()[iCut];
  325.                 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; });
  326.                 if (v.begin() == it)
  327.                 {
  328.                         return v.size();
  329.                 }
  330.                 --it;
  331.                 return (it->second > m_ct.Time()[iNode]) ? (it - v.begin()) : v.size();
  332.         }
  333.         vector<int> m_vTimeToNode;
  334.         vector<int> m_vNodeToRegion;//各节点所在区域
  335.         const CCutPoint m_ct;
  336. };
  337. class Solution {
  338. public:
  339.         vector<int> countPairsOfConnectableServers(vector<vector<int>>& edges, int signalSpeed) {
  340.                 m_c = edges.size() + 1;
  341.                 m_vDisToRoot.resize(m_c);
  342.                 m_vParent.resize(m_c);
  343.                 m_vLeve.resize(m_c);
  344.                 auto neiBo = CNeiBo::Three(m_c, edges, false, 0);
  345.                 DFS(neiBo, 0, -1, 0, 0);
  346.                 C2Parents par(m_vParent, m_vLeve);
  347.                 auto neiBo2 = CNeiBo::Two(m_c, edges, false, 0);
  348.                 CConnectAfterCutPoint cut(neiBo2);
  349.                 vector<int> vRet(m_c);
  350.                 for (int c = 0; c < m_c; c++)
  351.                 {
  352.                         auto regs = cut.GetSubRegionOfCut(c);
  353.                         int left = 0;
  354.                         int& iRet = vRet[c];
  355.                         for (const auto& subRegion : regs)
  356.                         {
  357.                                 int cur = 0;
  358.                                 for (const auto& ab : subRegion)
  359.                                 {
  360.                                         const int pub = par.GetPublicParent(ab, c);
  361.                                         const int len = m_vDisToRoot[ab] + m_vDisToRoot[c] - 2 * m_vDisToRoot[pub];
  362.                                         if (0 != len % signalSpeed)
  363.                                         {
  364.                                                 continue;
  365.                                         }
  366.                                         cur++;
  367.                                 }
  368.                                 iRet += left * cur;
  369.                                 left += cur;
  370.                         }
  371.                 }
  372.                 return vRet;
  373.         }
  374.         void DFS(vector<vector<std::pair<int, int>>>& neiBo, int cur, int par, int leve, int dis)
  375.         {
  376.                 m_vDisToRoot[cur] = dis;
  377.                 m_vParent[cur] = par;
  378.                 m_vLeve[cur] = leve;
  379.                 for (const auto& [next, len] : neiBo[cur])
  380.                 {
  381.                         if (next == par)
  382.                         {
  383.                                 continue;
  384.                         }
  385.                         DFS(neiBo, next, cur, leve + 1, dis + len);
  386.                 }
  387.         }
  388.         vector<int> m_vDisToRoot, m_vParent, m_vLeve;
  389.         int m_c;
  390. };
复制代码
DFS取代树上倍增

时间复杂度的瓶颈在 树上倍增。可以直接DFS 最近公共先人。

扩展阅读

视频课程

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


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

曂沅仴駦

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表