运筹学网络模型

1.最小生成树

1.1 什么是树?

如果一个无向连通图不包含回路(连通图中不存在环),那么就是一个树。
如下图所示即为一个数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pm3hK7WV-1650418044140)(C:\Users\Lin\AppData\Roaming\Typora\typora-user-images\1650351532633.png)]

1.2 什么是最小生成树

最小生成树,顾名思义,就是在某个图结构中进行选取构造,构造出一个树。一个有N个点的图,边一定是大于等于N-1条的。图的最小生成树,就是在这些边中选择N-1条出来,连接所有的N个点。这N-1条边的边权之和是所有方案中最小的。

1.3 最小生成树的构造

(1)prim算法

prim算法又称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。

也就是说,每次迭代将会把图中某一点选中,确保点到已连接区域为最短距离。

代码如下(代码来源)

/*
*邮箱:unique_powerhouse@qq.com
*blog:https://me.csdn.net/hzf0701
*注:文章若有任何问题请私信我或评论区留言,谢谢支持。
*
*/
#include<bits/stdc++.h>	//POJ不支持

#define rep(i,a,n) for (int i=a;i<=n;i++)//i为循环变量,a为初始值,n为界限值,递增
#define per(i,a,n) for (int i=a;i>=n;i--)//i为循环变量, a为初始值,n为界限值,递减。
#define pb push_back
#define IOS ios::sync_with_stdio(false);cin.tie(0); cout.tie(0)
#define fi first
#define se second
#define mp make_pair

using namespace std;

const int inf = 0x3f3f3f3f;//无穷大
const int maxn = 1e3;//最大值。
typedef long long ll;
typedef long double ld;
typedef pair<ll, ll>  pll;
typedef pair<int, int> pii;


int n,m;//图的大小和边数。
int graph[maxn][maxn];//图
int lowcost[maxn],closest[maxn];//lowcost[i]表示i到距离集合最近的距离,closest[i]表示i与之相连边的顶点序号。
int sum;//计算最小生成树的权值总和。
void Prim(int s){
	//初始化操作,获取基本信息。
	rep(i,1,n){
		if(i==s)
			lowcost[i]=0;
		else
			lowcost[i]=graph[s][i];
		closest[i]=s;
	}
	int minn,pos;//距离集合最近的边,pos代表该点的终边下标。
	sum=0;
	rep(i,1,n){
		minn=inf;
		rep(j,1,n){
			//找出距离点集合最近的边。
			if(lowcost[j]!=0&&lowcost[j]<minn){
				minn=lowcost[j];
				pos=j;
			}
		}
		if(minn==inf)break;//说明没有找到。
		sum+=minn;//计算最小生成树的权值之和。
		lowcost[pos]=0;//加入点集合。
		rep(j,1,n){
			//由于点集合中加入了新的点,我们要去更新。
			if(lowcost[j]!=0&&graph[pos][j]<lowcost[j]){
				lowcost[j]=graph[pos][j];
				closest[j]=pos;//改变与顶点j相连的顶点序号。
			}
		}
	}
	cout<<sum<<endl;//closest数组就是我们要的最小生成树。它代表的就是边。
}
void print(int s){
	//打印最小生成树。
	int temp;
	rep(i,1,n){
		//等于s自然不算,故除去这个为n-1条边。
		if(i!=s){
			temp=closest[i];
			cout<<temp<<"->"<<i<<"边权值为:"<<graph[temp][i]<<endl;
		}
	}
}
int main(){
	//freopen("in.txt", "r", stdin);//提交的时候要注释掉
	IOS;
	while(cin>>n>>m){
		memset(graph,inf,sizeof(graph));//初始化。
		int u,v,w;//临时变量。
		rep(i,1,m){
			cin>>u>>v>>w;
			//视情况而论,我这里以无向图为例。
			graph[u][v]=graph[v][u]=w;
		}
		//任取根结点,我这里默认取1.
		Prim(1);
		print(1);//打印最小生成树。
	}
	return 0;
}


