前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >《并查集的黑科技:路径压缩×按秩合并×带权扩展|算法核心原理与工程级实践指南》

《并查集的黑科技:路径压缩×按秩合并×带权扩展|算法核心原理与工程级实践指南》

作者头像
IsLand1314
发布于 2025-04-05 07:10:03
发布于 2025-04-05 07:10:03
14900
代码可运行
举报
文章被收录于专栏:学习之路学习之路
运行总次数:0
代码可运行

一、概念与特点

🔥 在一些应用问题中,需要将n个不同的元素划分成一些不相交的集合。开始时,每个元素自成一个单元素集合,然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一个元素归属于那个集合的运算。适合于描述这类问题的抽象数据类型成为并查集(union-findset)

1. 核心概念
  • 动态连通性管理:维护多个 不相交集合,支持快速 合并集合查询元素所属集合
  • 树形结构:每个集合用一棵树表示,父节点指针实现
  • 路径压缩:缩短查找路径,优化查询时间
  • 按秩合并:避免树的高度过高,保证平衡性
2. 时间复杂度

操作

时间复杂度

初始化

O(n)

Find

O(α(n)) (接近常数)

Union

O(α(n))

其中 α(n) 是阿克曼函数的反函数,增长极其缓慢

优化策略对比表

优化类型

单次操作时间复杂度

空间复杂度

适用场景

基本实现

O(log n)

O(n)

小规模数据

仅路径压缩

O(α(n))

O(n)

查询密集型操作

仅按秩合并

O(log n)

O(n)

合并操作较多的情况

双优化

O(α(n))

O(n)

通用场景

带权并查集

O(α(n))

O(n)

需要维护附加关系的情况

阿克曼函数解析

阿克曼函数 A(m, n) 是递归定义的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
A(m, n) = 
    n+1                  if m=0
    A(m-1, 1)           if m>0 and n=0
    A(m-1, A(m, n-1))   otherwise

其反函数 α(n) 是满足 A(α(n), α(n)) ≥ n 的最小整数。对于所有实际物理世界中的n值,α(n) ≤ 5。


二、应用场景

  1. 社交网络:判断两人是否属于同一朋友圈
  2. 图论:判断图的连通性、最小生成树(Kruskal算法)
  3. 棋盘问题:岛屿数量、矩阵连通块
  4. 动态连接:实时合并/查询数据集合

三、并查集技巧

1. 普通并查集基本模板
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class UnionFind {
private:
    vector<int> fa;    // 父节点数组
    vector<int> cnt;   // 连通块内点的数量
    vector<int> rank;  // 秩(树的高度)
    int count = 0;     // 新增:连通块数量
    
public:
    // 1. 初始化
    UnionFind(int n) {   
        fa.resize(n + 1);
        rank.resize(n + 1, 1);
        cnt.resize(n + 1, 1);
        count = n;      // 初始每个节点都是独立连通块,总共有n个
        for(int i = 1; i <= n; ++i) fa[i] = i;
    }

    // 2. 查找根节点(路径压缩)
    int find(int x) {    
        return fa[x] == x ? x : fa[x] = find(fa[x]);
    }

    // 3. 合并集合(按秩合并)
    void unite(int a, int b) { 
        int x = find(a), y = find(b);
        if (x == y) return;

        // 合并时连通块数量减少1
        count--;

        // 按秩合并
        if (rank[x] < rank[y]) {
            fa[x] = y;
            cnt[y] += cnt[x];
        } else {
            fa[y] = x;
            cnt[x] += cnt[y];
            if (rank[x] == rank[y]) rank[x]++;
        }
    }

    // 4. 判断连通性
    bool isConnected(int x, int y) { 
        return find(x) == find(y);
    }

    // 5. 获取连通块大小
    int getCnt(int x) {
        return cnt[find(x)];
    }

    // 6. 获取连通块数量
    int getCount() const {
        return count;
    }
};
2. 带权并查集
2.1 基本概念

带权并查集是并查集的扩展版本,在维护连通性的基础上,额外记录节点之间的相对关系(如距离、比例、差值等)。通过路径压缩和按秩合并的优化,实现高效的关系维护与查询

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class WeightedUnionFind {
private:
    vector<int> parent;  // 父节点数组
    vector<int> rank;    // 秩(树的高度)
    vector<double> weight; // 权重数组:记录节点到父节点的关系权重

public:
    WeightedUnionFind(int n) {
        parent.resize(n);
        iota(parent.begin(), parent.end(), 0); // 初始父节点为自身
        rank.resize(n, 1);
        weight.resize(n, 1.0); // 初始权重为1.0
    }

    // 查找操作(带路径压缩)
    pair<int, double> find(int x) {
        if (parent[x] != x) {
            auto [root, w] = find(parent[x]); // 递归查找根节点
            parent[x] = root;                 // 路径压缩
            weight[x] *= w;                   // 更新当前节点到根的权重
        }
        return {parent[x], weight[x]};
    }

    // 合并操作(带权重关系处理)
    void unite(int x, int y, double value) {
        auto [rootX, wX] = find(x); // x到根的权重wX
        auto [rootY, wY] = find(y); // y到根的权重wY
        
        if (rootX == rootY) return;

        // 合并策略(按秩合并)
        if (rank[rootX] < rank[rootY]) {
            parent[rootX] = rootY;
            weight[rootX] = value * wY / wX; // 计算新权重关系
        } else {
            parent[rootY] = rootX;
            weight[rootY] = wX / (value * wY); // 权重关系反向
            if (rank[rootX] == rank[rootY]) 
                rank[rootX]++;
        }
    }

    // 查询x和y的关系值
    double getValue(int x, int y) {
        auto [rootX, wX] = find(x);
        auto [rootY, wY] = find(y);
        if (rootX != rootY) return -1.0; // 不连通
        return wX / wY; // 返回x/y的值
    }
};

复杂度与优化

操作

时间复杂度

空间复杂度

构造函数

O(n)

O(n)

find

O(α(n))

O(1)

unite

O(α(n))

O(1)

getValue

O(α(n))

O(1)

2.2 权重更新原理
  • 数学建模:设 x 到父节点的权重为 w[x],表示 x = w[x] * parent[x]
  • 路径压缩:当查找路径被压缩时,直接计算 x 到根的累计权重
  • 合并公式:合并 xy 时,根据 x/y = value 推导新权重关系
2.3 合并操作示例

假设合并 xy,已知 x = a * rootX, y = b * rootY,合并条件为 x = value * y,则

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
a*rootX = value*(b*rootY)=> rootX = (value*b/a) * rootY

工程优化技巧

  1. 离散化处理:对非连续标识(如字符串)建立映射表
  2. 双缓冲机制:在频繁修改的场景下使用双数组交替更新
  3. 批量操作优化:预处理所有操作后统一处理路径压缩
