• 支配树的背景

    • 流程图
    • 食物链
    • GC 场景
  • DAG 上求支配树
    • 树上倍增
    • 模板: P2597 灾难
  • 一般有向图上求支配树
    • Lengauer-Tarjan 算法
    • 模板: P5180 【模板】支配树

$0 背景

问题

给定有向图,起点 S,终点 T。若从 S 到 T 的每条路径都要经过某个点 u,则 u 是有向图从 S 到 T 的必经点:所有 S 到 T 的路径中点的交集。

若从 S 到 T 的每条路径都要经过某条边 e = u v e=uv e=uv,则 e 是有向图从 S 到 T 的必经边(桥):所有 S 到 T 的路径中边的交集。

这是比较难的问题,因为环上的点可能是必经点也可能不是必经点,所以把 SCC 缩点形成 DAG 这种有向图中常用的办法在这里不能用。

作为对比,无向图的必经点和必经边的问题相对简单

  • 无向图的割点
  • 无向图的桥

场景

分析内存泄漏问题时,可能用到一种叫支配树视图的工具。如果把对象之间的引用关系看做一张有向图(可以存在环)的话,对象的支配树体现了对象之间的支配关系。

如果所有指向对象B的路径都要经过对象A,则认为对象A支配对象B。如果对象A是离对象B最近的支配对象,则认为对象A是对象B的直接支配者。

支配树(Dominated Tree)的定义

对于有向图 D, 起点 S,终点 T,S -> 可能有很多条路径,在所有 S -> T 的路径中都必经的点,称为支配点

删了该点,将不再存在 S -> T 的路径,因此称起点为 S 时,该点支配了 T

S -> T 中可能有很多支配点,由这些支配点构成的树,称为支配树

支配树的性质

  • S 为树根
  • 根到树上任一点的路径经过的点均为根支配的点

任意子树也有上述性质

  • 子树的根到子树内任一点的路径经过的点为该子树的根支配的点

例子

有向图

支配树

通俗理解

对于一个流程图(单源点的有向图),除源点外都有每个点都有唯一的idom(直接支配者),且不成环,故所有的 (idom(w), w) 边形成一棵树,u 支配 v 当且仅当 u 是树中 v 的祖先。

GC 场景支配树的应用

在 GC 领域,可以用来求深堆的大小。GC 中深堆和浅堆的定义如下:

  • 浅堆: 表示一个对象结构所占用的内存大小
  • 深堆: 表示一个对象被 GC 回收后,可真实释放的内存大小

如果释放对象 A,则对象 A 对应的支配树上的子树都将被释放,因为子树上的对象都不可达了,应该被 GC 回收。所以支配树上某个对象节点的子树上所有对象的大小就是该对象的深堆大小

有向图求支配树的算法

对于 DAG: 拓扑排序 + 树上倍增

  • 拓扑排序
  • LCA

对于一般有向图: 支配树 / Lengauer-Tarjan

  • dfs 树
  • dfn 序
  • 带权并查集

$1 DAG 上求支配树

如果是 DAG,可以不用 Lengauer Targan。

算法

  • 原图 D 的基础上建立 RD
  • 将 D 进行拓扑排序,若 D 有多个入度为 0 的结点,可以用超级源
  • 按拓扑序遍历节点,当前为 u:
    • 在 RD 中,u 的祖先的支配点已经建立好
    • 找到 u 在 RD 中的所有父节点 fa,这些父节点的 lca 就是 u 的支配点 idom[u]。

最终得到的 DT 为答案

模板题 P2597 灾难

问题

用一种叫做食物网的有向图来描述生物之间的关系:

  • 一个食物网有 n 个点,代表 n 种生物,生物从 1 到 n 编号。
  • 如果生物 x 可以吃生物 y,那么从 y 向 x 连一个有向边。
  • 这个图没有环。
  • 图中有一些点没有连出边,这些点代表的生物都是生产者,可以通过光合作用来生存。
  • 而有连出边的点代表的都是消费者,它们必须通过吃其他生物来生存。
  • 如果某个消费者的所有食物都灭绝了,它会跟着灭绝。

我们定义一个生物在食物网中的“灾难值”为,如果它突然灭绝,那么会跟着一起灭绝的生物的种数。

分析

建图 D:当前点 -> 以当前点为食的点(一个点的入度越多,它可以选择的食物越多)

