7.16小学期基础语法记录

📌 C++ initializer_list

1. 概念与作用

✅ 定义

std::initializer_list 是 C++11 引入的一个 轻量容器类模板,用于 接收一个用花括号 {} 包围的值列表

template <class T>
class initializer_list;
  • 常用于构造函数、函数参数的统一初始化
  • 与 C 风格的 T arr[] = {1,2,3}; 类似,但更加现代化,并且是 类型安全 的。

✅ 典型用途

  1. 构造对象时批量初始化成员变量
  2. 向函数传递一组不定数量的参数
  3. 简化容器初始化(如 std::vector, std::map

2. 语法与使用

✨ 基本用法

#include <initializer_list>
#include <iostream>
using namespace std;

void printList(initializer_list<int> lst) {
    for (auto v : lst) cout << v << " ";
    cout << endl;
}

int main() {
    initializer_list<int> list1 = {1, 2, 3, 4, 5}; // 声明并初始化
    printList({10, 20, 30});                       // 直接传递花括号列表
}

输出:

10 20 30

✨ 常见的使用场景

2.1 构造函数重载

#include <initializer_list>
#include <vector>
using namespace std;

class MyClass {
public:
    MyClass(initializer_list<int> lst) {
        for (auto i : lst) data.push_back(i);
    }
    void show() {
        for (auto i : data) cout << i << " ";
        cout << endl;
    }
private:
    vector<int> data;
};

int main() {
    MyClass obj = {1, 2, 3, 4}; // 直接用花括号列表调用
    obj.show();
}

2.2 与 STL 容器结合

vector<int> v = {1, 2, 3, 4};   // vector自动支持initializer_list
map<int, string> m = {{1, "a"}, {2, "b"}};

2.3 可变参数函数的简化版

相比于 ... 可变参数,initializer_list 类型安全且更简洁:

int sum(initializer_list<int> lst) {
    int s = 0;
    for (auto i : lst) s += i;
    return s;
}

int main() {
    cout << sum({1, 2, 3, 4, 5}); // 输出15
}

3. 底层原理

initializer_list 的数据结构

它并不是一个真正的容器,只是一个 轻量级的包装类,内部保存:

  1. 一个指向首元素的指针 const T\*
  2. 一个元素数量 size_t

所以它本质上是 只读视图,不允许修改元素。


✅ 常用接口

方法 作用
size() 返回元素个数
begin() 返回指向首元素的迭代器
end() 返回尾后迭代器

示例:

initializer_list<int> lst = {1,2,3};
for (auto it = lst.begin(); it != lst.end(); ++it)
    cout << *it << " ";

4. 与其他初始化方式的区别

对比对象 区别
C数组初始化 initializer_list 是类型安全的,支持模板推导,而 C 数组需要明确长度
变长参数(varargs/... initializer_list 在编译期进行类型检查,更安全
普通构造函数重载 如果有 initializer_list 构造函数,会 优先匹配花括号 {}

⚠️ 优先级例子

class Test {
public:
    Test(int a, int b) { cout << "普通构造\n"; }
    Test(initializer_list<int> l) { cout << "initializer_list 构造\n"; }
};

Test t1(1, 2);     // 普通构造
Test t2{1, 2};     // initializer_list 构造 (优先选择)

5. 注意事项与易错点

✅ 1. 只读限制

initializer_list 不允许修改元素

initializer_list<int> lst = {1,2,3};
// *lst.begin() = 10; ❌ 编译错误

✅ 2. 生命周期问题

initializer_list 只是 指针+长度 的包装,对象生命周期由编译器管理,不要返回其引用。

错误示例:

initializer_list<int> badFunc() {
    return {1,2,3}; // ✅ 可以返回副本,但返回引用会悬空
}

✅ 3. 性能注意

由于是浅拷贝(拷贝时只复制指针和大小),性能非常轻量。

✅ 4. 重载冲突

当一个类同时有普通构造函数和 initializer_list 构造函数时,花括号初始化会优先匹配 initializer_list,可能导致意外调用。


6. 高级用法

✨ 1. 模板与泛型

template<typename T>
void print(initializer_list<T> lst) {
    for (auto &v : lst) cout << v << " ";
    cout << endl;
}

print({1,2,3});         // T自动推导为int
print({"a","b","c"});   // T自动推导为const char*

✨ 2. 嵌套初始化(配合容器)

vector<vector<int>> vv = {
    {1, 2, 3},
    {4, 5, 6}
};

✨ 3. 自定义类的赋值重载

支持 赋值运算符的initializer_list重载

class MyVec {
public:
    MyVec& operator=(initializer_list<int> lst) {
        data.assign(lst.begin(), lst.end());
        return *this;
    }
private:
    vector<int> data;
};

MyVec v;
v = {10,20,30};

7. 总结思维导图(文字版)

initializer_list
├── 定义与作用
│   ├── 统一初始化
│   ├── 类型安全的值列表
├── 常用场景
│   ├── 函数参数
│   ├── 构造函数
│   └── STL容器初始化
├── 原理
│   ├── 内部结构:const T* + size_t
│   └── 只读视图,浅拷贝
├── 特性与区别
│   ├── 优先匹配花括号构造
│   ├── 与变长参数相比更安全
├── 注意事项
│   ├── 不可修改元素
│   ├── 生命周期受限
│   └── 重载优先级问题
└── 高级用法
    ├── 模板推导
    ├── 嵌套初始化
    └── 赋值运算符重载

好的,下面 补充一节 ——详细解析 initializer_list 在标准库(STL)中的实际应用与实现原理,帮助你从源码层面理解其重要性。


8. initializer_list 在标准库源码中的应用与实现原理

8.1 STL 中的典型应用场景

1. std::vector 的支持

(1) 直接初始化
std::vector<int> v = {1, 2, 3, 4};

这背后调用的是 vectorinitializer_list 构造函数

explicit vector(std::initializer_list<T> ilist,
                const Allocator& alloc = Allocator());

其源码逻辑大致如下(简化):

vector(std::initializer_list<T> ilist) {
    reserve(ilist.size());          // 预留空间
    for (auto& elem : ilist)
        push_back(elem);            // 逐个拷贝
}

⚠️ 为什么不直接 memcpy?
因为 push_back 保证了元素逐一构造,并能调用 T 的构造函数,而不是简单内存拷贝。


(2) 赋值运算符重载
std::vector<int> v;
v = {5, 6, 7};

背后调用:

vector& operator=(std::initializer_list<T> ilist);

其实现类似于:

vector& operator=(std::initializer_list<T> ilist) {
    assign(ilist.begin(), ilist.end());
    return *this;
}

2. std::map / std::unordered_map

直接用花括号初始化键值对:

std::map<int, std::string> m = {{1, "one"}, {2, "two"}};

对应构造函数:

explicit map(std::initializer_list<value_type> il,
             const Compare& comp = Compare(),
             const Allocator& alloc = Allocator());

源码逻辑:遍历 ilist,调用 insert() 插入键值对。


3. 其他容器

容器 典型用法
std::set std::set<int> s = {1, 2, 3};
std::array std::array<int,3> arr = {1,2,3};
std::string std::string s = {'H','i'};(字符列表)

所有这些都是通过 提供 initializer_list 构造函数重载 实现的。


8.2 源码实现原理分析

1. 头文件定义

initializer_list<initializer_list> 中定义,简化后的源码如下:

template<class T>
class initializer_list {
private:
    const T* _array;     // 指向首元素
    size_t   _size;      // 元素数量

    // 私有构造函数,编译器会在花括号列表初始化时调用
    constexpr initializer_list(const T* a, size_t s)
        : _array(a), _size(s) {}
public:
    using value_type      = T;
    using reference       = const T&;
    using const_reference = const T&;
    using size_type       = size_t;
    using iterator        = const T*;
    using const_iterator  = const T*;

    constexpr initializer_list() noexcept : _array(nullptr), _size(0) {}

    constexpr size_t size()  const noexcept { return _size; }
    constexpr const T* begin() const noexcept { return _array; }
    constexpr const T* end()   const noexcept { return _array + _size; }
};

2. 编译器的角色

编译器在你写 {1,2,3} 时,自动将其转化为一个临时的 initializer_list 对象。

例如:

vector<int> v = {1,2,3};

大致等价于:

const int __temp[] = {1,2,3};
std::initializer_list<int> __il(__temp, 3);
vector<int> v(__il);

⚠️ 注意initializer_list 并不拥有这些元素的所有权,只是指针引用,元素通常是编译器生成的临时静态数组。


3. 生命周期管理

  • 临时数组的生命周期 会延长到 initializer_list 对象销毁为止。
  • 因此 不要保留 initializer_list 中元素的指针,它可能指向已经销毁的内存。

错误示例:

const int* p = nullptr;
{
    initializer_list<int> lst = {1,2,3};
    p = lst.begin();
}
cout << *p; // ❌ 未定义行为

8.3 优先级与陷阱(STL 中常见)

1. 构造函数重载优先级问题

例如 std::vector

std::vector<int> v1(10, 1); // 10个元素,每个值1
std::vector<int> v2{10, 1}; // ❗ 被认为是initializer_list,只有两个元素

这是因为:

  • 圆括号 () 匹配普通构造函数
  • 花括号 {} 优先匹配 initializer_list 构造函数

若要避免歧义,显式指定:

std::vector<int> v3({10, 1}); // initializer_list
std::vector<int> v4(10, 1);   // 普通构造

2. 自动类型推导陷阱

auto 推导时,{} 不会直接推导成 initializer_list,而是推导成 std::initializer_list 需要显式声明:

auto a = {1, 2, 3}; // a的类型是initializer_list<int>

但是不能混合类型:

auto b = {1, 2.0}; // ❌ 错误:initializer_list要求类型统一

8.4 总结(源码层面理解)

  1. 轻量封装:内部只有指针+大小,拷贝成本极低
  2. 编译器自动生成临时数组:不需要手动管理内存
  3. 容器普遍提供对应的构造与赋值重载,便于统一初始化
  4. 花括号优先级高:导致与传统构造函数的重载选择不同
  5. 生命周期受限:元素多为临时对象,不可长期保存指针


🚲 std::bitset 基础概念

std::bitset 是 C++ 标准库中提供的一种 定长二进制位集容器,它适合用于存储、操作和高效处理一组 二进制标志(bit flags),在算法、位运算优化、状态压缩等场景非常常用。下面我从定义、常用操作、底层原理、高级用法、易错点几个方面详细讲解。


✅ 1. 基本定义

#include <bitset>

std::bitset<N> bs;        // 创建一个大小为 N 位的 bitset,每一位初始化为 0
std::bitset<N> bs(val);   // val 可以是 unsigned long long,表示用其二进制初始化
std::bitset<N> bs(str);   // str 是由 '0' 和 '1' 组成的字符串,从右向左赋值

示例:

std::bitset<8> b1;           // 00000000
std::bitset<8> b2(5);        // 00000101(二进制形式)
std::bitset<8> b3("10101");  // 00010101

✅ 2. 常用方法与操作

2.1 位访问与修改

操作 示例 说明
bs[i] bs[3] = 1; 下标从 0 开始,对应二进制的 最低位
bs.test(i) if(bs.test(3)) 判断第 i 位是否为 1
bs.set(i) bs.set(2); 将第 i 位设置为 1
bs.set() bs.set(); 所有位设置为 1
bs.reset(i) bs.reset(2); 将第 i 位设置为 0
bs.reset() bs.reset(); 所有位清零
bs.flip(i) bs.flip(2); 反转第 i 位
bs.flip() bs.flip(); 所有位取反

示例:

std::bitset<8> bs(5);    // 00000101
bs.set(2);               // 00000101 -> 00000101 (本来就是1)
bs.flip(0);              // 00000100
bs.reset(2);             // 00000000

2.2 属性查询

方法 说明
bs.count() 返回 1 的数量
bs.any() 是否存在 1
bs.none() 是否全为 0
bs.all() 是否全为 1
bs.size() 返回 bitset 的位数

示例:

std::bitset<8> bs("101100");
std::cout << bs.count();  // 3
std::cout << bs.any();    // true
std::cout << bs.none();   // false
std::cout << bs.size();   // 8

2.3 数值与字符串转换

方法 说明
bs.to_ulong() 转为 unsigned long注意溢出
bs.to_ullong() 转为 unsigned long long,比 to_ulong 安全
bs.to_string() 转为由 '0''1' 组成的字符串,高位在前

示例:

std::bitset<8> bs("101");
std::cout << bs.to_ulong();   // 输出 5
std::cout << bs.to_string();  // 输出 "00000101"

2.4 运算符支持

bitset 支持按位运算(与整数类似):

~bs         // 按位取反
bs1 & bs2   // 按位与
bs1 | bs2   // 按位或
bs1 ^ bs2   // 按位异或
bs << n     // 左移 n 位
bs >> n     // 右移 n 位

示例:

std::bitset<8> a("1010");
std::bitset<8> b("1100");
auto c = a & b;  // 1000
std::cout << c;  // 00001000

✅ 3. 典型应用场景

(1) 状态压缩/子集枚举

适合处理 n ≤ 64/128 的状态集合:

int n = 5;
for(int i = 0; i < (1 << n); i++){
    std::bitset<5> bs(i);
    std::cout << bs << '\n';
}

(2) 快速筛法(素数筛/并集运算)

vector<bool> 更高效:

const int N = 1e6;
std::bitset<N> isPrime;
isPrime.set();     // 全部置 1
isPrime[0] = isPrime[1] = 0;
for (int i = 2; i * i < N; i++){
    if(isPrime[i]) 
        for (int j = i * i; j < N; j += i)
            isPrime[j] = 0;
}

(3) 快速集合运算

std::bitset<8> s1("10101010");
std::bitset<8> s2("11001100");
auto inter = s1 & s2; // 交集
auto uni   = s1 | s2; // 并集
auto diff  = s1 ^ s2; // 对称差

(4) 图算法/DP优化

状压DP传递闭包快速布尔矩阵乘法中用 bitset 可显著优化,例如:

  • 传递闭包 Floyd 的优化:g[i] |= g[k] & g[i];

✅ 4. 性能与底层原理

  • 底层实现通常是定长数组,内部用 unsigned long / unsigned long long 做块状存储,比 vector<bool> 高效且无特殊代理引用。
  • 大小在编译期固定,不支持动态调整(要动态大小请用 std::vector<bool>boost::dynamic_bitset)。
  • 位运算在硬件级别并行执行,适合大规模布尔运算。

✅ 5. 易错点与注意事项

  1. 固定大小,编译期确定

    int n; std::cin >> n;
    std::bitset<n> bs;  // ❌ 不允许,n 必须是编译期常量
    
  2. to_ulong 溢出
    如果 bitset 超过 unsigned long 能表示的范围,会抛出 std::overflow_error,推荐用 to_ullong

  3. 下标方向
    bs[0]最低有效位(右侧),容易与字符串直观方向搞混。

  4. 大规模时内存占用
    对于 bitset<1e7>,占用约 1.25MB,内存分配在栈上可能爆栈,应使用静态/全局变量。


✅ 6. 进阶:自定义函数封装

例:并查集中的快速集合合并

struct BitsetDSU {
    std::vector<std::bitset<1000>> sets;
    void unite(int a, int b) {
        sets[a] |= sets[b];   // 快速并集
    }
    bool query(int a, int b) {
        return (sets[a] & sets[b]).any(); // 判断是否有交集
    }
};

好的,Eigh18n,我给你整理一份《图论+DP中 bitset 优化模板合集》,这份笔记尽量全面、详细、可直接套用,适合算法竞赛和工程实践。内容按常见问题场景分类,并且每个模板会包含:适用场景、核心思路、复杂度分析、代码模板、注意事项



🗺️ 图论 + DP 中 bitset 优化模板合集


1. Floyd 传递闭包优化

适用场景

  • 求有向图的传递闭包(可达性矩阵)。
  • 求是否存在路径可达性
  • 动态规划中,判断状态间是否存在可转移关系

核心思路

  • Floyd 的本质是:若 i→ki\to k 且 k→jk\to j,则 i→ji\to j。
  • bitset 表示一行 g[i]g[i]:g[i][j]=1 表示 i 可达 j。
  • 更新时:
    如果 i 能到 k,那么 i 可达集合 += k 的可达集合。
    g[i] |= g[k]

复杂度

  • 原始复杂度:O(n3)O(n^3)
  • 优化后:O(n3/64)O(n^3 / 64) (硬件并行加速)

模板代码

#include <bits/stdc++.h>
using namespace std;

const int N = 2000;   // 最大节点数
bitset<N> g[N];       // g[i][j] = i 是否可达 j

void floyd_bitset(int n) {
    for (int k = 0; k < n; k++) {
        for (int i = 0; i < n; i++) {
            if (g[i][k])   // 若 i->k 可达
                g[i] |= g[k]; // 把 k 的可达集合并入 i
        }
    }
}

注意事项

  1. 初始化:若有边 i→ji\to j,则 g[i][j]=1g[i][i]=1(自环)。
  2. n 很大时:bitset 在栈上可能爆栈,建议用 全局变量

2. 布尔矩阵乘法(多步可达性 / 快速幂)

适用场景

  • 求“经过 k 步是否可达”。
  • 图的传递闭包的快速幂(k 步可达性)。
  • 状态转移矩阵的布尔乘法。

核心思路

  • 布尔矩阵乘法定义:
    C[i][j]=⋁k(A[i][k]∧B[k][j])C[i][j] = \bigvee_{k} (A[i][k] \land B[k][j])
  • 通过转置列存储减少随机访问。

模板代码

const int N = 500;
bitset<N> A[N], B_T[N], C[N]; // B_T 是 B 的转置

void boolean_matrix_multiply(int n) {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            if ((A[i] & B_T[j]).any())  // 按位与后判断是否有交集
                C[i][j] = 1;
        }
    }
}

