数据结构与算法-图论-复习2(差分约束,强连通分量,二分图,LCA,拓扑排序,欧拉路径和欧拉回路)

7. 差分约束

原理

差分约束系统是一种特殊的不等式组,形如 xi​−xj​≤c。可以将其转化为图论中的最短路或最长路问题。

最短路求最大值:当我们要找出满足所有不等式的最大解时,使用最短路算法。对于不等式 xi​−xj​≤c,可以转化为 xi​≤xj​+c,这类似于最短路中的松弛操作 disti≤distj+w(j,i),所以建图时从 j 到 i 连一条权值为 c 的边,然后通过最短路算法求解。

最长路求最小值:当要找出满足所有不等式的最小解时,使用最长路算法。对于不等式 xi​−xj​≥c(可由 xj​−xi​≤−c 变形得到),转化为 xi​≥xj​+c,类似最长路的松弛操作 disti≥distj+w(j,i),建图时从 j 到 i 连一条权值为 −c 的边,用最长路算法求解。

过程

通常需要加入一个超级源点,将超级源点与所有节点相连,边权为 0,以保证图是连通的,然后进行对应的最短路或最长路算法。

代码:

#include <iostream>#include <vector>#include <queue>#include <climits>using namespace std;

const int MAXN = 1005;struct Edge {

int to, w;};

vector<Edge> adjMAXN;int distMAXN;bool in_queueMAXN;

// 最长路算法(求最小值)bool spfa(int s, int n) {

fill(dist, dist + MAXN, INT_MIN);

fill(in_queue, in_queue + MAXN, false);

dists = 0;

queue<int> q;

q.push(s);

in_queues = true;

while (!q.empty()) {

int u = q.front();

q.pop();

in_queueu = false;

for (auto v, w : adju) {

if (distv < distu + w) {

distv = distu + w;

if (!in_queuev) {

q.push(v);

in_queuev = true;

}

}

}

}

return true;}

int main() {

int n, m;

cin >> n >> m;

for (int i = 0; i < m; i++) {

int i, j, c;

cin >> i >> j >> c;

// 求最小值,建图 j -> i, cost: -c

adjj.push_back({i, -c});

}

// 加入超级源点 0

for (int i = 1; i <= n; i++) {

adj0.push_back({i, 0});

}

if (spfa(0, n)) {

for (int i = 1; i <= n; i++) {

cout << disti << " ";

}

cout << endl;

}

return 0;}

8. 有向图的强连通分量

floyd很好理解,本身就是一个用来解决传递闭包的算法

tarjan:通过dfn和low数组,利用dfs的方式找出强连通分量
注意:tarjan得到的序号的逆序就是拓扑序

Floyd 算法

Floyd 算法本身是用于求解全源最短路的算法,但也可以用于解决传递闭包问题。传递闭包是指对于图中的任意两个节点 i 和 j,判断是否存在从 i 到 j 的路径。

代码:

#include <iostream>#include <vector>using namespace std;

const int MAXN = 1005;bool graphMAXNMAXN;

void floyd(int n) {

for (int k = 1; k <= n; k++) {

for (int i = 1; i <= n; i++) {

for (int j = 1; j <= n; j++) {

graphij = graphij || (graphik && graphkj);

}

}

}}

int main() {

int n, m;

cin >> n >> m;

for (int i = 0; i < m; i++) {

int u, v;

cin >> u >> v;

graphuv = true;

}

floyd(n);

// 输出传递闭包结果

for (int i = 1; i <= n; i++) {

for (int j = 1; j <= n; j++) {

cout << graphij << " ";

}

cout << endl;

}

return 0;}

Tarjan 算法

Tarjan 算法通过深度优先搜索(DFS)和两个数组 dfn 和 low 来找出有向图中的强连通分量。

dfnu:表示节点 u 在 DFS 过程中的时间戳,即第一次访问节点 u 的顺序。

