常用算法代码模板与题单 | D - 图论

【图的存储与遍历】

算法模板

邻接矩阵

// 邻接矩阵存储
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);  // 递归插入到右子树
    }
}

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 38 二叉树的镜像
AcWing 71 二叉树深度
AcWing 419 FBI树
AcWing 476 对称二叉树
AcWing 1255 医院设置
AcWing 1497 树的层序遍历(数组)
AcWing 1589 构建二叉搜索树2(BST)
AcWing 3384 二叉树遍历(指针)
AcWing 3598 二叉树的遍历(前序中序求后序-字符串) 简单 ⭐⭐ 递归分治 / DFS 先序遍历的第一个字符即为当前子树的根节点,在中序遍历中找到该根节点的位置 \(k\),则左侧为左子树(长度为 \(k\))、右侧为右子树,递归处理左子树(先序子串 pre.substr(1,k)、中序子串 ins.substr(0,k))和右子树(先序子串 pre.substr(k+1)、中序子串 ins.substr(k+1)),后序遍历最后输出根节点。
洛谷 P1030 求先序排列 普及-
洛谷 P1305 新二叉树 普及-
洛谷 P4913 二叉树深度 普及-
洛谷 P5908 猫猫和企鹅 普及-
洛谷 P11962 树上漫步 普及-
洛谷 [[P14076 货物运输]] 普及-
洛谷 P17013 满二叉树 普及- ⭐⭐⭐ DFS-树 / 递归判定 后序遍历每个节点,叶子节点 flag=1, dep=1, ans++;非叶子节点当且仅当左右子树均为满二叉树且 dep[l]==dep[r]flag=1, dep=dep[l]+1, ans++,否则 flag=0, dep=max(dep[l],dep[r])+1,时间复杂度 \(O(n)\),空间复杂度 \(O(n)\)
洛谷 AT_abc361_e Tree and Hamilton Path 2 普及
洛谷 P1395 会议 普及
洛谷 P2052 道路修建 普及
洛谷 P9304 3-1 普及
洛谷 [[P10722 二叉树]] 普及
洛谷 P14919 路径覆盖 普及
洛谷 P15801 完全二叉树 普及 ⭐⭐⭐⭐ DFS-树 / 递归判定 后序遍历每个节点,维护子树节点数 cnt、深度 dep、是否完全二叉树 flag,当前节点为完全二叉树当且仅当左右子树均为完全二叉树且满足"深度相等+左满"或"左深1+右满",时间复杂度 \(O(n)\),空间复杂度 \(O(n)\)
洛谷 P1099 树网的核 普及+
洛谷 P5018 对称二叉树 普及+
洛谷 P5658 括号树 普及+
洛谷 [[P10723 黑白翻转]] 普及+
洛谷 P11855 部署 普及+
洛谷 AT_abc362_f Perfect Matching on a Tree 提高+
AtCoder AT_awc0011_d Family Tree and Inheritance
AtCoder AT_awc0101_c Chain of Infection ⭐⭐⭐⭐ 树形DP / BFS层级传播(多轮感染模拟) 初始感染 \(D_i > 0\) 的节点,然后按轮次从叶子向上模拟感染传播:每轮中所有满足"被感染子节点数 $a > $ 未被感染子节点数 \(b\)"的未感染节点同时被感染,重复直到无新增感染,由于每轮感染状态基于该轮开始时的状态同时判定,可用BFS按层处理或反复DFS直到收敛,时间复杂度 \(O(N \times \text{轮数})\)
其他 [[树的深度优先遍历]]

【DFS-图】

算法模板

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 511 联合权值
AcWing 517 信息传递
AcWing 6053 一笔画问题
洛谷 AT_abc333_d Erase Leaves 普及-
洛谷 AT_abc396_d Minimum XOR Path 普及-
洛谷 AT_abc404_c Cycle Graph 普及-
洛谷 P5318 查找文献 普及-
洛谷 [[P10109 工作沟通]] 普及-
洛谷 AT_abc373_d Hidden Weights 普及
洛谷 AT_abc412_d Make 2-Regular Graph 普及
洛谷 AT_abc417_e A Path in A Dictionary 普及
洛谷 AT_abc441_d Paid Walk 普及
洛谷 [[P1444 虫洞]] 普及
洛谷 [[P11249 小杨寻宝]] 普及
洛谷 P11964 图上移动 普及
洛谷 P17014 染色 普及 ⭐⭐⭐ 图论 / 环的染色性质 每个节点度数为 2 的无向图由若干个不相交的环构成,偶环是二分图只需 2 色,奇环需 3 色,答案为所有连通块颜色数的最大值,时间复杂度 \(O(t \cdot n)\),空间复杂度 \(O(n)\)
洛谷 AT_abc451_e Tree Distance 普及+
洛谷 P5022 旅行 普及+
洛谷 [[P6066 Watchcow]] 普及+
洛谷 P8900 Barn Tree 普及+
洛谷 P1041 传染病控制 提高+
洛谷 P1073 最优贸易 提高+
洛谷 [[P1394 山上的国度]]
AtCoder AT_awc0030_d Telephone Game of Messages
其他 [[图的遍历(邻接矩阵)]]
其他 [[图的遍历(vector数组)]]