快速幂:k 步可达性

// 矩阵快速幂求 A^k
vector<bitset<N>> mat_pow(vector<bitset<N>> base, long long k){
    vector<bitset<N>> res = base;
    while(--k){
        // res = res * base
        vector<bitset<N>> tmp(N);
        for(int i=0;i<N;i++){
            for(int j=0;j<N;j++){
                if((res[i] & base[j]).any())
                    tmp[i][j] = 1;
            }
        }
        res = tmp;
    }
    return res;
}

3. 背包型 DP 优化(0-1 背包 / 子集和问题)

适用场景

  • 判断能否恰好装满重量 W。
  • 求最大可达重量。
  • 多次物品选取(需要背包 DP)。

核心思路

  • dp[i] 表示“能否达到重量 i”。
  • 状态转移:
    传统dp[i+w]=true if dp[i]=true
    bitsetdp |= dp << w

模板代码

const int MAXW = 100000;
bitset<MAXW+1> dp;
vector<int> weight = {1,3,5,7}; 

void knapsack_bitset(int W){
    dp.reset();
    dp[0] = 1;
    for(int w : weight)
        dp |= (dp << w);  // 批量更新
    cout << (dp[W] ? "YES" : "NO");
}

复杂度

  • O(n * W / 64) → 对 W=1e5,n=1e3 也能轻松通过。

