【图的存储与遍历】
算法模板
邻接矩阵
// 邻接矩阵存储
for (int i=1; i<=n; i++)
{
int a, b;
cin >> a >> b; // 父节点是a,子节点是b
tree[a][b]=1;
}
邻接表
// vector基本操作
vector<int> g;
g.front(); // 返回容器g中的第一个元素
g.back(); // 返回容器g中的最后一个元素
g[i]; // 返回容器g中的第i+1个元素(前提是存在g[i]);
g.clear(); // 清空容器g
g.empty(); // 判断容器g是否为空
g.pop_back(); // 删除容器g中的最后一个元素
g.push_back(x); // 在容器g的尾部插入一个值为x的元素
g.size(); // 返回容器g中的元素个数
sort(g.begin(), g.end()); // 将容器g里的所有元素从小到大排序
g.resize(10); // 将容器g中的元素个数调整为10个,多则删除、少则补随机值
g.resize(10, 2); // 将容器g中的元素个数调整为10个,多则删除,少则补2
// 邻接表存储
vector<int> child[N];
for (int i=1; i<=n; i++)
{
int a, b;
cin >> a >> b;
child[a].push_back(b);
}
链式前向星
// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx;
// 添加一条边a->b
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
// 初始化
idx = 0;
memset(h, -1, sizeof h); // main函数中需执行
for (int i=1; i<=n; i++)
{
int a, b;
cin >> a >> b;
add(a, b);
}
// 链式前向星加带权重的边
int idx; //初始为0
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// 打印所有节点与边的关系
void print_edges(int x) {
cout << "所有边(链式前向星存储):" << endl;
for (int u = 0; u < x; u++) {
cout << "节点 " << u << " 的出边: ";
bool has_edge = false;
for (int i = h[u]; i != -1; i = ne[i]) {
int v = e[i];
int weight = w[i];
cout << "(" << u << "->" << v << ", w=" << weight << ", idx=" << i << ") ";
has_edge = true;
}
if (!has_edge) {
cout << "无";
}
cout << endl;
}
}
// 链式前向星记录起始和终点
// from记录起始节点,to记录终止节点
void add(int a, int b)
{
from[idx] = a, to[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
【枚举-图】
算法模板
// 找树根
for (int i=1; i<=n; i++)
{
if (pa[i]!=0)
{
root = pa[i];
while (pa[root]!=0)
root = pa[root];
break;
}
}
cout << root << endl; // root即为根
算法应用
| 题目来源 |
标题 |
难度 |
星级 |
考察算法 |
一句话思路总结 |
| AcWing |
1254 找树根和孩子 |
简单 |
⭐ |
树的双亲表示法 + 简单枚举 |
用 par[y]=x 记录双亲关系,根结点为无双亲且作为双亲出现过的结点,孩子最多的结点通过统计每个结点的出度(孩子数)确定,最后按编号升序输出其孩子,时间复杂度 \(O(N+M)\)。 |
| 洛谷 |
AT_abc393_c Make it Simple |
普及- |
|
|
|
【DFS-树】
算法模板
二叉树的遍历
int n, a[N], r[N], l[N]; // n: 节点数量,a: 节点值,l: 左孩子数组,r: 右孩子数组
void preorder(int root) // 先序遍历
{
if (a[x]==0) return;
cout << a[x] << " ";
preoder(l[x]);
preoder(r[x]);
}
void inorder(int x) // 中序遍历
{
if (a[x]==0) return;
inorder(l[x]);
cout << a[x] << " ";
inorder(r[x]);
}
void postorder(int root) // 后序遍历
{
if (a[x]==0) return;
postorder(l[x]);
postorder(r[x]);
cout << a[x] << " ";
}
二叉搜索树
// 插入结点到二叉搜索树
void insert(int x, int i)
{
if (a[i] < a[x]) // 如果要插入的节点值小于当前节点值
{
if (l[x] == 0) // 如果当前节点没有左孩子
l[x] = i; // 将节点i作为左孩子
else
insert(l[x], i); // 递归插入到左子树
}
else // 如果要插入的节点值大于等于当前节点值
{
if (r[x] == 0) // 如果当前节点没有右孩子
r[x] = i; // 将节点i作为右孩子
else
insert(r[x], i); // 递归插入到右子树
}
}
算法应用
【DFS-图】
算法模板
算法应用
【BFS-图】
算法模板
// 链式前向星构图
void bfs()
{
memset(dist, 0x3f, sizeof(dist));
queue<int> q;
dist[st] = 0;
q.push(st);
while (!q.empty())
{
int u = q.front(); q.pop();
for (int i=h[u]; i!=-1; i=ne[i])
{
int v = e[i];
if (dist[v]>dist[u]+1)
{
dist[v] = dist[u] + 1;
q.push(v);
}
}
}
}
// 邻接表
void bfs()
{
memset(dist, 0x3f, sizeof(dist));
queue<int> q;
dist[st] = 0;
q.push(st);
while (!q.empty())
{
int u = q.front(); q.pop();
for (int i=0; i<a[u].size(); i++)
{
int v = a[u][i];
if (dist[v]>dist[u]+1)
{
dist[v] = dist[u] + 1;
q.push(v);
}
}
}
}
算法应用
| 题目来源 |
标题 |
难度 |
星级 |
考察算法 |
一句话思路总结 |
| AcWing |
514 寻找道路 |
|
|
|
|
| AcWing |
847 图中点的层次 |
简单 |
⭐⭐ |
BFS(无权图最短路径) |
用邻接表(链式前向星或 vector)存储有向图,add(a,b) 时 e[idx]=b, ne[idx]=h[a], h[a]=idx++;BFS 从起点 \(1\) 开始,d 数组初始化为 \(-1\) 表示未访问,d[1]=0,每次取出队首节点后遍历其所有邻接点,若 d[j]==-1 则更新 d[j]=d[t]+1 并入队;由于所有边权为 \(1\),BFS 第一次到达节点 \(n\) 时的距离即为最短路径,输出 d[n](若仍为 \(-1\) 表示不可达);时间复杂度 \(O(n+m)\)。 |
| AcWing |
920 最优乘车 |
|
|
|
|
| AcWing |
1131 拯救大兵瑞恩 |
|
|
|
|
| AcWing |
1134 最短路计数 |
|
|
|
|
| AcWing |
1164 加工零件 |
|
|
|
|
| AcWing |
6048 家庭问题 |
|
|
|
|
| 洛谷 |
AT_abc376_d Cycle |
普及 |
|
|
|
| 洛谷 |
[[P1673 Part Acquisition]] |
普及 |
|
|
|
| 洛谷 |
P2296 寻找道路 |
普及 |
|
|
|
| 洛谷 |
P3916 图的遍历 |
普及 |
|
|
|
| 洛谷 |
[[P6770 Checking an Alibi 不在场的证明]] |
普及 |
|
|
|
| 洛谷 |
P11280 Jom & Terry |
普及 |
⭐⭐ |
BFS(无权图最短距离) + 博弈论分析 |
从根节点 \(r\) 出发 BFS 计算每个节点到根的最短距离 dist[i];博弈分析发现 Terry 能到达根的条件是 dist[a] <= dist[b](Terry 到根的距离不超过 Jom 到根的距离),因为 Terry 先手且双方每回合最多走一步,距离更近的一方可以先到达目标;每次查询 \(O(1)\) 比较距离输出 Terry 或 Jom;先输出 I'm here!,BFS 时间复杂度 \(O(n+m)\),总时间复杂度 \(O(n+m+q)\)。 |
| 洛谷 |
P14921 城市规划 |
普及 |
|
|
|
| 洛谷 |
AT_abc394_e Palindromic Shortest Path |
普及+ |
|
|
|
| 洛谷 |
P1144 最短路计数 |
普及+ |
|
|
|
| 洛谷 |
P5543 The Great Revegetation |
普及+ |
⭐⭐⭐ |
BFS 扩展染色(二分图 + 带权并查集思想) |
用两个邻接表 s(相同关系)和 d(不同关系)分别存储约束,对每个未访问节点启动 BFS:相同关系邻居染同色(color[v] = color[u]),不同关系邻居染异色(color[v] = -color[u]),若出现矛盾(相同关系颜色不同或不同关系颜色相同)则输出 0;每个连通分量独立,有 \(2\) 种染色方案,总方案数为 \(2^{\text{连通分量数}}\),二进制表示为 1 后跟连通分量数个 0;时间复杂度 \(O(N+M)\)。 |
| 洛谷 |
P5663 加工零件 |
普及+ |
|
|
|
| 洛谷 |
P5683 道路拆除 |
普及+ |
|
|
|
| 洛谷 |
P9751 旅游巴士 |
普及+ |
|
|
|
| 洛谷 |
[[P10726 空间跳跃]] |
普及+ |
|
|
|
| 洛谷 |
P8817 假期计划 |
提高+ |
|
|
|
| AtCoder |
AT_awc0029_d Network Installation |
|
|
|
|
| AtCoder |
AT_awc0081_c Spread of Rumors |
|
⭐⭐ |
BFS(无权图最短路 + 最远距离) |
用邻接表存储有向图,从起点 \(S\) 开始 BFS 计算 dist[i](第 \(i\) 个人知道谣言的最小天数),dist 初始化为 \(-1\) 表示未访问;BFS 结束后遍历所有节点,若存在 dist[i] == -1 则输出 \(-1\)(有人不可达),否则取所有 dist[i] 的最大值作为答案(即谣言传播到所有人需要的最长时间);时间复杂度 \(O(N+M)\)。 |
| 其他 |
[[图的遍历(广度优先)]] |
|
|
|
|
【拓扑排序】
算法模板
数组模拟
bool topsort()
{
int hh=0; tt=-1; // BFS中tt为0,这里为-1,是因为要添加一个元素进去
for (int i=1; i<=n; i++)
if (!d[i])
q[++tt] = i; //对于入度为0的点加入到队列中
while (hh<=tt)
{
int t = q[hh++]; // 取出队头
for (int i=h[t]; i!=-1; i=ne[i])
{
int j = e[i];
d[j]--; // 删除j与t的边,j的入度减1
if (d[j]==0) q[++tt]=j; //如果入度为0就加入到队列中
}
}
return tt == n-1; // 判断是否可以遍历节点
}
for (int i=0; i<n; i++) printf("%d ", q[i]); // 打印队列中所有点,即拓扑序列
优先队列
void topsort()
{
priority_queue<int, vector<int>, greater<int> > q;
for (int i=1; i<=n; i++)
if (ind[i]==0)
q.push(i);
while (!q.empty())
{
int u = q.top();
q.pop();
seq.push_back(u);
for (int i=h[u]; i!=-1; i=ne[i])
{
int j = e[i];
ind[j]--;
if (ind[j]==0)
q.push(j);
}
}
}
算法应用
【Dijkstra】
算法模板
朴素版dijkstra
int g[N][N]; // 存储每条边(稠密图才用邻接矩阵)
int dist[N]; // 存储1号点到每个点的最短距离
bool st[N]; // 存储每个点的最短路是否已经确定
// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0; // 如果源点不是s,修改为dist[s]
for (int i=1; i<=n; i++)
{
int t = -1; // 在还未确定最短路的点中,寻找距离最小的点
for (int j=1; j<=n; j++)
{
if (!st[j] && (t==-1 || dist[t]>dist[j]))
{
t = j;
}
}
if (t==-1) return; //炸不到小于INF的d[u],说明顶点和源点不连通
// 用t更新其他点的距离
st[t]=true;
for (int j=1; j<=n; j++)
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
堆优化版dijkstra
// 有返回值,邻接表
// 求1号点到n号点的最短距离,如果不存在,则返回-1
// STL队列
typedef pair<int,int> PII; //源点到该节点距离,节点编号
priority_queue<PII, vector<PII>, greater<PII> > heap; //优先队列,小根堆
vector<Node> h[N]; //邻接表
int n, m; //n个点m条边
int dist[N]; //源点到i点的距离
bool st[N]; //点i的距离是否确定
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0; //源点距离为0
heap.push({0, 1}); // first存储距离,second存储节点编号
while (!heap.empty())
{
Node nd = heap.top(); heap.pop();
int veid = nd.second, distance = nd.first;
if (st[veid]==true) continue;
st[veid] = true;
for (int i=0; i<h[veid].size(); i++)
{
Node ndi = h[veid][i];
int j = ndi.second, distance=ndi.first;
if (dist[j] > dist[veid] + distance)
{
dist[j] = dist[veid] + distance;
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
// 对于long long范围的dijkstra,初始化和判别需要修改
const int INF = 1e18;
for (int i = 1; i <= n; i++) {
dist[i] = INF;
}
if (dist[t]>=INF/2) cout << -1 << endl;
// 无返回值,邻接表
struct Node
{
int v, w; // v: 目标节点, w: 边权
};
priority_queue<Node, vector<Node> > pq; // 优先队列(大顶堆)
// 优先队列比较函数:边权小的优先
bool operator<(Node x, Node y)
{
return x.w > y.w; // 注意:priority_queue默认是大顶堆,这里重载<使小边权优先
}
void dijkstra(int idx)
{
memset(st, 0, sizeof(st)); // 重置访问标记
memset(dist, 0x3f, sizeof(dist));
pq.push({idx, 0}); // 起点入队,距离为0
dist[idx] = 0;
// 二维矩阵写法
// dist[idx][idx] = 0;
while (!pq.empty())
{
int u = pq.top().v; // 当前节点
int w = pq.top().w; // 当前距离
pq.pop();
if (st[u] == true) // 如果已访问,跳过
{
continue;
}
st[u] = true; // 标记为已访问
dist[u] = w;
for (int i=0; i<a[u].size(); i++)
{
int v = g[u][i].v;
if (dist[v]>dist[u] + g[u][i].w)
{
dist[v] = dist[u] + g[u][i].w;
pq.push({v, dist[v]});
}
}
// 二维矩阵写法
// d[idx][u] = d[u][idx] = w;
// // 遍历当前节点的所有邻接边
// for (int i = 0; i < a[u].size(); i++)
// {
// int v = a[u][i].v; // 邻接节点
// if (d[idx][v] > a[u][i].w + w) // 如果可以更新最短距离
// {
// d[idx][v] = a[u][i].w + w; // 更新距离
// pq.push({v, d[idx][v]}); // 入队
// }
// }
}
}
// 有返回值,链式前向星
int n; // 点的数量
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N]; // 存储所有点到1号点的距离
int pre[N]; // 记录路径
bool st[N]; // 存储每个点的最短距离是否已确定
typedef pair<int, int> PII;
priority_queue<PII, vector<PII>, greater<PII> > heap;
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0; //源点距离为0
heap.push({0, 1}); // first存储距离,second存储节点编号
while (!heap.empty()) {
auto t = heap.top(); heap.pop();
int veid = t.second, distance = t.first;
if (st[veid]==true) continue;
st[veid] = true;
for (int i = h[veid]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > distance + w[i])
{
dist[j] = distance + w[i];
pre[j] = veid; // 记录前驱点
heap.push({dist[j], j});
}
}
}
if (dist[j] == 0x3f3f3f3f) return -1;
return dist[n];
}
// 无返回值,链式前向星
int n; // 点的数量
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N]; // 存储所有点到1号点的距离
bool st[N]; // 存储每个点的最短距离是否已确定
typedef pair<int, int> PII;
priority_queue<PII, vector<PII>, greater<PII> > heap;
void dijkstra(int u)
{
// memset(dist, 0x3f3f3f3f, sizeof dist);
for (int i=1; i<=n; i++)
{
dist[i] = INF;
}
memset(st, 0, sizeof(st));
while (!heap.empty()) heap.pop();
dist[u] = 0; //源点距离为0
heap.push({0, u}); // first存储距离,second存储节点编号
while (!heap.empty()) {
auto t = heap.top(); heap.pop();
int u = t.second, distance = t.first;
if (st[u]==true) continue;
st[u] = true;
for (int i = h[u]; i != -1; i = ne[i])
{
int v = e[i];
if (dist[v] > distance + w[i])
{
dist[v] = distance + w[i];
heap.push({dist[v], v});
}
}
}
// return dist[n];
}
保存路径
// 递归输出路径
void dfs_path(int u)
{
if (u==s) // s为源点
{
cout << u << " ";
return ;
}
dfs_path(pre[u]);
cout << u << " ";
}
算法应用
| 题目来源 |
标题 |
难度 |
星级 |
考察算法 |
一句话思路总结 |
| AcWing |
342 道路与航线 |
|
|
|
|
| AcWing |
383 观光 |
|
|
|
|
| AcWing |
849 Dijkstra求最短路I |
简单 |
⭐⭐ |
Dijkstra(朴素版) |
初始化 dist[1]=0 其余为 \(\infty\),循环 \(n\) 次每次在未确定节点中找到 dist 最小的节点 \(t\) 标记为已确定,然后用 \(t\) 的所有出边松弛更新相邻节点的 dist,最终 dist[n] 即为答案(不可达则输出 \(-1\))。 |
| AcWing |
850 Dijkstra II-优化 |
简单 |
⭐⭐⭐ |
Dijkstra(堆优化版) |
用链式前向星存储稀疏图,初始化 dist[1]=0 后将其入小根堆,每次取出堆顶最小距离节点 \(ver\)(若已确定则跳过),遍历其邻接边松弛更新 dist[j] 并将更新后的 \((dist[j], j)\) 入堆,最终 dist[n] 即为答案(不可达则输出 \(-1\)),时间复杂度 \(O((n+m)\log n)\)。 |
| AcWing |
903 昂贵的聘礼 |
|
|
|
|
| AcWing |
1126 最小花费 |
|
|
|
|
| AcWing |
4247 糖果 |
|
|
|
|
| AcWing |
6056 城市路 |
|
|
|
|
| AcWing |
6057 最短路 |
|
|
|
|
| 洛谷 |
AT_abc340_d Super Takahashi Bros |
普及 |
|
|
|
| 洛谷 |
AT_abc362_d Shortest Path 3 |
普及 |
|
|
|
| 洛谷 |
AT_abc463_e Roads and Gates |
普及 |
⭐⭐⭐⭐ |
Dijkstra / 虚拟节点技巧 |
传送门两两之间构成完全图,直接建边 \(O(N^2)\) 不可行,引入虚拟节点 \(0\),将传送代价 \(X_i+X_j+Y\) 拆分为 \(i\to0\)(权 \(X_i\))和 \(0\to j\)(权 \(X_j+Y\)),将完全图降为 \(O(N)\) 条边,再用 Dijkstra 求从城市 \(1\) 到各城市的最短路。 |
| 洛谷 |
[[P1339 Heat Wave]] |
普及 |
|
|
|
| 洛谷 |
[[P2984 Chocolate Giving]] |
普及 |
|
|
|
| 洛谷 |
P3371 单源最短路径(弱化版) |
普及 |
|
|
|
| 洛谷 |
P11966 上学 |
普及 |
|
|
|
| 洛谷 |
P1078 文化之旅 |
普及+ |
|
|
|
| 洛谷 |
P14923 猫和老鼠 |
普及+ |
|
|
|
| 洛谷 |
P15803 物流网络 |
普及+ |
⭐⭐⭐⭐ |
Dijkstra + 枚举优化 |
将公路按景观评分升序排序后倒序枚举"免去的公路"(即当前评分最高的),每次用 Dijkstra 求 \(1 \to n\) 最短路(该边费用为 0),取所有枚举中的最小值,时间复杂度 \(O(m \cdot (n+m) \log n)\),空间复杂度 \(O(n+m)\) |
| 洛谷 |
P15805 子图最短路 |
普及+ |
⭐⭐⭐⭐⭐ |
枚举子图 + Dijkstra + Floyd 增量更新 |
枚举左端点 \(\ell\),从 \(\ell+1\) 到 \(n\) 逐个加入节点,每次加入节点 \(j\) 时用 Dijkstra 计算 \(j\) 到子图中已有节点的最短路,再用 Floyd 以 \(j\) 为中间点更新子图内所有点对的距离,累加连通点对的距离和,时间复杂度 \(O(n^3 \log n + n^4)\),空间复杂度 \(O(n^2)\) |
| 洛谷 |
P15804 消息查找 |
提高+ |
⭐⭐⭐⭐⭐ |
图论建模 + 关键点 Dijkstra + 离线查询 |
将消息编号视为数轴,操作视为边(左移边权 1,引用边权 1),利用"引用消息至多 1000 条"的稀疏性提取关键点,预计算关键点间最短路,查询时分解为"普通移动到最近关键点 → 关键点间最短路 → 普通移动到目标",时间复杂度 \(O(k^2 \log k + q \log k)\)(\(k \le 2000\) 为关键点数),空间复杂度 \(O(k^2)\) |
| AtCoder |
AT_awc0014_d Round-Trip Delivery |
|
|
|
|
| AtCoder |
AT_awc0021_d Checkpoint Rally |
|
⭐⭐⭐ |
Dijkstra / 多段最短路 |
将必经检查点序列扩展为完整路径 \(1 \to P_1 \to P_2 \to \cdots \to P_K \to N\),对每一段子路径分别运行 Dijkstra 求最短路并累加,若任意一段不可达则输出 \(-1\),否则输出总距离之和。 |
| AtCoder |
AT_awc0033_d Cheapest Commute Route |
|
⭐⭐⭐ |
Dijkstra / 多目标点最短路 |
将每条道路的边权设为两端城市人口的乘积 \(M_u \times M_v\),从城市 \(1\) 出发运行一次 Dijkstra 求到所有城市的最短距离,最终答案为所有机场城市中最小的 dist[E_i]。 |
| AtCoder |
AT_awc0044_d Optimizing Delivery Routes |
|
⭐⭐⭐ |
Dijkstra / 边权预处理 |
读入 \(M\) 条边后,将 \(K\) 条受限道路的边权预先乘以 \(2\),再用修改后的边权建无向图,最后从起点 \(1\) 运行 Dijkstra 求到终点 \(N\) 的最短距离,不可达则输出 \(-1\)。 |
| AtCoder |
AT_awc0089_d Cheapest Route |
|
|
|
|
【Bellman-Ford】
算法模板
int n, m; // n表示点数,m表示边数
int dist[N]; // dist[x]存储1到x的最短路距离
struct Edge
{ // 边,a表示出点,b表示入点,w表示边的权重
int a, b, w;
}edges[M];
// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
memset(dist, 0x3f, sizeof(dist));
dist[1] = 0;
// 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
for (int i=0; i<k; i++)
{
mecpy(backup, dist, sizeof(dist));
for (int j=0; j<m; j++)
{
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
dist[b] = min(dist[b], backup[a]+w);
}
}
return dist[n];
}
算法应用
| 题目来源 |
标题 |
难度 |
星级 |
考察算法 |
一句话思路总结 |
| AcWing |
853 有边数限制的最短路 |
简单 |
⭐⭐⭐ |
Bellman-Ford(边数限制版) |
初始化 dist[1]=0 后执行 \(k\) 轮松弛,每轮开始前用 backup 数组备份上一轮的距离,遍历所有 \(m\) 条边执行 \(dist[b] = \min(dist[b],\, backup[a]+w)\) 确保本轮每条边最多使用一次,最终 dist[n] 即为最多经过 \(k\) 条边的最短路(若仍 \(\ge INF/2\) 则输出 impossible)。 |
【Floyd】
算法模板
// 初始化:
for (int i=1; i<=n; i++)
for (int j=1; j<=n; j++)
if (i==j) d[i][j] = 0;
else d[i][j] = INF;
// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
for (int k=1; k<=n; k++)
for (int i=1; i<=n; i++)
for (int j=1; j<=n; j++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
算法应用
【SPFA】
算法模板
求最短距离
int n; // 总点数
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N]; // 存储每个点到1号点的最短距离
bool st[N]; // 存储每个点是否在队列中
// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
memset(dist, 0x3f, sizeof(dist));
dist[1] = 0; // 如果有指定起点be和终点en,这里dist[1]修改为dist[be]
queue<int> q;
q.push(1);
st[1] = true;
while (q.size())
{
int t = q.front(); q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if (!st[j])
{ // 如果队列中已存在j,则不需要将j重复插入
q.push(j);
st[j] = true;
}
}
}
}
return dist[n]; // 如果有指定起点be和终点en,这里dist[n]修改为dist[en]
}
判断图中是否存在负环
int n; // 总点数
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N], cnt[N]; // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
bool st[N]; // 存储每个点是否在队列中
// 如果存在负环,则返回true,否则返回false。
bool spfa()
{
// 不需要初始化dist数组
// 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。
queue<int> q;
for (int i=1; i<=n; i++)
{
q.push(i);
st[i] = true;
}
st[1] = true;
while (!q.empty())
{
int t = q.front(); q.pop();
st[t] = false;
for (int i=h[t]; i!=-1; i=ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j]>=n) return true; // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
算法应用
【差分约束】
算法模板
for (int i=1; i<=n; i++) add(0, i, 0); //添加虚拟源点0,连接到所有点
bool spfa() {
queue<int> q;
// 初始化所有点
memset(dist, 0x3f, sizeof(dist));
dist[0] = 0;
// 从虚拟源点开始
q.push(0);
st[0] = true;
while (!q.empty()) {
int t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[t] + w[i]) {
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
// 经过n+1条边,说明有负环
if (cnt[j] >= n + 1) {
return false;
}
if (!st[j]) {
q.push(j);
st[j] = true;
}
}
}
}
return true;
}
for (int i = 1; i <= n; i++)
cout << dist[i] << " "; // dist[i]就是每个i的解
算法应用
【生成树】
算法模板
朴素版Prim
const int INF = 0x3f3f3f3f;
int n; // n表示点数
int g[N][N]; // 邻接矩阵,存储所有边
int dist[N], distp[N]; // 存储其他点到当前最小生成树的距离
bool st[N]; // 存储每个点是否已经在生成树中
vector<int> ans1; // 保存生成树中的点的编号
// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
int res = 0;
for (int i=0; i<n; i++)
{
int t = -1;
for (int j=1; j<=n; j++)
if (!st[j] && (t==-1 || dist[t]>dist[j]))
t = j;
if (i && dist[t]==INF) return INF;
if (i) res += dist[t]; //更新生成树边权值
for (int j=1; j<=n; j++)
{
// dist[j] = min(dist[j], g[t][j]);
if (!st[j] && dist[j]>g[t][j])
{
dist[j] = g[t][j];
// 记录生成树节点方案
distp[j] = t;
}
}
st[t] = true;
}
return res;
}
Kruskal
int n, m; // n是点数,m是边数
int p[N]; // 并查集的父节点数组
struct Edge // 存储边
{
int a, b, w;
bool operator < (const Edge &E)const {
return w < E.w;
}
}edges[M];
int find(int x) // 并查集核心操作
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int kruskal()
{
sort(edges+1, edges+m+1); // 从1开始读入数据
for (int i=1; i<=n; i++) p[i] = i; // 初始化并查集
int res = 0, cnt = 0;
for (int i=1; i<=m; i++)
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if (a != b)
{ // 如果两个连通块不连通,则将这两个连通块合并
p[a] = b;
res += w;
cnt ++;
}
}
if (cnt<n-1) return INF;
return res;
}
算法应用
【最近公共祖先】
算法模板
倍增法
//预处理节点深度
void bfs(int root)
{
memset(dep, 0x3f, sizeof(dep)); //初始化为无穷大
dep[0]=0, dep[root]=1; //计算节点深度,虚拟0节点深度为0
q.push(root);
while (!q.empty())
{ //计算节点深度
int t = q.front(); q.pop();
for (int i=h[t]; i!=-1; i=ne[i])
{
int j=e[i];
if (dep[j]>dep[t]+1)
{ //计算t的子节点j深度
dep[j]=dep[t]+1;
q.push(j); //j入列
fa[j][0]=t; //j向上走2^0=1步即为父节点t
for (int k=1; k<=15; k++)
{ //递推走fa[j][k];
//j向上走2^k等于k向上走2^(k-1)后再走2^(k-1)步
fa[j][k] = fa[fa[j][k-1]][k-1];
}
}
}
}
}
int lca(int x, int y) //LCA倍增算法,计算节点x和节点y的lca
{
if (dep[x]<dep[y]) swap(x, y); //保证x为深度较大的节点
//步骤1:把节点x和节点y的深度调整一致
//例深度相差11,先向上走2^3=8,再走2^1=2,2^0=1
for (int k=15; k>=0; k--) //上走2^k,k最大值是15
if (dep[fa[x][k]]>=dep[y])
x = fa[x][k];
if (x==y) return x;
//步骤2:两个节点同时向上跳,跳到公共祖先的下一层
//例向上跳11,先向上走2^3=8,再走2^1=2,2^0=1
for (int k=15; k>=0; k--)
{
if (fa[x][k] != fa[y][k])
{ //如果跳2^i后不一样,就都跳上去
x = fa[x][k];
y = fa[y][k];
}
}
return fa[x][0]; //返回x或y的父节点
}
LCA-Tarjan
//求每个点和1号点(根节点)的距离dist,深搜。u, fa当前节点及其父节点
void dfs(int u, int fa)
{
for (int i=h[u]; i!=-1; i=ne[i])
{ //遍历u的儿子j
int j = e[i];
if (j==fa) continue; //u的邻接点是其父节点
dist[j]=dist[u]+w[i];
dfs(j,u); //深搜求其他节点到根的距离
}
}
int find(int x) //找x的根
{
if (p[x]!=x) p[x] = find(p[x]);
return p[x];
}
//Tarjan求LCA,u当前节点,从根节点开始深搜
void tarjan(int u)
{
st[u]=1; //正在搜索的类型 1
for (int i=h[u]; i!=-1; i=ne[i])
{ //遍历u的儿子j
int j = e[i];
if (!st[j])
{ //点j没被访问,深搜访问
tarjan(j);
p[j]=u; //指定儿子j的父亲是u
}
}
st[u]=2; //u为根的子树访问结束,再回溯到u,标记节点u已经访问+回溯
for (int i=0; i<query[u].size(); i++)
{ //与当前节点相关的询问
PII item = query[u][i];
int y = item.first; //当前节点u的另外一个点y,second存查询编号
int id = item.second;
if (st[y]==2)
{ //另外一个点y已经访问且回溯
int ans = find(y); //前结点u的另外一个点y的LCA
res[id] = dist[u]+dist[y]-2*dist[ans];
}
}
}
算法应用
| 题目来源 |
标题 |
难度 |
星级 |
考察算法 |
一句话思路总结 |
| AcWing |
356 次小生成树 |
|
|
|
|
| AcWing |
506 货车运输 |
|
|
|
|
| AcWing |
1171 距离 |
中等 |
⭐⭐⭐⭐ |
Tarjan 离线 LCA / 并查集 |
先 DFS 预处理每个节点到根的距离 dist[],再用 Tarjan 算法离线处理所有询问:DFS 过程中用并查集维护当前搜索路径上的祖先,当处理节点 \(u\) 时若询问的另一端点 \(y\) 已回溯则 \(LCA(u,y)=find(y)\),答案为 \(dist[u]+dist[y]-2\times dist[LCA]\)。 |
| AcWing |
1172 祖孙询问 |
中等 |
⭐⭐⭐ |
倍增法 LCA |
从根节点 BFS 预处理每个节点的深度 dep[] 和倍增祖先数组 fa[x][k](\(fa[x][k]=fa[fa[x][k-1]][k-1]\)),查询时先将较深的节点提升到同一深度,若重合则直接返回,否则从大到小同步跳跃直到 LCA 的下一层,最后根据 \(LCA(a,b)\) 等于 \(a\)、\(b\) 或都不是分别输出 \(1\)、\(2\)、\(0\)。 |
| 洛谷 |
P3379 最近公共祖先(LCA) |
普及 |
⭐⭐ |
倍增法 / Tarjan离线算法 |
倍增法:DFS 预处理每个节点的深度 dep[u] 和 \(2^i\) 级祖先 fa[u][i],查询时先将较深的节点上跳至同深度,再让两节点同步上跳直到父节点相同,该父节点即为 LCA,预处理 \(O(n \log n)\),单次查询 \(O(\log n)\);Tarjan 离线法:DFS 遍历树的同时用并查集维护已访问节点的祖先关系,当某询问的两个端点都被访问过时,find(v) 即为 LCA,总复杂度 \(O((n+m)\alpha(n))\)。 |
| 洛谷 |
P5836 Milk Visits |
普及 |
|
|
|
| 洛谷 |
P3398 仓鼠找 sugar |
普及+ |
|
|
|
| 洛谷 |
[[P10289 小杨的旅游]] |
普及+ |
|
|
|
| 洛谷 |
P11967 割裂 |
普及+ |
|
|
|
【强连通分量】
算法模板
缩点
// 邻接矩阵版本
vector<int> e[N];
int dfn[N], low[N], tot;
int stk[N], instk[N], top;
int scc[N], cnt;
int din[N], dout[N];
void tarjan(int x)
{
dfn[x] = low[x] = ++tot;
stk[++top] = x; instk[x] = 1;
for (int y : e[x])
{
if (!dfn[y])
{
tarjan(y);
low[x] = min(low[x], low[y]);
}
else if (instk[y])
{
low[x] = min(low[x], dfn[y]);
}
}
if (dfn[x] == low[x])
{
int y; ++cnt;
do
{
y = stk[top--];
instk[y] = 0;
scc[y] = cnt;
}
while (y != x);
}
}
// 邻接表版本
void tarjan(int x)
{
dfn[x] = low[x] = ++tot; // 初始化dfn和low为时间戳
stk[++top] = x; // 节点入栈
instk[x] = 1; // 标记节点在栈中
for (int i = h[x]; i != -1; i = ne[i]) // 遍历x的所有邻接点
{
int y = e[i]; // 邻接点y
if (!dfn[y]) // 如果y未被访问
{
tarjan(y); // 递归访问y
low[x] = min(low[x], low[y]); // 更新low[x]
}
else if (instk[y]) // 如果y在栈中
{
low[x] = min(low[x], dfn[y]); // 更新low[x]
}
}
if (dfn[x] == low[x]) // 如果x是强连通分量的根
{
int y;
++con_cnt; // 增加强连通分量计数
do
{
y = stk[top--]; // 弹出栈顶元素
instk[y] = 0; // 标记不在栈中
con_id[y] = con_cnt; // 记录节点y所属的分量编号
con_size[con_cnt]++; // 更新分量大小
ans[con_cnt].push_back(y); // 将节点加入分量
}
while (y != x); // 直到弹出x为止
}
}
割点
vector<int> e[N];
int dfn[N], low[N], tot;
int cut[N], root;
void tarjan(int x)
{
dfn[x] = low[x] = ++tot;
int child = 0;
for (int y : e[x])
{
if (!dfn[y])
{
tarjan(y);
low[x] = min(low[x], low[y]);
if (low[y]>=dfn[x])
{
child++;
if (x!=root || child>1)
cut[x] = true; // 标记节点 x 为割点
}
}
else
low[x] = min(low[x], dfn[y]);
}
}
割边
struct edge
{
int v, ne;
}e[M];
int h[N], idx = 1;
int dfn[N], low[N], tot, cnt;
struct bridge
{
int x, y;
}bri[M];
void add(int a, int b)
{
e[++idx] = {b, h[a]};
h[a] = idx;
}
void tarjan(int x, int in_edge)
{
dfn[x] = low[x] = ++tot;
for (int i=h[x]; i; i=e[i].ne)
{
int y = e[i].v;
if (!dfn[y])
{
tarjan(y, i);
low[x] = min(low[x], low[y]);
if (low[y] > dfn[x])
bri[cnt++] = {x, y};
}
else if (i != (in_edge^1))
low[x] = min(low[x], dfn[y]);
}
}
算法应用
| 题目来源 |
标题 |
难度 |
星级 |
考察算法 |
一句话思路总结 |
| AcWing |
367 学校网络 |
中等 |
⭐⭐⭐⭐ |
强连通分量(Tarjan)/ 缩点 |
用 Tarjan 求 SCC 后缩点得到 DAG,统计各分量入度/出度:问题一答案为入度为 \(0\) 的分量数(这些分量无法被其他分量到达,必须直接发放),问题二答案为若只有一个 SCC 则 \(0\),否则为 \(\max(\text{入度为0数}, \text{出度为0数})\)(将出度为0的分量连向入度为0的分量形成环)。 |
| AcWing |
368 银河 |
中等 |
⭐⭐⭐⭐⭐ |
差分约束 / 强连通分量(Tarjan)/ 缩点 / DAG上最长路 |
将5种约束转化为 \(x_v \ge x_u + w\) 形式的有向边并添加超级源点 \(0\) 保证下界为 \(1\),先用 Tarjan 求 SCC 检测内部正权边(存在则正环无解输出 \(-1\)),缩点后按 SCC 编号逆序在 DAG 上求最长路 \(dist[k] = \max(dist[k],\,dist[i]+w)\),最终答案为 \(\sum dist[i] \times siz[i]\)。 |
| AcWing |
1174 受欢迎的牛 |
中等 |
⭐⭐⭐⭐ |
强连通分量(Tarjan)/ 缩点 |
用 Tarjan 算法求出所有强连通分量并缩点,缩点后统计每个分量的出度,若出度为 \(0\) 的分量恰好有 \(1\) 个则该分量内的所有牛被所有其他牛认为受欢迎(答案为该分量大小),否则答案为 \(0\)。 |
| AcWing |
1175 最大半连通子图 |
中等 |
⭐⭐⭐⭐⭐ |
强连通分量(Tarjan)/ 缩点 / DAG上DP |
先用 Tarjan 求 SCC 后缩点得到 DAG(用哈希集合去重边),定义 \(f[i]\) 为以 SCC \(i\) 结尾的最大半连通子图节点数、\(g[i]\) 为方案数,按 SCC 编号逆序(拓扑序)进行 DP 转移 \(f[j]=\max(f[j],\,f[i]+siz[j])\),最终答案为所有 \(f[i]\) 的最大值及对应 \(g[i]\) 之和(对 \(X\) 取模)。 |
| AcWing |
1183 电力 |
|
|
|
|
| 洛谷 |
B3609 强连通分量 |
普及+ |
|
|
|
| 洛谷 |
P2341 受欢迎的牛 |
普及+ |
⭐⭐⭐⭐ |
Tarjan求强连通分量(SCC)+ 缩点 |
先用 Tarjan 算法求出所有强连通分量并缩点,明星奶牛必须被所有奶牛喜欢即缩点后的 DAG 中出度为 \(0\) 的强连通分量,若出度为 \(0\) 的分量唯一则答案为该分量大小,否则为 \(0\),时间复杂度 \(O(N+M)\)。 |
| 洛谷 |
P2863 The Cow Prom |
普及+ |
|
|
|
| 洛谷 |
P3387 缩点 |
普及+ |
|
|
|
| 洛谷 |
P3388 割点 |
普及+ |
⭐⭐⭐⭐ |
Tarjan / 割点判定 |
DFS 遍历无向图维护每个节点的 dfn(时间戳)和 low(能回溯到的最早祖先),对于非根节点若存在子节点 \(y\) 满足 low[y] >= dfn[x] 则 \(x\) 是割点,对于根节点需有两个以上独立子树才是割点,最终统计并输出所有割点。 |
| 洛谷 |
P8186 Redistributing Gifts |
普及+ |
|
|
|
| 洛谷 |
P2812 校园网络 |
提高+ |
|
|
|
【双连通分量】
算法模板
边双连通分量缩点
int h[N], e[M], ne[M], idx;
int dfn[N]; // DFS序(时间戳)
int low[N]; // 通过回边能到达的最小DFN值
int tot; // 时间戳计数器
stack<int> stk; // Tarjan算法栈
int bri[M]; // 标记边是否为桥
int con_cnt, con_id[N], con_size[N];
vector<int> ans[N];
// 使用idx从0开始
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// Tarjan算法求边双连通分量
void tarjan(int x, int in_edge)
{
dfn[x] = low[x] = ++tot; // 初始化dfn和low为时间戳
stk.push(x); // 节点入栈
for (int i = h[x]; i != -1; i = ne[i]) // 遍历x的所有邻接点
{
int y = e[i]; // 邻接点y
if (!dfn[y]) // 如果y未被访问
{
tarjan(y, i); // 递归访问y
low[x] = min(low[x], low[y]); // 更新low[x]
if (low[y] > dfn[x]) // 判断边(x,y)是否为桥
bri[i] = bri[i ^ 1] = true; // 标记该边和其反向边为桥
}
else if (i != (in_edge ^ 1)) // 如果是回退边且不是反向边
low[x] = min(low[x], dfn[y]); // 更新low[x]
}
if (dfn[x] == low[x]) // 如果x是边双连通分量的根
{
++con_cnt; // 增加边双连通分量计数
while (1)
{
int y = stk.top(); // 取出栈顶
stk.pop(); // 弹出栈顶
con_id[y] = con_cnt; // 记录节点y所属的分量编号
con_size[con_cnt]++; // 更新分量大小
ans[con_cnt].push_back(y); // 将节点加入分量
if (y == x) // 直到弹出x为止
break;
}
}
}
点双连通分量缩点
vector<int> e[N], ne[N];
int dfn[N], low[N], tot, cnt;
stack<int> stk;
vector<int> dcc[N];
int cut[N], root, num, id[N];
void tarjan(int x)
{
dfn[x] = low[x] = ++tot;
stk.push(x);
if (!e[x].size())
{
dcc[++cnt].push_back(x);
return;
}
int child = 0;
for (int y : e[x])
{
if (!dfn[y])
{
tarjan(y);
low[x] = min(low[x], low[y]);
if (low[y] >= dfn[x])
{
child++;
if (x!=root || child>1)
cut[x] = true;
cnt++;
while (1)
{
int z = stk.top(); stk.pop();
dcc[cnt].push_back(z);
if (z==y) break;
}
dcc[cnt].push_back(x);
}
}
else
low[x] = min(low[x], dfn[y]);
}
}
void tarjan(int u, int fa)
{
int child = 0; // 子树计数
dfn[u] = low[u] = ++tot; // 初始化dfn和low为时间戳
stk.push(u); // 节点入栈
for (int i = h[u]; i != -1; i = ne[i]) // 遍历u的所有邻接点
{
int v = e[i]; // 邻接点v
if (!dfn[v]) // 如果v未被访问
{
child++;
tarjan(v, u); // 递归访问v
low[u] = min(low[u], low[v]); // 更新low[u]
if (low[v] >= dfn[u]) // 判断u是否为割点
{
cut[u] = true; // 标记u为割点
con_cnt++; // 增加点双连通分量计数
while (1) // 弹出栈中节点,直到遇到v
{
int y = stk.top(); // 取出栈顶
stk.pop(); // 弹出栈顶
con_id[y] = con_cnt; // 记录节点y所属的分量编号
con_size[con_cnt]++; // 更新分量大小
ans[con_cnt].push_back(y); // 将节点加入分量
if (y == v) // 直到弹出v为止
break;
}
ans[con_cnt].push_back(u); // 将割点u加入分量
}
}
else if (v != fa) // 如果是回退边且不是父节点
{
low[u] = min(low[u], dfn[v]); // 更新low[u]
}
}
if (fa == 0 && child == 0) // 如果u是孤立节点
{
con_cnt++; // 增加点双连通分量计数
con_id[u] = con_cnt; // 记录节点u所属的分量编号
con_size[con_cnt]++; // 更新分量大小
ans[con_cnt].push_back(u); // 将节点加入分量
stk.pop(); // 弹出栈顶
}
}
算法应用
| 题目来源 |
标题 |
难度 |
星级 |
考察算法 |
一句话思路总结 |
| AcWing |
395 冗余路径 |
简单 |
⭐⭐⭐⭐⭐ |
边双连通分量(Tarjan)/ 缩点 |
用 Tarjan 算法通过 low[y] > dfn[x] 标记所有桥边,DFS 过程中用栈求出边双连通分量缩点,缩点后桥边形成一棵树,统计树中度数为 \(1\) 的叶子节点数 \(cnt\),答案为 \((cnt+1)/2\)(将叶子两两配对连接使全图边双连通)。 |
| AcWing |
396 矿场搭建 |
困难 |
⭐⭐⭐⭐⭐ |
点双连通分量(Tarjan)/ 割点分类讨论 |
用 Tarjan 求出所有点双连通分量后,对每个分量统计其中割点数量 \(tmp\):若 \(tmp=0\) 且大小为1则需1个出口(方案1),大小\(\ge2\)则需2个出口(方案 \(C(size,2)\));若 \(tmp=1\) 则需1个出口在非割点位置(方案 \(size-1\));若 \(tmp\ge2\) 则无需出口(被多个割点保护),最终累加所有分量的最少出口数与方案数乘积。 |
| 洛谷 |
P1656 炸铁路 |
普及- |
⭐⭐⭐ |
Tarjan / 桥(割边)判定 |
DFS 遍历无向图维护每个节点的 dfn(时间戳)和 low(能回溯到的最小 dfn),对于树边 \((x,y)\) 若 low[y] > dfn[x] 则该边是桥(删除后 \(y\) 的子树与图其余部分断开),用 j != (in_edg ^ 1) 避免走回头路,最后将所有桥按端点排序输出。 |
| 洛谷 |
P8435 点双连通分量 |
普及+ |
⭐⭐⭐⭐ |
Tarjan / 点双连通分量 |
DFS 遍历无向图维护 dfn 和 low,对于子节点 \(y\) 若 low[y] >= dfn[x] 则 \(x\) 是割点,此时从栈中弹出节点直到 \(y\) 并与 \(x\) 一起构成一个点双连通分量(割点会保留在栈中供后续分量复用),孤立节点单独成分量,最终输出所有分量。 |
| 洛谷 |
P8436 边双连通分量 |
普及+ |
|
|
|
| 洛谷 |
P2860 Redundant Paths |
提高+ |
⭐⭐⭐⭐⭐ |
边双连通分量(Tarjan)/ 缩点 |
用 Tarjan 算法标记所有桥并求出边双连通分量缩点,缩点后得到一棵树,统计树中度数为 \(1\) 的叶子节点数量 \(leaf\),最少需要添加 \((leaf+1)/2\) 条新边将叶子两两配对连接,使图变为边双连通。 |
【二分图】
算法模板
匈牙利算法
vector<int> G[N]; // 定义邻接表:存储图的边关系
bool st[N]; // 定义数组:标记右部顶点是否被访问过
int match[N]; // 定义数组:记录右部顶点匹配的左部顶点
// 查找函数:尝试为左部节点x寻找增光路
// 返回值:是否成功为节点x找到匹配
bool find(int x)
{
// 遍历节点x的所有邻接点(即所有可能的右部匹配对象)
for (int i=h[x]; i!=-1; i=ne[i])
{
int j = e[i]; // 右部节点j
// 如果右部节点j在本轮搜索中还未被考虑
if (!st[j])
{
st[j] = true; // 标记节点j已被本轮考虑
// 情况1:节点j还未被匹配
// 情况2:节点j已被匹配,但可以尝试为它的原匹配对象match[j]寻找新的匹配
if (match[j]==0 || find(match[j]))
{
match[j] = x; // 将节点j匹配给节点x
return true; // 成功找到匹配
}
}
}
// 尝试了所有邻接点都失败,返回false
return false;
}
int res = 0;
for (int i=1; i<=n; i++)
{
memset(st, false, sizeof(st));
if (find(i))
res++;
}
// res就是最大匹配数
染色法判定二分图
int n; // n表示点数
int h[N], e[M], ne[M], idx; // 邻接表存储图
int color[N]; // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色
// 参数:u表示当前节点,c表示当前点的颜色
bool dfs(int u, int c)
{
color[u] = c;
if (color[u]==1) ans.push_back(u);
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (color[j] == 0)
{
if (dfs(j,3-c)==false)
return false;
}
else if (color[j] == c) return false;
}
return true;
}
bool check()
{
bool flag = true;
for (int i = 1; i <= n; i ++ )
if (color[i] == 0)
if (dfs(i, 1)==false)
{
flag = false;
break;
}
return flag;
}
Dinic算法
// Dinic算法模板这里不再赘述,与之前一模一样
// 读入k条边,建立二分图的网络流模型
while (k--)
{
int u, v;
cin >> u >> v;
// 左部点u连向右部点v(右部点编号偏移+n,避免冲突)
// 容量为1(每条边最多匹配一次)
add(u, v + n, 1);
add(v + n, u, 0); // 反向边
}
// 设置源点和汇点
s = 0; // 源点编号0
t = n + m + 1; // 汇点编号n+m+1
// 源点s向所有左部点连边,容量为1(每个左部点最多匹配一次)
for (int i = 1; i <= n; i++)
{
add(s, i, 1);
add(i, s, 0); // 反向边
}
// 所有右部点向汇点t连边,容量为1(每个右部点最多匹配一次)
for (int i = 1; i <= m; i++)
{
add(i + n, t, 1);
add(t, i + n, 0); // 反向边
}
最大完美匹配
// DFS增广:尝试为左部点x找到匹配
// 在相等子图中寻找增广路
bool dfs(int x)
{
va[x] = 1; // 标记x在交错树中
// 枚举所有右部点y
for (int y = 1; y <= n; y++)
{
if (!vb[y]) // y不在交错树中
{
// 检查x-y是否在相等子图中(顶标和等于边权)
if (la[x] + lb[y] - w[x][y] == 0)
{
vb[y] = 1; // 将y加入交错树
// y未匹配,或y的匹配点可以找到新的匹配(递归增广)
if (!match[y] || dfs(match[y]))
{
match[y] = x; // x匹配y
return 1; // 增广成功
}
}
else
{
// 不在相等子图中,更新松弛量
// d[y]记录y到交错树的最小距离
d[y] = min(d[y], la[x] + lb[y] - w[x][y]);
}
}
}
return 0; // 增广失败
}
// KM算法(Kuhn-Munkres):求二分图最大权完美匹配
int KM()
{
// 初始化左部顶标:取每个左部点连出的最大边权
for (int i = 1; i <= n; i++)
la[i] = -1e18;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
la[i] = max(la[i], w[i][j]);
// 初始化右部顶标为0
for (int i = 1; i <= n; i++)
lb[i] = 0;
// 依次为每个左部点寻找匹配
for (int i = 1; i <= n; i++)
{
while (true)
{
// 每次尝试增广前,清空标记和松弛量
fill(va + 1, va + n + 1, 0);
fill(vb + 1, vb + n + 1, 0);
fill(d + 1, d + n + 1, 1e18);
// 尝试在相等子图中为左部点i找到增广路
if (dfs(i))
break; // 增广成功,退出循环
// 增广失败,需要调整顶标,扩大相等子图
int delta = 1e18;
// 找到不在交错树中的右部点的最小松弛量
for (int j = 1; j <= n; j++)
if (!vb[j])
delta = min(delta, d[j]);
// 调整顶标:交错树中的左部点顶标减delta,右部点顶标加delta
for (int j = 1; j <= n; j++)
{
if (va[j])
la[j] -= delta; // 左部点在交错树中,顶标减小
if (vb[j])
lb[j] += delta; // 右部点在交错树中,顶标增大
}
// 调整后,至少有一条新边进入相等子图,继续尝试增广
}
}
// 计算最大权匹配的总权值
int res = 0;
for (int i = 1; i <= n; i++)
res += w[match[i]][i]; // match[i]是与右部点i匹配的左部点
return res;
}
算法应用
| 题目来源 |
标题 |
难度 |
星级 |
考察算法 |
一句话思路总结 |
| AcWing |
257 关押罪犯 |
|
|
|
|
| AcWing |
372 棋盘覆盖 |
|
|
|
|
| AcWing |
376 机器任务 |
|
|
|
|
| AcWing |
378 骑士放置 |
|
|
|
|
| AcWing |
379 捉迷藏 |
中等 |
⭐⭐⭐⭐ |
最小路径覆盖 / 二分图最大匹配(匈牙利算法) |
先用 Floyd-Warshall 求传递闭包得到可达矩阵,将每个节点拆分为左右两部构建二分图(若 \(i\) 可达 \(j\) 则连边),用匈牙利算法求最大匹配数 \(res\),由 Dilworth 定理知答案为 \(N - res\)(最小路径覆盖数)。 |
| AcWing |
860 染色法判定二分图 |
简单 |
⭐⭐ |
DFS 染色 / 二分图判定 |
对每个未染色节点 DFS 染颜色 \(1\) 或 \(2\),遍历邻接点时若未染色则递归染相反颜色 \(3-c\),若已染色且颜色相同则存在奇环不是二分图,所有连通块染色成功则输出 Yes 否则 No。 |
| AcWing |
861 二分图的最大匹配 |
简单 |
⭐⭐⭐ |
匈牙利算法(增广路) |
从左部每个节点出发 DFS 寻找增广路:遍历其邻接的右部节点 \(y\),若 \(y\) 未被访问则标记,若 \(y\) 未匹配或 \(y\) 的原匹配对象能找到新的匹配,则将 \(y\) 匹配给当前节点,每轮搜索前重置 st 数组,最终统计成功匹配数即为最大匹配。 |
| 洛谷 |
P1330 封锁阳光大学 |
普及 |
⭐⭐⭐ |
二分图判定(染色法)/ 贪心 |
对每个未染色的连通块 DFS 染色并统计两种颜色的数量 \(cnt[1]\) 和 \(cnt[2]\),若发现相邻节点同色则存在奇环输出 Impossible,否则每个连通块选择 \(\min(cnt[1], cnt[2])\) 累加即为最少河蟹数,孤立点无需河蟹直接跳过。 |
| 洛谷 |
[[P10378 交流问题]] |
普及 |
|
|
|
| 洛谷 |
P14552 乌拉尔冰球赛 |
普及 |
⭐⭐⭐ |
二分图染色 / 独立集 |
将每场比赛视为无向边构建图(每个节点度数为2,图由若干环组成,必为二分图),DFS 染色后将所有红色节点存入 ans,若 ans.size() >= K 则输出前 \(K\) 个红色节点(同色集合内节点互不相邻,构成独立集),否则输出 \(0\)。 |
| 洛谷 |
AT_abc398_e Tree Game |
普及+ |
|
|
|
| 洛谷 |
[[P3386 二分图最大匹配]] |
普及+ |
⭐⭐⭐ |
二分图最大匹配(匈牙利算法 / Dinic网络流) |
匈牙利算法:从左部点出发 DFS 找增广路,对每个左部点重置访问标记后尝试匹配,若当前右部点未匹配或能为其原匹配找到新的增广路,则完成匹配,时间复杂度 \(O(VE)\);Dinic 网络流:将二分图转化为网络流模型(源点→左部点→右部点→汇点,容量均为 1),用 Dinic 求最大流即为最大匹配数,时间复杂度 \(O(E\sqrt{V})\),两者均基于增广路定理:不断寻找增广路直到不存在为止,此时匹配数达到最大。 |
| 洛谷 |
P1155 双栈排序 |
提高+ |
|
|
|
| 洛谷 |
[[P6577 二分图最大完美匹配]] |
省选 |
⭐⭐⭐⭐⭐ |
KM算法(Kuhn-Munkres,DFS/BFS版本) |
基于顶标和相等子图思想,初始化左部点顶标为其连出的最大边权、右部点顶标为 0,每次在相等子图(满足 \(la[x] + lb[y] = w[x][y]\) 的边)中找增广路:DFS 版本对每个左部点反复 DFS 直到成功,失败时计算不在交错树中的右部点的最小松弛量 \(\delta\) 调整顶标(左部减 \(\delta\)、右部加 \(\delta\))以扩大相等子图;BFS 版本用队列分层扩展交错树,一次 BFS 中可能多次调整顶标,效率更高,两者时间复杂度均为 \(O(n^3)\)。 |
| 洛谷 |
UVA11419 SAM I AM |
省选 |
|
|
|
| AtCoder |
AT_awc0013_e Part-Time Job Shift Assignment |
|
|
|
|
【分层图】
算法模板
算法应用
【网络流】
算法模板
EK算法
// BFS寻找从源点s到汇点t的增广路径( Edmonds-Karp算法,用BFS找最短增广路)
bool bfs()
{
// 初始化:mf数组为0表示未访问
memset(mf, 0, sizeof(mf));
queue<int> q;
q.push(s); // 从源点出发
mf[s] = 1e9; // 源点的瓶颈流量设为无穷大
while (!q.empty())
{
int u = q.front(); q.pop();
// 遍历u的所有出边
for (int i = h[u]; i != -1; i = ne[i])
{
int v = e[i]; // 边的终点
// v未访问且该边还有剩余容量(w[i] > 0)
if (mf[v] == 0 && w[i])
{
// 更新v的瓶颈流量:从源点到u的瓶颈 与 当前边容量 的较小值
mf[v] = min(mf[u], w[i]);
pre[v] = i; // 记录到达v的边编号,用于后续回溯路径
q.push(v); // v入队继续BFS
if (v == t) // 找到汇点,增广路径存在
return true;
}
}
}
return false; // 无法到达汇点,不存在增广路径
}
// Edmonds-Karp算法:不断BFS找增广路,直到不存在增广路为止
int EK()
{
int flow = 0; // 总流量
while (bfs()) // 当还存在增广路径时
{
int v = t;
// 从汇点回溯到源点,更新残量网络
while (v != s)
{
int i = pre[v]; // 到达v的边编号
// 正向边容量减少(已使用mf[t]的流量)
w[i] -= mf[t];
// 反向边容量增加(i^1是i的反向边,因为边是成对添加的)
w[i ^ 1] += mf[t];
// 通过反向边的终点信息,回溯到上一个节点
v = e[i ^ 1];
}
flow += mf[t]; // 累加本次增广的流量
}
return flow; // 返回最大流
}
Dinic算法
int h[N], e[M], w[M], ne[M], idx = 2; // idx从2开始,方便找反向边
int d[N], cur[N]; // d: 层次深度,cur: 当前弧优化数组
// 添加有向边和反向边
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// BFS构建层次图
bool bfs()
{
memset(d, 0, sizeof(d));
queue<int> q;
q.push(s);
d[s] = 1;
while (q.size())
{
int u = q.front();
q.pop();
for (int i = h[u]; i != -1; i = ne[i])
{
int v = e[i];
if (d[v] == 0 && w[i]) // 如果v未访问且边有容量
{
d[v] = d[u] + 1;
q.push(v);
if (v == t)
{
return true; // 找到汇点
}
}
}
}
return false; // 无法到达汇点
}
// DFS进行多路增广
int dfs(int u, int mf)
{
if (u == t)
{
return mf; // 到达汇点
}
int sum = 0;
for (int i = cur[u]; i != -1; i = ne[i])
{
cur[u] = i; // 当前弧优化
int v = e[i];
if (d[v] == d[u] + 1 && w[i]) // 满足层次关系和容量限制
{
int f = dfs(v, min(mf, w[i]));
w[i] -= f; // 正向边减少容量
w[i ^ 1] += f; // 反向边增加容量
sum += f;
mf -= f;
if (mf == 0)
{
break; // 流量已用完
}
}
}
if (sum == 0)
{
d[u] = 0; // 残量网络中u不可达
}
return sum;
}
// Dinic算法主函数
int dinic()
{
int flow = 0;
while (bfs()) // 每次BFS构建新的层次图
{
memcpy(cur, h, sizeof(h)); // 重置当前弧
flow += dfs(s, 1e9); // 从源点开始增广
}
return flow;
}
算法应用
| 题目来源 |
标题 |
难度 |
星级 |
考察算法 |
一句话思路总结 |
| AcWing |
410 排版幻灯片 |
|
|
|
|
| 洛谷 |
[[P3376 网络最大流]] |
提高+ |
⭐⭐⭐⭐ |
最大流(EK / Dinic) |
EK 算法:每次 BFS 在残量网络中寻找一条从源点到汇点的最短增广路径,计算路径上的瓶颈流量并更新残量网络(正向边减、反向边加),重复直到不存在增广路,时间复杂度 \(O(VE^2)\);Dinic 算法:每次 BFS 构建分层图后,用 DFS 进行多路增广(配合当前弧优化),重复直到汇点不可达,时间复杂度 \(O(V^2E)\),两者核心思想均基于 Ford-Fulkerson 方法的增广路定理:最大流等于最小割。 |
| AtCoder |
AT_awc0031_e Power Grid Blackout Crisis |
|
⭐⭐⭐⭐⭐ |
网络流(Dinic)/ 超级源汇点 |
每次发电厂故障后重新建图:添加超级源点 \(ss\) 向每个运行中的发电厂连容量 \(W_k\) 的边,每个工厂向超级汇点 \(tt\) 连容量 \(B_i\) 的边,原输电线路双向连容量 \(C_j\) 的边,运行 Dinic 求最大流,若最大流 \(\ge \sum B_i\) 则输出 Yes 否则 No。 |
【费用流】
算法模板
EK算法
// SPFA寻找从源点s到汇点t的最短费用增广路径(按费用作为边权)
bool spfa()
{
// 初始化距离为无穷大
memset(d, 0x3f, sizeof(d));
// 初始化瓶颈流量为0(表示未访问)
memset(mf, 0, sizeof(mf));
queue<int> q;
q.push(s); // 从源点出发
d[s] = 0; // 源点到自身距离为0
mf[s] = 1e9; // 源点瓶颈流量为无穷大
vis[s] = 1; // 标记源点在队列中
while (!q.empty())
{
int u = q.front(); q.pop();
vis[u] = 0; // u出队,标记为不在队列中
// 遍历u的所有出边
for (int i = h[u]; i != -1; i = ne[i])
{
int v = e[i]; // 边的终点
// 松弛操作:若经过u到v更短,且该边还有剩余容量(c[i] > 0)
if (d[v] > d[u] + w[i] && c[i])
{
d[v] = d[u] + w[i]; // 更新v的最短距离
mf[v] = min(mf[u], c[i]); // 更新v的瓶颈流量
pre[v] = i; // 记录到达v的边编号
if (!vis[v]) // v不在队列中,入队
{
q.push(v);
vis[v] = 1;
}
}
}
}
// 若mf[t] > 0,说明找到了增广路径
return mf[t] > 0;
}
// Edmonds-Karp算法的费用流版本:不断SPFA找最短费用增广路
void EK()
{
while (spfa()) // 当还存在增广路径时
{
// 从汇点回溯到源点,更新残量网络
for (int v = t; v != s; )
{
int i = pre[v]; // 到达v的边编号
// 正向边容量减少(使用了mf[t]的流量)
c[i] -= mf[t];
// 反向边容量增加(i^1是i的反向边,因为边是成对添加的)
c[i ^ 1] += mf[t];
// 通过反向边的终点信息,回溯到上一个节点
v = e[i ^ 1];
}
flow += mf[t]; // 累加本次增广的流量
cost += mf[t] * d[t]; // 累加本次增广的费用(流量 * 单位费用距离)
}
}
算法应用
| 题目来源 |
标题 |
难度 |
星级 |
考察算法 |
一句话思路总结 |
| 洛谷 |
[[P3381 最小费用最大流]] |
提高+ |
⭐⭐⭐⭐⭐ |
最小费用最大流(MCMF / EK费用流) |
在残量网络上每次使用 SPFA(或 Dijkstra)寻找从源点到汇点的最短费用增广路径(以单位流量费用为边权),沿该路径增广并更新残量网络(正向边减容量、反向边加容量,反向边费用为负以实现退流),重复直到不存在增广路,最终得到最大流前提下的最小总费用,时间复杂度 \(O(F \cdot VE)\)(\(F\) 为最大流)。 |
【2-SAT】
算法模板
算法应用
| 题目来源 |
标题 |
难度 |
星级 |
考察算法 |
一句话思路总结 |
| 洛谷 |
[[P4782 2-SAT]] |
提高+ |
|
|
|
【欧拉路径】
算法模板
算法应用
| 题目来源 |
标题 |
难度 |
星级 |
考察算法 |
一句话思路总结 |
| AcWing |
1123 铲雪车 |
|
|
|
|
| AcWing |
1124 骑马修栅栏 |
|
|
|
|
| AcWing |
[[1184 欧拉回路]] |
|
|
|
|
| AcWing |
1185 单词游戏 |
简单 |
⭐⭐⭐ |
欧拉路径 / 并查集 |
将每个单词视为从首字母到尾字母的有向边,统计26个字母的入度/出度,用并查集检查连通性,满足欧拉路径条件(所有点入度=出度,或恰有一个起点出度=入度+1和一个终点入度=出度+1)且所有出现过的字母连通则输出 Ordering is possible.,否则 The door cannot be opened.。 |
| 其他 |
[[欧拉判定]] |
|
|
|
|
| 其他 |
[[一笔画]] |
|
|
|
|