原图中存在一些没有入度的点(生产者,不依赖食物)

给定一个DAG,求如果删去一个点,有多少个点会被影响

如果删去某个点,对于任意一个原先有入度的点来说,如果因此没有入度,则该点死去:删去与该点相连的边,重复迭代直至所有点死去。

如果删去一个点后,某个点无法连向原图中无入度的点(超级源),则它死。

算法

建图 D 和 反图 RD
对 D 加超级源,对 RD 加超级汇,并求 D 的 indegrees
带着超级源求原图 D 的拓扑序 vector<int> topo_order;
初始化支配树 vector<vector<int>> DT(n + 1);

DAG 求支配树的过程

(1) 按原图的拓扑序枚举节点 u,找到 u 在 RD 中的所有父节点 fa,这些父节点的 lca 就是 u 的支配点 idom[u]。

int k = RD[u].size();
int idom = RD[u][0];
for(int j = 1; j < k; ++j)
{int fa = RD[u][j];idom = lca(idom, fa, d, f);
}

(2) DT 中连接一条边 idom -> u

DT[idom].push_back(u);

(3) DT 中 u 的祖先链在 f[u][k] 中要更新

f[u][0] = idom;
d[u] = d[idom] + 1;
int p = 1; // step = 2 ^ p
while(p <= m && f[f[u][p - 1]][p - 1] != -1)
{f[u][p] = f[f[u][p - 1]][p - 1];++p;
}

(1) 中有一步 lca 的过程,这一步就是树上倍增求 LCA 的模板,参考 LCA,树上倍增,树上差分

求出支配树 DT 后,对 DT 做 dfs 求各个节点的 size,size - 1 为当前节点的答案。

代码

#include <iostream>
#include <fstream>
#include <vector>
#include <queue>
#include <cmath>using namespace std;int lowbit(int n)
{return n & (-n);
}int highbit(int n)
{int p = lowbit(n);while(p != n){n -= p;p = lowbit(n);}return p;
}int lca(int x, int y, const vector<int>& d, const vector<vector<int>>& f)
{// d[x] >= d[y]if(d[x] < d[y])return lca(y, x, d, f);// 将 y 向上调整直到和 x 一个深度int delta = d[x] - d[y];while(delta > 0){x = f[x][log2(highbit(delta))];delta -= highbit(delta);}if(x == y)return x;int n = d.size();int m = log2(n);while(true){if(f[x][0] == f[y][0])break;int k = 1;while(k <= m){if(f[x][k] == -1 || f[y][k] == -1)break;if(f[x][k] == f[y][k])break;++k;}x = f[x][k - 1];y = f[y][k - 1];}return f[x][0];
}int dfs(const vector<vector<int>>& DT, int u, vector<int>& result)
{int size = 1;for(int v: DT[u])size += dfs(DT, v, result);result[u] = size - 1;return size;
}int main()
{int n;cin >> n;vector<vector<int>> D(n + 1);vector<vector<int>> RD(n + 1);// 建图for(int i = 1; i <= n; ++i){int v;while(cin >> v && v > 0){D[v].push_back(i);RD[i].push_back(v);}}// 超级源 : 0vector<int> indegree(n + 1);for(int i = 1; i <= n; ++i){if(RD[i].empty()){RD[i].push_back(0);D[0].push_back(i);}indegree[i] = RD[i].size();}// 拓扑排序vector<int> topo_order;queue<int> q;q.push(0); // 超级源while(!q.empty()){int u = q.front();q.pop();topo_order.push_back(u);for(int v: D[u]){--indegree[v];if(indegree[v] == 0)q.push(v);}}vector<vector<int>> DT(n + 1);// 2 ^ m <= n// log2(2^m) <= log2(n)// m <= log2(n)int m = log2(n);vector<vector<int>> f(n + 1, vector<int>(m + 1, -1));// f[u][k] := u 的第 2^k 个祖先vector<int> d(n + 1, -1);// d[u] := u 在 DT 中的深度,超级源为 0d[0] = 0;for(int u: topo_order){if(RD[u].empty()) // DT 的树根是超级源continue;int k = RD[u].size();int idom = RD[u][0];for(int j = 1; j < k; ++j){int fa = RD[u][j];idom = lca(idom, fa, d, f);}DT[idom].push_back(u);// DT 中连接一条边 idom -> u// u 的祖先链在 f 中要更新f[u][0] = idom;d[u] = d[idom] + 1;int p = 1; // step = 2 ^ pwhile(p <= m && f[f[u][p - 1]][p - 1] != -1){f[u][p] = f[f[u][p - 1]][p - 1];++p;}}vector<int> result(n + 1);// result[i] := DT 中 i 的 size - 1dfs(DT, 0, result);for(int u = 1; u <= n; ++u)cout << result[u] << endl;
}