lowu:表示节点 u 能够回溯到的最早的节点的时间戳。

代码:

#include <iostream>#include <vector>#include <stack>using namespace std;

const int N = 10005;

vector<int> graphN;int dfnN, lowN, index = 0;bool inStackN;

stack<int> st;int colorN, cnt = 0;

void tarjan(int u) {

dfnu = lowu = ++index;

st.push(u);

inStacku = true;

for (int v : graphu) {

if (!dfnv) {

tarjan(v);

lowu = min(lowu, lowv);

} else if (inStackv) {

lowu = min(lowu, dfnv);

}

}

if (dfnu == lowu) {

cnt++;

while (true) {

int v = st.top();

colorv = cnt;

st.pop();

inStackv = false;

if (v == u) break;

}

}}

int main() {

int n, m;

cin >> n >> m;

for (int i = 0; i < m; i++) {

int u, v;

cin >> u >> v;

graphu.push_back(v);

}

for (int i = 1; i <= n; i++) {

if (!dfni) {

tarjan(i);

}

}

// 输出每个节点所属的强连通分量编号

for (int i = 1; i <= n; i++) {

cout << "Node " << i << " belongs to SCC " << colori << endl;

}

return 0;}

9. 二分图

染色法判定二分图

二分图是指可以将图中的节点分成两个不相交的集合 A 和 B,使得图中的每条边的两个端点分别属于 A 和 B。染色法的基本思想是对图中的节点进行染色,相邻节点染不同的颜色,如果在染色过程中发现相邻节点颜色相同,则该图不是二分图。

代码:

#include <iostream>#include <vector>using namespace std;

const int MAXN = 10005;

vector<int> adjMAXN;int colorMAXN;

bool dfs(int u, int c) {

coloru = c;

for (int v : adju) {

if (colorv == c) {

return false;

}

if (colorv == 0) {

if (!dfs(v, 3 - c)) {

return false;

}

}

}

return true;}

bool isBipartite(int n) {

fill(color, color + MAXN, 0);

for (int i = 1; i <= n; i++) {

if (colori == 0) {

if (!dfs(i, 1)) {

return false;

}

}

}

return true;}

int main() {

int n, m;

cin >> n >> m;

for (int i = 0; i < m; i++) {

int u, v;

cin >> u >> v;

adju.push_back(v);

adjv.push_back(u);

}

if (isBipartite(n)) {

cout << "The graph is bipartite." << endl;

} else {

cout << "The graph is not bipartite." << endl;

}

return 0;}

匈牙利算法求最大边匹配

匈牙利算法的核心思想是不断寻找增广路径,通过交换匹配边和非匹配边来增加匹配的边数。

情景模拟:

女生开始找男朋友,女生i对m个男生有意思,

她就从头开始问这m个男生:

如果1,这个男生没有女朋友,她就和她组成情侣,

如果2,这个男生有女朋友,他们尝试为他现在的女朋友找一个新的并且这个男生的女朋友中意的新的男朋友。

如果上面的两次尝试都失败了,她就去看下一个她中意的。

如果逛了一圈都没有她中意的,她就是一个剩女了

最大边匹配,最小路径点覆盖,最大独立集,最小路径重复点覆盖概念是什么。

他们的关系:最大边匹配=最小路径点覆盖, 最大独立集=最小路径重复点覆盖=点总数-最大匹配边

代码:

#include <iostream>#include <vector>#include <cstring>using namespace std;

const int MAXN = 1005;

vector<int> adjMAXN;int matchMAXN;bool visMAXN;

bool dfs(int u) {

for (int v : adju) {

if (!visv) {

visv = true;

if (matchv == 0 || dfs(matchv)) {

matchv = u;

return true;

}

}

}

return false;}

int hungarian(int n) {

int ans = 0;

memset(match, 0, sizeof(match));

for (int i = 1; i <= n; i++) {

memset(vis, false, sizeof(vis));

if (dfs(i)) {

ans++;

}

}

return ans;}