算法流程:

1.初始化三大件visited数组,lowcost数组,closest,其中lowcost[i]表示i到距离集合最近的距离,closest[i]表示i与之相连边的顶点序号。visited数组可以被优化掉,因为当lowcost数组元素值为0的时候,就代表该点在生成树中。

2.首先根据起始点,将lowcost初始化为到起始点距离,closest初始化为起始点。

3.随后开始在lowcost中寻找距离生成树区域的最短距离及点,可以使用优先队列优化。

4.随后利用新的点,更新lowcost数组和closest数组。

5.随后重复3-4操作,直到所有点都被加入到生成树区域中。

(2)Kruskal算法

如果说,prim算法是插点法的话,克鲁斯卡尔就是插边法,每次在图中选择出一条最小边,并将这条边加入生成树中,如果满足树结构,就保留,否则就挑选下一个。因此克鲁斯卡尔算法中图需要使用edge边数组存储。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Au3zDMqj-1650418044142)(C:\Users\Lin\AppData\Roaming\Typora\typora-user-images\1650379029687.png)]

具体实现代码如下(代码来源):

#include <bits/stdc++.h>

using namespace std;

const int N = 200, M = 0x7fffffff;
typedef char vextype;

typedef struct 
{
    vextype vex[N];
    int arcs[N][N];
    int vexnum, arcnum;
}AMGraph;

struct EdgeM
{
    vextype Head;
    vextype Tail;
    int lowcost;
}Edge[N];

int Vexset[N];//辅助数组,用于排除Kruskal出现环的情况
/*思路:在Vexset数组中分别查找v1和v2所在的连通分量vs1和vs2,进行判断
1. 如果vs1 != vs2 表明两个点处于不同的连通分量,输出此边,合并vs1和vs2两个连通分量
2. 如果vs1和vs2相等,表明所选两个顶点属于同一个连通分量,那么则舍去此边选择下一个权值最小的边
*/
int minspatree_matrix[N][N];//最小生成树的邻接矩阵
pair <int, int> ans[N];

void CreatGraph(AMGraph &G)
{
    cout << "请输入顶点数目和边的数目:" << endl;
    cin >> G.vexnum >> G.arcnum;
    
    for (int i = 0; i < G.vexnum; ++ i) {
        for (int j = 0; j < G.vexnum; ++ j)
            G.arcs[i][j] = M;
    }
    cout << "请输入所有顶点名称:" << endl;
    for (int i = 0; i < G.vexnum; ++ i) cin >> G.vex[i];

    cout << "请输入所有边的权值:" << endl;
    int x, y, w;
    for (int i = 0; i < G.arcnum; ++ i) {
        cin >> x >> y >> w;
        G.arcs[x][y] = G.arcs[y][x] = w;
        Edge[i] = {G.vex[x], G.vex[y], w};
    }
}
/*下面是对书中Sort函数的实现*/
bool cmp(const EdgeM &a, const EdgeM &b)
{
    return a.lowcost < b.lowcost;
}

void Sort(AMGraph G)
{
    sort(Edge, Edge + G.arcnum, cmp);
}

int Locate(AMGraph G, vextype v)
{
    for (int i = 0; i < G.arcnum; ++ i) {
        if (G.vex[i] == v) return i;
    }
    return -1;
}