$2 一般有向图上求支配树

  • 参考1:快速构造支配树的Lengauer-Tarjan算法
  • 参考2:《A Fast Algorithm for Finding Dominators in a Flowgraph》

算法推导

dfn[u] := u 的 dfn 序
sdom[u] := x 的半支配点
idom[u] := x 的最近支配点

半支配点

定义

对于 u,存在 v,可以通过一些点 v i v_{i} vi​ (不含 u, v),到达 u ( v → v i → u v \rightarrow v_{i} \rightarrow u v→vi​→u),且 ∀ i \forall i ∀i, d f n [ v i ] > d f n [ u ] dfn[v_{i}] > dfn[u] dfn[vi​]>dfn[u],对于所有满足条件的 v,dfn[v] 最小的 v 称为半支配点,记 sdom[u] = v

性质

  1. 半支配点唯一(支配点可以有多个)
  2. 对任意 u,其半支配点必为其在 dfs 树上的祖先
  3. 对任意 u ( u ≠ S u \neq S u​=S),其支配点必为 u 的半支配点的祖先节点
  4. 若 u 为 v 的祖先,则 idom[v] 是 u 的后代或者是 idom[u] 的祖先

sdom[u] 到 u 的路径,相当于 dfs 树外有一条路,并且 sdom[u] 为离根最近的那个点。

求法

依赖定理及其证明,有点难,下面直接记录结论,证明参考前面的两个链接。

对于 u,枚举 ( v , u ) ∈ E (v, u) \in E (v,u)∈E (反图)

  • dfn[u] > dfn[v] : sdom[u] = min(v)
  • dfn[u] < dfn[v] : sdom = min(sdom[k]), k 为 dfn[k] > dfn[u] 且 dfs 树中 k 能到 v

按照 dfn 序从大到小枚举 u,则考查 u 时,sdom[k] 已经有了

第二种情况,怎样高效找到 k,sdom[k] 的最小值, u 的祖先链上 sdom 的最小值,用并查集。

最近支配点

定义

若图中存在 u, v,且 v 支配 u,且 u 的其它支配点均支配 v

则 v 是 u 的最近支配点,记为 idom[u] = v

性质及求法

依赖定理及其证明,有点难,下面直接记录结论,证明参考前面的两个链接。

对于 u(非源点),k 为满足以下条件的点 v 中 sdom[v] 最小的一个

  • dfs 树中存在以下路径:sdom[u] -> v -> u
  • 等价条件:
    • dfn[v] > dfn[u]
    • v 在 u 的子树中, 即 v 沿着父亲链能到 u
idom[u] = sdom[u]  (sdom[u] = sdom[k])
idom[u] = idom[k]  (sdom[k] < sdom[u])

高效求 sdom[u]

参考之前【半支配点-求法】

对于任意 u(非源点r):

KaTeX parse error: No such environment: align at position 8: \begin{̲a̲l̲i̲g̲n̲}̲ sdom[u] = &min…

算法 (Lengauer Tarjan)

算法流程

  1. 初始化、跑一遍 DFS 得到 DFS 树和标号
  2. 按 dfn 序从大到小利用【高效求 sdom[u]】求出 sdom
  3. 按照【最近支配点-性质及求法】求出所有能确定的 idom,剩下的点记录下和哪个点的 idom 是相同的
  4. 按照 dfn 序从小到大再跑一次,得到所有点的 idom

实现

要维护的数据:

dfn_id(dfnid): dfn 序为 dfnid 的点
id_dfn(u): 节点 u 的 dfn 序
fa[u]: u 在 dfs 树上的父节点
sdom[u]: u 的 sdom 的 dfs 序, 初始化为 u
idom[u]: u 的 idom 节点
RD[u]: 有边直接连接到 u 的点集
sdom_set[u]: sdom 对应的节点为 u 的点集