int main() {

int n, m;

cin >> n >> m;

for (int i = 0; i < m; i++) {

int u, v;

cin >> u >> v;

adju.push_back(v);

}

int max_match = hungarian(n);

cout << "The maximum matching size is: " << max_match << endl;

return 0;}

相关概念

最大边匹配:二分图中匹配边的最大数量。

最小路径点覆盖:在有向无环图中,用最少的不相交路径覆盖所有节点。对于二分图,最大边匹配数等于最小路径点覆盖数。

最大独立集:图中最大的节点集合,使得集合中任意两个节点之间没有边相连。最大独立集的节点数等于节点总数减去最大匹配边数。

最小路径重复点覆盖:在有向图中,用最少的路径覆盖所有节点,路径可以相交。最小路径重复点覆盖数等于最大独立集的节点数。

10. LCA(最近公共祖先)

向上标记法

向上标记法的基本思想是先从节点 a 开始,将其到根节点的路径上的所有节点标记,然后从节点 b 开始向上遍历,第一个遇到的已标记节点就是 a 和 b 的最近公共祖先。

代码:

#include <iostream>#include <vector>using namespace std;

const int MAXN = 10005;

vector<int> adjMAXN;bool visMAXN;int parentMAXN;

void dfs(int u, int p) {

parentu = p;

for (int v : adju) {

if (v != p) {

dfs(v, u);

}

}}

int lca(int a, int b) {

// 标记 a 到根节点的路径

while (a) {

visa = true;

a = parenta;

}

// 从 b 开始向上遍历,找到第一个已标记的节点

while (b) {

if (visb) {

return b;

}

b = parentb;

}

return -1;}

int main() {

int n, m;

cin >> n >> m;

for (int i = 0; i < n - 1; i++) {

int u, v;

cin >> u >> v;

adju.push_back(v);

adjv.push_back(u);

}

dfs(1, 0);

for (int i = 0; i < m; i++) {

int a, b;

cin >> a >> b;

fill(vis, vis + MAXN, false);

int anc = lca(a, b);

cout << "The LCA of " << a << " and " << b << " is: " << anc << endl;

}

return 0;}

倍增算法

倍增算法通过预处理一个二维数组 fui 表示节点 u 向上跳 2i 步到达的节点,然后利用二进制拆分的思想,快速找到两个节点的最近公共祖先。

代码:

#include <bits/stdc++.h>using namespace std;const int N = 40010, M = 17;int n, m;

unordered_map<int, vector<int>> e;int fNM = {0};int root = 0;int disN;

// 预处理数组void bz() {

memset(dis, 0x3f, sizeof dis);

dis0 = 0;

disroot = 1;

queue<int> q;

q.push(root);

while (q.size()) {

int u = q.front();

q.pop();

for (int v : eu) {

if (disv > disu) {

disv = disu + 1;

q.push(v);

fv0 = u;

for (int i = 1; i <= 15; i++) {

fvi = ff\[vi - 1]i - 1;

}

}

}

}}

int get(int x, int y) {

if (disx < disy) swap(x, y);

for (int i = 15; i >= 0; i--) {

if (disf\[xi] >= disy) {

x = fxi;

}

}

if (x == y) return x;

for (int k = 15; k >= 0; k--) {

if (fxk != fyk) {

x = fxk;

y = fyk;

}

}

return fx0;}

int main() {

cin >> n;

for (int i = 0; i < n; i++) {

int a, b;

scanf("%d%d", &a, &b);

if (b == -1) root = a;

else {

ea.push_back(b);

eb.push_back(a);

}

}

bz();

cin >> m;

for (int i = 0; i < m; i++) {

int u, v;

scanf("%d%d", &u, &v);

int anc = get(u, v);

if (anc == u) cout << 1 << endl;

else if (anc == v) cout << 2 << endl;

else cout << 0 << endl;

}

return 0;}

Tarjan 算法

初始化:

