<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>OopsYanxi</title><description>Blog Site</description><link>https://fuwari.vercel.app/</link><language>zh_CN</language><item><title>EBCC（边双连通分量）</title><link>https://fuwari.vercel.app/posts/ai/graph/ebcc-%E8%BE%B9%E5%8F%8C%E8%BF%9E%E9%80%9A%E5%88%86%E9%87%8F/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/ai/graph/ebcc-%E8%BE%B9%E5%8F%8C%E8%BF%9E%E9%80%9A%E5%88%86%E9%87%8F/</guid><description>EBCC的相关笔记</description><pubDate>Sat, 18 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;EBCC（边双连通分量）&lt;/h1&gt;
&lt;h2&gt;1. 定义&lt;/h2&gt;
&lt;p&gt;边双连通分量（Edge Biconnected Component, e-BCC）是无向图中的一个极大子图，满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;删掉子图内任意一条边后，这个子图仍然连通。&lt;/li&gt;
&lt;li&gt;等价地说，子图内不存在桥。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见等价结论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;两个点属于同一个 e-BCC，当且仅当它们之间存在至少两条边不相交路径。&lt;/li&gt;
&lt;li&gt;把整张图里的所有桥删掉，剩下的每个连通块就是一个 e-BCC。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 这个模板能做什么&lt;/h2&gt;
&lt;p&gt;这份模板完成了 3 件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;求无向图中的所有桥。&lt;/li&gt;
&lt;li&gt;求所有边双连通分量。&lt;/li&gt;
&lt;li&gt;把每个 e-BCC 缩成一个点，得到一棵树或森林。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;复杂度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：&lt;code&gt;O(V + E)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;空间复杂度：&lt;code&gt;O(V + E)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 核心性质&lt;/h2&gt;
&lt;h3&gt;3.1 桥的判定&lt;/h3&gt;
&lt;p&gt;设 &lt;code&gt;dfn[u]&lt;/code&gt; 表示 &lt;code&gt;u&lt;/code&gt; 被 DFS 首次访问的时间戳，&lt;code&gt;low[u]&lt;/code&gt; 表示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 &lt;code&gt;u&lt;/code&gt; 出发，&lt;/li&gt;
&lt;li&gt;只走 DFS 树边向下，&lt;/li&gt;
&lt;li&gt;最多经过一条返祖边，&lt;/li&gt;
&lt;li&gt;能回到的最早祖先时间戳。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;若 DFS 树边 &lt;code&gt;u -&amp;gt; v&lt;/code&gt; 满足：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;low[v] &amp;gt; dfn[u]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则这条边是桥。&lt;/p&gt;
&lt;p&gt;含义是：&lt;code&gt;v&lt;/code&gt; 这棵子树无论怎么绕，都回不到 &lt;code&gt;u&lt;/code&gt; 或 &lt;code&gt;u&lt;/code&gt; 的祖先，所以 &lt;code&gt;u-v&lt;/code&gt; 一断，图就被分开了。&lt;/p&gt;
&lt;h3&gt;3.2 缩点后的图一定无环&lt;/h3&gt;
&lt;p&gt;把每个 e-BCC 缩成一个点后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原图中的桥，变成新图中的边。&lt;/li&gt;
&lt;li&gt;新图一定是一棵树或森林。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原因很直接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果缩点图里还有环，那么环上的边都不是桥。&lt;/li&gt;
&lt;li&gt;这和“缩点图中的边全部来自原图桥”矛盾。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 为什么无向图找桥必须用边编号&lt;/h2&gt;
&lt;p&gt;这是这类题最容易写错的地方。&lt;/p&gt;
&lt;p&gt;在无向图里，一条无向边会拆成两条方向相反的邻接记录。如果你只用“父节点编号”判断回边：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (v == parent) continue;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么遇到重边时会直接出错。&lt;/p&gt;
&lt;h3&gt;反例：两个点之间有两条边&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1 == 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果从 &lt;code&gt;1&lt;/code&gt; 走到 &lt;code&gt;2&lt;/code&gt; 用的是第一条边，那么从 &lt;code&gt;2&lt;/code&gt; 回看 &lt;code&gt;1&lt;/code&gt; 时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一条边确实是“原路返回”，应该跳过。&lt;/li&gt;
&lt;li&gt;第二条边其实是一条合法返祖边，必须参与更新 &lt;code&gt;low&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但如果你写的是 &lt;code&gt;if (v == parent) continue;&lt;/code&gt;，这两条边都会被跳过，结果会把本来不是桥的边误判成桥。&lt;/p&gt;
&lt;p&gt;所以正确做法是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给每条无向边一个唯一的 &lt;code&gt;edge_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;DFS 时记录“进入当前点所使用的那条边编号”&lt;/li&gt;
&lt;li&gt;只跳过这同一条边，而不是跳过父节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;模板里的关键写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (id == in_edge_id) continue;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是支持重边的标准写法。&lt;/p&gt;
&lt;h2&gt;5. 模板字段说明&lt;/h2&gt;
&lt;h3&gt;5.1 边结构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct Edge {
    int to;
    int id;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;to&lt;/code&gt;：边的终点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;：无向边编号，同一条无向边的两个方向共用同一个 &lt;code&gt;id&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 关键数组&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;graph[u]&lt;/code&gt;：邻接表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dfn[u]&lt;/code&gt;：DFS 访问序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;low[u]&lt;/code&gt;：追溯值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stk&lt;/code&gt;：当前 DFS 栈中尚未归属分量的点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_bridge[id]&lt;/code&gt;：第 &lt;code&gt;id&lt;/code&gt; 条无向边是否为桥&lt;/li&gt;
&lt;li&gt;&lt;code&gt;edccs&lt;/code&gt;：每个 e-BCC 内包含哪些点&lt;/li&gt;
&lt;li&gt;&lt;code&gt;edcc_id[u]&lt;/code&gt;：点 &lt;code&gt;u&lt;/code&gt; 属于哪个 e-BCC&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6. 模板流程&lt;/h2&gt;
&lt;h3&gt;6.1 建图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void addEdge(int u, int v) {
    graph[u].push_back({v, edge_counter});
    graph[v].push_back({u, edge_counter});
    is_bridge.push_back(0);
    edge_counter++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无向边 &lt;code&gt;(u, v)&lt;/code&gt; 会被拆成两条邻接边，但共享同一个 &lt;code&gt;edge_id&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;6.2 主过程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void build() {
    for (int i = 0; i &amp;lt; n; i++) {
        if (!dfn[i]) {
            tarjan(i, -1);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为原图可能不连通，所以要从每个未访问点出发。&lt;/p&gt;
&lt;h3&gt;6.3 Tarjan 过程&lt;/h3&gt;
&lt;p&gt;核心逻辑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;dfn[u] = low[u] = ++timer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;u&lt;/code&gt; 入栈&lt;/li&gt;
&lt;li&gt;枚举所有邻边&lt;/li&gt;
&lt;li&gt;遇到未访问点就递归&lt;/li&gt;
&lt;li&gt;回溯时更新 &lt;code&gt;low&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;若 &lt;code&gt;low[v] &amp;gt; dfn[u]&lt;/code&gt;，则 &lt;code&gt;u-v&lt;/code&gt; 是桥&lt;/li&gt;
&lt;li&gt;若 &lt;code&gt;dfn[u] == low[u]&lt;/code&gt;，说明当前形成一个 e-BCC&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对应代码里的关键部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!dfn[v]) {
    tarjan(v, id);
    low[u] = std::min(low[u], low[v]);
    if (low[v] &amp;gt; dfn[u]) {
        is_bridge[id] = 1;
    }
} else {
    low[u] = std::min(low[u], dfn[v]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里返祖边更新必须是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;low[u] = min(low[u], dfn[v]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不能写成 &lt;code&gt;low[v]&lt;/code&gt;。这是 Tarjan 的标准写法。&lt;/p&gt;
&lt;h2&gt;7. 为什么 &lt;code&gt;dfn[u] == low[u]&lt;/code&gt; 时可以弹出一个 e-BCC&lt;/h2&gt;
&lt;p&gt;在这份模板里，栈中存的是“点”。&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;dfn[u] == low[u]&lt;/code&gt; 时，说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 &lt;code&gt;u&lt;/code&gt; 往下这一整段 DFS 子树，&lt;/li&gt;
&lt;li&gt;已经无法通过返祖边再连到 &lt;code&gt;u&lt;/code&gt; 上方的点，&lt;/li&gt;
&lt;li&gt;所以从栈顶到 &lt;code&gt;u&lt;/code&gt; 的这些点恰好构成一个完整的边双连通分量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (dfn[u] == low[u]) {
    std::vector&amp;lt;int&amp;gt; current_edcc;
    int node;
    do {
        node = stk.back();
        stk.pop_back();
        current_edcc.push_back(node);
        edcc_id[node] = edccs.size();
    } while (node != u);
    edccs.push_back(current_edcc);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;8. 缩点图怎么建&lt;/h2&gt;
&lt;p&gt;模板里的做法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (edcc_id[u] != edcc_id[v]) {
    tree[edcc_id[u]].push_back(edcc_id[v]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若原图一条边的两个端点属于不同 e-BCC&lt;/li&gt;
&lt;li&gt;说明它必然是一条桥&lt;/li&gt;
&lt;li&gt;那么它就在缩点图中连接这两个分量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这里得到的是无向图邻接表，所以一条桥会在两个方向各记录一次。&lt;/li&gt;
&lt;li&gt;如果你统计缩点图边数，通常要记得除以 &lt;code&gt;2&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;9. 使用方式&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;int n = 5;
EBCC solver(n);

solver.addEdge(0, 1);
solver.addEdge(1, 2);
solver.addEdge(2, 0);
solver.addEdge(1, 3);
solver.addEdge(3, 4);

solver.build();

// 1. 判断桥
for (int id = 0; id &amp;lt; solver.edge_counter; id++) {
    if (solver.is_bridge[id]) {
        // id 是桥
    }
}

// 2. 查看所有 e-BCC
for (auto &amp;amp;comp : solver.edccs) {
    // comp 是一个边双连通分量中的所有点
}

// 3. 查看点属于哪个 e-BCC
int cid = solver.edcc_id[3];

// 4. 建缩点图
auto tree = solver.buildShrunkenGraph();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;10. 例子理解&lt;/h2&gt;
&lt;h3&gt;10.1 图结构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;0 - 1 - 3 - 4
 \ /
  2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0-1-2-0&lt;/code&gt; 构成一个环，不存在桥&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1-3&lt;/code&gt; 是桥&lt;/li&gt;
&lt;li&gt;&lt;code&gt;3-4&lt;/code&gt; 是桥&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此 e-BCC 为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{0, 1, 2}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{3}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{4}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缩点后得到一条链：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{0,1,2} - {3} - {4}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;11. 常见坑&lt;/h2&gt;
&lt;h3&gt;11.1 用父节点判回边&lt;/h3&gt;
&lt;p&gt;错误写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (v == parent) continue;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会在重边图上出错。&lt;/p&gt;
&lt;h3&gt;11.2 返祖边用 &lt;code&gt;low[v]&lt;/code&gt; 更新&lt;/h3&gt;
&lt;p&gt;错误写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;low[u] = min(low[u], low[v]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返祖边必须用 &lt;code&gt;dfn[v]&lt;/code&gt; 更新。&lt;/p&gt;
&lt;h3&gt;11.3 忘记处理非连通图&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;build()&lt;/code&gt; 必须从每个未访问点都启动一次 DFS。&lt;/p&gt;
&lt;h3&gt;11.4 把边双和点双混了&lt;/h3&gt;
&lt;p&gt;边双连通分量看的是“删边后是否断开”，核心对象是桥。&lt;br /&gt;
点双连通分量看的是“删点后是否断开”，核心对象是割点。&lt;/p&gt;
&lt;p&gt;两者不是一个东西，模板也不同。&lt;/p&gt;
&lt;h2&gt;12. 适用题型&lt;/h2&gt;
&lt;p&gt;这份模板常用于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;求桥的数量、桥的列表&lt;/li&gt;
&lt;li&gt;求每个点属于哪个边双连通分量&lt;/li&gt;
&lt;li&gt;缩点后在树上做 DP&lt;/li&gt;
&lt;li&gt;统计删去某条边后的连通性变化&lt;/li&gt;
&lt;li&gt;处理“至少两条边不相交路径”的判定类问题&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;13. 记忆版总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;e-BCC = 删掉所有桥之后剩下的连通块&lt;/li&gt;
&lt;li&gt;无向图找桥：&lt;code&gt;low[v] &amp;gt; dfn[u]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;重边防错：跳过的是 &lt;code&gt;in_edge_id&lt;/code&gt;，不是父节点&lt;/li&gt;
&lt;li&gt;缩点之后得到树 / 森林&lt;/li&gt;
&lt;li&gt;这份模板一次 DFS 同时求桥、e-BCC、缩点映射&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>虚树（Virtual Tree / Auxiliary Tree）</title><link>https://fuwari.vercel.app/posts/ai/graph/%E8%99%9A%E6%A0%91-virtualtree/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/ai/graph/%E8%99%9A%E6%A0%91-virtualtree/</guid><description>虚树的相关笔记</description><pubDate>Sat, 18 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;虚树（Virtual Tree / Auxiliary Tree）&lt;/h1&gt;
&lt;h2&gt;1. 定义&lt;/h2&gt;
&lt;p&gt;虚树用于处理“树上多次查询，每次只关心少量关键点”的问题。&lt;/p&gt;
&lt;p&gt;给定原树上的一组关键点 &lt;code&gt;key_nodes&lt;/code&gt;，虚树会：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保留所有关键点&lt;/li&gt;
&lt;li&gt;补上必要的 LCA&lt;/li&gt;
&lt;li&gt;只保留这些点之间的祖先关系&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终得到一棵规模远小于原树的“浓缩树”。&lt;/p&gt;
&lt;p&gt;它的意义是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原来一次查询可能要扫整棵树，复杂度接近 &lt;code&gt;O(N)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;用虚树后，只需要在与关键点相关的极少数节点上做 DP / 统计&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 适用场景&lt;/h2&gt;
&lt;p&gt;典型特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原图是一棵树&lt;/li&gt;
&lt;li&gt;有很多组询问&lt;/li&gt;
&lt;li&gt;每次询问只给出 &lt;code&gt;K&lt;/code&gt; 个关键点&lt;/li&gt;
&lt;li&gt;需要在这些关键点之间做路径、覆盖、DP、贡献统计&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见题型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;树上关键点最短连通子图&lt;/li&gt;
&lt;li&gt;树上路径覆盖 / 染色 / 切断类 DP&lt;/li&gt;
&lt;li&gt;一组点之间的距离和、边权贡献&lt;/li&gt;
&lt;li&gt;每次只对少量特殊点做转移的树形 DP&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 核心性质&lt;/h2&gt;
&lt;h3&gt;3.1 虚树只保留“必要节点”&lt;/h3&gt;
&lt;p&gt;虚树中的点只包含两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原询问给出的关键点&lt;/li&gt;
&lt;li&gt;为了保持树结构而补上的 LCA&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此虚树规模通常很小。&lt;/p&gt;
&lt;h3&gt;3.2 点数上界是 &lt;code&gt;2K - 1&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;设去重后的关键点数量为 &lt;code&gt;K&lt;/code&gt;，则虚树中的节点总数不超过：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2K - 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每加入一个新的关键点，最多只会额外引入一个新的分叉 LCA&lt;/li&gt;
&lt;li&gt;所以补点数量不会超过 &lt;code&gt;K - 1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 虚树保留祖先关系&lt;/h3&gt;
&lt;p&gt;虚树上的父子关系，就是原树中的祖先关系压缩后得到的关系。&lt;/p&gt;
&lt;p&gt;因此：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以直接在虚树上做树形 DP&lt;/li&gt;
&lt;li&gt;如果需要边权或路径长度，可以用原树信息补回来&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如原树有 &lt;code&gt;dist[u]&lt;/code&gt; 表示根到 &lt;code&gt;u&lt;/code&gt; 的距离，那么虚树边 &lt;code&gt;(fa, son)&lt;/code&gt; 的真实长度就是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dist[son] - dist[fa]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 预处理前提&lt;/h2&gt;
&lt;p&gt;构建虚树前，原树必须先支持以下信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dfn[u]&lt;/code&gt;：DFS 序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;depth[u]&lt;/code&gt;：节点深度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_lca(u, v)&lt;/code&gt;：能快速查询 LCA&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见 LCA 实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;倍增 LCA&lt;/li&gt;
&lt;li&gt;Euler Tour + RMQ&lt;/li&gt;
&lt;li&gt;树剖求 LCA&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 这份模板做了什么&lt;/h2&gt;
&lt;p&gt;模板接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int build(std::vector&amp;lt;int&amp;gt; key_nodes,
          const std::vector&amp;lt;int&amp;gt;&amp;amp; dfn,
          const std::vector&amp;lt;int&amp;gt;&amp;amp; depth,
          const std::function&amp;lt;int(int, int)&amp;gt;&amp;amp; get_lca)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;返回虚树根节点编号&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结果存储：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;graph[u]&lt;/code&gt;：虚树中 &lt;code&gt;u&lt;/code&gt; 向下连到哪些儿子&lt;/li&gt;
&lt;li&gt;&lt;code&gt;used_nodes&lt;/code&gt;：本次构建用到的所有节点，便于下次 &lt;code&gt;O(K)&lt;/code&gt; 清空&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意这份实现的一个细节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数参数是 &lt;code&gt;std::vector&amp;lt;int&amp;gt; key_nodes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;这是按值传参&lt;/li&gt;
&lt;li&gt;所以函数内部确实会排序、去重&lt;/li&gt;
&lt;li&gt;但不会修改调用者手里的原数组&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，代码行为比注释更安全。&lt;/p&gt;
&lt;h2&gt;6. 为什么按 DFN 排序&lt;/h2&gt;
&lt;p&gt;虚树构造的核心前提是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::sort(key_nodes.begin(), key_nodes.end(),
          [&amp;amp;](int u, int v) { return dfn[u] &amp;lt; dfn[v]; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按 DFS 序排序后，关键点在原树上的出现顺序与“子树区间”一致&lt;/li&gt;
&lt;li&gt;相邻关键点之间的 LCA 足以恢复整棵虚树结构&lt;/li&gt;
&lt;li&gt;配合单调栈，可以只维护一条“当前右链”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是整套做法成立的基础。&lt;/p&gt;
&lt;h2&gt;7. 单调栈在维护什么&lt;/h2&gt;
&lt;p&gt;栈中维护的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前已经处理过的点&lt;/li&gt;
&lt;li&gt;在虚树中尚未完成连边的那条祖先链&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以把它理解成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;栈底更高&lt;/li&gt;
&lt;li&gt;栈顶更深&lt;/li&gt;
&lt;li&gt;栈里节点的 &lt;code&gt;dfn&lt;/code&gt; 单调递增&lt;/li&gt;
&lt;li&gt;同时它们在原树上形成一条“右边界链”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每来一个新点 &lt;code&gt;u&lt;/code&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;求 &lt;code&gt;lca = LCA(u, stk.back())&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;lca&lt;/code&gt; 在栈顶上方，说明要把一些点先退栈并连边&lt;/li&gt;
&lt;li&gt;必要时把 &lt;code&gt;lca&lt;/code&gt; 作为新的分叉点插入栈&lt;/li&gt;
&lt;li&gt;再把 &lt;code&gt;u&lt;/code&gt; 入栈&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;8. 模板流程拆解&lt;/h2&gt;
&lt;h3&gt;8.1 清空上一次查询的残留&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;for (int u : used_nodes) {
    graph[u].clear();
}
used_nodes.clear();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是虚树模板里非常重要的优化。&lt;/p&gt;
&lt;p&gt;不能每次都把整张 &lt;code&gt;graph&lt;/code&gt; 全清空，因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原树有 &lt;code&gt;N&lt;/code&gt; 个点&lt;/li&gt;
&lt;li&gt;单次查询只会用到 &lt;code&gt;O(K)&lt;/code&gt; 个点&lt;/li&gt;
&lt;li&gt;全清空会退化成每次 &lt;code&gt;O(N)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以要靠 &lt;code&gt;used_nodes&lt;/code&gt; 精准回收。&lt;/p&gt;
&lt;h3&gt;8.2 排序 + 去重&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;std::sort(key_nodes.begin(), key_nodes.end(),
          [&amp;amp;](int u, int v) { return dfn[u] &amp;lt; dfn[v]; });

key_nodes.erase(std::unique(key_nodes.begin(), key_nodes.end()), key_nodes.end());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保证构造顺序正确&lt;/li&gt;
&lt;li&gt;防止输入中重复关键点导致虚树出现脏结构&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8.3 初始化栈&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;std::vector&amp;lt;int&amp;gt; stk;
stk.push_back(key_nodes[0]);
used_nodes.push_back(key_nodes[0]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一个关键点先作为当前链的起点。&lt;/p&gt;
&lt;h3&gt;8.4 处理新关键点&lt;/h3&gt;
&lt;p&gt;核心代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int u = key_nodes[i];
int lca = get_lca(u, stk.back());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若 &lt;code&gt;lca != stk.back()&lt;/code&gt;，说明当前点 &lt;code&gt;u&lt;/code&gt; 不在栈顶节点的子树内部最深位置，需要调整结构。&lt;/p&gt;
&lt;h4&gt;情况 1：持续退栈&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;while (stk.size() &amp;gt; 1 &amp;amp;&amp;amp; depth[stk[stk.size() - 2]] &amp;gt;= depth[lca]) {
    graph[stk[stk.size() - 2]].push_back(stk.back());
    stk.pop_back();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只要次栈顶仍然不低于 &lt;code&gt;lca&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;栈顶节点就已经确定应该挂到次栈顶下面&lt;/li&gt;
&lt;li&gt;可以直接连边并弹出&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;情况 2：补入 LCA 分叉点&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;if (depth[stk.back()] &amp;gt; depth[lca]) {
    graph[lca].push_back(stk.back());
    stk.pop_back();

    if (stk.empty() || stk.back() != lca) {
        stk.push_back(lca);
        used_nodes.push_back(lca);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一步表示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;栈顶节点仍然在 &lt;code&gt;lca&lt;/code&gt; 下方&lt;/li&gt;
&lt;li&gt;但更高层已经不属于它这一支了&lt;/li&gt;
&lt;li&gt;所以 &lt;code&gt;lca&lt;/code&gt; 必须显式出现，作为一个新分叉点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后再把当前节点 &lt;code&gt;u&lt;/code&gt; 入栈：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stk.push_back(u);
used_nodes.push_back(u);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;8.5 收尾&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;while (stk.size() &amp;gt; 1) {
    graph[stk[stk.size() - 2]].push_back(stk.back());
    stk.pop_back();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终栈中剩下的一条链，依次连起来即可。&lt;/p&gt;
&lt;p&gt;最后：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return stk[0];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;栈底就是虚树根。&lt;/p&gt;
&lt;h2&gt;9. 为什么这棵树是“有向树”&lt;/h2&gt;
&lt;p&gt;模板里建边方式是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph[parent].push_back(child);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说方向固定为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;父节点指向子节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样做的好处是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接适配树形 DP&lt;/li&gt;
&lt;li&gt;不需要再带父节点参数防止走回头路&lt;/li&gt;
&lt;li&gt;单次查询构出的图天然就是一棵 rooted tree&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;10. 复杂度要说准确&lt;/h2&gt;
&lt;p&gt;这份模板常被口头写成 &lt;code&gt;O(K log K)&lt;/code&gt;，但严格来说应分开看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;排序：&lt;code&gt;O(K log K)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;去重：&lt;code&gt;O(K)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;单调栈建树：&lt;code&gt;O(K)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;LCA 查询：一共 &lt;code&gt;O(K)&lt;/code&gt; 次，每次代价取决于你的 LCA&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此总复杂度更准确地写成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;O(K log K + K * T_lca)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LCA = O(1)&lt;/code&gt;，则整体是 &lt;code&gt;O(K log K)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LCA = O(log N)&lt;/code&gt;，则整体是 &lt;code&gt;O(K log K + K log N)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;11. 使用方式&lt;/h2&gt;
&lt;p&gt;假设你已经对原树完成了 DFS 和 LCA 预处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int n = ...;
VirtualTree vt(n);

std::vector&amp;lt;int&amp;gt; key_nodes = {5, 9, 13, 20};

int root = vt.build(
    key_nodes,
    dfn,
    depth,
    [&amp;amp;](int u, int v) {
        return lca(u, v);
    }
);

// 在虚树上做 DP
std::function&amp;lt;void(int)&amp;gt; dfs = [&amp;amp;](int u) {
    for (int v : vt.graph[u]) {
        dfs(v);
    }
};

dfs(root);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果题目需要知道“哪些点是原始关键点”，通常会额外维护：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;is_key[u] = true / false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在虚树 DP 时区分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原始关键点&lt;/li&gt;
&lt;li&gt;补出来的 LCA 点&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;12. 例子理解&lt;/h2&gt;
&lt;p&gt;设原树中查询关键点是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{4, 5, 8, 9}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它们在原树上的结构大致是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        1
      /   \
     2     3
    / \   / \
   4   5 8   9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么虚树中需要保留的点是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{4, 5, 8, 9, 2, 3, 1}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虚树结构就是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        1
      /   \
     2     3
    / \   / \
   4   5 8   9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果一组关键点本来几乎都落在同一条链上，虚树会更小。&lt;/p&gt;
&lt;p&gt;比如关键点是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{10, 15, 18}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而这三个点祖先链很接近，那么虚树可能只包含少量补点。&lt;/p&gt;
&lt;h2&gt;13. 常见坑&lt;/h2&gt;
&lt;h3&gt;13.1 没按 &lt;code&gt;dfn&lt;/code&gt; 排序&lt;/h3&gt;
&lt;p&gt;不排序，整套单调栈逻辑都不成立。&lt;/p&gt;
&lt;h3&gt;13.2 忘记去重&lt;/h3&gt;
&lt;p&gt;重复关键点可能导致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无意义重复入栈&lt;/li&gt;
&lt;li&gt;虚树结构异常&lt;/li&gt;
&lt;li&gt;DP 统计重复&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;13.3 每次查询都整图清空&lt;/h3&gt;
&lt;p&gt;错误做法会把单次复杂度拖回 &lt;code&gt;O(N)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;虚树模板必须利用 &lt;code&gt;used_nodes&lt;/code&gt; 做局部清空。&lt;/p&gt;
&lt;h3&gt;13.4 把虚树边当成原树边长为 &lt;code&gt;1&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;虚树中的一条边，代表原树中的一整段祖先路径。&lt;/p&gt;
&lt;p&gt;如果题目涉及距离、边权、路径贡献，必须用原树信息恢复真实代价。&lt;/p&gt;
&lt;h3&gt;13.5 忘记标记关键点和补点的区别&lt;/h3&gt;
&lt;p&gt;很多题的转移只对原始关键点生效。&lt;/p&gt;
&lt;p&gt;LCA 补点只是结构点，不一定有题意贡献。&lt;/p&gt;
&lt;h3&gt;13.6 节点编号体系混乱&lt;/h3&gt;
&lt;p&gt;这份模板 &lt;code&gt;graph.assign(max_nodes + 1, ...)&lt;/code&gt; 默认更偏向：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;节点编号从 &lt;code&gt;1&lt;/code&gt; 开始&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你题里是 &lt;code&gt;0&lt;/code&gt; 下标，也能用，但需要整套保持一致。&lt;/p&gt;
&lt;h2&gt;14. 记忆版总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;虚树 = 关键点 + 必要 LCA 的浓缩树&lt;/li&gt;
&lt;li&gt;构造顺序：&lt;code&gt;dfn&lt;/code&gt; 排序 -&amp;gt; 去重 -&amp;gt; 单调栈建树&lt;/li&gt;
&lt;li&gt;栈维护的是当前未完成连边的祖先链&lt;/li&gt;
&lt;li&gt;虚树规模最多 &lt;code&gt;2K - 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;单次构造复杂度是 &lt;code&gt;O(K log K + K * T_lca)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;模板中的边是“父 -&amp;gt; 子”的有向边，适合直接做树形 DP&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>网络编程基本概念</title><link>https://fuwari.vercel.app/posts/ai/others/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/ai/others/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/</guid><description>对计算机网络一些基本概念进行了解释</description><pubDate>Thu, 16 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;网络通信和协议&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;网络通信&lt;/strong&gt; 指的是不同设备之间通过网络交换数据的过程。&lt;br /&gt;
两台主机想要正确通信，必须遵循统一的规则，这些规则就叫做 &lt;strong&gt;协议&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;协议本质上约定了：数据如何封装、如何发送、如何接收、如何校验，以及出现错误后如何处理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;网络协议和OSI网络模型&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;网络协议&lt;/strong&gt; 是一组通信规则，用来保证不同厂商、不同系统的设备能够互联互通。&lt;br /&gt;
为了更清晰地理解网络通信过程，人们把复杂的通信过程分层描述，典型代表就是 &lt;strong&gt;OSI 七层模型&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OSI 七层模型&lt;/strong&gt; 自下而上分别是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;物理层&lt;/strong&gt;：负责比特流传输，例如网线、电信号、光信号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据链路层&lt;/strong&gt;：负责相邻节点之间的数据传输，例如 MAC 地址、帧。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络层&lt;/strong&gt;：负责路径选择与跨网络传输，例如 IP 协议。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传输层&lt;/strong&gt;：负责端到端通信，例如 TCP、UDP。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会话层&lt;/strong&gt;：负责建立、管理、终止会话。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;表示层&lt;/strong&gt;：负责数据格式转换、加密解密、压缩解压。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用层&lt;/strong&gt;：直接为用户程序提供服务，例如 HTTP、FTP、SMTP。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;OSI 模型的意义&lt;/strong&gt; 主要在于：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;降低网络系统设计复杂度。&lt;/li&gt;
&lt;li&gt;让每一层只关注自己的职责。&lt;/li&gt;
&lt;li&gt;便于协议标准化与模块化开发。&lt;/li&gt;
&lt;li&gt;出现故障时更容易定位问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;TCP/IP模型&lt;/h3&gt;
&lt;p&gt;在实际互联网中，更常用的是 &lt;strong&gt;TCP/IP 模型&lt;/strong&gt;。&lt;br /&gt;
它比 OSI 模型更贴近真实网络实现，通常分为四层或五层。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常见四层 TCP/IP 模型&lt;/strong&gt; 包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;网络接口层&lt;/strong&gt;：对应 OSI 的物理层和数据链路层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络层&lt;/strong&gt;：主要协议是 IP，负责寻址和路由。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传输层&lt;/strong&gt;：主要协议是 TCP 和 UDP，负责进程之间的通信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用层&lt;/strong&gt;：包含 HTTP、HTTPS、DNS、FTP 等协议。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;数据在发送时会经历逐层封装：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;应用层产生原始数据。&lt;/li&gt;
&lt;li&gt;传输层添加 TCP 头或 UDP 头。&lt;/li&gt;
&lt;li&gt;网络层添加 IP 头。&lt;/li&gt;
&lt;li&gt;网络接口层再封装成帧并发送。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;接收方则进行逐层拆包，还原出原始数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;IP地址&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;IP 地址&lt;/strong&gt; 用于唯一标识网络中的一台主机。&lt;br /&gt;
可以把它理解为设备在网络中的地址，没有 IP 地址，数据就不知道该发送到哪里。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IP 地址主要分为两类：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;IPv4&lt;/strong&gt;：由 32 位二进制组成，通常写成点分十进制形式，例如 &lt;code&gt;192.168.1.10&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPv6&lt;/strong&gt;：由 128 位二进制组成，地址空间更大，例如 &lt;code&gt;2001:db8::1&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;按照使用范围，IP 地址还可以分为：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;公网 IP&lt;/strong&gt;：可在互联网中直接访问。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;私网 IP&lt;/strong&gt;：通常用于局域网内部，常见网段有 &lt;code&gt;192.168.x.x&lt;/code&gt;、&lt;code&gt;10.x.x.x&lt;/code&gt;、&lt;code&gt;172.16.x.x&lt;/code&gt; 到 &lt;code&gt;172.31.x.x&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;在网络通信中，&lt;strong&gt;IP 地址负责找到目标主机&lt;/strong&gt;，但只靠 IP 地址还不能确定具体是主机上的哪个程序在通信，因此还需要 &lt;strong&gt;端口号&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;端口号&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;端口号&lt;/strong&gt; 用于标识一台主机上的某个具体应用程序或服务。&lt;br /&gt;
IP 地址定位的是主机，端口号定位的是主机中的进程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;端口号范围&lt;/strong&gt; 是 &lt;code&gt;0 ~ 65535&lt;/code&gt;，通常分为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;知名端口&lt;/strong&gt;：&lt;code&gt;0 ~ 1023&lt;/code&gt;，例如 HTTP 默认端口 &lt;code&gt;80&lt;/code&gt;，HTTPS 默认端口 &lt;code&gt;443&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注册端口&lt;/strong&gt;：&lt;code&gt;1024 ~ 49151&lt;/code&gt;，常用于某些固定服务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态或私有端口&lt;/strong&gt;：&lt;code&gt;49152 ~ 65535&lt;/code&gt;，通常由客户端临时使用。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;例如访问网站时，目标可能是：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;服务器IP + 80端口&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这表示把数据发送到某台服务器上的 &lt;strong&gt;Web 服务程序&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;客户端和服务端&lt;/h2&gt;
&lt;p&gt;网络程序通常分为 &lt;strong&gt;客户端&lt;/strong&gt; 和 &lt;strong&gt;服务端&lt;/strong&gt; 两部分。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;服务端&lt;/strong&gt;：负责提供服务，通常长期运行并监听固定端口，等待客户端连接或请求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端&lt;/strong&gt;：负责发起请求，主动去连接服务端并获取结果。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;以浏览器访问网页为例：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;浏览器是客户端。&lt;/li&gt;
&lt;li&gt;网站服务器是服务端。&lt;/li&gt;
&lt;li&gt;浏览器向服务器发起请求。&lt;/li&gt;
&lt;li&gt;服务器返回网页数据给浏览器。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;在编程中，两者职责不同：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;服务端&lt;/strong&gt; 重点在于监听、接收连接、处理多个请求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端&lt;/strong&gt; 重点在于连接目标主机、发送请求、接收响应。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;UDP和TCP&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;UDP 和 TCP&lt;/strong&gt; 都是传输层协议，用于实现进程与进程之间的通信，但两者设计目标不同。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;协议&lt;/th&gt;
&lt;th&gt;主要特点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UDP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;无连接、速度快、开销小，但不保证可靠性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;面向连接、可靠、有序，但开销较大&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;简单理解：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;UDP&lt;/strong&gt; 更像“发消息”，发出去就结束，不确认对方是否收到。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TCP&lt;/strong&gt; 更像“打电话”，先建立连接，再进行可靠通信。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;UDP网络协议&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;UDP&lt;/strong&gt; 全称是 &lt;em&gt;User Datagram Protocol&lt;/em&gt;，即 &lt;strong&gt;用户数据报协议&lt;/strong&gt;。&lt;br /&gt;
它在发送数据之前不需要建立连接，直接把数据打包后发出去，因此效率较高。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;UDP 的通信特点是 &lt;strong&gt;面向数据报&lt;/strong&gt;。发送方每发送一次，就是一个独立的数据包；接收方一次接收的也是一个完整数据报。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;UDP编程步骤&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;UDP 编程的一般流程如下：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建 Socket 对象，指定使用 UDP 协议。&lt;/li&gt;
&lt;li&gt;如果是接收方，可以绑定本地 IP 和端口。&lt;/li&gt;
&lt;li&gt;发送方直接调用发送接口，把数据发送到目标 IP 和端口。&lt;/li&gt;
&lt;li&gt;接收方调用接收接口，获取数据报。&lt;/li&gt;
&lt;li&gt;通信结束后关闭 Socket。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;UDP 通信通常不需要先建立连接，因此实现简单，适合快速收发短消息。&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;UDP特点与优缺点&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;UDP 的特点：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;无连接&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不保证数据一定到达&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不保证数据按顺序到达&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有重传机制&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;首部开销小，效率较高&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;UDP 的优点：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;传输速度快。&lt;/li&gt;
&lt;li&gt;实现简单。&lt;/li&gt;
&lt;li&gt;资源消耗较小。&lt;/li&gt;
&lt;li&gt;适合实时性要求高的场景。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;UDP 的缺点：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;容易丢包。&lt;/li&gt;
&lt;li&gt;数据可能乱序。&lt;/li&gt;
&lt;li&gt;不能自动处理可靠传输问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;UDP应用场景&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;UDP 常用于&lt;/strong&gt; 对实时性要求高、对少量丢包可容忍的场景，例如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;语音通话。&lt;/li&gt;
&lt;li&gt;视频直播。&lt;/li&gt;
&lt;li&gt;在线游戏中的实时状态同步。&lt;/li&gt;
&lt;li&gt;DNS 查询。&lt;/li&gt;
&lt;li&gt;局域网广播和组播。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;TCP网络协议&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;TCP&lt;/strong&gt; 全称是 &lt;em&gt;Transmission Control Protocol&lt;/em&gt;，即 &lt;strong&gt;传输控制协议&lt;/strong&gt;。&lt;br /&gt;
它是一种 &lt;strong&gt;面向连接、可靠、有序、基于字节流&lt;/strong&gt; 的传输协议。&lt;/p&gt;
&lt;p&gt;在正式通信之前，TCP 需要先建立连接；通信结束后，还要断开连接。&lt;br /&gt;
为了保证可靠性，TCP 提供 &lt;strong&gt;确认应答、超时重传、流量控制、拥塞控制&lt;/strong&gt; 等机制。&lt;/p&gt;
&lt;h4&gt;TCP编程步骤&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;TCP 编程的一般流程如下：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;服务端步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建 ServerSocket 或监听 Socket 对象。&lt;/li&gt;
&lt;li&gt;绑定本地 IP 和端口。&lt;/li&gt;
&lt;li&gt;监听客户端连接。&lt;/li&gt;
&lt;li&gt;接收到连接后，得到与客户端通信的 Socket 对象。&lt;/li&gt;
&lt;li&gt;通过输入输出流或收发接口与客户端交换数据。&lt;/li&gt;
&lt;li&gt;通信结束后关闭连接。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;客户端步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建 Socket 对象。&lt;/li&gt;
&lt;li&gt;主动连接服务端 IP 和端口。&lt;/li&gt;
&lt;li&gt;连接成功后发送数据。&lt;/li&gt;
&lt;li&gt;接收服务端返回的数据。&lt;/li&gt;
&lt;li&gt;通信结束后关闭连接。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;TCP特点与优缺点&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;TCP 的特点：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;面向连接&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可靠传输&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;按顺序到达&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基于字节流&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;具有流量控制和拥塞控制机制&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;TCP 的优点：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据可靠，不易丢失。&lt;/li&gt;
&lt;li&gt;能保证顺序。&lt;/li&gt;
&lt;li&gt;适合传输重要业务数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;TCP 的缺点：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;建立连接和断开连接有额外开销。&lt;/li&gt;
&lt;li&gt;传输速度通常比 UDP 慢。&lt;/li&gt;
&lt;li&gt;协议实现更复杂。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;TCP应用场景&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;TCP 适用于&lt;/strong&gt; 对准确性、完整性要求高的场景，例如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;网页访问。&lt;/li&gt;
&lt;li&gt;文件传输。&lt;/li&gt;
&lt;li&gt;邮件发送。&lt;/li&gt;
&lt;li&gt;数据库连接。&lt;/li&gt;
&lt;li&gt;即时通信中的可靠消息传输。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Socket编程&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Socket&lt;/strong&gt; 可以理解为网络通信的“插座”或“接口”，应用程序通过它与传输层协议进行交互。&lt;br /&gt;
无论是 TCP 还是 UDP 编程，最终通常都要通过 Socket 来完成数据发送和接收。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Socket 编程的核心作用包括：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建网络通信端点。&lt;/li&gt;
&lt;li&gt;绑定本地地址和端口。&lt;/li&gt;
&lt;li&gt;建立连接或直接发送数据。&lt;/li&gt;
&lt;li&gt;接收远程主机发来的数据。&lt;/li&gt;
&lt;li&gt;在通信结束后释放资源。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;根据协议不同，Socket 编程可以分为两类：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;UDP Socket&lt;/strong&gt;：无连接，直接发送数据报。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TCP Socket&lt;/strong&gt;：面向连接，需要先建立连接再通信。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item></channel></rss>