图论总结
文章目录
算法这东西,容易忘记, 捡起来很快, 但也要时间, 因此,做一些简短的记录!
图是什么?
树可以存线, 图可以存树,树是一种无环图, 线是最简单的数据结构, 图是较为复杂的一种数据结构!
图的分类
有向 | 无向 | |
---|---|---|
有权 | 有向有权图 | 无向有权图 |
无权 | 有向无权图 | 无向无权图 |
图的表示
- 邻接矩阵
vaecotr<vector<int> > Graph4;
i点到j点的距离! 空间复杂度: V^2 , 求一个点的临接点: V
- 邻接表(链表)
只存每个点能到达的点
空间复杂度: O(V+E ),建图: O(E*V),求一个点的临接点:(O(V))
可以用 hash 或者 红黑树 替换 链表
const int maxn = 1e5 + 1;
struct Node{
int to, cost;
}
// 无向无权图
vector<int> Graph1[maxn];
// 无向有权图
vector<Node> Graph2[maxn];
// hash 找很快! 红黑树 to cost
map<int, int>Graph3[maxn]; // map 默认为0,
图的遍历
DFS
- 树
分为 前序, (二叉树)中序, 后序, 不用记录遍历过的点!
- 图
分为 先序,后续(基本不用),要记录每一个每一个点是否遍历过, visited 记录
模版:
// 递归
void dfs41(int index, bool *visited) {
visited[index] = 1;
cout << index << " -> "; // 前序遍历*************
for (int i = 0; i < len; i++) {
if (!visited[i] && Graph4[index][i] != 0) {
dfs41(i, visited);
}
}
// cout << index << " -> ";后序遍历*****************
}
// 非递归
void dfs42(int start) {
stack<int> stack;
stack.push(start);
bool visited[len] = {0};
while (!stack.empty()) {
int temp = stack.top();
stack.pop();
visited[temp] = 1;
cout << temp << " -> ";
for (int i = 0; i < len; i++)
if (!visited[i] && Graph4[temp][i] != 0) {
stack.push(i);
visited[i] = 1;
}
}
}
BFS
树的BFS和图的BFS是一样的!
无权图的最短路径
// 广度优先
void bfs4(int start) {
queue<int> q;
q.push(start);
bool visited[len] = {0};
while (!q.empty()) {
int temp = q.front();
q.pop();
visited[temp] = 1;
cout << temp << " -> ";
for (int i = 0; i < len; i++)
if (!visited[i] && Graph4[temp][i]) {
visited[i] = 1;
q.push(i);
}
}
}
比较
做题最好用dfs, 基于递归不用容器(自带的栈)存储太多东西!
栈(DFS),队列(BFS), 随机容器(迷宫生成!)
typedef vector<vector<int> > vvint;
typedef vector<int> vint;
int rs_len = 2000;
vint cap(rs_len); //容器
int cap_len = 0;
//自己造一个随机容器!
bool cap_empty() { return cap_len == 0 ? 1 : 0; }
void cap_put(int value) { cap[cap_len++] = value; }
int cap_get() {
if (cap_empty()) {
cout << "error: out of index!\n";
return -1;
}
srand(time(NULL)); // 随机获取一个数
int index = rand() % cap_len;
// cout << "random" << index << " " << cap_len << endl;
int ret = cap[index];
for (int i = index; i < cap_len - 1; i++) cap[i] = cap[i + 1];
cap_len--;
return ret;
}
void gen_map(vvint map);
// @param 迷宫的长度
void rs4(int len) {
rs_len = len;
vvint map(rs_len * rs_len, vint(rs_len * rs_len));
for (int i = 0; i < rs_len; i++) {
for (int j = 0; j < rs_len - 1; j++) {
map[j + i * rs_len][j + i * rs_len + 1] = 1;
map[j + i * rs_len + 1][j + i * rs_len] = 1;
map[j * rs_len + i][(j * rs_len + i) + rs_len] = 1;
map[j * rs_len + i + rs_len][j * rs_len + i] = 1;
}
}
// 从 0 开始 随机搜索, 迷宫入口
cap_put(0);
bool visited[rs_len * rs_len] = {0};
visited[0] = 1;
while (!cap_empty()) {
int temp = cap_get();
for (int i = 0; i < rs_len * rs_len; i++)
if (!visited[i] && map[temp][i] == 1) {
//cout << temp << " " << i << endl;
map[temp][i] = map[i][temp] = 2;
cap_put(i);
visited[i] = 1;
}
}
// traverse4(map);
gen_map(map);
}
// 把遍历的经过转换成地图
void gen_map(vvint map) {
vvint temp(rs_len * 2, vint(rs_len * 2));
for (int i = 0; i < rs_len; i++) {
for (int j = 0; j < rs_len - 1; j++) {
if (map[j + i * rs_len][j + i * rs_len + 1] == 2) { // 每一行
//cout << i << " * " << j;
//cout << " " << j + i * rs_len << " " << j + i * rs_len + 1 << endl;
temp[i * 2][j * 2] = temp[i * 2][(j + 1) * 2] = temp[i * 2][j * 2 + 1] =
1;
}
if (map[j * rs_len + i][(j * rs_len + i) + rs_len] == 2) { // 每一列
//cout << j << " / " << i;
//cout << " " << j * rs_len + i << " " << j * rs_len + i + rs_len << endl;
temp[j * 2][i * 2] = temp[(j + 1) * 2][i * 2] = temp[j * 2 + 1][i * 2] =
1;
}
}
}
for (int i = 0; i < rs_len * 2 - 1; i++) {
for (int j = 0; j < rs_len * 2 - 1; j++) {
int out = (temp[i][j] == 1) ? 1 : 0;
cout << out << " ";
}
cout << endl;
}
}
应用
- 求联通分量(图分成了多少块!?)
void component() {
int ans = 0;
bool visited[len] = {0};
for (int i = 0; i < len; i++) {
if(!visited[i]) ans ++; // 只要有没有访问到的, 就计数加上一
queue<int> q; // 其它 更遍历是一样的
q.push(0);
while (!q.empty()) {
int temp = q.front();
q.pop();
visited[temp] = 1;
//cout << temp << " -> ";
for (int i = 0; i < len; i++)
if (!visited[i] && Graph4[temp][i]) {
visited[i] = 1;
q.push(i);
}
}
}
cout << "unicom component is : " << ans << endl;
}
- 路径问题(从一个点到另一个点是否通?)回溯(dfs), 广搜
bool is_connected(int a, int b) {
bool visited[len] = {0};
queue<int> q;
q.push(a);
while (!q.empty()) {
int temp = q.front();
q.pop();
visited[temp] = 1;
for (int i = 0; i < len; i++)
if (Graph4[temp][i]) {
if (i == b)
return true;
else if (!visited[i]) {
visited[i] = 1;
q.push(i);
}
}
}
return false;
}
-
检测环
-
二分图检测
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4gr16MPZ-1583062762736)(https://raw.githubusercontent.com/Fierygit/picbed/master/20200219211503.png)]
定点V可以分为两部分, 所有边的两个顶点分别属于这两部分!
floodfill算法
把图联通的某一部分填满!
算法思路:
直接遍历!
例题: 游戏开发 -> 扫雷, 最大人工岛屿
桥和隔点
桥: 删除了某一条边,联通分量发生改变!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e6TakjUb-1583062762737)(https://raw.githubusercontent.com/Fierygit/picbed/master/20200219215253.png)]
寻找所有桥!只有DFS可以求!(dfs遍历树)
对于每一条边 v - w , 通过w, 能否通过另一条路回到v
隔点: 删除隔点, 图的联通分量产生变化!
类似寻找桥的算法!
欧拉回路和欧拉路径
哈密尔顿回路
从一个点出发, 经过每个点一次,回到原点
欧拉回路
从一条点出发, 经过每一条边一次, 回到原点
// 离散数学知识, 不会考
状态压缩
无权图可以直接使用位来存储图
最小生成树
Kruskal 需要用到并查集判断有无环, 因此先列一下!
并查集
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。
example:
某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?
测试输入包含若干测试用例。每个测试用例的第1行给出两个正整数,分别是城镇数目N ( < 1000 )和道路数目M;
随后的M行对应M条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。
为简单起见,城镇从1到N编号。
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。
https://blog.youkuaiyun.com/niushuai666/article/details/6662911
#include <stdio.h>
using namespace std;
int pre[1010]; //里面全是掌门
int unionsearch(int root) {
int son, tmp;
son = root;
while (root != pre[root]) //寻找掌门ing……
root = pre[root];
while (son != root) { //路径压缩
tmp = pre[son];
pre[son] = root;
son = tmp;
}
return root; //掌门驾到~
}
int main() {
int num, road, total, i, start, end, root1, root2;
while (scanf("%d%d", &num, &road) , num) {
total = num - 1; //共num-1个门派
for (i = 1; i <= num; ++i) //每条路都是掌门
pre[i] = i;
while (road--) {
scanf("%d%d", &start, &end); //他俩要结拜
root1 = unionsearch(start);
root2 = unionsearch(end);
if (root1 != root2) { //掌门不同?踢馆!~
pre[root1] = root2;
total--; //门派少一个,敌人(要建的路)就少一个
}
}
printf("%d\n", total); //天下局势:还剩几个门派
}
return 0;
}
Kruskal
首先排序, 从最小的边开始选取, 只要不构成环加入!
int isCircle(int root, int pre[]) {
int tmp, son = root;
while (pre[root] != root) root = pre[root];
while (son != root) {
tmp = pre[son];
pre[son] = root;
son = tmp;
}
return root;
}
void kruskal() {
vector<Edge *> edge(edges);
sort(edge.begin(), edge.end(),
[](Edge *a, Edge *b) { return a->value < b->value; });
int pre[len] = {0}; //用作并查集的父节点
for (int i = 0; i < len; i++) pre[i] = i;
for (int i = 1; i < len; i++) {
int a = isCircle(edge[i]->start, pre);
int b = isCircle(edge[i]->end, pre);
if (a != b) { // 不是环
cout << edge[i]->start << " " << edge[i]->end << endl;
pre[b] = a; // 合并
}
}
}
prim
选择离 已扩充节点集合 距离最小的点作为扩充点!
void prime() {
int dis[len] = {INF}; // 关键 !!!!!
int p[len] = {0};
for (int i = 0; i < len; i++) dis[i] = INF;
bool visited[len] = {0};
for (int i = 0; i < len; i++) {
int index, min = -1;
for (int j = 0; j < len; j++) {
if (!visited[j] && (min == -1 || dis[j] < min)) {
min = dis[j];
index = j;
}
}
visited[index] = 1;
cout << p[index] << " " << index << endl;
if (Graph4[p[index]][index] != 0) addNode(root1, p[index], index);
for (int j = 0; j < len; j++) {
if (Graph4[index][j] != 0 && dis[j] > Graph4[index][j]) {
dis[j] = Graph4[index][j];
p[j] = index;
}
}
}
cout << "the tree is : \n";
printTree();
}
最短路径
dijkstra和单源最短路径
无负数, 一直选取当前未访问的最近的节点作为扩展!
void dijkstra(int start) {
bool visited[len] = {0};
int dis[len];
memset(dis, 0x3f, sizeof(int) * len);
dis[start] = 0;
for (int i = 0; i < len; i++) {
int index = -1;
for (int j = 0; j < len; j++)
//选取当前未访问的最近的点作为扩展
if (!visited[j] && (index == -1 || dis[index] > dis[j]))
index = j;
visited[index] = 1;
for (int j = 0; j < len; j++)
if (Graph4[index][j] != 0 && dis[index] + Graph4[index][j] < dis[j])
dis[j] = dis[index] + Graph4[index][j];
}
cout << "dijkstra: " << endl;
cout << INF <<endl;
for (int i = 0; i < len; i++)
cout << start << " to " << i << " len: " << ((dis[i] == INF)? -1 : dis[i]) << endl;
}
Bellman-Ford
迭代n次, 每次按边来缩短dis, 如果某一次迭代没有变化,停止迭代!
void ford(int start) {
int dis[len];
memset(dis, 0x3f, sizeof(int) * len);
dis[start] = 0;
for (int i = 0; i < len; i++) { // 循环 len 次
for (int j = 0; j < edges.size(); j++) { // 对于每一条边
if (dis[edges[j]->start] + edges[j]->value < dis[edges[j]->end]) {
dis[edges[j]->end] = dis[edges[j]->start] + edges[j]->value;
}
}
}
cout << "ford:\n";
for (int i = 0; i < len; i++)
cout << start << " to " << i << " len: " << ((dis[i] == INF) ? -1 : dis[i])
<< endl;
}
Floyed-Warshall
所有点对最短路径, 动态规划, 子的状态直接用在
void floyed() {
int dis[len][len];
memset(dis, 0x3f, sizeof(int) * len * len);
for (int i = 0; i < edges.size(); i++)
dis[edges[i]->start][edges[i]->end] = dis[edges[i]->end][edges[i]->start] =
edges[i]->value;
for (int i = 0; i < len; i++) {
for (int j = 0; j < len; j++) {
for (int k = 0; k < len; k++) {
if (i != j && i != k && j != k) dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
}
}
cout << "floyed:\n";
for (int i = 0; i < len; i++) {
for (int j = 0; j < len; j++) {
if (dis[i][j] != INF)
cout << i << " to " << j << " is " << dis[i][j] << "; ";
}
cout << endl;
}
}
拓扑排序
一.定义
对一个有向无环图(Directed Acyclic Graph, DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若<u,v> ∈E(G),则u在线性序列中出现在v之前。
通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。
注意:
1)只有有向无环图才存在拓扑序列;
2)对于一个DAG,可能存在多个拓扑序列;
二.拓扑序列算法思想
(1)从有向图中选取一个没有前驱(即入度为0)的顶点,并输出之;
(2)从有向图中删去此顶点以及所有以它为尾的弧;
重复上述两步,直至图空,或者图不空但找不到无前驱的顶点为止。
void DAG() {
printG2();
queue<int> q;
for (int i = 0; i < len; i++)
if (in_cnt[i] == 0 && Graph2[i].size() != 0) q.push(i);
int count = 0;
cout << "start topology:\n";
while (!q.empty()) {
int a = q.front();
q.pop();
count++;
cout << a << endl;
for (int i = 0; i < Graph2[a].size(); i++)
if (!--in_cnt[Graph2[a][i]->to]) q.push(Graph2[a][i]->to);
}
cout << "count: " << count << endl;
}
以下用的比较少, 不做总结!
网络流和最大流
Ford-Fulkerson
Edmonds-Karp
匈牙利算法
BFS
DFS