3. 离散化并查集
3.1 基本概念
  • 离散化映射
    • 收集所有元素并去重。
    • 为每个唯一元素分配一个连续整数索引(如 0, 1, 2, ...)。
    • 使用哈希表(字典)记录元素到索引的映射关系。
  • 并查集操作
    • 基于离散化后的索引初始化父节点数组。
    • 实现路径压缩(find)和按秩合并(union

特点

  1. 动态映射:将离散的、大范围的数据(如大整数、字符串等)映射到连续的或较小的键空间中。
  2. 节省空间:避免为稀疏数据预分配大数组,使用哈希表动态存储。
  3. 灵活处理未知元素:动态添加新元素到并查集中,无需预先知道所有元素。
3.2 案例代码
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <vector>
#include <unordered_map>
#include <set>
#include <numeric>
#include <algorithm>
#include <iostream>

class DisjointSet {
private:
    std::unordered_map<int, int> element_to_idx; // 元素到索引的映射
    std::vector<int> parent;                     // 父节点数组
    std::vector<int> rank;                       // 秩数组(树的高度)

public:
    // 构造函数:离散化元素并初始化并查集
    DisjointSet(const std::vector<int>& elements) {
        // 1. 去重并排序
        std::set<int> unique_sorted_elements(elements.begin(), elements.end());

        // 2. 建立元素到索引的映射
        int idx = 0;
        for (int elem : unique_sorted_elements) {
            element_to_idx[elem] = idx++;
        }

        // 3. 初始化 parent 和 rank
        parent.resize(unique_sorted_elements.size());
        std::iota(parent.begin(), parent.end(), 0); // 初始父节点为自身
        rank.resize(unique_sorted_elements.size(), 1); // 初始秩为1
    }

    // 查找元素的根节点(路径压缩)
    int find(int x) {
        int x_idx = element_to_idx[x];
        if (parent[x_idx] != x_idx) {
            parent[x_idx] = find_by_idx(parent[x_idx]); // 路径压缩
        }
        return parent[x_idx];
    }

private:
    // 辅助函数:通过索引查找根节点(递归实现)
    int find_by_idx(int x_idx) {
        if (parent[x_idx] != x_idx) {
            parent[x_idx] = find_by_idx(parent[x_idx]); // 路径压缩
        }
        return parent[x_idx];
    }

public:
    // 合并两个集合(按秩合并)
    void union_sets(int x, int y) {
        int x_root = find(x);
        int y_root = find(y);
        if (x_root == y_root) return;

        // 按秩合并:小树合并到大树
        if (rank[x_root] < rank[y_root]) {
            parent[x_root] = y_root;
        }
        else {
            parent[y_root] = x_root;
            if (rank[x_root] == rank[y_root]) {
                rank[x_root]++;
            }
        }
    }
};

int main() {
    // 测试用例
    std::vector<int> elements = { 1, 3, 7, 100000, 3, 1 };
    DisjointSet ds(elements);

    ds.union_sets(1, 3);
    ds.union_sets(7, 100000);

    std::cout << std::boolalpha << (ds.find(1) == ds.find(3)) << std::endl;     // 输出 true
    std::cout << (ds.find(7) == ds.find(100000)) << std::endl; // 输出 true
    std::cout << (ds.find(1) == ds.find(7)) << std::endl;     // 输出 false

    return 0;
}
3.3 适用场景
  1. 大范围稀疏数据: 元素范围极大(如 1e18),但实际数量较少(如 1e5),无法直接用数组存下标。
  2. 非数值类型元素: 元素是字符串、对象等,需映射为整数索引。
  3. 动态元素处理(需扩展代码): 若元素动态增加,可用字典动态分配索引,并扩展父节点数组。

注意事项

  • 去重:离散化前必须去重,确保每个元素唯一对应一个索引。
  • 一致性:所有操作需通过哈希表转为索引,避免直接操作原始元素。
  • 动态扩展:若允许动态添加元素,需在每次遇到新元素时扩展数据结构
4. 可撤销并查集
4.1 基本概念

可撤销并查集(Rollback Disjoint Set Union,Rollback DSU) 是并查集的扩展版本,在支持 合并(Union)查询(Find) 操作的基础上,新增了 撤销(Rollback) 功能。它允许用户回退到之前的某个状态,适用于需要支持 操作回滚分支回溯 的场景。


4.2 核心特性

特性

普通并查集

可撤销并查集

合并操作

✔️

✔️

路径压缩

✔️

❌(破坏可撤销性)

按秩合并

✔️

✔️(必需)

撤销操作

✔️(核心功能)

时间复杂度

O(α(n))

O(log n) 每次操作


4.3 算法代码

1. 数据结构设计

  • 父节点数组 parent[]:记录每个节点的父节点
  • 秩数组 rank[]:记录树的高度(用于按秩合并)
  • 操作栈 stack:存储合并操作的上下文信息

2. 案例代码

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
#include <vector>
#include <stack>
#include <tuple> // 添加元组支持

using namespace std;

class RollbackUnionFind {
private:
    // 操作记录类型:子根 | 父根 | 原父秩
    using Operation = tuple<int, int, int>;

    vector<int> fa;     // 父节点数组
    vector<int> rank;   // 树的高度
    stack<Operation> st; // 操作记录栈
    int count;          // 连通块数量

public:
    // 初始化
    RollbackUnionFind(int n) : count(n) {
        fa.resize(n + 1);
        rank.resize(n + 1, 1);
        for (int i = 1; i <= n; ++i)
            fa[i] = i;
    }

    // 查找根节点(无路径压缩)
    int find(int x) const {
        while (fa[x] != x)
            x = fa[x];
        return x;
    }

    // 合并操作
    bool unite(int x, int y) {
        x = find(x);
        y = find(y);
        if (x == y) {
            st.emplace(-1, -1, -1); // 无效操作标记
            return false;
        }

        // 按秩合并
        if (rank[x] < rank[y]) {
            st.emplace(x, y, rank[y]);
            fa[x] = y;
        }
        else {
            st.emplace(y, x, rank[x]);
            fa[y] = x;
            if (rank[x] == rank[y])
                ++rank[x]; // 树高增加
        }
        --count; // 连通块数量减少
        return true;
    }

    // 撤销操作
    void rollback() {
        if (st.empty()) return;
        Operation t = st.top();
        int child = get<0>(t), parent_root = get<1>(t), old_rank = get<2>(t);
        st.pop();

        if (child == -1) return; // 无效操作

        // 恢复父子关系
        fa[child] = child;
        // 恢复原秩值
        if (rank[parent_root] != old_rank) {
            rank[parent_root] = old_rank;
        }
        ++count; // 连通块数量恢复
    }

    // 获取当前连通块数量
    int getCount() const { return count; }

    // 判断是否连通
    bool isConnected(int x, int y) const {
        return find(x) == find(y);
    }
};

int main() {
    RollbackUnionFind uf(5); // 5个元素

    uf.unite(1, 2);    // 连通块:{1,2}, {3}, {4}, {5} (count=4)
    uf.unite(3, 4);    // 连通块:{1,2}, {3,4}, {5} (count=3)
    uf.rollback();      // 撤销上一步 (count=4)
    uf.unite(2, 3);     // 连通块:{1,2,3}, {4}, {5} (count=3)

    cout << uf.isConnected(1, 3) << endl; // 输出1(连通)
    uf.rollback();      // 撤销unite(2,3)
    cout << uf.isConnected(1, 3) << endl; // 输出0(不连通)
    cout << uf.getCount() << endl;        // 输出4
}
5. 其他进阶技巧
5.1 动态大小处理

当数据规模动态增长时,可以使用哈希表代替数组:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class DynamicUnionFind {
private:
    unordered_map<int, int> parent;
    unordered_map<int, int> rank;
public:
    void add(int x) {
        if (!parent.count(x)) {
            parent[x] = x;
            rank[x] = 1;
        }
    }
    // find 和 unite 方法与标准实现类似
};
5.2 连通块分量统计优化

当我们不清楚点的数量的时候,但是要计算连通块数量,就可以用哈希的方法,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class UnionFind {
public:
    unordered_map<int, int> fa;
    int count = 0; // 总共有多少不连通的并查集

    int find(int x) {    
        // 构建并查集时候,假如key值不在并查集中则构建哈希表映射,count++
        if(fa.find(x) == fa.end()){
            fa[x] = x;
            count++;
        }
        // 查找并查集的头节点并优化
        if(fa[x] != x) fa[x] = find(fa[x]);
        return fa[x];
    }

    // 合并集合
    void unite(int a, int b) { 
        int x = find(a), y = find(b);
        if (x == y) return;
        fa[x] = y;
        count--; 
    }
};

四、板子训练

1. 合并集合

一共有 𝑛 个数,编号是 1∼𝑛,最开始每个数各自在一个集合中。

现在要进行 𝑚 个操作,操作共有两种:

  1. M a b,将编号为 a𝑎 和 b𝑏 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b,询问编号为 a𝑎 和 b𝑏 的两个数是否在同一个集合中;

输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 M a bQ a b 中的一种。

输出格式

对于每个询问指令 Q a b,都要输出一个结果,如果 a𝑎 和 b𝑏 在同一集合内,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1≤𝑛,𝑚≤10^5

输入样例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4

输出样例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Yes
No
Yes

代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int p[N];

int find(int x)
{
    return p[x] == x ? x : p[x] = find(p[x]);
}

int main()
{
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++) p[i] = i;
    char op;
    int a, b;
    while(m--){
        cin >> op >> a >> b;
        if(op == 'M'){
            p[find(a)] = find(b); // 集合合并操作
        }
        else{
            if(find(a) == find(b)) cout << "Yes" << endl;
            else cout << "No" << endl;
        }
    }
    return 0;
}
2. 连通块中点的数量

