图论
图的存储
直接存储
//代码更少的做法
vector<tuple<int,int,int>> e(m);
for(auto &[f, t, w] : e) {
cin >> f >> t >> w;
// -- u, -- v;
}
//-----------------------------------------------
struct edge {
int f, t, w;
};
vector<edge> e(m);
for(int i = 0; i < m; i ++) {
cin >> e[i].f >> e[i].t >> e[i],w;
}
邻接矩阵
vector<vector<int>> e(n + 1, vector<int> (n + 1, 0));
邻接表
类似哈希表拉链法,对每个点都开一个vector
vector<int> e(n);
链式前向星(快)
链表实现邻接表
int e[N * M], ne[N * M], idx, head[N];
memset(head, - 1, sizeof head);
void add(int f, int t) {
e[idx] = t, ne[idx] = head[f], head[f] = idx ++;
}
DFS
利用函数栈空间来实现
标记节点是否被遍历过,递归到下一层
void dfs (int x) {
ban[x] = 1;
for(auto it : e[x]) {
if(!ban[it])
dfs(it);
}
}
BFS
手写队列来实现
每次拿出队头元素,搜索每个子节点并标记并将每个子节点放入队列中去
void b (int x) {
queue<int> q;
q.push(x);
ban[x] = 1;
while(!q.empty()) {
int t = q.front ();
q.pop();
for(auto it : e[t]) {
if(!ban[it]) {
q.push(it);
ban[it] = 1;
}
}
}
}
有向图的拓扑排序
排序后,边的方向都由前指向后
具体流程:
1、让所有入度为0的点入队
2、BFS,每次遍历都删去被遍历的边,即让终点的入度--
3、让入度减为0的点出队
4、数组模拟的队列可以直接输出,STL的队列需要将中间出队的点都存下来
vector<int> d(n + 1, 0), q(n);
vector<vector<int>> e(n +);
bool topsort() {
head = 0, tail = - 1;
for(int i = 1;i <= n;i ++) {
if(d[i] == 0) {
q[++ tail] = i;
}
}
while(tail >= head) {
int t = q[head ++];
for(auto it : e[t]) {
d[it] --;
if(!d[it])
q[++ tail] = it;
}
}
return tail == n - 1;
}
for(int i = 0;i < m;i ++) {
int f, t;
cin >> f >> t;
//add(f, t);
//d[t] ++;
}
最短路
Dijkstra
基于贪心证明,适用于非负权值图
具体流程:
1、创建一个集合\(S\)存放已经确认最短路的点
2、初始化dis数组为$ +\infty$,将源点的dis赋为\(0\)
3、在未被添加到\(S\)集合中的点中选择dis最小的点添加到\(S\)中
4、更新每个dis的上限(松弛操作)
5、重复操作3和4,直至所有点都在\(S\)集合中
暴力做法:
注释可以输出路径
时间复杂度\(O(n^2 + m)\)
邻接表:
typedef pair<int, int> PII;
vector<vector<PII>> e;
vector<int> dis;
//vector<int> path;
int n, m;
void Dijkstra(int n, int s) {
dis.resize(n + 1,0x3f3f3f3f);
//path.resize(n + 1, 0);
//int pre = 0;
dis[s] = 0;
vector<bool> vis(n + 1, 0);
for(int i = 1;i <= n;i ++) {
int u = 0, mind = 0x3f3f3f3f;
for(int j = 1;j <= n;j ++) {
if(dis[j] < mind && !vis[j]) {
u = j, mind = dis[j];
}
}
vis[u] = 1;
//pre = u;
for(auto it : e[u]) {
int v = it.first, w = it.second;
if(dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
//path[v] = pre;
}
}
}
}
邻接矩阵:
vector<int> dis;
vector<vector<int>> e;
void Dijkstra(int n, int s) {
dis.resize(n + 1,0x3f3f3f3f);
//path.resize(n + 1, 0);
//int pre = 0;
dis[s] = 0;
vector<bool> vis(n + 1, 0);
for(int i = 1;i <= n;i ++) {
int u = 0, mind = 0x3f3f3f3f;
for(int j = 1;j <= n;j ++) {
if(dis[j] < mind && !vis[j]) {
u = j, mind = dis[j];
}
}
vis[u] = 1;
//pre = u;
for(int x = 1;x <= n;x ++) {
if(dis[x] > dis[u] + e[u][x]) {
dis[x] = dis[u] + e[u][x];
//path[v] = pre;
}
}
}
}
堆优化做法:
注释可以输出路径
时间复杂度\(O(m\ log\ m)\)
typedef pair<int, int> PII;
vector<vector<PII>> e;
vector<int> dis;
//vector<int> path;
int n, m;
void Dijkstra(int n, int s) {
dis.resize(n + 1,0x3f3f3f3f);
//path.resize(n + 1, 0);
//int pre = 0;
dis[s] = 0;
vector<bool> vis(n + 1, 0);
priority_queue<PII, vector<PII>, greater<PII>> q;
q.push({0, s});
while(!q.empty()) {
PII u = q.top();
q.pop();
int tmpdis = u.first, tmpu = u.second;
if(vis[tmpu]) continue;
vis[tmpu] = 1;
for(auto it : e[tmpu]) {
int v = it.first, w = it.second;
if(dis[v] > dis[tmpu] + w) {
dis[v] = dis[tmpu] + w;
q.push({dis[v], v});
//path[v] = pre;
}
}
}
}
Bellman-Ford
可以用来求路径长度不大于k的最短路,可以存在负权边,可以检测负环
时间复杂度\(O(n\times m)\)
具体流程
进行n轮操作(如要求路径长度不大于k则进行k轮操作,并对前一轮的dis进行备份),对每一条边都进行松弛操作
注释表示进行k轮操作(并且不需要flag)
bool Bellmanford(int n, int s) {
dis.resize(n + 1, 0x3f3f3f3f);
dis[s] = 0;
bool flag = 0;
for(int i = 1; i <= n; i ++) {
flag = 0;
//vector<int>tmpdis = dis;
for(int j = 1; j <= n; j ++) {
for(auto it : e[j]) {
int u = it.first, w = it.second;
if(dis[u] > dis[j] + w) {
dis[u] = dis[j] + w;
flag = 1;
}
//if(dis[u] > tmpdis[j] + w) {
// dis[u] = tmpdis[j] + w;
// flag = 1;
//}
}
}
}
//返回是否存在负环
return flag;
}
SPFA
基于bellman-ford算法的优化,用队列来维护,将上一次循环更新dis的点push队列,可以存在负权边,可以检测负环
时间复杂度一般\(O(m)\),最坏\(O(n \times m)\)
bool spfa(int n, int s) {
dis.resize(n + 1, 0x3f3f3f3f);
dis[s] = 0;
queue<int> q;
vector<int> vis(n + 1, 0), cnt(n + 1, 0);
q.push(s);
vis[s] = 1;
while(!q.empty()) {
int v = q.front();
q.pop();
vis[v] = 0;
for(auto it : e[v]) {
int u = it.first, w = it.second;
if(dis[u] > dis[v] + w ) {
dis[u] = dis[v] + w;
cnt[u] ++;
if(cnt[u] >= n) {
return 1;
}
if(!vis[u]) {
q.push({u});
vis[u] = 1;
}
}
}
}
//返回是否存在负环
return 0;
}
Floyd
基于动态规划证明,下面给出优化后的代码
时间复杂度\(O(n^3)\)
void floyd() {
dis.resize(n + 1, vector<int>(n + 1), 0x3f3f3f3f);
for(int i = 0; i < m; i ++) {
int v, u, w;
cin >> v >> u >> w;
dis[v][u] = min(dis[v][u])
}
for(int i = 0; i <= n; i ++) {
dis[i][i] = 0;
}
for(int k = 1; k <= n; k ++) {
for(int i = 1;i <= n; i ++) {
for(in j = 1; j <= n; j ++) {
dis[i][j] = min(dis[i][j], dis[k][j] + dis[i][k]);
}
}
}
}
最小生成树
Prim
时间复杂度\(O(n^2)\)
vector<vector<int>> e(n + 1, vector<int>(n + 1));
vector<int> dis;
int prim() {
int res = 0;
dis.resize(n + 1, 0x3f3f3f3f);
vector<bool> vis(n + 1, 0);
dis[1] = 0;
for(int i = 0;i < n;i ++) {
int u = 0, mind = 0x3f3f3f3f, flag = 1;
for(int j = 1;j <= n;j ++) {
if(dis[j] < mind && !vis[j]) {
u = j, mind = dis[j];
flag = 0;
}
}
if(flag) return 0x3f3f3f3f;
vis[u] = 1;
res += dis[u];
for(int x = 1;x <= n;x ++) {
dis[x] = min(dis[x], e[u][x]);
}
}
return res;
}
Kruskal
具体流程:
按边权从小到大遍历每条边,询问边的节点是否在同一集合中,如果不在则合并,最终得到的结果就是最小生成树
时间复杂度\(O(m\ log\ m)\)
struct Edge {
int v, u, w;
bool operator< (const edge &a) const {
return w < a.w;
}
}
vector<Edge> e(m);
for(int i = 0; i <= n; i ++) {
p[i] = i;
}
vector<int>p(n + 1);
int find(int x) {
return p[x] == x ? x : p[x] = find(p[x]);
}
int Kruskal() {
int res = 0, cnt = 0;
sort(e.begin(), e.end());
for(auto it : e) {
int x = find(it.v), y = find(it.u);
if(x != y) {
res += it.w;
cnt ++;
p[y] = x;
}
}
if(cnt == n - 1) return res;
return -1;//表示不存在最小生成树
}