void MiniSpanTree(AMGraph &G)
{
    Sort(G);
    int cnt = 0;
    for (int i = 0; i < G.vexnum; ++ i) Vexset[i] = i;//初始时,每个点为一个单独的连通分量                                                 
    for (int i = 0; i < G.arcnum; ++ i) {//*这层循环是有问题的,选边的总数为G.vexnum - 1
        int v1 = Locate(G, Edge[i].Head);//而不是遍历所有的边,但是书上面就是这么写的
        int v2 = Locate(G, Edge[i].Tail);//仅供基础学习基本思路。

        int vs1 = Vexset[v1];
        int vs2 = Vexset[v2];

        if (vs1 != vs2)
        {
            cout << Edge[i].Head << ' ' << Edge[i].Tail << endl;
            ans[cnt ++] = {Locate(G, Edge[i].Head), Locate(G, Edge[i].Tail)};//c++11标准,c99会警告,分开赋值即可
            for (int j = 0; j < G.vexnum; ++ j) {
                if (Vexset[j] == vs2) Vexset[j] = vs1;
            }
        }
    }
    // for (int i = 0; i < G.vexnum - 1; ++ i) {
    //     int x = ans[i].first, y = ans[i].second;//将已经选择完成的边,放入最小生成树矩阵中
    //     minspatree_matrix[x][y] = minspatree_matrix[y][x] = G.arcs[x][y];
    // }
}

int main()
{
    AMGraph G;
    CreatGraph(G);
    MiniSpanTree(G);
    /*输出原始的矩阵*/
    // for (int i = 0; i < G.vexnum; ++ i) {
    //     for (int j = 0; j < G.vexnum; ++ j)
    //         cout << G.arcs[i][j] << ' ';
    //     cout <<endl;
    // }
    /*输出最小生成树的邻接矩阵*/
    // for (int i = 0; i < G.vexnum; ++ i) //cout << Vexset[i] << ' ';
    // {
    //     for (int j = 0; j < G.vexnum; ++ j)
    //         cout << minspatree_matrix[i][j] << ' ';
    //     cout << endl;
    // }

    system("pause");

    return 0;
}
/*
测试用例:
4 5
A B C D
0 1 3
0 3 4
1 2 6
2 3 7
1 3 5
*/

算法流程为:

1.将每条边(i,j,w)采用结构体方式保存,并进行排序。

2.选取剩余边中的最小边,将其加入到生成树中,并判断生成树是否结构合理,如果合理,继续选取下一条边,如果不合理,那也是继续选取下一条边,但是并不保留刚刚选取的数字。

3.重复2过程直到达到指定边数。

2.最短路问题

2.1 Dijstra算法

最短路问题的Dijstra算法我在这篇文章已经见过这里就不再赘述。

2.2 Floyd算法

我们都知道,Dijstra算法只能解决权重为真的图的最短路径,而Floya尽管时间复杂度(O(n3))更高,但是其具有更少的缺点。

