C++ 极速复习

这是一篇关于C++的快速Review。默认C++17-O2。大概是工具/库这种东西和算法分成两个部分来讲,最后加一点复杂度分析。

1 工具/库

1.1 STL

1.1.1 数据结构

string

字符串通常就是用三个函数:s.substr(),s.find(),s.compare()

string s = "abcdef";
string t = s.substr(2, 3); // "cde"

size_t p = s.find("abc");
if (p != string::npos) // found

string a = "abc";
string b = "abd";
if (a.compare(b) == 0) // equal

注意s.find()s.size(),s.length()一样,返回的是size_t类型。


vector

就是Array数组,但是更优。动态。可以嵌套。经常会看到

vector<vector<int>> v(m,vector<int>(n,0));

这么初始化的。

插入元素用v.push_back(),在分配堆空间之前不要用索引,除非提前v.resize(n)了。
获取长度用

int n=(int) v.size();

或者

size_t n=v.size();

v.length()也行。


stack

Stack是LIFO数据结构的一种实现,一般用st.push(),st.pop(),st.top()三个函数。


queue

Queue是FIFO数据结构的一种实现,一般用q.push(),q.pop(),q.front()三个函数。


priority_queue

优先队列是Heap数据结构的实现,一般用pq.push(),pq.pop(),pq.top()三个函数。

注意priority_queue的优先级方向是从下往上,这意味着greater是小顶堆(下面的greater than上面的)。


哈希表unordered_set和unordered_map

哈希表原理是根据一个输入,通过某个哈希函数算出一个key,这个key就是索引。哈希表是个优化版的桶排序。常见写法:

unordered_set<int> s;
s.reserve(n);
for (int i = 0; i < n; ++i)
    s.insert(d.find(i));

用于去重。

unordered_map<int, vector<int>> graph;
for (const auto& edge : edges)
{
    int a = edge[0], b = edge[1];
    graph[a].push_back(b);
    graph[b].push_back(a);
}

是一种邻接表写法。

注意这里的find()返回的是迭代器。


红黑树set和map

这个比哈希表多出来的有序部分可以二分查找:

for (auto it = s.lower_bound(3); it != s.upper_bound(7); ++it) // [3,7]

1.1.2 算法

sort

内部是快排实现。默认是less升序(前面的less than后面的)。降序写法是:

sort(v.begin(), v.end(), greater<int>());

也可以结合lambda表达式:

sort(v.begin(), v.end(), [](const pair<int,int>& a, const pair<int,int>& b)
{
    if (a.first != b.first)
        return a.first < b.first;
    return a.second > b.second;
});

自定义排序逻辑。


lower_bound和upper_bound

是简单二分查找(左闭右开)的封装。

auto l = lower_bound(v.begin(), v.end(), x);
auto r = upper_bound(v.begin(), v.end(), x);
int cnt = r - l;   // x times

注意返回的是迭代器类型,如果要取索引:

int i = lower_bound(v.begin(), v.end(), x) - v.begin();

1.2 通用

引用

也叫“别名”或者“取地址”。

int x = 5;
int& r = x;
for(const auto&i : s) // do

在定义变量,传参或者迭代器里面常见,减少不必要的内存。另外,对迭代器加const也是好习惯。const函数则是维护成员值不修改。


结构化绑定

这个在pair, tuple作为元素时非常有用。

pair<int,int> p = {3, 5};
auto [x, y] = p;

map<int,int> mp = {{1,10}, {2,20}};
for (auto& [k, v] : mp)
    v += 1;

然而C++并不像Python可以用_丢弃变量。


迭代器

迭代器就是指针*it


range-for

是比for更快速的遍历。

for (int i : v) // do

这个处理不了索引,是直接取值。


lambda匿名函数

lambda函数等价于懒得写一个真函数。这个经常写在自定义排序里面。可以捕获外部变量:

int a = 10;
auto f = [a](int x){return a + x;};

2 算法

2.1 图

很多图算法的邻接表是\(\mathcal O(E\log E)\)适合稀疏图(稠密就是\(\mathcal O(V^2\log V)\)了),邻接矩阵\(\mathcal O(V^2)\)适合稠密图。一般邻接表用的更多,写作vector<vector<pair<int,int>>>这种类型。

2.1.1 并查集DSU

DSU有size和rank两种变体,size更常用。

模板:

class Solution 
{
private:
    vector<int> parent, sz;

    int find(int x) 
    {
        if (parent[x] != x)
            parent[x] = find(parent[x]);
        return parent[x];
    }

    bool unite(int x, int y) 
    {
        x = find(x);
        y = find(y);
        if (x == y) return false;
        if (sz[x] < sz[y]) swap(x, y);
        parent[y] = x;
        sz[x] += sz[y];
        return true;
    }

public:
    void init(int n) 
    {
        parent.resize(n);
        sz.assign(n, 1);
        iota(parent.begin(), parent.end(), 0);
    }

    bool same(int x, int y) 
    {
        return find(x) == find(y);
    }

    int size(int x) 
    {
        return sz[find(x)];
    }

