【挑战程序设计】- 2.5 图论(最短路、最小生成树)

2.5 图论(最短路、最小生成树)

文章目录

  • 2.5 图论(最短路、最小生成树)
    • 2.5.1 定义们
    • 2.5.2 图的表示
    • 2.5.3 图的搜索
    • 2.5.4 最短路问题
      • 单源1:bellman-ford
      • 单源2:dijkstra算法
      • (单源3:spfa)
      • 任意两点:floyd-warshall
      • 路径还原
    • 2.5.5 最小生成树
      • Prim算法
      • Kruskal算法
    • 2.5.6 应用解题

2.5.1 定义们

图:点和边组成。
V为顶点集,E为边集,图记为G(V,E),连接两点u和v的边用e=(u,v)表示。

分为有向图和无向图。边上有权值的是带权图。

连通图:任意两点间均可达。度数是顶点连的边数。

树:没有圈的连通图。边数=点数-1。

有向图:度数分为入度和出度。

没有圈的有向图称为DAG(Direct Acyclic Graph)。
对DAG给顶点标记顺序,就是拓扑序。求拓扑序的算法是拓扑排序。

2.5.2 图的表示

邻接矩阵:|V|*|V|二维数组来表示图。g[i] [j]表示顶点 i 和 j 的关系

在权值情况下,通常没连的边设为INF。

邻接表:空间占的少,取值慢一点。

一种STL写法:

vector G[VMAX];
for(int i = 0; i < E; i++){
    int s,t;
    scanf("%d%d",&s,&t);
    G[s].push_back(t);
}

另一种头接法(更少空间、更快插入、更慢查找):

struct Edge{
    int to,nex;
}e[EMAX];

int etot = 0;
int head[NMAX]; //每个点的第一个边

void init(){
    etot=0;
    memset(head,-1,sizeof(head));
}

void addedge(int u,int v){
    e[etot].to = v;
    e[etot].next = head[u];
    head[u] = etot++;
}

//遍历like:
for(int i=head[now];i!=-1;i=head[i]){
    Edge te = e[i];
}

2.5.3 图的搜索

例题1:二分图判定

给定一个具有n个顶点的图,用两种颜色给图上每个顶点染色,相邻顶点颜色不同。无重复的边和自环。
问能否染色?
1<=n<=1000

思路:dfs一遍搞定。±1分别代表黑白色,若未染过就是0。