弗洛伊德是基于动态规划而构思的,弗洛伊德算法定义了两个二维矩阵:

  1. 矩阵D记录顶点间的最小路径
    例如D[0] [3] = 10,说明顶点0 到 3 的最短路径为10;
  2. 矩阵P记录顶点间最小路径中的中转点
    例如P[[0] [3] = 1 说明,0 到 3的最短路径轨迹为:0 -> 1 -> 3。

其代码为(代码来源)

#include <stdio.h>
#include <stdlib.h>

#define MAXN 10 
#define INF = 1000

typedef struct struct_graph{
    char vexs[MAXN];
    int vexnum;//顶点数 
    int edgnum;//边数 
    int matirx[MAXN][MAXN];//邻接矩阵 
} Graph;

int pathmatirx[MAXN][MAXN];//记录对应点的最小路径的前驱点,例如p(1,3) = 2 说明顶点1到顶点3的最小路径要经过2 
int shortPath[MAXN][MAXN];//记录顶点间的最小路径值

void short_path_floyd(Graph G, int P[MAXN][MAXN], int D[MAXN][MAXN]){
    int v, w, k;
    //初始化floyd算法的两个矩阵 
    for(v = 0; v < G.vexnum; v++){
        for(w = 0; w < G.vexnum; w++){
            D[v][w] = G.matirx[v][w];
            P[v][w] = w;
        }
    }

    //这里是弗洛伊德算法的核心部分 
    //k为中间点 
    for(k = 0; k < G.vexnum; k++){
        //v为起点 
        for(v = 0 ; v < G.vexnum; v++){
            //w为终点 
            for(w =0; w < G.vexnum; w++){
                if(D[v][w] > (D[v][k] + D[k][w])){
                    D[v][w] = D[v][k] + D[k][w];//更新最小路径 
                    P[v][w] = P[v][k];//更新最小路径中间顶点 
                }
            }
        }
    }

    printf("\n初始化的D矩阵\n");
    for(v = 0; v < G.vexnum; v++){
        for(w = 0; w < G.vexnum; w++){
            printf("%d ", D[v][w]);
        }
        printf("\n");
    }

    printf("\n初始化的P矩阵\n");
    for(v = 0; v < G.vexnum; v++){
        for(w = 0; w < G.vexnum; w++){
            printf("%d", P[v][w]);
        }
        printf("\n");
    }

    v = 0;
    w = 3;
    //求 0 到 3的最小路径
    printf("\n%d -> %d 的最小路径为:%d\n", v, w, D[v][w]);
    k = P[v][w];
    printf("path: %d", v);//打印起点
    while(k != w){
        printf("-> %d", k);//打印中间点
        k = P[k][w]; 
    }
    printf("-> %d\n", w);
}

int main(){
    int v, w;
    Graph G;
    printf("请输入顶点数:\n");
    scanf("%d", &G.vexnum);
    printf("请输入初始矩阵值:\n");
    for(v = 0; v < G.vexnum; v++){
        for(w = 0; w < G.vexnum; w++){
            scanf("%d", &G.matirx[v][w]);
        }
    }
    printf("\n输入的矩阵值:\n");
    for(v = 0; v < G.vexnum; v++){
        for(w = 0; w < G.vexnum; w++){
            printf("%d ", G.matirx[v][w]);
        }
        printf("\n");
    }
    short_path_floyd(G, pathmatirx, shortPath);
}

弗洛伊德算法的核心在于,遍历每个结点,将每个结点作为中间结点,更新整个邻接图矩阵的最短路径。

for(k = 0; k < G.vexnum; k++){
    //v为起点 
    for(v = 0 ; v < G.vexnum; v++){
        //w为终点 
        for(w =0; w < G.vexnum; w++){
            if(D[v][w] > (D[v][k] + D[k][w])){
                D[v][w] = D[v][k] + D[k][w];//更新最小路径 
                P[v][w] = P[v][k];//更新最小路径中间顶点 
            }
        }
    }
}

2.3 spfa算法

SPFA算法是求解单源最短路径问题的一种算法,由理查德·贝尔曼(Richard Bellman) 和 莱斯特·福特 创立的。有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为 Edward F. Moore 也为这个算法的发展做出了贡献。它的原理是对图进行V-1次松弛操作,得到所有可能的最短路径。其优于Dijstra算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达 O(VE)。但算法可以进行若干种优化,提高了效率。

我们用数组dist记录每个结点的最短路径估计值,用邻接表或邻接矩阵来存储图G。

采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XVvgNYz8-1650418044143)(C:\Users\Lin\AppData\Roaming\Typora\typora-user-images\1650381541651.png)]

代码如下(代码来源)

void Spfa(int s)
{
    flag=false;
    for(int i=0;i<maxn;i++)
        dis[i]=2e18;
    memset(vis,0,sizeof(vis));
    dis[s]=0,cont[s]=1;
    queue<int>q;
    q.push(s);
    while(q.size())
    {
        int u=q.front();q.pop();
        for(int i=head[u];i;i=p[i].nxt)
        {
            int v=p[i].to;
            if(dis[v]>dis[u]+p[i].val)
            {
                dis[v]=dis[u]+p[i].val;
                cont[v]++;
                if(cont[v]>=n)
                {
                    flag=true;
                    break;
                }
                q.push(v);
            }
        }
    }
}

代码流程如下:

1.首先构建dis数组,起点赋值为0,其他节点均赋值为无穷大。将起点放入队列中。

2.随后开始循环,循环的判断条件为队列是否为空,当循环不为空,出栈一个元素,随后根据这个元素作为中间结点更新dis数组,如果更新了数组,就将该新的结点放入队列中。