【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)\) 比较距离输出 TerryJom;先输出 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);
	    }
	}
}

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 164 可达性统计
AcWing 456 车站分级 中等 ⭐⭐⭐⭐ 拓扑排序 / 最长路 / 虚拟节点优化 对每趟车次在途经区间 \([start,end]\) 内建立约束:未停靠站 \(\to\) 虚拟节点(权 \(0\))、虚拟节点 \(\to\) 停靠站(权 \(1\)),将 \(O(s_i^2)\) 条边优化为 \(O(s_i)\) 条,然后拓扑排序求 DAG 最长路,所有原站点的最大层级即为最少级别数。
AcWing 848 有向图的拓扑序列 简单 ⭐⭐ 拓扑排序(Kahn算法) 维护每个节点的入度数组,将所有入度为 \(0\) 的节点入队,BFS 依次取出队头节点并将其邻接点入度减 \(1\),若邻接点入度变为 \(0\) 则入队,最终若队列中处理了 \(n\) 个节点则输出拓扑序列,否则图中存在环输出 \(-1\)
AcWing 1011 挖地雷
AcWing 1191 家谱树
AcWing 1192 奖金
AcWing 1193 病毒
洛谷 P2661 信息传递 普及
洛谷 P7113 排水系统 普及
洛谷 AT_abc335_e Non-Decreasing Colorful Path 普及+
洛谷 P1038 神经网络 普及+
洛谷 P1983 车站分级 普及+
洛谷 [[P3243 菜肴制作]] 普及+
洛谷 AT_abc399_e Replace 提高+
洛谷 P7077 函数调用 提高+
AtCoder AT_awc0028_d Course Enrollment Order ⭐⭐⭐ 拓扑排序 / 优先队列 在 Kahn 算法基础上,将普通队列替换为小根堆,每次从所有入度为 \(0\) 的节点中取出编号最小的加入拓扑序列,遍历其出边将邻接点入度减 \(1\),若入度变为 \(0\) 则入堆,最终输出字典序最小的拓扑序列。
其他 [[确定比赛名次]]

【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]);
}

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 343 排序
AcWing 344 观光之旅
AcWing 345 牛站
AcWing 854 Floyd求最短路 简单 ⭐⭐ Floyd-Warshall 初始化 \(d[i][i]=0\)\(d[i][j]=INF\,(i\neq j)\) 后读入边更新 \(d[a][b]=\min(d[a][b],w)\),执行三重循环 \(d[i][j]=\min(d[i][j],\,d[i][k]+d[k][j])\) 枚举中间点 \(k\) 松弛所有节点对,最后对每个询问输出 \(d[x][y]\)(若 \(\ge INF/2\) 则输出 impossible)。
AcWing 1125 牛的旅行
AcWing 1128 信使
AcWing 1189 刻录光盘
AcWing 1190 珍珠
AcWing 5492 产生数
AcWing 6054 最短路径问题
洛谷 P1700 Milk Factory 普及-
洛谷 B3647 Floyd 普及
洛谷 P1037 产生数 普及
洛谷 P2419 Cow Contest 普及
洛谷 P2910 Clear And Present Danger 普及
洛谷 P6175 无向图的最小环问题 普及 ⭐⭐⭐ Floyd算法(最小环) 在标准 Floyd 算法的外层循环中,当枚举到中间节点 \(k\) 时,利用尚未被 \(k\) 更新dist[i][j](即只经过节点 \(1 \sim k-1\) 的最短路径)加上原始边 \(g[i][k] + g[k][j]\) 构成环 \(i \to \dots \to j \to k \to i\),取所有此类环的最小值即为答案,时间复杂度 \(O(n^3)\)
洛谷 AT_abc416_e Development 普及+
洛谷 P1027 Car的旅行路线 普及+
洛谷 P2047 社交网络 普及+
洛谷 [[P1744 采购特价商品]]