给定一个包含 𝑛 个点(编号为 1∼𝑛)的无向图,初始时图中没有边。

现在要进行 𝑚 个操作,操作共有三种:

  1. C a b,在点 𝑎 和点 𝑏 之间连一条边,a 和 𝑏 可能相等;
  2. Q1 a b,询问点 𝑎 和点 𝑏 是否在同一个连通块中,𝑎 和 𝑏 可能相等;
  3. Q2 a,询问点 𝑎 所在连通块中点的数量;

输入格式

第一行输入整数 𝑛 和 𝑚。

接下来 𝑚 行,每行包含一个操作指令,指令为 C a bQ1 a bQ2 a 中的一种。

输出格式

对于每个询问指令 Q1 a b,如果 𝑎 和 𝑏 在同一个连通块中,则输出 Yes,否则输出 No

对于每个询问指令 Q2 a,输出一个整数表示点 a𝑎 所在连通块中点的数量

每个结果占一行。

数据范围

1≤𝑛,𝑚≤10^5

输入样例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5

输出样例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Yes
2
3

代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
#include <string>
using namespace std;
const int N = 1e5 + 10;

int fa[N], cnt[N];
int n, m;

// 1. 初始化
void init()
{
    for(int i = 1; i <= n; i++){
        fa[i] = i;
        cnt[i] = 1;
    }
}

// 2. 查询根节点
int find(int x)
{
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}

// 3. 合并连通块
void merge(int a, int b)
{
    int x = find(a), y = find(b);
    fa[x] = y;
    cnt[y] += cnt[x]; // y 连通块 + x 连通块内点数量
}

// 4. 判断连通
bool isconnected(int a, int b){
    return find(a) == find(b);
}

int main()
{
    cin >> n >> m;
    init();
    string op;
    int a, b;
    while(m--)
    {
        cin >> op;
        if(op == "C") {
            cin >> a >> b;
            // 更新连通块数量之前一定需要先判断两者是否在一起
            if(!isconnected(a, b))merge(a, b);
        }
        else if(op == "Q1"){
            cin >> a >> b;
            if(isconnected(a, b)) cout << "Yes" << endl;
            else cout << "No" << endl;
        }
        else{
            cin >> a;
            cout << cnt[find(a)] << endl;
        }
    }
}
3. 食物链

动物王国中有三类动物 𝐴,𝐵,𝐶,这三类动物的食物链构成了有趣的环形。

𝐴 吃 𝐵,𝐵 吃 𝐶,C𝐶 吃 𝐴。

现有 N𝑁 个动物,以 1∼N1∼𝑁 编号。

每个动物都是 𝐴,𝐵,𝐶 中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 𝑁 个动物所构成的食物链关系进行描述:

第一种说法是 1 X Y,表示 𝑋 和 𝑌 是同类。

第二种说法是 2 X Y,表示 𝑋 吃 𝑌。

此人对 𝑁 个动物,用上述两种说法,一句接一句地说出 𝐾 句话,这 𝐾 句话有的是真的,有的是假的。

当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

  1. 当前的话与前面的某些真的话冲突,就是假话;
  2. 当前的话中 𝑋 或 𝑌 比 𝑁 大,就是假话;
  3. 当前的话表示 𝑋 吃 𝑋,就是假话。

你的任务是根据给定的 𝑁 和 𝐾 句话,输出假话的总数。

输入格式

第一行是两个整数 𝑁 和 𝐾,以一个空格分隔。

以下 𝐾 行每行是三个正整数 𝐷,𝑋,𝑌,两数之间用一个空格隔开,其中 𝐷 表示说法的种类。