    int countComponents(int n, vector<vector<int>>& edges) 
    {
        init(n);
        for (auto& e : edges) 
            unite(e[0], e[1]);
        int cnt = 0;
        for (int i = 0; i < n; ++i) 
            if (find(i) == i) cnt++;
        return cnt;
    }
};

以连通分支计数为例,一般用DFS/BFS(DSU比DFS常数大)。

2.1.2 深搜DFS

注意不是所有DFS都要回溯(路径搜索需要,节点搜索不需要)。
一般使用递归写法。

节点搜索模板:

dfs(u):
    visited[u] = true
    for v in neighbors(u):
        if not visited[v]:
            dfs(v)

路径搜索模板:

dfs(state):
    if 是解:
        记录解
        return
    for choice in choices(state):
        做选择(choice)
        dfs(新 state)
        回溯(choice)

DFS的路径搜索优势是空间复杂度低于BFS。

2.1.3 广搜BFS

即使是多源BFS也只用一个队列。也分路径搜索和节点搜索。
一般使用队列写法。

节点搜索模板:

queue Q
Q.push(start)
visited[start] = true
dist[start] = 0
while Q 非空:
    u = Q.pop()
    for v in neighbors(u):
        if not visited[v]:
            visited[v] = true
            dist[v] = dist[u] + 1
            Q.push(v)

路径搜索模板:

