一、图
1,完全图:
任意两个点都有一条边相连
无向完全图的边数:n(n-1)/2
有向完全图的边数:n(n-1)
2.稀疏图:
有很少边成弧的图(e<nlogn)
网:边/弧带权的图
邻接:有边相连的两个顶点之间的关系
3.顶点的度:与该顶点相关联的边的数目
4.连通图(强连通图)
任意两个顶点v,u间都存在v到u的路径
子图。。。。
5.连通分量(强连通分量):
无向图G的极大连通子图称为G的连通分量,有向图为强连通分量
极大(顶点数已经是达到最大了)连通子图:该子图是G连通子图,将G的任何不在孩子图中的顶点加入,子图就不连通了
强连通分量算法:
Tarjan和Kosaraju
==》那么极小连通子图就是生成树啦
二、图的存储结构
G=(V,E) ==>>Graph=(vertex,edge)
V:顶点集
E:边集
1.邻接矩阵
有一个顶点表和一个邻接矩阵(就是一个二维数组)用来反映顶点之间的关系
假设G=(V,E)有n个顶点
顶点表
i | 0 | 1 | 2 | ... | n-1 |
Vertex[i] | V1 | V2 | V3 | Vn |
或者是网=》有权图
const int inf = 0x7FFFFFFF;
const int MAXN = 100;
typedef char VerTexType;
typedef int Arctype;
typedef struct {
VerTexType vexs[MAXN << 2];
Arctype arcs[MAXN << 2][MAXN << 2];
int vexnum, arcnum;
}Graph;
void CreateUDN(Graph& G) {
cin >> G.vexnum >> G.arcnum;
for (int i; i < G.vexnum; ++i) {
cin >> G.vexs[i];
}
for (int i = 0; i < G.vexnum; i++) {
for (int j = 0; j < G.vexnum; j++) {
G.arcs[i][j] = inf;
}
}
int v1, v2, dis;
for (int k = 0; k < G.arcnum; k++) {
cin >> v1 >> v2 >> dis;
G.arcs[v1][v2] = dis;
G.arcs[v2][v1] = dis;
}
}
学习数据结构这门课,我最大收获就是代码规范性和可读性,要是以前,我都直接建个二维数组直接弄了;
可是一般的,邻接矩阵遇到大量数据就裂开了,所以一般我都不用OvO
2.邻接表
邻接表可以说解决了上面的大部分问题,利用链表存储图。
当然,这种用链表实现的邻接表是真的复杂,所以之后我会分享两个不同方式实现的邻接表
链表实现就当基本功吧哈哈哈,知道邻接表长这个样子就行OvO
2.1.1 STL的vector实现邻接表
struct edge {
int from, to, val;
};
const int MAXN = 100;
vector<edge>MAP[MAXN<<2];
void add_edge() {
cin >> n >> m;
while (m--)
{
edge e;
cin >> e.from >> e.to >> e.val;
MAP[e.from].push_back(e);
}
}
遍历方法:
for (int i = 1; i <= n; i++)
{
for (int j = 0; j < MAP[i].size(); j++)
{
edge e = MAP[i][j];
cout << i << e.to << e.val << endl;
}
}
2.1.2 链式前向星(邻接表的数组表示)
有1说1,这个大一在杭电刘老师的算法班学到的图存储方法让我记忆深刻,不完全是他的这个高逼格的名字,还有他的方便以及便于理解(真的挺好理解的OvO)
链式前向星有一个head数组,几个结点就有几个数,然后是edge结构数组(表数组),存储三个数值 to,dis,next 那么head数组的下标就可以理解为from
刚开始head值都是-1当插入,from:1 to:2 dis:19 这样我们的head数组就能更新为edge数组的下标,edge的next也可以更新为head的值
再加入,from:1 to:3 dis:23重复上述操作
草稿而已,忽略字谢谢
struct node
{
int to;
int val;
int next;
}E[maxn << 2];
void add(int from, int to, int w)
{
cnt++;
E[cnt].to = to;
E[cnt].val = w;
E[cnt].next = head[from];
head[from] = cnt;
}
for (int i = 1; i <= n; i++) {
dis[i] = inf;
cin >> x >> y;
Vnode[i].data = { x,y };
Vnode[i].name = s[i-1];
head[i] = -1;
vis[i] = 0;
}
cin >> m;
for (int i = 0; i < m; i++)
{
int u, v, w;
cin >> u >> v >> w;
add(u+1, v+1, w);
add(v+1, u+1, w);
}
这边我们利用链式前向星的遍历以及easyx画一张图的可视化:
void printG(int n) {
double xp, yp, xt, yt;
for (int node = 1; node <= n; node++){
setfillcolor(BLUE);
settextcolor(WHITE);
setbkmode(TRANSPARENT);
fillcircle(Vnode[node].data.x, Vnode[node].data.y, 20);
outtextxy(Vnode[node].data.x - 5, Vnode[node].data.y - 5, Vnode[node].name);
double x = Vnode[node].data.x;
double y = Vnode[node].data.y;
for (int i = head[node]; i!=-1 ; i = E[i].next) {
xt = Vnode[E[i].to].data.x, yt = Vnode[E[i].to].data.y;
setfillcolor(BLUE);
settextcolor(WHITE);
setbkmode(TRANSPARENT);
fillcircle(xt, yt, 20);
outtextxy(xt - 5, yt - 5, Vnode[E[i].to].name);
/* line(x, y, xt,yt);*/
/* Sleep(5000);*/
if (x < xt) {
for (xp = x, yp = y; xp <= xt;) {
xp += (xt - x) / 100, yp = ((y - yt) / (x - xt)) * (xp - x) + y;
line(x, y, xp, yp);
Sleep(15);
}
}
else if (x > xt) {
for (xp = x, yp = y; xp >= xt;) {
xp += (xt - x) / 100, yp = ((y - yt) / (x - xt)) * (xp - x) + y;
line(x, y, xp, yp);
Sleep(15);
}
}
settextcolor(RED);
setbkmode(TRANSPARENT);
char a[100];
_stprintf(a, _T("%d"), E[i].val);
outtextxy((xt+x)/2+10, (yt +y)/2+10, a);
}
}
}
过程很简单,就是画点和连线,和二叉树的动画一致
3, 图的遍历
3.1深度优先DFS--递归,堆栈,类似树的先根遍历
3.2广度优先BFS--队列,类似树的层次遍历
4,最小生成树
我们其实可以通过dfs和bfs的遍历,得到一棵无向生成树---极小连通子图
那如果要得到一棵权值和最小的生成树---最小生成树还需要用到算法
4.1.1MST性质
权值最小的边会包含在最小生成树中
那利用这个思想我们可以试想:贪心法,先给所有权值排个序,在从小到大连线,或者因为每个点都要包括,那就随便找个起点找与他相连接的最短权值;
这一种加点法就是kruskal算法,另一种点连边法就是prim算法
4.1.2prim算法
实质上算法思想就在上面大概提到了。
所以我们的目标就是一个点一个点找最小权值边,然后连起来,直到边数为n(顶点数)-1
我首先想到的是从第一个结点开始,遍历他的“邻居”,并更新存储权值数组dis的值,然后找到最小的权值对应的点再更新dis数组,同时遍历过的点用vis记录
int Prim()
{
dis[1].dis = 1;
vis[1] = 1;
int now = 1;
char sumx[100];
string s="权值为:";
setfillcolor(YELLOW);
setlinecolor(GREEN);
settextcolor(BLACK);
setbkmode(TRANSPARENT);
fillcircle(Vnode[now].data.x, Vnode[now].data.y, 20);
outtextxy(Vnode[now].data.x - 5, Vnode[now].data.y - 5, Vnode[now].name);
Sleep(500);
for (int i = head[now]; i!=-1; i = E[i].next)
{
int u = E[i].to;
if (dis[u].dis > E[i].val) {
dis[u].dis = E[i].val;
dis[u].from = now;
dis[u].to = i;
}
}
int tot = 0;
int sum = 0;
while (tot < n - 1)
{
int old = now;
int MIN = inf;
for (int i = 1; i <= n; i++)
{
if (!vis[i] && dis[i].dis < MIN)
{
now = i;
MIN = dis[i].dis;
old = dis[i].from;
}
}
if (MIN == inf)return -1;
tot++;
sum += MIN;
Sleep(500);
vis[now] = 1;
setfillcolor(YELLOW);
settextcolor(BLACK);
setbkmode(TRANSPARENT);
fillcircle(Vnode[now].data.x, Vnode[now].data.y, 20);
outtextxy(Vnode[now].data.x - 5, Vnode[now].data.y - 5, Vnode[now].name);
double xt = Vnode[now].data.x, yt = Vnode[now].data.y, x = Vnode[old].data.x, y=Vnode[old].data.y;
setlinecolor(GREEN);
setlinestyle(PS_SOLID, 3);
line(x, y,xt, yt);
Sleep(600);
setfillcolor(YELLOW);
settextcolor(BLACK);
setbkmode(TRANSPARENT);
fillcircle(Vnode[now].data.x, Vnode[now].data.y, 20);
outtextxy(Vnode[now].data.x - 5, Vnode[now].data.y - 5, Vnode[now].name);
for (int i = head[now]; i!=-1; i = E[i].next)
{
int u = E[i].to;
if (vis[u] == 1)continue;
if (dis[u].dis > E[i].val) {
dis[u].dis = E[i].val;
dis[u].from = now;
dis[u].to = i;
}
}
_stprintf(sumx, _T("%d"), MIN);
if (tot == n - 1) {
s += sumx;
s += '=';
_stprintf(sumx, _T("%d"), sum);
s += sumx;
}
else {
s += sumx;
s += '+';
}
char c[100];
strcpy(c, s.data());
settextcolor(RED);
outtextxy(0, 400, c);
}
return sum;
}
然后我想可不可以用优先队列实现,因为他是把每个点与他连线的权值进行比较,那就可以将权值小的先出队
基于这个思想,我决定先不弄它(下次一定)
同时,我帮别人看代码的时候也顺带写了一下邻接矩阵的生成树
struct edgenodes {
int from, to;
int dis;
};//用来保存加入lowcost的坐标
void Prim()//求图G中从vs顶点到达其余各顶点的最短路径
{
int dis[100];
char sumx[100];
//int v = 0;
string s = "权值为:";
int now = 0, old;
//v是顶点集,u是已经加入的顶点集
edgenodes lowcost[100];//最短边
int MIN;
int sum = 0;
int vis[100];//因为是加入点,所以要看到底有没有拜访过这个点,所以用vis记录有没有拜访过
for (int i = 0; i < G.vexnum; i++) {
lowcost[i].dis = INT_MAX;
vis[i] = 0;
}//规范一下先给他们初始化,最短边都为无穷,vis都没拜访过
//int closest[100];//最近点
//int i, j, k = 0;
//for (int i = 0; i < G.vexnum; i++)
//{
// lowcost[i] = G.arcs[now][i];
// closest[i] = now;
//}
vis[now] = 1;//这里就是说now已经拜访过了
for (int i = now; i < G.vexnum; i++)
{
if (G.arcs[now][i] < lowcost[i].dis) {
lowcost[i].dis = G.arcs[now][i];//因为记录的是最小边,如果现在的点和i点权值比记录的lowcost小,那就进来
dis[i] = lowcost[i].dis;
lowcost[i].from = now;
lowcost[i].to = i;
}
}
//这边意思是找个点第一个加入 顶点集u 中,像这里加的就是 第一个点 v=0
// 所以我把v改成now就是要加入的点,old是下次要加入的点
//然后找与v相连的点的权值,G.arcs[v][i]就是v到与他相连的点的权值
//
//
//这里我是让加入的点都变个色,就是每一个v加入u中,我就让他变成红色
setfillcolor(RED);
fillcircle(G.vexs[now].x, G.vexs[now].y, R);
//文字
settextcolor(BLACK);
setbkmode(TRANSPARENT);
outtextxy(G.vexs[now].x - 5, G.vexs[now].y - 5, G.vexs[now].data);//变色会改掉原有文字,所以再变一下
for (int i = 1; i < G.vexnum; i++)//vexnum-1遍遍历
{
//这边用old记入上次加入的顶点,目的是画图,就是等下要加入的点和旧点要进行连线
MIN = INT_MAX;
for (int j = 0; j < G.vexnum; j++)
{
if (!vis[j] && lowcost[j].dis < MIN&&lowcost[j].dis!= INT_MAX)//没拜访过,且这条边还比最小的小
{
MIN = lowcost[j].dis;
/*k = j*/;
now = j;//找到新的点,给它更新一下
old = lowcost[j].from;//这边用old记入上次加入的顶点,目的是画图,就是等下要加入的点和旧点要进行连线
}
}
sum += MIN;
//然后画出新的点
setfillcolor(RED);
fillcircle(G.vexs[now].x, G.vexs[now].y, R);
settextcolor(BLACK);
setbkmode(TRANSPARENT);
outtextxy(G.vexs[now].x - 5, G.vexs[now].y - 5, G.vexs[now].data);//变色会改掉原有文字,所以再变一下
//旧的点和新的点连线
setlinecolor(RED);
setlinestyle(PS_SOLID, 3);
line(G.vexs[now].x, G.vexs[now].y, G.vexs[old].x, G.vexs[old].y);
settextcolor(BLACK);
setbkmode(TRANSPARENT);
outtextxy(G.vexs[now].x - 5, G.vexs[now].y - 5, G.vexs[now].data);//画线也会改掉原有文字,所以再写一下
Sleep(600);
vis[now] = 1;//给新的点记录拜访过了
/* for (j = 0; j < G.vexnum; j++)
{
if (lowcost[j] != 0 && G.arcs[k][j] < lowcost[j])
{
lowcost[j] = G.arcs[k][j];
closest[j] = k;
}
}*/
//下面就是将更新一下和now相连的边的权值
for (int k = 0; k < G.vexnum; k++)
{
if (vis[k] == 1)continue;
if (G.arcs[now][k] < lowcost[k].dis) {
lowcost[k].dis = G.arcs[now][k];//因为记录的是最小边,如果现在的点和i点权值比记录的lowcost小,那就进来
lowcost[k].from = now;
lowcost[k].to = k;
dis[k] = lowcost[k].dis;
}
}
_stprintf(sumx, _T("%d"), MIN);
if (i == G.vexnum - 1) {
s += sumx;
s += '=';
_stprintf(sumx, _T("%d"), sum);
s += sumx;
}
else {
s += sumx;
s += '+';
}
char c[100];
strcpy(c, s.data());
settextcolor(RED);
outtextxy(0, 400, c);
}
}
4.1.2krusal算法
首先思想明确:先把权值排序,直接sort,然后从小到大连线
这里注意:你连线要注意连上去会不会成环,所以你要判断,我这里刚开始是想用拓扑排序来判断的,但好像每加一条,就判断一下,有点那个。
所以我翻了一下大一杭电算法课的视频和笔记,才发现,我把并查集给忘了,就是那个找祖先的,然后并在一起的“畅通工程”
为什么并查集可以实现不成环:如果一条边的两个顶点属于一个集合,右加入一条边不就成环了吗
所以思路清晰,贪心+并查集,下次再写
这里附加一段之前写的畅通工程hdu1233
#include<iostream>
#include<algorithm>
using namespace std;
struct edge
{
int start;
int end;
int v;
int cost;
};
edge qiao[5000];
bool cmp(edge a, edge b)
{
return a.cost < b.cost;
}
void chusi(int n)
{
for (int i = 1; i <= n; i++)
{
qiao[i].v = i;
}
}
int find(int x)
{
if (qiao[x].v == x)
{
return x;
}
else
{
return qiao[x].v = find(qiao[x].v);
}
}
void par(int k, int x)
{
k = find(k);
x = find(x);
if (k != x)
{
qiao[k].v = x;
}
}
bool same(int x, int y)
{
return find(x) == find(y);
}
int main()
{
int m, n;
while (~scanf("%d", &n) && n)
{
m = n * (n - 1) / 2;
for (int i = 1; i <= m; i++)
{
scanf("%d", &qiao[i].start);
scanf("%d", &qiao[i].end);
scanf("%d", &qiao[i].cost);
}
sort(qiao + 1, qiao + 1 + m, cmp);
chusi(m);
int sum = 0;
for (int i = 1; i <= m; i++)
{
edge e = qiao[i];
if (same(e.start, e.end)==false)
{
par(e.start, e.end);
sum += e.cost;
}
}
printf("%d\n", sum);
}
}
同时,我之前链式前向星也写了很多算法,像迪杰斯特拉,强连通分量等等,好用的很
写的有点少还有什么拓扑排序,AOE都没整,那就真的下次一定。。。。