3.重复执行2过程,直到队列为空。

3.最大流问题

3.1 什么是最大流问题

  1. 我们有图 G=(V, E),V是顶点的集合,E是边的集合。

  2. 图中边的权重都为非负数 (满足1,2两点有时称之为流网络)。

  3. 对于这个图G,有两个顶点很重要,一个是源头s,一个是汇聚点t,我们想考虑的是从源头s流向汇聚点t的

我们可以知道有以下约束:

1.对于图中非s和t的普通结点,流进量等于流出量
2.我们非常关心总运输流量,比如这个下水道系统,究竟从s点到t点最多能运输多少立方米的水?我们把它记成|f|,这个|f|极其重要,是我们研究的目的所在。
3.当然,每条边是有运输上限的,就像某条公路车流是有上限的一样,若运输量无穷无尽,我们的研究也就没有意义了。我们将从u点到v点的运输上限,或者说是运载能力记为c(u,v)。对于从u点到v点的流量,记作f(u,v)。显然对所有边(u,v)我们有f(u,v)<=c(u,v)。

视图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dH0H3mEU-1650418044143)(C:\Users\Lin\AppData\Roaming\Typora\typora-user-images\1650382272246.png)]

3.2 求解思路

(1)单纯形法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jEwxYanH-1650418044144)(C:\Users\Lin\AppData\Roaming\Typora\typora-user-images\1650382501521.png)]

本质上,最大流问题就是一个线性规划的问题,因此,求解过程中我们可以采用最原始的单纯形法进行求解,但是不太推荐,因为求解过程十分繁杂。

(2)标号法

这实际上是一种代码的思路,思路如下。

1.首先找到一个可行流,也就是满足所有条件的一个流,比如在上个图中每个路径选择流量均为0,在流的中间允许出现负回路。

2.找到一条可行支流,画图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TYORDYVf-1650418044144)(file:///C:\Users\Lin\Documents\Tencent Files\2243197885\Image\C2C\CA81D926ECD575C448A0404D65E154BA.png)]

如图中的1-3-4-7和1-3-2-4-7都是可行支流。

3.随后对每个点进行逐一检测,计算标号值。

如果某一条路径为正路径,如1-3,theta = cij - fij,其中cij为路径最大承载,fij为实际承载。

如果某一条路径为负路径,如3-2,theta = fij。

4.随后计算某一条支流上的所有theta值,取最小值min(theta),为什么取最小,本质含义就是将支流中所有路径接近填满,但是又要防止某一条支流炸掉。

5.随后得到在图中选取新的可行流,重复2-4操作,直到没有还可以填充的可行流。

(3)割集与隔量

官方定义为割集是从网络发点到收点的一组的集合,从网络中去掉这组弧就断开了网络,发点将不能到达收点。

在这里插入图片描述

通俗来说,割集就是能把发点和收点断开的弧的组合。

如图所示,其中长度为5、10、15这块白色区域的弧所组成的路径(a,b)集合即为一个割集,割集中的割量也就是其路径对应容量之和。

定理:网络中的最大流量等于最小割量

这个其实很好理解,最小流量代表所有弧都能接受的一个总流量。

4.最小费用流

4.1 什么是最小费用流

在网络中的弧,不仅给出了容量,还有单位流量的费用。

可以将最小费用流问题分为两种类型:

1.固定流量下的最费用最小

2.最小费用最大流问题

4.2 求解最小费用流

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m1IGA67o-1650418044145)(C:\Users\Lin\AppData\Roaming\Typora\typora-user-images\1650417730582.png)]

求解最小费用流的表达是一个很复杂的过程,这里就不说了。

我们主要采用的求解方法就是使用编程解决,可以看看这篇文章作为参考。

67o-1650418044145)]

求解最小费用流的表达是一个很复杂的过程,这里就不说了。

我们主要采用的求解方法就是使用编程解决,可以看看这篇文章作为参考。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值