4. Hamilton 路径 / 状压 DP 可达性优化

适用场景

  • TSP(旅行商问题)仅判定是否存在 Hamilton 路径。
  • 大规模 n≈20~25 的可达性问题

核心思路

  • bitset 批量转移:
    dp[v][mask] = true if dp[u][mask^(1<<v)] && g[u][v]

模板代码

const int N = 20;
bitset<1<<N> dp[N];
bool g[N][N];

void hamilton_bitset(int n){
    dp[0][1<<0] = 1;  // 起点
    for(int mask=1;mask<(1<<n);mask++){
        for(int v=0;v<n;v++){
            if(!(mask>>v & 1)) continue;
            for(int u=0;u<n;u++){
                if(u!=v && (mask>>u &1) && g[u][v] && dp[u][mask^(1<<v)])
                    dp[v][mask] = 1;
            }
        }
    }
}
  • 可用 bitset 压缩 dp 的第二维(这里略)。

5. 最大团 / 最大独立集搜索优化

适用场景

  • 无向图求最大团或最大独立集。

核心思路

  • 经典回溯:逐点枚举 + 剪枝。
  • bitset 存储当前候选集合,快速取交集。

模板代码

const int N = 100;
bitset<N> G[N];
int ans=0, n;

void dfs(bitset<N> cand, int cnt){
    if(cand.none()) {
        ans = max(ans, cnt);
        return;
    }
    while(cand.any()){
        int v = cand._Find_first(); // 找最低位1
        cand.reset(v);
        dfs(cand & G[v], cnt + 1);  // 只保留与v相连的点
    }
}