int color[VMAX];
int V;
bool judge(int now, int c){
    for(int i=head[now]; i!=-1; i=head[i]){
        int v = e[i].to;
        if(color[v]==0 && !judge(v,-c))
        	return false;
        if(color[v]==c) 
            return false;
    }
    return true;
}
for(int i=0;i

2.5.4 最短路问题

给定起点 s 和终点 t,求边权值和最小的路径。

单源1:bellman-ford

d[i] = min{d[j] + e(j,i) }

思路:
【初始化】将所有d[j] 设为 INF,除了d[s]为0
【不断更新】d的值,直到无法再更新

复杂度是O(|E|*|V|)

struct edge{int from,to,cost;};
edge es[EMAX];
int d[VMAX];
int V,E;
void bellman_ford(){
	for(int i=0; i

每一次迭代,至少能确认一个点。

而我们已经确认了起点位置,因此最多要进行|V|-1次迭代(while true循环)——所有点一直线的情况。

如果第|V|次依然更新,说明存在【负环】,bellman-ford判负环:

【初始化】d都为0。

bool bellman_ford_negative_loop(){
	memset(d,0,sizeof(d));
    for(int j=1; j<=V; j++){
        for(int i=0; i

单源2:dijkstra算法

**【没有负环】**的情况下考虑:d[ j ] = d[ i ] + e( i, j )

每次循环检查所有边太浪费时间。修改:

找到最短距离已经确定的点,从它出发进行更新。

【最短距离已定的点】距离d[i]最小的点。

复杂度 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)

int cost[VMAX][VMAX];
int d[VMAX];
bool used[VMAX];
int V;
void dijkstra(){
    fill(d, d+V, INF);
    fill(used, used+V, false);
    d[s] = 0;
    while(true){
        //在未使用的顶点中选择距离最小的
        int v = -1;
        for(int u = 0; u < V; u++){
            if(!used[u] && (v==-1 || d[u]

每次循环的首先操作:实质是每次找到最优的点并删除,这个部分可以用优先队列优化。

更新数据的操作要|E|次,复杂度 O ( ∣ E ∣ l o g ∣ V ∣ ) O(|E|log|V|) O(ElogV)

struct edge{int to,cost};
typedef pair P;
int V;
vector G[VMAX];
int d[VMAX];

void dijkstra(int s){
	//小顶堆,小的在上面
	priority_queue, greater

> pq; fill(d, d+V, INF); d[s] = 0; pq.push(P(0,s)); while(!pq.empty()){ P p = pq.top(); pq.pop(); int v = p.second; if(d[v] < p.first) continue; for(int i=0; id[v]+e.cost){ d[e.to] = d[v]+e.cost; pq.push(P(d[e.to], e.to)); } } } }

再次记住dijkstra的条件是【没有负环】

(单源3:spfa)

最差:O(|V|*|E|)

struct edge{int to,cost};

int V;
vector G[VMAX];
int d[VMAX], inq[VMAX];

void spfa(int s){
    queue q;
    fill(d, d+V, INF);
    fill(inq, inq+V, 0);
    d[s] = 0;
    q.push(s);
    while(!q.empty()){
        int u = q.front();
        q.pop();
        inq[u] = 0;
        for(int i=0; ie.cost+d[u]){
                d[e.to]=e.cost+d[u];
                if(!inq[e.to]){
                    inq[e.to]=1;
                    q.push(e.to);
                }
            }
        }
    }
}

任意两点:floyd-warshall

d[i] [j] = min(d[i] [j] , d[i] [k] + d[k] [j])

【可以处理负权边】判断d[i] [i] 为负数的点

复杂度: O ( ∣ V ∣ 3 ) O(|V|^3) O(V3)

int d[VMAX][VMAX]; //d[i][i]=0, 有边时边权,无边时INF
int V;
void floyd(){
    for(int i=0; i

路径还原

以dijkstra为例,记录前向点prev,存入数组将其reverse就是答案。

int cost[VMAX][VMAX];
int d[VMAX];
bool used[VMAX];
int V;

//记录前向点
int prev[VMAX];

void dijkstra(int s){
    fill(d, d+V, INF);
    fill(used, used+V, false);
    //初始化-1
    fill(prev, prev+V, -1);
    
    d[s] = 0;
    while(true){
        //在未使用的顶点中选择距离最小的
        int v = -1;
        for(int u = 0; u < V; u++){
            if(!used[u] && (v==-1 || d[u] d[v]+cost[v][u]){
            	d[u] = d[v]+cost[v][u];
                prev[u]=v;
            }
        }
    }
}

vector getpath(int t){
    vector path;
    while(t!=-1){
        path.push_back(t);
        t=prev[t];
    }
    reverse(path.begin(), path.end());
    return path;
}

2.5.5 最小生成树

生成树:无向图中某个子图中,任意两个顶点都互相连通且是一棵树。

最小生成树:使得边权和最小的生成树。

Prim算法

【思想】:对于当前树T,不断贪心地选取T和其他顶点之间最小权值的边,并将其加入T。

(充满dijkstra内味)

复杂度: O ( ∣ V ∣ 2 ) O(|V|^2) O(V2) 优先队列优化: O ( ∣ E ∣ l o g ∣ V ∣ ) O(|E|log|V|) O(ElogV)

int cost[VMAX][VMAX];
int mincost[VMAX];
bool used[VMAX];
int V;

int prim(){
    fill(mincost, mincost+V, INF);
    fill(used, used+V, false);
    mincost[0] = 0;
    int res = 0;
    while(true){
        int v = -1;
        for(int u = 0; umincost[v]+cost[v][u]){
                mincost[u] = mincost[v]+cost[v][u];
            }
        }
    }
    return res;
}

Kruskal算法

按照边权从小到大check一遍,不产生圈就加入。

【是否产生圈】:用并查集高效查看是否在同一个连通分量中

复杂度 O ( ∣ E ∣ l o g ∣ V ∣ ) O(|E|log|V|) O(ElogV)

struct edge{int u, v, cost;};
bool cmp(const edge& e1, const edge& e2){
	return e1.cost

2.5.6 应用解题

例1:次短路 POJ 3255

有R个道路,N个路口,双向通行、问从1号路口到N号路口的次短路长度。

1<=N<=5000, 1<=R<=100000

思路:【次短距离】d2[v] = d[u]+e(u,v) OR d2[u]+e(u,v),

因此queue中最短路和次短路都要加入

【这题四年前没AC,就是没理解最短路和次短路都要加队列这一点!】

#include 
#include 
#include 
#include 
using namespace std;
const int VMAX=5050;
const int INF=0x3f3f3f3f;

typedef pair P;
struct edge{
    int to,cost;
    edge(int a,int b):to(a),cost(b){}
};
vector G[VMAX];
int d[VMAX], d2[VMAX];//次短距离d2[v]=d[u]+e(u,v) 或者 d2[u]+e(u,v)
bool used[VMAX];
int R,V;

void dijkstra(int s){
    fill(d, d+V, INF);
    fill(d2, d2+V, INF);
    priority_queue,greater

>pq; pq.push(P(0,s)); d[s] = 0; while(!pq.empty()){ P p = pq.top(); pq.pop(); int u = p.second, dist = p.first; if(d2[u] d[e.to] && dv

例2:征兵顺序 POJ3723

需要召集N个女兵,M个男兵。
给出若干男女之间的亲密度,征募某个人的费用=10000-已招募人员亲密度最大值。
通过适当的招募顺序使得总费用最低。

1<=N,M<=10000, 0 ≤ R ≤ 50,000,
0 ≤ xi < N, 0 ≤ yi < M, 0 < di < 10000

思路:最小生成树裸题。

#include 
#include 
#include 
using namespace std;

const int EMAX = 50050, VMAX=20020;
struct edge{int u, v, cost;};
bool cmp(const edge& e1, const edge& e2){
	return e1.cost

例3 排列牛 POJ3169

有N头牛,编号1-N。按顺序拍成一排。
有些牛关系好(AL,BL,DL)距离不超过DL,有些牛关系不好(AD,BD,DD)距离不小于DD。

求1和N的牛的最大距离。不存在输出-1。无限大输出-2。
2 <= N <= 1,000, 1 <= A < B <= N,1 <= D <= 1,000,000

思路:

BL<=AL+DL, AL向BL连一条DL
BD>=AD+DD,即 AD<=BD-DD, BD向AD连一条-DD
另外有B>=A,即A<=B+0,B向A连0

求最短路,若有负环-1,若为INF则-2,其他情况正常输出。

#include 
#include 
using namespace std;
const int INF = 0x3f3f3f3f;
const int EMAX = 30020, VMAX=1010;
struct edge{int from,to,cost;};
edge es[EMAX];
int d[VMAX];
int V,E;

int bellman_ford_negative_loop(){
	fill(d,d+V+1,INF);
    d[1]=0;
    for(int j=1; j<=V; j++){
        bool update = false;
        for(int i=0; i

你可能感兴趣的:(挑战程序设计竞赛,算法,图论,算法)