对给定的树进行预处理,将每个节点的初始状态设置为未遍历(状态 3),并查集的每个元素的父节点初始化为自身,代表每个节点初始时属于独立的集合。

记录所有需要查询最近公共祖先的节点对,以便后续处理。

深度优先搜索:

从树的根节点开始进行深度优先搜索。

当访问到一个节点时,将其状态设置为已遍历但子节点未回溯完(状态 2),即该节点已进入系统栈。

按照从左到右的顺序递归遍历该节点的所有子节点。在递归返回后,说明该子节点的子树已经全部遍历完毕,将该子节点的状态设置为已遍历且子节点全部回溯完(状态 1),同时将子节点所在的并查集合并到当前节点所在的并查集(即将子节点的并查集代表元素设置为当前节点)。

对于每个与当前节点相关的查询对(u,v)或(v,u):

如果u搜索到v已经处于状态 1(即已遍历且子节点全部回溯完),则通过并查集查找u或v所在集合的代表元素,这个代表元素就是u和v的最近公共祖先。因为此时它们在系统栈中的共同祖先节点,通过并查集的合并操作,已经成为了它们所在集合的代表元素。

将找到的最近公共祖先记录下来,用于后续输出或其他处理。

输出结果:

遍历所有的查询对,根据记录的最近公共祖先信息,输出每对节点的最近公共祖先。

代码:

#include <iostream>#include <vector>#include <utility>using namespace std;

const int MAXN = 10005;

vector<pair<int, int>> adjMAXN;

vector<pair<int, int>> queryMAXN;int resMAXN;int n, m;int pMAXN;int stMAXN;int depMAXN;

int find(int x) {

if (x != px) px = find(px);

return px;}

void tarjan(int u) {

stu = 2;

for (auto v, w : adju) {

if (!stv) {

depv = depu + 1;

pv = u;

tarjan(v);

}

}

int pu = find(u);

for (auto v, id : queryu) {

if (stv == 1) {

resid = depu + depv - 2 * deppu;

}

}

stu = 1;}

int main() {

cin >> n >> m;

for (int i = 1; i <= n; i++) {

pi = i;

}

for (int i = 0; i < n - 1; i++) {

int u, v, w;

cin >> u >> v >> w;

adju.emplace_back(v, w);

adjv.emplace_back(u, w);

}

for (int i = 0; i < m; i++) {

int u, v;

cin >> u >> v;

queryu.emplace_back(v, i);

queryv.emplace_back(u, i);

}

tarjan(1);

for (int i = 0; i < m; i++) {

cout << "The distance between the LCA of query " << i + 1 << " is: " << resi << endl;

}

return 0;}

11. 拓扑排序

Kahn 算法

Kahn 算法的基本思想是不断选择入度为 0 的节点,并将其从图中删除,同时更新其邻接节点的入度。

代码:

#include <iostream>#include <vector>#include <queue>using namespace std;

vector<int> topologicalSort(int n, vector<vector<int>>& graph) {

vector<int> inDegree(n, 0);

vector<int> result;

queue<int> q;

for (int i = 0; i < n; ++i) {

for (int neighbor : graphi) {

++inDegreeneighbor;

}

}

for (int i = 0; i < n; ++i) {

if (inDegreei == 0) {

q.push(i);

}

}

while (!q.empty()) {

int u = q.front();

q.pop();

result.push_back(u);

for (int v : graphu) {

--inDegreev;

if (inDegreev == 0) {

q.push(v);

}

}

}

if (result.size() != n) {

return {};

}

return result;}

int main() {

int n, m;

cin >> n >> m;

vector<vector<int>> graph(n);

for (int i = 0; i < m; i++) {

int u, v;

cin >> u >> v;

graphu.push_back(v);

}

vector<int> topo = topologicalSort(n, graph);

if (topo.empty()) {

cout << "The graph contains a cycle." << endl;

} else {

for (int node : topo) {

cout << node << " ";

}

cout << endl;

}

return 0;}

