哈哈哈,开始总结图论了...
这一篇感觉会很长,这就是我学习的成果了。
图的建立
1.邻接表(这个一般存储 稀疏图 )
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010;
int h[N],e[N],ne[N],idx; //数组模拟单链表
void add(int a,int b) //这个是加一条从a到b的边
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
int main()
{
memset(h , -1 ,sizeof h); // 这个函数是在cstring头文件里的 赋初值的函数
int a,b;
cin >> a >> b;
add(a,b);
}
2.邻接矩阵(这个一般存储 稠密图)
#include <iostream>
using namespace std;
const int N = 10010;
int g[N][N]; // 这个就是邻接表的二维数组
int main()
{
int a,b,w;
cin >> a >> b >> w;
g[a][b] = w; // 这个就是 点a 到 点b 有一条权值为w的边
return 0;
}
这个怎么看是稀疏图还是稠密图 , 第一种 就是看 点 和 边 的数量如果 边的数量 接近 点数量的平方则是稠密图,反之则是稀疏图, 第二种 就是看算法题给的 输入模板是怎么样的。
图的深度优先遍历 和 宽度优先遍历
1.深度优先遍历是利用 栈 来实现的但是不需要自己来写栈,电脑会帮我们实现栈。
基本思想:首先我们需要定义一个 bool 数组来确定这个点 是否被遍历过,我们需要想到该怎么样递归才能实现我们算法思想,而且需要想到我们每次递归需要想到 返回 值的意义,和什么时候结束递归。
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010;
bool st[N];
void dfs(int u)
{
if(/* 达到什么地方就开始返回,或者输出 */)
st[u] = true;
for(int i = h [u] ; i != h[a] ; i ++) // 这个就是遍历单链表的模板
{
int j = e[i];
if(!st[j]) dfs(j);
}
}
int main()
{ int x;
cin >> x;
dfs(x);
return 0;
}
2. 宽度优先遍历就是 用队列来写的咯,这个就需要自己来手写队列,但是如果你会stl容器的话就不需要自己手写。
基本思想:首先也需要开一个 bool数组 来记录该点是否被遍历过了,每次将 每个点所连接的点 一一入队,进入队列后每次取出对头来继续遍历这个点 ,之前被遍历的点就不会在遍历了,直到找到我们需要的那个点,就可以了。
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 100010;
queue <int>q;
bool st[N];
void bfs()
{
st[1]] = true;
q.push(1);
while(q.size())
{
int t = q.front() //取出队头
q.pop(); //弹出对头 因为我们等下需要遍历下一个点
for(int i = h[t]; i != -1; i ++)
{
int j = e[i];
if(!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
拓扑排序
这个意思是 看一个图是不是 拓扑图,首先要是一个有向无环图,其次还有两个条件
-
每个顶点出现且只出现一次。
-
若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前
基本思想 : 这个就比较简单了,首先我们先记录图每个点的 入度 ,利用队列将每次入度为 0 的点入队,当每个点都遍历过的话 ,如果队列的长度还是没有达到 点数 - 1,就认为不是,反之则是拓扑图。
bool topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ ) // 先将入度为 0 的点 入队
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; i != -1; i = ne[i]) //遍历每个点 将他的入度减1
{
int j = e[i];
if (-- d[j] == 0) // 看看减 1 之后可不可以入队
q[ ++ tt] = j;
}
}
return tt == n - 1; // 如果最后有n - 1个点入对了,就是拓扑图
}
最短路算法
首先就是最经典的dijkstra 算法 (只可以求全是正权边的图的最短路)
基本思想就是:首先先定义一个 dist 数组,这个就是记录每个点到起点的距离,其次就是将dist的值 全部赋值为无穷大 ,这样我们后面才可以用 短的边来更新无穷大,循环n - 1 次,在里面嵌套一个循环(用来找到最短的距离),后面的循环就是利用 最短的边 来更新其他的边看看可不可以更新,最后在判断题意。
int dijkstra()
{
memset( dist, 0x3f ,sizeof dist);
dist[1]= 0;
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;
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;
else return dist[n];
}
Bellman-Ford算法(求有边数限制的最短路 可以求带有负权边)
这个就比较简单啦,可以开结构体,然后遍历结构体就可以了,每次选最小的值就可以了,但是需要开备份数组,应为每次循环 数值 就只可以 从上次数组还没有开始改变修改,如果不加备份数组就会发生这次改了的数值去更新这次循环后面的数值。
struct Edge{
int a,b,w;
}edges[N];
void bell_man()
{
memset(dist , 0x3f , sizeof dist);
dist[1]=0;
for(int i = 1; i <= k ; i ++)
{
memcpy(backup , dist ,sizeof dist);
for(int j = 1; j <= m ; j ++)
{
int a = edges[j].a;
int b = edges[j].b;
int w = edges[j].w;
dist[b] = min(dist[b] , backup[a]+w);
}
}
}
spfa算法(求带有负权边的最短路)
这个求带有负权边的最短路一般是最好的,它有时候还可以求全是正权边的最短路,但是有时候会卡,(所以还是用dijkstra 好点)。
基本思想:这个和dijkstra不同的是,dijkstra是两重循环 每次找到最短的那条边,去更新其他的边,而spfa则是找到每次更新的边,然后入队,再取出对头更新其他点到起点的距离。
void spfa()
{
memset(dist , 0x3f ,sizeof dist);
dist[1] = 0;
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])
{
q.push(j);
st[j] =true;
}
}
}
}
}
而求图中是否有负环就是,记录每个点到 起点 的边的数量 ,如果大于等于 点的数量则证明有负环。
最小生成树
1.朴素版的prim算法(求稠密图的最小生成树)
基本思想 : 这个和dijkstra 很像 ,开一个dist 数组,但是这个和dijkstra不一样的是这个是到 已经是最小生成树集合的最短距离,首先找到最短距离,然后判断是不是正无穷,如果是就可以判断是不连通的了,就直接可以退出,反之则是,就拿这个去更新其他的边,如果更新了就入队,继续这样循环下去,直到循环完,最后返回权值。
//INF是无穷大
int prim()
{
memset(dist, 0x3f, sizeof dist);
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];
st[t] = true;
for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
}
return res;
}
2.Kruskal算法(求稀疏图的最小生成树)
基本思想:Kruskal 算法 利用到 并查集 来维护两个集合,首先我们将并查集的赋初值,按照 权值排列,先取出最小的权值的边,然后判断 这两个点是否已经再一个集合内 ,如果再的话就不需要修改,如果不在的话就需要将两个点的集合进行合并,最后看集合内点的数量 和 总的点数量来判断是不是连通图。
//INF是无穷大
int kruskal()
{
sort(edges, edges + m);
for (int i = 1; i <= n; i ++ ) p[i] = i; // 初始化并查集
int res = 0, cnt = 0;
//res记录的是现在最小生成树的所有边的权值大小,cnt记录的是集合内的点的数量
for (int i = 0; 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;
}
二分图
1.染色法
首先有一个定理 ,二分图中一定没有奇数环,没有奇数环的图一定是二分图。
奇数环:就是一个环中边的数量是奇数。
如果是一个二分图一定可以用 1(表示蓝色),2(表示红色),一定可以用这两种颜色染色,如果不可以或者出现矛盾就说明这个不是二分图。
染色的话一般用的是深搜来染色。
bool dfs(int u, int c)
{
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (color[j] == -1) //当它还没有染色
{
if (!dfs(j, !c)) return false; //就继续染色
}
else if (color[j] == c) return false;
//当它染过色 并且与这个点 染色颜色一样的话也可以判断不是二分图
}
return true;
}
bool check()
{
memset(color, -1, sizeof color);
bool flag = true; //定义一个标志 如果返回false 就说明不是二分图
for (int i = 1; i <= n; i ++ )
if (color[i] == -1)
if (!dfs(i, 0)) //当它发生了冲突的话,返回 false
{
flag = false;
break;
}
return flag;
}
2.匈牙利算法
这个算法就很有意思了,就是配对的问题。
基本思想:有两个集合,集合内部没有联系,但是两个集合之间的某一个元素可能会和另一个集合之间的几个元素之间有联系。
就比如说 集合 1 里面全是男生 ,2 里面全是女生,但是集合 1 里面的男生比较花心,可能会脚踏两条船,甚至多条船,但是有些男生就只会喜欢上一个女生,这个时候就出现了矛盾,解决的方法就是 看一下这个男生是不是脚踏两条船,如果是的话就把现在和这个男生匹配的女生给让出来,给这个比较坚持的男生,这样就可以两两都匹配,但是 如果这个男生也只有一个女生就不会让出来,那就不是一个二分图。---渣男算法
bool find(int x) // 寻找能否匹配
{
for (int i = h[x]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) // 当这个女生没有被匹配
{
st[j] = true;
if (match[j] == 0 || find(match[j])) //或者可以找到下家
{
match[j] = x;
return true;
}
}
}
return false;
}
以上,差不多就是我的全部总结咯 ...
慢慢加油.......