queue Q
Q.push([start])
while Q 非空:
    P = Q.pop()
    u = P.last()
    for v in neighbors(u):
        if v not in P:
            P' = P + [v]
            Q.push(P')

2.1.4 最小生成树MST

MST算法原理都是Cut Property,一个用并查集,一个用堆(Prim像Dijkstra)。

Kruskal

模板:

class Solution
{
private:
    vector<int> parent, rankv;

    int find(int x) 
    {
        if (parent[x] != x)
            parent[x] = find(parent[x]);
        return parent[x];
    }

    bool unite(int x, int y) 
    {
        x = find(x);
        y = find(y);
        if (x == y) return false;
        if (rankv[x] < rankv[y]) 
            parent[x] = y;
        else if (rankv[x] > rankv[y]) 
            parent[y] = x;
        else 
        {
            parent[y] = x;
            rankv[x]++;
        }
        return true;
    }

public:
    int kruskal(int n, vector<vector<int>>& edges) {
        parent.resize(n);
        rankv.assign(n, 0);
        iota(parent.begin(), parent.end(), 0);
        sort(edges.begin(), edges.end(), [](auto& a, auto& b){return a[2] < b[2];});
        int mst = 0;
        int cnt = 0;
        for (auto& e : edges) 
        {
            if (unite(e[0], e[1])) 
            {
                mst += e[2];
                cnt++;
                if (cnt == n - 1) break;
            }
        }
        if (cnt != n - 1) return -1;
        return mst;
    }
};

Prim

模板:

class Solution 
{
public:
    int prim(int n, vector<vector<pair<int,int>>>& adj) {
        vector<bool> vis(n, false);
        priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;
        pq.push({0, 0});
        int mst = 0;
        int cnt = 0;
        while (!pq.empty()) 
        {
            auto [w, u] = pq.top(); pq.pop();
            if (vis[u]) continue;
            vis[u] = true;
            mst += w;
            cnt++;
            for (auto& [v, wt] : adj[u]) 
            {
                if (!vis[v])
                    pq.push({wt, v});
            }
        }
        if (cnt != n) return -1;
        return mst;
    }
};

细节是关于if (vis[u]) continue;的冗余处理,也可以用if (key[u]!=w)来判断。选择其一即可。

2.1.5 单源最短路SSSP

Floyd-Warshall

模板:

class Solution 
{
public:
    vector<vector<long long>> floyd(int n, vector<vector<long long>> dist) 
    {
        const long long INF = (1LL<<60);
        for (int k = 0; k < n; ++k) 
        {
            for (int i = 0; i < n; ++i) 
            {
                if (dist[i][k] == INF) continue;
                for (int j = 0; j < n; ++j) 
                {
                    if (dist[k][j] == INF) continue;
                    long long cand = dist[i][k] + dist[k][j];
                    if (cand < dist[i][j]) dist[i][j] = cand;
                }
            }
        }
        return dist;
    }
};

Dijkstra

模板:

class Solution 
{
public:
    vector<long long> dijkstra(int n, vector<vector<pair<int,int>>>& adj, int src) 
    {
        const long long INF = (1LL<<60);
        vector<long long> dist(n, INF);
        priority_queue<pair<long long,int>, vector<pair<long long,int>>, greater<pair<long long,int>>> pq;
        dist[src] = 0;
        pq.push({0, src});
        while (!pq.empty()) 
        {
            auto [d, u] = pq.top(); pq.pop();
            if (d != dist[u]) continue;
            for (auto& [v, w] : adj[u]) 
            {
                if (dist[v] > d + w) 
                {
                    dist[v] = d + w;
                    pq.push({dist[v], v});
                }
            }
        }
        return dist;
    }
};

和Prim算法一样,可以用一个vis数组去除堆里的过期条目。Dijkstra的贪心性质导致没法做负权问题。理论上Dijkstra是最快的解法。

Bellman-Ford

模板:

class Solution 
{
public:
    vector<long long> bellmanFord(int n, vector<vector<int>>& edges, int src, bool& negCycle) 
    {
        const long long INF = (1LL<<60);
        vector<long long> dist(n, INF);
        dist[src] = 0;
        negCycle = false;
        for (int i = 0; i < n - 1; ++i) 
        {
            bool changed = false;
            for (auto& e : edges) 
            {
                int u = e[0], v = e[1], w = e[2];
                if (dist[u] == INF) continue;
                if (dist[v] > dist[u] + w) 
                {
                    dist[v] = dist[u] + w;
                    changed = true;
                }
            }
            if (!changed) break;
        }
        for (auto& e : edges) 
        {
            int u = e[0], v = e[1], w = e[2];
            if (dist[u] == INF) continue;
            if (dist[v] > dist[u] + w) 
            {
                negCycle = true;
                break;
            }
        }
        return dist;
    }
};

这是一种DP,但是k-1轮被k滚动掉了所以只用一维DP。

2.2 动态规划DP

DP适用于有重叠结构的子问题。(在数学里似乎叫数列递推式?高中数学?)和这种问题相反的没有重叠子结构的叫“归并”算法。
DP有两种写法,recursion(就是记忆化数组memoization)和iteration(狭义的动态规划)。一般用后者,通常更快。
核心是边界条件+转移方程。
经典的DP问题如下:

Paint Fence

class Solution 
{
public:
    int numWays(int n, int k) 
    {
        vector<int> dp(n,0);
        if(n==1) return k;
        if(n==2) return k*k;
        dp[0]=k;
        dp[1]=k*k;
        for(int i=2;i<n;i++)
            dp[i]=(k-1)*(dp[i-1]+dp[i-2]);
        return dp[n-1];
    }
};

2.3 二叉搜索BS

BS适用于单调问题。
简单的STL已经封装成lower_bound了。复杂一些的需要手写。
经典的BS问题如下:

Koko Eating Bananas

class Solution {
public:
    int minEatingSpeed(vector<int>& piles, int h) 
    {
        sort(piles.begin(),piles.end());
        int n=piles.size();
        int l=1;
        int r=piles[n-1];
        while(l<r)
        {
            int mid=l+(r-l)/2;
            if(check(piles,h,mid))
                r=mid;
            else
                l=mid+1;
        }
        return r;
    }
private:
    bool check(vector<int>& piles, int h, int k)
    {
        int time=0;
        for(int i: piles)
            time+=ceil(1.0*i/k);
        if(time<=h)
            return true;
        return false;
    }
};

2.4 堆Heap

Heap适用于需要一直维护极值的问题。
一般用priority_queue
经典的Heap问题如下:

Meeting Rooms II

class Solution {
public:
    int minMeetingRooms(vector<vector<int>>& intervals) 
    {
        if (intervals.empty()) return 0;
        sort(intervals.begin(), intervals.end());
        priority_queue<int, vector<int>, greater<int>> pq;
        for (const auto& it : intervals) 
        {
            int s = it[0], e = it[1];
            if (!pq.empty() && pq.top() <= s)
                pq.pop();
            pq.push(e);
        }
        return (int)pq.size();
    }
};

Find Median from Data Stream

class MedianFinder 
{
private:
    priority_queue<int> low;
    priority_queue<int, vector<int>, greater<int>> high;

public:
    MedianFinder() = default;
    void addNum(int num) 
    {
        if (low.empty() || num <= low.top())
            low.push(num);
        else
            high.push(num);
        if (low.size() > high.size() + 1) 
        {
            high.push(low.top());
            low.pop();
        } 
        else if (high.size() > low.size()) 
        {
            low.push(high.top());
            high.pop();
        }
    }

    double findMedian() const 
    {
        if (low.size() > high.size())
            return static_cast<double>(low.top());
        return (static_cast<double>(low.top()) + static_cast<double>(high.top())) / 2.0;
    }
};

3 复杂度

for (int i = 1; i <= n; ++i) 
{
    for (int j = i; j <= n; j += i) 
    {
        // do
    }
}

时间\(\mathcal O(n\log n)\)

while (!pq.empty()) 
{
        auto [d, u] = pq.top(); pq.pop();
        if (d != dist[u]) continue;
        for (auto& [v, w] : adj[u]) 
    {
        if (dist[v] > d + w) 
        {
            dist[v] = d + w;
            pq.push({dist[v], v});
        }
    }
}

时间\(\mathcal O(E\log E)\)。 为什么?因为不是所有for都跑了一遍。最多只跑\(\mathcal O(E)\)次入堆,堆的重排一次是\(\mathcal O(\log E)\)

void dfs(int u) 
{
    visited[u] = true;
    for (int v : adj[u]) 
    {
        if (!visited[v])
            dfs(v);
    }
}

时间\(\mathcal O(V+E)\)。 为什么?因为这是节点搜索型DFS,有剪枝。路径搜索的DFS确实是指数级。BFS也是同理。

posted @ 2026-01-24 13:51  rainrzk  阅读(1)  评论(0)    收藏  举报