算法流程中的第 2,3 步可以一起做。

通过一个数据结构维护一个森林,支持加入一条 dfs 树的边 add(u, v) 和查询点 u 到根的路径上的点 v 的 sdom 的最小值对应的节点,记为 k, k = query(u)

求 u 的 sdom 只需要对它的所有直接前驱 query 一次,对每个前驱 v:k = query(v) sdom 取最小值时的节点 k, sdom[k] 为该前驱提供的候选。

参照【高效求 sdom[u]】中的流程:

  • 第一类点编号比它小,它们还没有处理过,所以自己就是根,query(v) 就能取得它们的值
  • 对于第二类点,query(v) 查询的就是满足 dfs 树中 v 在 k 的子树中的 k 的 sdom[k] 的最小值对应的节点。和【高效求 sdom[u]】中是一致的。

然后把该点 u 加入它的 sdom 节点对应的 set 里,在 dfs 树中连上 u 与父节点 fa[u] 的边。
现在 u 的父节点到 u 的这棵子树中已经处理完了,所以可以对父亲的 set 里的每个点求一次 idom 并且清空 set。

对于 set 里的每个点 v,求出 query(v),此时 dfs 树中 fa[u] -> query(v) -> v,于是直接按照【最近支配点-性质及求法】:

  • 如果 sdom[query(v)] == sdom[v],则 idom[v] = sdom[v] = fa[u]
  • 否则可以记下 idom[v] = idom[query[v]],实现时可以暂时先写 idom[v] = query[v],留到第 4 步处理。

最后从小到大扫一遍完成第4步,对于每个 u:

  • idom[u] == sdom[u] 的话,就已经是第3步求出的正确的 idom 了
  • 否则这是第 3 步留下的待处理点,令 idom[u] = idom[idom[u]] 即可。

对于这个数据结构,可以选择并查集。不过因为需要查询到根路径上的信息,所以不方便写按秩合并,但是仍然可以路径压缩,压缩时保留路径上的最值就可以了,只有路径压缩的并查集的复杂度是 O ( l o g N ) O(logN) O(logN)。

原论文还提到了一个实现方法,能够把这个并查集优化到 α \alpha α 的复杂度,即原文《A Fast Algorithm for Finding Dominators in a Flowgraph》里面的参考文献 14 – Tarjan 的另一篇文章《Applications of Path Compression on Balanced Trees》。

例子

原图与对应的 dfs 树,dfn 序


模板题: P5180 【模板】支配树

问题

给定一张有向图,求从1号点出发,每个点能支配的点的个数(包括自己)

n <= 2e5
m <= 3e5

代码

代码注释中说明了维护的变量的含义,与前面的算法中对照。