若 𝐷=1,则表示 𝑋 和 𝑌 是同类。

若 𝐷=2,则表示 𝑋 吃 𝑌。

输出格式

只有一个整数,表示假话的数目。

数据范围

  • 1≤𝑁≤50000,
  • 0≤𝐾≤100000

输入样例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5

输出样例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
3

方法一:并查集(拓展域)

这道题目我们主要是要开三个拓展的域:同类域(A类),捕食域(B 类),以及天敌域(C 类)

  1. 如果 x,y 是同类,但是 x 的捕食域有 y,那么错误
  2. 如果 x,y 是同类,但是 x 的天敌域有 y,那么错误
  3. 如果 x 是 y 的天敌,但是 x 的同类域中有 y,那么错误
  4. 如果 x 是 y 的天敌,但是 x 的天敌域中有 y,那么错误

首先, 在带扩展域的并查集 中 x 不再只是一个 值,而是一个事件;

  1. 规定 x 为 “事件 x 为 A 类动物”;
  2. 规定 x + N 为 “事件 x 为 B 类动物”;
  3. 规定 x + N * 2 为 “事件 x 为 C 类动物”;

p[find(X)] = find(Y) 表示:事件 X 为 A 类动物 和 事件 Y 为 A 类动物 同时发生 X 与 Y 为同种动物 等价于

  • p[ find(X) ] = find(Y);
  • p[ find(X + N)] = find(Y + N);
  • p[ find(X + N * 2)] = find(Y + N * 2);

p[find(X)] = find(Y + N) 表示:事件 X 为 A 类动物 和 事件 Y 为 B 类动物 同时发生 X 吃 Y 等价于

  • p[ find(X) ] = find(Y + N);
  • p[ find(X + N)] = find(Y + N * 2);
  • p[ find(X + N * 2)] = find(Y);
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
using namespace std;
int fa[200000];
int n, k;
void init(){
    for(int i = 1;i <= 3*n; ++i) fa[i]=i;
}
int find(int x){
    return x == fa[x] ? x : fa[x] = find(fa[x]);
}

void merge(int a, int b){
    int x = find(a), y = find(b);
    fa[x] = y; 
}

int main()
{
    cin >> n >> k;
    init();
    int ans = 0;
    while(k--)
    {
        int D, x, y;
        cin >> D >> x >> y;
        if(x > n || y > n) ans++;
        else if(D == 1)
        {
            // 如果x,y是同类,但是x是y的捕食中的动物,或者x是y天敌中的动物,那么错误.
            if(find(x) == find(y + n) || find(x) == find(y + n + n)) ans++;
            else{
                merge(x, y); // 同类域
                merge(x + n, y + n); // 捕食域
                merge(x + n + n, y + n + n); // 天敌域
            }
        }
        else // x 吃 y
        {
            // x就是y,或者他们是同类,再或者是y的同类中有x
            if(x == y || find(x) == find(y) || find(x) == find(y+n)) ans++; // 都是假话
            else{
                merge(x, y + n + n);     // x 同类域 加入 y 天敌域
                merge(x + n, y);         // x 捕食域 加入 y 同类域
                merge(x + n + n, y + n); // x 天敌域 加入 y 捕食域
            }
        }
    }

    cout << ans << endl;
    return 0;
}

**方法二:**带权并查集

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
using namespace std;

const int N = 500010;
int p[N], d[N]; //size[]表示的是每一个集合点的数量
int n, k;

int find(int x)
{
    if (p[x] != x){
        int t = find(p[x]);  //开始递归;最终用t储存根节点
        d[x] += d[p[x]];     //在递归过程中求得每个点到根节点的距离
        p[x] = t;            //将每个点的父节点指向根节点
    }
    return p[x];           //返回父节点
}

int main()
{
	cin >> n >> k;
	for (int i = 1; i <= n; i++) p[i] = i;

	int res = 0;
	while (k--)
	{
		int D, x, y;
		cin >> D >> x >> y;
		int px = find(x), py = find(y); // px,py分别储存根节点
		if (x > n || y > n) res++;
		else if(D == 1){
		    if (px == py && (d[x] - d[y]) % 3) res++; //d[x]-d[y]!=0说明两个不是同类
			else if (px != py){
				p[px] = py;
				d[px] = d[y] - d[x]; //(d[x]+d[px]-d[y])%3==0
			}
		}
		else{
	        if (px == py && (d[x] - d[y] - 1) % 3) res++;   // x,y祖宗相同,并且x y 属于同类
			else if (px != py){
				p[px] = py;
				d[px] = d[y] + 1 - d[x]; //(d[x]+d[px]-d[y]-1)%3==0
			}
		}
	}
	cout << res << endl;
	return 0;
}

五、经典例题

提一句:下面的函数都是上面板子写过的,下面代码中就没有特别演示出来了

例题1: 省份数量 (LeetCode 547)

题目链接:547. 省份数量(LeetCode)

题目: 有 n 个城市,其中一些彼此相连。省份是一组直接或间接相连的城市。给定邻接矩阵 isConnected,求省份数量

  • 思路:使用普通并查集,获取连通块数量

代码实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int findCircleNum(vector<vector<int>>& isConnected) {
    int n = isConnected.size();
    UnionFind uf(n);
    for (int i = 0; i < n; i++) //由于该矩阵对称,只考虑右上角
    {
        for (int j = i + 1; j < n; j++)
        {
            if (isConnected[i][j] == 1) uf.unite(i, j);
        }
    }
    // 下面两种获取连通块数量的方法都行
    int ans = 0;
    for (int i = 0; i < n; i++){
        if (uf.find(i) == i) ans++;
    }
    // return ans;
    return uf.getCount();
}
例题2: 连通网络的操作次数 (LeetCode 1319)

题目链接:1319. 连通网络的操作次数 - 力扣(LeetCode)

题目: 有 n 台计算机,给定连接线 connections。要使网络连通至少需要移动多少根线?

  • 思路:使用普通并查集,然后获取连通块数量即可

代码实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int makeConnected(int n, vector<vector<int>>& connections) {
    if(connections.size() < n - 1) return -1;
    UnionFind uf(n);
    int extra = 0;
    for(auto& conn: connections){
        uf.unite(conn[0], conn[1]);
    }

    return uf.getCount() - 1;
}
案例3: 冗余连接 (LeetCode 684)

题目链接684. 冗余连接(LeetCode)

问题:找出使树变成图的多余边

  • 思路:通过标准并查集检测环,无需维护大小

代码实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
    int n = edges.size();
    UnionFind uf(n+1); // 节点编号从1开始
    
    for(auto& e : edges) {
        if(uf.isConnected(e[0], e[1]))
            return {e[0], e[1]};
        uf.unite(e[0], e[1]);
    }
    return {};
}
案例4: 岛屿数量 (LeetCode 200)