12. 欧拉路径和欧拉回路

概念

有向图中:

欧拉路径:

定义:在有向图中,从一个顶点出发,经过每条边恰好一次,并且遍历所有顶点的路径称为有向图的欧拉路径。

特征:有向图存在欧拉路径,当且仅当该图是连通的,且除了两个顶点外,其余顶点的入度等于出度。这两个特殊顶点中,一个顶点的入度比出度大 1(终点),另一个顶点的出度比入度大 1(起点)。如果全部点的入度和出度数都是相等的也可以。

欧拉回路:

定义:在有向图中,从一个顶点出发,经过每条边恰好一次,最后回到起始顶点的路径称为有向图的欧拉回路。

特征:有向图存在欧拉回路,当且仅当该图是连通的,且所有顶点的入度等于出度。

无向图中:

欧拉路径:

定义:在无向图中,从一个顶点出发,经过每条边恰好一次,并且遍历所有顶点的路径称为无向图的欧拉路径。

特征:无向图存在欧拉路径,当且仅当该图是连通的,且图中奇度顶点(度数为奇数的顶点)的个数为 0 或 2。若奇度顶点个数为 0,则可以从任意顶点出发;若奇度顶点个数为 2,则必须从其中一个奇度顶点出发,到另一个奇度顶点结束。

欧拉回路:

定义:在无向图中,从一个顶点出发,经过每条边恰好一次,最后回到起始顶点的路径称为无向图的欧拉回路。

特征:无向图存在欧拉回路,当且仅当该图是连通的,且所有顶点的度数均为偶数。

有向图的欧拉路径

代码:

#include <bits/stdc++.h>using namespace std;const int N = 100005;

vector<int> gN;int inN;int outN;int sN;int top = 0;

void dfs(int u) {

while (!gu.empty()) {

int v = gu.back();

gu.pop_back();

dfs(v);

}

s++top = u;}

int main() {

int n, m;

cin >> n >> m;

for (int i = 0; i < m; i++) {

int u, v;

cin >> u >> v;

gu.push_back(v);

inv++;

outu++;

}

for (int i = 1; i <= n; i++) {

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

}

int start = 1;

int cnt = 0;

bool flag = true;

for (int i = 1; i <= n; i++) {

if (outi - ini == 1) {

start = i;

cnt++;

} else if (outi - ini == -1) {

cnt++;

} else if (outi != ini) {

flag = false;

break;

}

}

if (cnt != 0 && cnt != 2) {

flag = false;

}

if (!flag) {

cout << "No" << endl;

} else {

dfs(start);

while (top) {

cout << stop--;

if (top) {

cout << " ";

}

}

cout << endl;

}

return 0;}

相关推荐
xxwl58510 小时前
一个原创题(二)
c++·算法
moeyui70510 小时前
LeetCode 380:Insert Delete GetRandom O(1) 题解和一些延伸
算法·leetcode·职场和发展
三千里10 小时前
路径规划算法-备忘
算法·自动驾驶·动态规划
退休倒计时10 小时前
【每日一题】LeetCode 15. 三数之和 TypeScript
数据结构·算法·leetcode·typescript
林爷万福10 小时前
MATLAB光谱数据分析从入门到项目实战
算法·光纤光谱仪
吴可可12310 小时前
AutoCAD2016二次开发环境配置指南
算法·机器学习
一条大祥脚11 小时前
ABC461 枚举|扫描线|动态前缀和|数论|dfs枚举子集
算法·深度优先
计算机安禾11 小时前
【数据库系统原理】第14篇:关系模式的语义约束:函数依赖的公理系统与闭包计算
人工智能·算法·机器学习
量化君也11 小时前
快速入门量化交易都要学些什么?
大数据·人工智能·python·算法·金融
AbandonForce11 小时前
滑动窗口:定长滑动窗口与不定长滑动窗口
数据结构·c++·算法