#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>using namespace std;class UnionFindSet
{public:UnionFindSet(int N){_father = vector<int>(N + 1);weight = vector<int>(N + 1);for(int i = 1; i <= N; ++i){_father[i] = i;weight[i] = i;}}int query(int u, const vector<int>& sdom){// 返回 u -> 根的路径上的节点 v 中 sdom 最小值对应的节点_find(u, sdom);return weight[u];}void add(int u, int v){// 加边 u -> v(fa[u])_father[u] = v;}private:vector<int> _father, weight;int _find(int u, const vector<int>& sdom){// 返回根,用于路径压缩if(_father[u] == u)return u;int root = _find(_father[u], sdom);// 路径压缩 _father[u] = root 之前做以下比较和更新if(sdom[weight[_father[u]]] < sdom[weight[u]])weight[u] = weight[_father[u]];return _father[u] = root;}
};void dfs(const vector<vector<int>>& D, int u, int& dfnid, vector<int>& id_dfn, vector<int>& dfn_id, vector<int>& fa, vector<int>& sdom)
{id_dfn[u] = ++dfnid;dfn_id[dfnid] = u;sdom[u] = id_dfn[u];for(int v: D[u])if(!id_dfn[v]) // v 已经访问过{dfs(D, v, dfnid, id_dfn, dfn_id, fa, sdom);fa[v] = u;}
}int dfs2(const vector<vector<int>>& DT, int u, vector<int>& cnts)
{int ans = 1;for(int v: DT[u]){if(u != v)ans += dfs2(DT, v, cnts);}return cnts[u] = ans;
}vector<vector<int>> dominated_tree(const vector<vector<int>>& D, const vector<vector<int>>& RD, const int S, const int N)
{int dfnid = 0;// dfn_id[dfnid] := dfn 序为 dfnid 的节点// id_dfn[u] = 节点 u 的 dfn 序;// fa[u] := dfs 树中 u 的父节点vector<int> id_dfn(N + 1), dfn_id(N + 1), fa(N + 1);// sdom[u] := u 的 sdom 的 dfn 序// idom[u] := u 的 idom 的节点编号vector<int> idom(N + 1), sdom(N + 1);// 预处理 dfn_id, id_dfn, fa, sdomdfs(D, S, dfnid, id_dfn, dfn_id, fa, sdom);UnionFindSet forest(N);vector<vector<int>> sdom_set(N + 1);// sdom_set[u] := sdom 为 id_dfn[u] 的点集// 求 sdom[u], 顺便求 sdom_set[fa[u]] 中的 v 的 idom[v]for(int dfn = dfnid; dfn >= 2; --dfn){int u = dfn_id[dfn];// 求 u 的 sdom 只需要对它的所有直接前驱 query 一次// 求得前驱中的 sdom 最小值即可for(int v : RD[u]){// id_dfn[u] > id_dfn[v] 时//   v 尚未访问到,所以它自己就是根, query 的结果就是它自己// id_dfn[u] < id_dfn[v] 时//   sdom[u] = min(sdom[k]) 其中 dfn[k] > dfn[u] 且 k 能到 u//     即 u -> v -> ... 的祖先链中 sdom 的最小值//   query(v) 求的正是 u -> v -> ... dfs 树上已经连边的 v 的根节点//     这条链上的 sdom 最小值对应的节点if(id_dfn[v]){sdom[u] = min(sdom[u], sdom[forest.query(v, sdom)]);}}sdom_set[dfn_id[sdom[u]]].push_back(u);int fp = fa[u];// 给森林加一条 dfs 树上的边forest.add(u, fa[u]);// 现在 fa[u] -> u 的这棵子树中已经处理完了// sdom 为 id_dfn[fa[u]] 的节点 v 可以处理了for(int v : sdom_set[fp]){int k = forest.query(v, sdom);// 此时 dfs 树中 fa[u] -> k -> vif(sdom[k] == sdom[v]){// sdom[v] 也就是 fa[u] 的 dfn 序idom[v] = fp;}else{// 按公式是 idom[v] = idom[k]// 先记下 idom[v] = k,之后集中处理idom[v] = k;}}sdom_set[fp].clear();}// 之后集中处理 idom[v] = idom[k] 的这些点for(int dfn = 2; dfn <= dfnid; ++dfn){int u = dfn_id[dfn];if(idom[u] == dfn_id[sdom[u]])idom[u] = idom[u];elseidom[u] = idom[idom[u]];}// 由 idom[u] 建 DTvector<vector<int>> DT(N + 1);for(int u = 1; u <= N; ++u)DT[idom[u]].push_back(u);return DT;
}int main()
{int n, m;cin >> n >> m;// 建图vector<vector<int>> D(n + 1), RD(n + 1);for(int i = 1; i <= m; ++i){int u, v;cin >> u >> v;D[u].push_back(v);RD[v].push_back(u);}// 源点,这里给了源点,不用设超级源int S = 1;// 给定图 D,反图 RD,源点 S,求支配树vector<vector<int>> DT = dominated_tree(D, RD, S, n);vector<int> cnts(n + 1);dfs2(DT, S, cnts);for(int u = 1; u <= n; ++u)cout << cnts[u] << " ";cout << endl;
}

$4 论文原文

A Fast Algorithm for Finding Dominators in a Flowgraph

有向图的必经点,支配树相关推荐

  1. 【学习笔记】DAG / 一般有向图的支配树 / 灭绝树

    定义与声明 一个有向图 GGG.给定一个起点 sss,假设 sss 能到达所有点. 若去掉某个点 iii 后,sss 无法到达 jjj,则称 iii 为 jjj 的支配点. 显然支配点存在传递关系. ...

  2. hihocoder #1343 : Stable Members(支配树)

    时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 Recently Little Hi joined an algorithm learning group. The gr ...

  3. 【学习小记】支配树【图论】

    Preface 给定一个有向图和一个起点 s t st st,我们需要知道起点到某个点的关于必经点的信息. 若起点到点v的所有路径均经过点u,则我们称点u支配点v,显然一个点支配自己本身 顾名思义,支 ...

  4. 控制流分析之构建支配树

    控制流分析之构建支配树 引言 1 分析有向图 2 构建支配树 2.1 求最小半支配点 2.2 求最近支配点 引言 如上一个带有起始入口点的有向图为例,从A到Q的必经结点有A.L.M.Q,我们称其为Q的 ...

  5. [支配树][lca][倍增][线段树][拓扑] Jzoj P4240 游行

    Description 恶梦是学校里面的学生会主席.他今天非常的兴奋,因为学校一年一度的学生节开始啦!! 在这次节日上总共有N个节目,并且总共也有N个舞台供大家表演.其中第i个节目的表演时间为第i个单 ...

  6. P5180-[模板]支配树

    正题 题目链接:https://www.luogu.com.cn/problem/P5180 题目大意 给出nnn个点的一张有向图,求每个点支配的点数量. 1≤n≤2×105,1≤m≤3×1051\l ...

  7. 支配树(洛谷-P5180)

    题目描述 给定一张有向图,求从1号点出发,每个点能支配的点的个数(包括自己) 输入输出格式 输入格式: 第一行两个正整数n,mn,m,表示点数和边数 接下来mm行,每行输入两个整数u,vu,v,表示有 ...

  8. Lengauer-Tarjan算法--支配树构造(bzoj 2815: [ZJOI2012]灾难)

    模型: 一个有向图G,设定一个点r,要求点r能到达G中所有的点,如果这样的点不存在,新建并向所有入度为0的点连边 支配点: 对于点u,如果在删掉点p之后,r不能到达u,那么称p(p!=u)点是u点的一 ...

  9. 支配树与Lengauer-Tarjan算法

    伪目录 给出支配树的定义 给出一些性质 介绍快速构造支配树的Lengauer-Tarjan算法及具体实现 支配树是啥 一个有源点的有向图,其支配树是满足下面条件的一个有向图: 对于支配树上一点,若断开 ...

最新文章

  1. Linux 查看 80 端口的占用情况
  2. JavaScript绑定键盘事件的多种写法
  3. Spring Boot 使用 AOP 防止重复提交
  4. Navicat for MySQL 连接 Mysql 8.0.11 出现1251- Client does not support authentication protocol
  5. Codeforces Round #632 (Div. 2) F. Kate and imperfection 数论 + 贪心
  6. TextBox禁止手动输入但是允许刷卡输入
  7. sql中exec是什么意思_SQL 中为什么经常要加NOLOCK?
  8. mysql java 问题_【Java】连接MySQL问题总结
  9. TortoiseSVN 使用详细步骤(三):安装
  10. 纯php实现中秋博饼游戏(2):掷骰子并输出结果
  11. 【社招】量化研究员(机器学习)-Akuna Capital -上海
  12. Arduino入门教程--连载
  13. <<计算机视觉CVPR>>2022:Grounded Language-Image Pre-training
  14. 客户端负载均衡Ribbon
  15. 计算机属性资源管理器已停止工作,经常出现“资源管理器已停止工作”怎么办的完美解决办法...
  16. python之父考虑重构python解释器_Python之父考虑重构Python解释器
  17. 软件测试1——PIE模型
  18. java毕业设计中山乡村文化旅游网络平台Mybatis+系统+数据库+调试部署
  19. 谷歌深度神经网络_本周关注我们:轻松阅读,神经网络和Google召集不良网站
  20. 【转发】日访问量百亿级的微博如何做缓存架构设计

热门文章

  1. 939. 最小面积矩形
  2. java必学之Redis中的数据结构
  3. 容器云负载均衡之一:容器云平台负载均衡解决方案的一些思考
  4. Kibana should not be run as root
  5. Android音频系统学习一:基本概念
  6. c语言入门----详解分支语句(switch语句)
  7. 强化学习 DQN Pytorch
  8. java-php-python-ssm学生宿舍管理系统计算机毕业设计
  9. 服务器ios文件夹是否存在,ios – 如何确定iCloud文件夹中是否存在文件?
  10. Android英语学习词典