复杂度

  • 实测可优化 10~100 倍,适合 n ≤ 100 的图。

6. BFS 层次优化(多源最短路)

适用场景

  • 多源最短路(无权图)。
  • 多次批量扩展。

核心思路

  • 当前层节点 + 邻接表的按位或,代替逐点遍历。

模板代码

const int N = 5000;
vector<bitset<N>> adj;
bitset<N> cur, nxt, vis;

void bfs_bitset(vector<int> sources, int n){
    cur.reset(); vis.reset();
    for(int s : sources) { cur.set(s); vis.set(s); }
    int d=0;
    while(cur.any()){
        nxt.reset();
        for(int i=0;i<n;i++)
            if(cur[i]) nxt |= adj[i]; // 批量加入邻居
        nxt &= ~vis;
        vis |= nxt;
        cur = nxt;
        d++;
    }
}

7. 二分图匹配的匈牙利算法优化

适用场景

  • 稠密图下的二分图最大匹配。

核心思路

  • 用 bitset 表示每个点的邻居集,增广路搜索时快速找到未访问点。

模板代码简要

const int N = 500;
bitset<N> adj[N], used;
int match[N];

bool dfs(int u){
    bitset<N> cand = adj[u] & ~used;
    while(cand.any()){
        int v = cand._Find_first();
        cand.reset(v);
        used.set(v);
        if(match[v]==-1 || dfs(match[v])){
            match[v]=u; return true;
        }
    }
    return false;
}

8. 位运算 DP(子集卷积 / 状态转移)

适用场景

  • 集合卷积问题:求 f(S)=Σg(T)h(S\T)。
  • 或/与/异或子集卷积:与 SOS DP 配合。

思路简述

  • bitset 直接存储 g(T),卷积时用按位运算和批量更新。

(这类模板更复杂,我可以单独详细展开 SOS DP + bitset 版。)


总结表

算法场景 关键操作 时间复杂度
Floyd 传递闭包 `g[i] = g[k]`
布尔矩阵乘法 (A[i] & B_T[j]).any() O(n3/64)O(n^3/64)
0-1 背包 `dp = dp << w`
Hamilton 路径可达 `dp[v] = dp[u]<<v`
最大团搜索 dfs(cand & G[v]) 剪枝后约 O(1.2n)O(1.2^n)
BFS 层次 `nxt = adj[i]`
匈牙利算法 cand = adj[u] & ~used O(n2/64)O(n^2/64)

posted @ 2025-07-16 20:58  十八Eigh18n  阅读(17)  评论(0)    收藏  举报