题目链接:200. 岛屿数量 - 力扣(LeetCode)

问题:矩阵连通性检测

  • 思路:使用普通并查集建立连接,然后只连接岛屿(连接右边和下边相邻的),然后再返回岛屿的连通块数量

代码实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int numIslands(vector<vector<char>>& grid) {
    if(grid.empty()) return 0;
    int n = grid.size(), m = grid[0].size();
    UnionFind uf(n * m);
    int water = 0;

    for(int i = 0; i < n; i++){
        for(int j = 0; j < m; j++){
            if(grid[i][j] == '0') {
                water++; // 统计水域
                continue;
            }
            int ind = i * m + j + 1; // 当前单元格的索引
            // 1. 合并右边相邻的陆地
            if (j + 1 < m && grid[i][j + 1] == '1') uf.unite(ind, ind + 1);   
            // 2. 合并下边相邻的陆地
            if (i + 1 < n && grid[i + 1][j] == '1') uf.unite(ind, ind + m);
        }
    }
    // 求岛屿数量的方法一
    int ans = 0; //计算连通岛屿中根节点的数目
    for (int i = 0; i < n; i++){
        for (int j = 0; j < m; j++){
            int ind = i * m + j + 1;
            if (grid[i][j] == '1' && uf.find(ind) == ind) ans++;
        }
    }

    // 求岛屿数量的方法二
    // uf.getCount() 返回的是 所有节点的连通块总数 (包括陆地和水域)
    // 因此需要从总连通块数中 减去水域的数量 
    return uf.getCount() - water;
}
案例5: 等式方程的可满足性 (LeetCode 990)

题目链接:990. 等式方程的可满足性 - 力扣(LeetCode)

  • 思路:既可以用 普通并查集来做,也可以用 带权并查集
  • 其实这里也是通过 -‘a’ 把字符离散映射成为了 数字

代码实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
bool equationsPossible(vector<string>& equations) {
    UnionFind uf(26);
    // 1. 把所有等式放入连通集里
    for(auto &eq: equations){
        int x = eq[0] - 'a', y = eq[3] - 'a';
        if(eq[1] == '='){
            uf.unite(x, y);
        }
    }
    // 2. 判断不等式
    for(auto &eq: equations){
        int x = eq[0] - 'a', y = eq[3] - 'a';
        if(eq[1] == '!'){
            if(uf.isConnected(x, y)) return false;
        }
    }
    return true;
}

// 方法二: 带权并查集来写, 设置默认权重为 1
bool equationsPossible(vector<string>& equations) {
    WeightedUnionFind uf(26); // 26个小写字母
    
    // 处理所有等式
    for (auto& eq : equations) {
        if (eq[1] == '=') {
            int x = eq[0]-'a', y = eq[3]-'a';
            uf.unite(x, y, 1.0); // x/y=1.0
        }
    }
    
    // 验证不等式
    for (auto& eq : equations) {
        if (eq[1] == '!') {
            int x = eq[0]-'a', y = eq[3]-'a';
            if (uf.getValue(x, y) == 1) // 检查是否相等
                return false;
        }
    }
    return true;
}
案例6: 除法求值 (LeetCode 399)

题目链接:399. 除法求值 - 力扣(LeetCode)

  • 思路:带权 + 离散化 并查集,先通过离散化 然后建立连接,再把[x, y] 与 权值 w 建立连接

代码实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
vector<double> calcEquation(vector<vector<string>>& equations, 
                           vector<double>& values,
                           vector<vector<string>>& queries) {
    unordered_map<string, int> str2id;
    int id = 0;
    
    // 字符串映射为数字ID
    for (auto& eq : equations) {
        if (!str2id.count(eq[0])) str2id[eq[0]] = id++;
        if (!str2id.count(eq[1])) str2id[eq[1]] = id++;
    }
    
    WeightedUnionFind uf(id);
    // 建立关系
    for (int i = 0; i < equations.size(); ++i) {
        int x = str2id[equations[i][0]];
        int y = str2id[equations[i][1]];
        uf.unite(x, y, values[i]); // x = values[i] * y
    }
    
    // 处理查询
    vector<double> res;
    for (auto& q : queries) {
        if (!str2id.count(q[0]) || !str2id.count(q[1])) {
            res.push_back(-1.0);
            continue;
        }
        int x = str2id[q[0]], y = str2id[q[1]];
        res.push_back(uf.getValue(x, y));
    }
    return res;
}
案例7: 冗余连接 II (LeetCode 685)

题目链接:685. 冗余连接 II - 力扣(LeetCode)

注意:该题和之前冗余连接不同的是,每个点有且只有一个父节点,因此不仅需要考虑环,还需要考虑入度为 2 的问题

问题特点

  • 有向图存在冗余边
  • 需要处理 入度=2 两种场景
  • 带权并查集需要记录 父节点关系

代码实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class DirectedUnionFind {
private:
    vector<int> parent; // 父节点(带路径压缩)
    vector<int> edgeTo; // 显式记录每个节点的直接父节点
    
public:
    DirectedUnionFind(int n) {
        parent.resize(n+1);
        edgeTo.resize(n+1, -1);
        iota(parent.begin(), parent.end(), 0);
    }

    int find(int x) { // 路径压缩查找根节点
        return x == parent[x] ? x : parent[x] = find(parent[x]);
    }

        int unite(int u, int v) { // u是v的父节点
        if (edgeTo[v] != -1) // 子节点v已经有父节点
            return v; 

        int rootU = find(u);
        if (rootU == find(v)) // 形成环(u和v已连通)
            return v;

        edgeTo[v] = u;       // v的直接父节点是u
        parent[v] = rootU;   // 合并到u的根
        return -1;
    }
};

class Solution {
public:
    vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
        int n = edges.size();
        // 记录入度为 2 的节点的两条侯选边
        vector<int> candidate1, candidate2; 
        unordered_map<int, int> inDegree;
        
        // 检测入度为2的节点
        for (auto& e : edges) {
            if (++inDegree[e[1]] == 2) {
                // 逆序遍历找到所有指向该节点的边
                vector<int> candidates; // 临时存储候选边的索引
                for (int i = n-1; i >= 0; --i) {
                    if (edges[i][1] == e[1]) {
                        candidates.push_back(i);
                        if (candidates.size() == 2) break; // 只需要最后两个
                    }
                }
                // 根据索引顺序赋值候选边
                candidate2 = edges[candidates[0]]; // 最后出现的边(索引较大)
                candidate1 = edges[candidates[1]]; // 倒数第二个出现的边
                break;
            }
        }
        