【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;
}

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 340 通信线路
AcWing 341 最优贸易
AcWing 361 观光奶牛
AcWing 851 SPFA求最短路 简单 ⭐⭐⭐ SPFA(Bellman-Ford 队列优化) 初始化 dist[1]=0 后将起点入队并标记,每次取出队首节点遍历其邻接边进行松弛,若邻接点距离被更新且不在队列中则入队,重复直到队列为空,最终 dist[n] 即为答案(若仍为 \(\infty\) 则输出 impossible)。
AcWing 852 SPFA判断负环 简单 ⭐⭐⭐ SPFA / 抽屉原理 将所有节点都入队初始化以确保检测任意连通分量中的负环,SPFA 松弛时维护 cnt[j] 记录到达 \(j\) 的边数,若 cnt[j] >= n 则由抽屉原理可知存在负权回路(否则最短路径最多含 \(n-1\) 条边),输出 Yes 否则 No
AcWing 904 虫洞
AcWing 1127 香甜的黄油
AcWing 1129 热浪
AcWing 1135 新年好
AcWing 1137 选择最佳线路
AcWing 1165 单词环
AcWing 6055 最短路径
洛谷 P1828 香甜的黄油 普及
洛谷 [[P2850 Wormholes]] 普及
洛谷 P3385 负环 普及
洛谷 P4779 单源最短路径(标准版) 普及
洛谷 P10110 商品交易 普及
洛谷 [[P1948 Telephone Lines]] 普及+
洛谷 P3199 最小圈 提高+

【差分约束】

算法模板

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的解

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 362 区间
AcWing 393 雇佣收银员
AcWing 1169 糖果 中等 ⭐⭐⭐⭐ 差分约束 / SPFA 求最长路 将5种约束转化为 \(x_j - x_i \ge c\) 形式的有向边,添加超级源点 \(0\) 向所有节点连权为 \(1\) 的边保证每人至少1颗糖果,用 SPFA(栈优化)求最长路并检测正环,若 cnt[j] >= n+1 则无解输出 \(-1\),否则答案为 \(\sum_{i=1}^{n} dist[i]\)
AcWing 1170 排队布局
洛谷 P1250 种树 普及
洛谷 [[P6145 Timeline]] 普及
洛谷 P10934 西瓜种植 普及
洛谷 P5960 差分约束 普及+
洛谷 P11453 Deforestation 普及+

【生成树】

算法模板

朴素版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;
}

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 346 走廊泼水节
AcWing 858 Prim算法求最小生成树 简单 ⭐⭐⭐ Prim(朴素版) 初始化 dist[1]=0 其余为 \(\infty\),循环 \(n\) 次每次在未加入生成树的节点中找到 dist 最小的节点 \(t\)(若 \(i>0\)dist[t]==INF 则图不连通),将 dist[t] 累加入答案,然后用 \(t\) 更新所有节点到生成树的最小距离 `dist[j]=min(dist[j], g[t][j])$,最终返回总权重。
AcWing 859 Kruskal算法求最小生成树 简单 ⭐⭐⭐ Kruskal / 并查集 将所有边按权值从小到大排序,依次遍历每条边,若其两端点不在同一连通块(并查集 find 根节点不同)则合并连通块并将该边权累加入答案,当选够 \(n-1\) 条边时停止,若边数不足 \(n-1\) 则图不连通输出 impossible
AcWing 1138 城市公交网建设问题
AcWing 1139 最优布线问题
AcWing 1140 最短网络
AcWing 1141 局域网
AcWing 1142 繁忙的都市
AcWing 1143 联络员
AcWing 1144 连接格点
AcWing 1145 北极通讯网络
AcWing 1146 新的开始
AcWing 1148 秘密的牛奶运输
AcWing 4295 QS网络
洛谷 AT_abc352_e Clique Connect 普及
洛谷 P1194 买礼物 普及
洛谷 P1195 口袋的天空 普及
洛谷 P1396 营救 普及
洛谷 [[P1547 Out of Hay]] 普及
洛谷 P1550 Watering Hole 普及
洛谷 P2820 局域网 普及
洛谷 P3366 最小生成树 普及
洛谷 [[P17016 线网建设]] 普及
洛谷 P2700 逐个击破 普及+
洛谷 P1967 货车运输 提高+
洛谷 P2161 会场预约 提高+
洛谷 [[P14080 最小生成树]] 提高+
AtCoder AT_awc0080_d Network Construction ⭐⭐⭐ Kruskal / 并查集 强制边约束 \

【最近公共祖先】

算法模板

倍增法

//预处理节点深度
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 遍历无向图维护 dfnlow,对于子节点 \(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

【分层图】

算法模板

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
洛谷 AT_abc410_d XOR Shortest Walk 普及
洛谷 AT_abc395_e Flip Edge 普及+

【网络流】

算法模板

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.
其他 [[欧拉判定]]
其他 [[一笔画]]
posted @ 2026-07-05 15:05  团爸讲算法  阅读(16)  评论(0)    收藏  举报