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也是同理。

浙公网安备 33010602011771号