图论2026Mar
图论2026Mar
我的图论水平差到了一定地步,整理一些题目。
11th Mar 2026.
最短路
floyd
时间复杂度: \(O(n^3)\)
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
if (d[i][j] > d[i][k] + d[k][j])
d[i][j] = d[i][k] + d[k][j];
dijkstra
时间复杂度: \(O(n^2)\)
inline void djs (int cur) {
for (int i = 1; i <= n; ++i) dis[i] = INF;
dis[cur] = 0;
for (int i = 1; i <= n; ++i) {
int k = 0;
for (int j = 1; j <= n; ++j) if (!vis[j]) {
if (!k) k = j;
else if (dis[k] > dis[j] && dis[j] != INF) k = j;
}
vis[k] = 1;
for (auto &j: g[k]) dis[j.y] = std:: min(dis[j.y], dis[k] + j.z);
}
}
堆优化的 dijkstra
时间复杂度: \(O((n+m)\log n)\) (所以有时候并不优)
inline void djs (int cur) {
std:: priority_queue < NODE > pq;
nd.x = cur, nd.z = 0; pq.push(nd);
for (int i = 1; i <= n; ++i) dis[i] = INF;
dis[cur] = 0;
while (!pq.empty()) {
nd = pq.top(); pq.pop();
int i = nd.x;
if (vis[i]) continue;
vis[i] = true;
for (auto &j: g[i])
if (dis[i] + j.z < dis[j.to]) {
dis[j.to] = dis[i] + j.z;
nd.x = j.to, nd.z = dis[j.to]; pq.push(nd);
}
}
}
遇到负环:可以同时记录最短路的边数,边数 \(\geq n\) 则存在负环。
联通问题
- 点双连通分量: 删一个点后 其余点仍然联通
- 边双连通分量: 删一个边后 其余点仍然联通
- 割点: 删去此点后不再联通
当且仅当:点 \(u\) 存在儿子 \(v\) 使得 \(low_v \geq dfn_u\) 或点 \(u\) 为搜索的起始点且有 \(\geq 2\) 个儿子时 \(u\) 为割点。 - 割边(桥): 删去此边后不再联通
当且仅当 \(low_v > dfn_u\) 时边 \((u,v)\) 为割边。
注意:有重边的时候,可以为边加上编号,同时限制无向边不走回头路即可。
以下各题均使用 tarjan 算法,利用时间戳跑 dfs 求解。时间复杂度 \(O(n+m)\)。
P8435 【模板】点双连通分量
int n, m, ans;
std:: vector < int > g[500003], av[500003];
int dfn[500003], low[500003], cnt = 0;
std:: stack < int > s;
void dfs (int x) {
dfn[x] = low[x] = ++cnt; s.push(x);
for (auto &i: g[x])
if (!dfn[i]) {
dfs(i);
low[x] = std:: min(low[x], low[i]);
if (low[i] >= dfn[x]) {
++ans;
while (s.size()) {
int t = s.top(); s.pop();
av[ans].push_back(t);
if (t == i) break;
}
av[ans].push_back(x);
}
}
else
low[x] = std:: min(low[x], dfn[i]);
}
inline void kagari () {
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; ++i) {
int x, y; scanf("%d %d", &x, &y);
if (x == y) continue;
g[x].push_back(y), g[y].push_back(x);
}
for (int i = 1; i <= n; ++i)
if (!dfn[i]) {
if (g[i].size()) dfs(i);
else av[++ans].push_back(i);
}
printf("%d\n", ans);
for (int i = 1; i <= ans; ++i) {
printf("%d ", av[i].size());
for (auto &j: av[i]) printf("%d ", j);
puts("");
}
return;
}
P3388 【模板】割点(割顶)
int n, m;
int head[20003], last[200003], to[200003], ccnt = 0;
#define addedge(x, y) last[++ccnt] = head[x], to[ccnt] = y, head[x] = ccnt
int dfn[20003], low[20003], cnt = 0;
bool f[20003]; int ans = 0;
inline void dfs (int x, int fa) {
dfn[x] = low[x] = ++cnt;
int child = 0;
#define y to[k]
for (int k = head[x]; k; k = last[k]) if (!dfn[y]) {
if (y != fa) dfs(y, x), ++child;
low[x] = min(low[x], low[y]);
if (!fa && child > 1 || fa && low[y] >= dfn[x]) f[x] = true;
} else low[x] = min(low[x], dfn[y]);
#undef y
}
inline void kagari () {
scanf("%d %d", &n, &m); for (int i = 1; i <= m; ++i) { int x, y; scanf("%d %d", &x, &y); addedge(x, y), addedge(y, x); }
for (int i = 1; i <= n; ++i) if (!dfn[i]) dfs(i, 0);
for (int i = 1; i <= n; ++i) if (f[i]) ++ans;
printf("%d\n", ans); for (int i = 1; i <= n; ++i) if (f[i]) printf("%d ", i);
return;
}
P8436 【模板】边双连通分量
和求桥类似,在 \(dfn_x==low_x\) 时栈顶到 \(x\) 都是一个边连通分量。
int n, m, ans;
std:: vector < std:: pair < int, int > > g[500003];
std:: vector < int > av[500003];
int dfn[500003], low[500003], cnt = 0;
std:: stack < int > s;
void dfs (int x, int last) {
dfn[x] = low[x] = ++cnt; s.push(x);
for (auto &i: g[x]) {
if (i.second == (last ^ 1)) continue; // 不走回头路
if (!dfn[i.first]) {
dfs(i.first, i.second);
low[x] = std:: min(low[x], low[i.first]);
}
else
low[x] = std:: min(low[x], dfn[i.first]);
}
if (dfn[x] == low[x]) {
++ans;
while (s.size()) {
int t = s.top(); s.pop();
av[ans].push_back(t);
if (t == x) break;
}
}
}
inline void kagari () {
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; ++i) {
int x, y; scanf("%d %d", &x, &y);
if (x == y) continue;
g[x].push_back(std:: make_pair(y, i << 1)), g[y].push_back(std:: make_pair(x, i << 1 | 1)); // 注意是 2i 和 2i+1
}
for (int i = 1; i <= n; ++i)
if (!dfn[i])
dfs(i, 0); // 不需要再特判是否是孤立点了,因为在 dfs 时还会加上这个答案
printf("%d\n", ans);
for (int i = 1; i <= ans; ++i) {
printf("%d ", av[i].size());
for (auto &j: av[i]) printf("%d ", j);
puts("");
}
return;
}
最小生成树
对一个图,边权和最小的生成树是其最小生成树。
使用 Kruskal 算法,贪心+并查集,每次枚举最短的那条边,时间复杂度为 \(O(m\log m)\)。
int n, m;
struct NODE {
int x, y, z;
inline bool operator < (const NODE &rhs) const { return z < rhs.z; }
} g[200003];
int fa[5003];
inline int getfa (int x) {
if (fa[x] == x) return x;
return (fa[x] = getfa(fa[x]));
}
inline void kagari () {
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; ++i) fa[i] = i;
for (int i = 1; i <= m; ++i) { int x, y, z; scanf("%d %d %d", &x, &y, &z); g[i].x = x, g[i].y = y, g[i].z = z; }
std:: sort(g + 1, g + m + 1);
int mnt = 0, ans = 0;
for (int i = 1; mnt < n && i <= m; ++i)
if (getfa(g[i].x) != getfa(g[i].y)) fa[getfa(g[i].x)] = getfa(g[i].y), ans += g[i].z, ++mnt;
if (mnt != n - 1) printf("orz\n");
else printf("%d\n", ans);
return;
}
树问题集合
树的直径
树中两个点距离的最大值,称为树的直径。
做法:\(O(n)\),两次 dfs, 第一次从 \(i\) 号开始找最远的点 \(u\),\(i\) 为任意数,第二次从 \(u\) 点开始找最远的点 \(v\)。则 \(u, v\) 构成直径。证明略。
树的中心
以点 \(u\) 为根时,从 \(u\) 出发的最长链最短,那么称 \(u\) 为这棵树的中心。它一定在树的直径上。
树链剖分
updating...
二分树
略。见 ICPC 2025 网络赛第一场。
( ゚∀゚)o彡゜ ヒーコー ヒーコー!

浙公网安备 33010602011771号