         DirectedUnionFind uf(n);
        // 处理入度=2的情况
        if (!candidate1.empty()) {
            // 先尝试删除最后出现的候选边candidate2
            bool hasConflict = false;
            for (auto& e : edges) {
                if (e == candidate2) continue;
                if (uf.unite(e[0], e[1]) != -1) {
                    hasConflict = true;
                    break;
                }
            }
            if (!hasConflict) return candidate2; // 删除candidate2后合法

            // 再验证candidate1
            uf = DirectedUnionFind(n);
            for (auto& e : edges) {
                if (e == candidate1) continue;
                if (uf.unite(e[0], e[1]) != -1) 
                    return candidate2; // 必须删除candidate2
            }
            return candidate1; // 可安全删除candidate1
        }
        
        // 处理环的情况
        for (auto& e : edges) {
            if (uf.unite(e[0], e[1]) != -1)
                return e;
        }
        return {};
    }
};
案例8:账户合并 (LeetCode 721)

题目链接:721. 账户合并 - 力扣(LeetCode)

问题特点

  • 用哈希表将邮箱字符串映射为整数
  • 需要合并具有相同邮箱的账户
  • 带权并查集需要维护 邮箱到账户的映射关系

代码实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class AccountUnionFind {
public:
    vector<int> parent;
    vector<string> owner; // 每个邮箱的账户名
    unordered_map<string, int> email2id; 
    
public:
    AccountUnionFind(const vector<vector<string>>& accounts) {
        int id = 0;
        // 邮箱离散化并建立映射
        for (auto& acc : accounts) {
            for (int i = 1; i < acc.size(); ++i) {
                string email = acc[i];
                if (!email2id.count(email)) {
                    email2id[email] = id++;
                    owner.push_back(acc[0]); // 保存账户名
                }
            }
        }
        parent.resize(id);
        iota(parent.begin(), parent.end(), 0); // 初始化父节点
    }

    int find(int x) {
        return x == parent[x] ? x : parent[x] = find(parent[x]);
    }

    void unite(int x, int y) {
        int rootX = find(x), rootY = find(y);
        if (rootX != rootY)
            parent[rootY] = rootX;
    }

    vector<vector<string>> getMerged() {
        unordered_map<int, set<string>> groups;
        
        // 根据根节点分组收集邮箱
        for (auto& [email, id] : email2id) {
            int root = find(id);
            groups[root].insert(email);
        }
        
        // 生成结果
        vector<vector<string>> res;
        for (auto& [root, emails] : groups) {
            vector<string> acc{owner[root]};
            acc.insert(acc.end(), emails.begin(), emails.end());
            res.push_back(acc);
        }
        return res;
    }
};

class Solution {
public:
    vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
        AccountUnionFind uf(accounts);
        
        // 合并同一账户的邮箱
        for (auto& acc : accounts) {
            if (acc.size() < 2) continue;
            string firstEmail = acc[1];
            int firstId = uf.email2id.at(firstEmail); // 使用成员变量
            for (int i = 2; i < acc.size(); ++i) {
                string email = acc[i];
                int id = uf.email2id.at(email);
                uf.unite(firstId, id);
            }
        }
        
        return uf.getMerged();
    }
};
案例9:最长连续序列(LeetCode 128)

题目链接LeetCode 128. 最长连续序列

需求:在未排序数组中,找到数字组成的最长连续序列长度(统计连通块大小)

  • 思路:将相邻数字合并,通过维护连通块大小统计最长序列(离散化并查集)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Solution {
public:
    vector<int> parent, cnt;

    int find(int x) {
        return parent[x] == x ? x : parent[x] = find(parent[x]);
    }

    void merge(int x, int y) {
        int fx = find(x), fy = find(y);
        if (fx != fy) {
            parent[fx] = fy;
            cnt[fy] += cnt[fx]; // 维护连通块大小
        }
    }

    int longestConsecutive(vector<int>& nums) {
        if (nums.empty()) return 0;
        unordered_map<int, int> num_map;
        int idx = 0;
        // 离散化去重
        for (int num : nums) {
            if (!num_map.count(num)) {
                num_map[num] = idx++;
                parent.push_back(idx-1);
                cnt.push_back(1);
            }
        }
        // 合并相邻数字
        for (auto& [num, id] : num_map) {
            if (num_map.count(num+1)) 
                merge(id, num_map[num+1]);
            if (num_map.count(num-1)) 
                merge(id, num_map[num-1]);
        }
        return *max_element(cnt.begin(), cnt.end());
    }
};
案例10:移除最多的同行或同列石头(LeetCode 947)

题目链接947. 移除最多的同行或同列石头 - 力扣(LeetCode)

  • 场景:石头的行和列可能很大(如 1e9),需将行和列视为节点合并。
  • 离散化技巧:将行 r 和列 c 映射为两个唯一键(如 r~c 避免冲突)

代码如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class UnionFind {
public:
    unordered_map<int, int> fa;
    int count = 0; // 总共有多少不连通的并查集

    int find(int x) {    
        // 构建并查集时候,假如key值不在并查集中则构建哈希表映射,count++
        if(fa.find(x) == fa.end()){
            fa[x] = x;
            count++;
        }
        // 查找并查集的头节点并优化
        if(fa[x] != x) fa[x] = find(fa[x]);
        return fa[x];
    }

    // 合并集合
    void unite(int a, int b) { 
        int x = find(a), y = find(b);
        if (x == y) return;
        fa[x] = y;
        count--; 
    }
};
class Solution {
public:
    int removeStones(vector<vector<int>>& stones) {
        UnionFind uf;
        for(auto &stone: stones){
            uf.unite(stone[0] + 10001, stone[1]);
        }
        return stones.size() - uf.count;
    }
};
案例11:按公因数计算最大组件大小(LeetCode 952)

题目链接952. 按公因数计算最大组件大小 - 力扣(LeetCode)

  • 场景:数字的因数可能很大,需合并有公因数的数字。
  • 离散化技巧:将因数作为节点,动态合并,用之前的并查集模板就可以写了
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Solution {
public:
    int largestComponentSize(vector<int>& nums) {
        int m = *max_element(nums.begin(), nums.end());
        UnionFind uf(m + 1);
        for(int num: nums){
            for(int i = 2; i * i <= num; i++){
                if(num % i == 0){
                    // [num, 公因数]
                    uf.unite(num, i); 
                    uf.unite(num, num / i); 
                }
            }
        }

        // 根节点映射
        vector<int> cnt(m + 1);
        int ans = 0;
        for(int num: nums){
            int root = uf.find(num); 
            cnt[root]++;
            ans = max(ans, cnt[root]);
        }
        return ans;
    }
};
案例12:由斜杠划分区域(LeetCode 959)

题目链接959. 由斜杠划分区域 - 力扣(LeetCode)

  • 需求:在分支决策算法中尝试不同路径后回退
  • 实现:在决策分支点保存状态,回溯时撤销操作
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class UnionFind {
private:
    vector<int> fa;    // 父节点数组
    vector<int> rank;  // 秩(树的高度)
    int count = 0;     // 新增:连通块数量
    
public:
    // 1. 初始化
    UnionFind(int n) {   
        fa.resize(n + 1);
        rank.resize(n + 1, 1);
        count = n;      // 初始每个节点都是独立连通块,总共有n个
        for(int i = 1; i <= n; ++i) fa[i] = i;
    }

    // 2. 查找根节点(路径压缩)
    int find(int x) {    
        return fa[x] == x ? x : fa[x] = find(fa[x]);
    }

    // 3. 合并集合(按秩合并)
    bool unite(int a, int b) { 
        int x = find(a), y = find(b);
        if (x == y) return false;

        // 合并时连通块数量减少1
        count--;

        // 按秩合并
        if (rank[x] < rank[y]) {
            fa[x] = y;
        } else {
            fa[y] = x;
            if (rank[x] == rank[y]) rank[x]++;
        }
        return true;
    }
};

class Solution {
public:
    int regionsBySlashes(vector<string>& grid) {
        int n = grid.size();
        //n * n的网格
        //以row = 0...n,col = 0...n 将所有点存入并查集,并将正方形网格轮廓的各个点连接在一起,表示初始时有一个正方形的环
        //再遍历所有的 / & \\ ,根据其位置连接点。连接过程中如果 两个点的根节点相同,说明新形成了一个环
        //(n + 1) (n + 1) 个点
        UnionFind uf((n + 1) * (n + 1));
        int res = 1; // 初始时有一个正方形
        for(int i = 0; i < n; i++){
            uf.unite(i, i + 1); // 最顶行
            uf.unite(n * (n + 1) + i, n * (n + 1) + i + 1); // 最底行
            uf.unite((n + 1) * i, (n + 1) * (i + 1)); // 最左列
            uf.unite((n + 1) * (i + 1) - 1, (n + 1) * (i + 2) - 1); // 最右列
        }
        // 遍历 / 和 '\\'
        char c = ' ';
        int pos = 0;
        for(int i = 0; i < n; i++){
            for(int j = 0; j < n; j++){
                c = grid[i][j];
                pos = i * (n + 1) + j; // 找到左上角的下标
                // 连接右上角和左下角
                if(c == '/'){ 
                    if(!uf.unite(pos + 1, pos + n + 1))res++;
                }
                else if(c == '\\'){
                    if(!uf.unite(pos, pos + n + 2))res++;
                }
            }
        }
        return res;
    }
};

六、工程实践要点

初始化优化

  • 对于连续整数型节点,优先使用数组存储
  • 对于非连续或动态节点,使用哈希表实现

内存管理

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 使用内存池优化动态分配
class MemoryPoolUnionFind {
private:
    vector<int> parent;
    vector<int> rank;
    stack<int> recycled; // 回收的节点索引
public:
    int newNode() {
        if(!recycled.empty()) {
            int idx = recycled.top();
            recycled.pop();
            return idx;
        }
        parent.push_back(parent.size());
        rank.push_back(1);
        return parent.size()-1;
    }
    
    void deleteNode(int x) {
        parent[x] = x; // 重置状态
        rank[x] = 1;
        recycled.push(x);
    }
};

并行化处理

  • 使用分片锁机制实现线程安全
  • 为每个连通分量分配独立锁
  • 采用读写锁优化查询密集型场景

七、扩展思考

1. 逆向并查集

处理动态删除操作的特殊场景:

  • 预先记录所有操作
  • 从最终状态倒序处理,将删除转换为添加
  • 典型应用:离线查询处理
2. 概率化并查集

引入随机化提升性能:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int find(int x) {
    if(parent[x] != x) {
        // 以50%概率进行路径压缩
        if(rand() % 2 == 0) 
            parent[x] = parent[parent[x]];
        return find(parent[x]);
    }
    return x;
}
3. 机器学习应用
  • 图像分割中的区域合并
  • 社交网络中的社区发现
  • 三维建模中的面片合并

通过以上内容的扩展,可以全面掌握并查集的核心原理和进阶应用技巧。建议通过LeetCode相关专题进行实践训练,加深对各类变种问题的理解。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-04-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Nginx下关于缓存控制字段cache-control的配置说明 - 运维小结
HTTP协议的Cache -Control指定请求和响应遵循的缓存机制。在请求消息或响应消息中设置 Cache-Control并不会影响另一个消息处理过程中的缓存处理过程。 请求时的缓存指令包括: no-cache、no-store、max-age、 max-stale、min-fresh、only-if-cached等。 响应消息中的指令包括: public、private、no-cache、no- store、no-transform、must-revalidate、proxy-revalidate、max-age。
洗尽了浮华
2019/05/25
9.3K0
Nginx配置文件中通过location块匹配静态资源类型,并添加缓存响应头
简介 Nginx的location块用于匹配请求URI,可以针对特定类型的静态资源配置缓存策略。
西里网
2025/04/21
4140
Http响应头里Cache-Control:no-cache、max-age=””和no-store
响应头:Cache-Control:no-cache,强制每次请求直接发送给源服务器,而不经过本地缓存版本的校验。这对于需要确认认证应用很有用(可以和public结合使用),或者严格要求使用最新数据 的应用(不惜牺牲使用缓存的所有好处)
全栈程序员站长
2022/07/01
5.5K0
Http响应头里Cache-Control:no-cache、max-age=””和no-store
跟我一起探索 HTTP-HTTP缓存
可复用性有几个优点。首先,由于不需要将请求传递到源服务器,因此客户端和缓存越近,响应速度就越快。最典型的例子是浏览器本身为浏览器请求存储缓存。
用户1418987
2023/10/16
4250
跟我一起探索 HTTP-HTTP缓存
一篇文章带你详解 HTTP 协议之报文首部及字段详解(中)
先来回顾一下首部字段在报文的位置,HTTP 报文包含报文首部和报文主体,报文首部包含请求行(或状态行)和首部字段。 在报文众多的字段当中,HTTP 首部字段包含的信息最为丰富。首部字段同时存在于请求和响应报文内,并涵盖 HTTP 报文相关的内容信息。使用首部字段是为了给客服端和服务器端提供报文主体大小、所使用的语言、认证信息等内容。
java进阶架构师
2018/08/15
2.1K0
一篇文章带你详解 HTTP 协议之报文首部及字段详解(中)
HTTP 的缓存为什么这么设计?
作为前端开发,缓存是整天接触的概念,面试必问、工作中也频繁接触到,可能大家对缓存的 header 记的比较熟了,可是大家有没有思考过为什么 HTTP 的缓存控制要这么设计呢?
神说要有光zxg
2022/06/06
2160
HTTP 的缓存为什么这么设计?
Apache开启 gzip 压缩与配置 Expire/Cache-Control 头
打开apache安装目录,找到conf目录,用记事本打开httpd.conf 文件。
星哥玩云
2022/07/13
7310
你了解 Cache-Control 作为请求头是什么意思吗?
Cache-Control 作为「响应头」,用以控制缓存策略,这也是前端 HTTP 缓存策略的基础。
山月
2022/11/02
3.3K0
你了解 Cache-Control 作为请求头是什么意思吗?
详解web缓存(转)
缓存分为服务端侧(server side,比如 Nginx、Apache)和客户端侧(client side,比如 web browser)。常用的服务端缓存有CDN缓存,客户端缓存就是指浏览器缓存。
山河木马
2019/03/05
6060
详解web缓存(转)
【前端 · 面试 】HTTP 总结(八)—— HTTP 强缓存
强缓存中,当请求再次发出时,浏览器会判断目标资源是否“命中”强缓存,如果命中则直接从缓存中获取资源,不会再与服务端发生通信。
编程三昧
2021/08/08
4250
【前端 · 面试 】HTTP 总结(八)—— HTTP 强缓存
HTTP 缓存
当某一个硬件要读取数据时候,会首先从缓存中查找数据,如果有,直接将数据返回,如果没有再从内存中获取数据。缓存获取数据的速度远比内存快。所以HTTP请求都采用缓存的策略,避免重复请求服务器,增加服务器压力。
Yif
2019/12/25
6751
浅谈CDN网络–之典型的CND架构与HTTP协议的缓存控制
what’s the CDN CDN(content delivery Network) 是一个复杂的系统,我们进行简化抽象来看,就能用下面几步来简单概括: 我们模拟北京电信用户访问我的www.okay686.cn为例(真实服务器在广州的腾讯机房): 北京电信用户请求首选DNS服务器(北京电信DNS服务器),要求解析www.okay686.cn。 如果北京电信DNS服务器没有该域名的缓存,就从该域名的权威域名服务器。如果有这个域名解析记录的缓存,直接返回 okay686.cn的权威域名服务器根据DNS视图技
老七Linux
2018/05/31
1K0
Cache-Control 请求头与浏览器强制刷新
Cache-Control 作为「响应头」,用以控制缓存策略,这也是前端 HTTP 缓存策略的基础。
山月
2023/01/10
2.6K0
Cache-Control 请求头与浏览器强制刷新
response headers添加Cache-Control 和request headers添加Cache-Control 到底有啥区别
缓存是一个复合的概念,本文是http缓存系列文章之一,仅讨论Cache-Control这个字段的两个值:no-cache和max-age=0的区别。关于Cache-Control更全面的介绍,请点击这里。
用户7413032
2020/06/11
5K0
response headers添加Cache-Control 和request headers添加Cache-Control 到底有啥区别
HTTP缓存(Cache-Control、Expires 、ETag)
HTTP缓存( ETag、Cache-Control)——谷歌开发者 HTTP缓存有利于web性能优化
代码之风
2018/10/31
5.6K0
HTTP协议详解
利用 TCP/IP 协议族进行网络通信时,会通过分层顺序与对方进行通信。发送端从应用层往下走,接收端则从链路层往上走。如下:
小柒吃地瓜
2020/04/23
2.2K0
如何用缓存(Cache)进行前端性能优化?
缓存是一种保存资源副本并在下次请求时直接使用该副本的技术,可减少等待时间和网络流量,显著提升网站性能。
Learn-anything.cn
2021/11/30
8610
网站 cache control 最佳实践
通常,是因为 cache control 缓存控制策略定义不正确,导致服务端最新部署之后客户端没有接收到最新的更改。
dys
2020/02/12
1.5K0
网站 cache control 最佳实践
你还在为 HTTP 的这些概念头疼吗?
上一篇文章我们大致讲解了一下 HTTP 的基本特征和使用,大家反响很不错,那么本篇文章我们就全面一下 HTTP 的特性。我们接着上篇文章没有说完的 HTTP 标头继续来介绍(此篇文章会介绍所有标头的概念,但没有深入底层)
cxuan
2020/02/19
2.5K0
你还在为 HTTP 的这些概念头疼吗?
在linux系统下Nginx缓存策略设置方式
在开发调试web的时候,经常会碰到因浏览器缓存(cache)而经常要去清空缓存或者强制刷新来测试的烦恼,提供下apache不缓存配置和nginx不缓存配置的设置。在常用的缓存设置里面有两种方式,都是使用add_header来设置:分别为Cache-Control和Pragma。
用户8989785
2021/10/13
2.4K0
推荐阅读
相关推荐
Nginx下关于缓存控制字段cache-control的配置说明 - 运维小结
更多 >
LV.0
这个人很懒,什么都没有留下~
目录
  • 一、概念与特点
    • 1. 核心概念
    • 2. 时间复杂度
  • 二、应用场景
  • 三、并查集技巧
    • 1. 普通并查集基本模板
    • 2. 带权并查集
      • 2.1 基本概念
      • 2.2 权重更新原理
      • 2.3 合并操作示例
    • 3. 离散化并查集
      • 3.1 基本概念
      • 3.2 案例代码
      • 3.3 适用场景
    • 4. 可撤销并查集
      • 4.1 基本概念
      • 4.2 核心特性
      • 4.3 算法代码
    • 5. 其他进阶技巧
      • 5.1 动态大小处理
      • 5.2 连通块分量统计优化
  • 四、板子训练
    • 1. 合并集合
    • 2. 连通块中点的数量
    • 3. 食物链
  • 五、经典例题
    • 例题1: 省份数量 (LeetCode 547)
    • 例题2: 连通网络的操作次数 (LeetCode 1319)
    • 案例3: 冗余连接 (LeetCode 684)
    • 案例4: 岛屿数量 (LeetCode 200)
    • 案例5: 等式方程的可满足性 (LeetCode 990)
    • 案例6: 除法求值 (LeetCode 399)
    • 案例7: 冗余连接 II (LeetCode 685)
    • 案例8:账户合并 (LeetCode 721)
    • 案例9:最长连续序列(LeetCode 128)
    • 案例10:移除最多的同行或同列石头(LeetCode 947)
    • 案例11:按公因数计算最大组件大小(LeetCode 952)
    • 案例12:由斜杠划分区域(LeetCode 959)
  • 六、工程实践要点
  • 七、扩展思考
    • 1. 逆向并查集
    • 2. 概率化并查集
    • 3. 机器学习应用
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档