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

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

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<int> 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<V;i++){
    if(color[v]==0){
        if(!dfs(i,1)){
            printf("NO\n");
            break;
        }
    }
}
printf("YES\n");

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; i++) 
		d[i] = INF;
	d[s]=0;
    while(true){
        bool update = false;
        for(int i=0; i<E; i++){
            edge e = es[i];
            if(d[e.from]!=INF && d[e.from]+e.cost<d[e.to]){
                d[e.to] = d[e.from]+e.cost;
                update = true;
            }
        }
        if(!update) break;
    } 
}

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

而我们已经确认了起点位置,因此最多要进行|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<E; i++){
            edge e = es[i];
            if(d[e.from]!=INF && d[e.from]+e.cost<d[e.to]){
                d[e.to] = d[e.from]+e.cost;
                //第V次仍然更新,说明存在负环。
                if(j==V) return false;
            }
        }
    } 
    return true;
}

单源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]<d[v]) )
                v = u;
        }
        //均已更新过
        if(v==-1) break;
        used[v] = true;
        for(int u = 0; u<V; u++){
            d[u] = min(d[u], d[v]+cost[v][u]);
        }
    }
}

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

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

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

void dijkstra(int s){
	//小顶堆,小的在上面
	priority_queue<P, vector<P>, greater<P> > 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; i<G[v].size(); i++){
            edge e = G[v][i];
            if(d[e.to]>d[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<edge> G[VMAX];
int d[VMAX], inq[VMAX];

void spfa(int s){
    queue<int> 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; i<G[v].size(); i++){
            edge e = G[v][i];
            if(d[e.to]>e.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<V; i++){
        for(int j=0; j<V; j++){
            for(int k=0; k<V; k++){
                d[i][j] = min(d[i][j], d[i][k]+d[k][j]);
            }
        }
    }
}

路径还原

以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]) )
                v = u;
        }
        //均已更新过
        if(v==-1) break;
        used[v] = true;
        for(int u = 0; u<V; u++){
            if(d[u] > d[v]+cost[v][u]){
            	d[u] = d[v]+cost[v][u];
                prev[u]=v;
            }
        }
    }
}

vector<int> getpath(int t){
    vector<int> 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; u<V; u++){
            if(!used[u] && (v==-1||mincost[u]<mincost[v]) )
                v = u;
        }
        if( v==-1 ) break;
        used[v] = true;
        res += mincost[v];
        for(int u=0; u<V; u++){
            if(mincost[u]>mincost[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<e2.cost;
}
edge es[EMAX];
int V,E;

int kruskal(){
    sort(es,es+E,cmp);
    init(V); // 并查集初始化
    int res = 0;
    for(int i=0; i< E; i++){
        edge e = es[i];
        if(!same(e.u,e.v)){
            unite(e.u,e.v); //并查集合并
            res+=e.cost;
        }
    }
    return res;
}

//并查集部分添加:
int fa[VMAX], rk[VMAX];
void init(int V){
    for(int i=0;i<V;i++)fa[i]=i;
    memset(rk,0,sizeof(rk));
}
int findf(int x){
    if(fa[x]==x)return x;
    return fa[x] = findf(fa[x]);
}
void unite(int x, int y){
    x = findf(x);
    y = findf(y);
    if(x==y) return;
    if(rk[x]<rk[y])
        fa[x]=y;
    else{
        fa[y]=x;
        if(rk[x]==rk[y]) rk[x]++;
    }
}
bool same(int x, int y){
    return findf(x)==findf(y);
}

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 <cstdio>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
const int VMAX=5050;
const int INF=0x3f3f3f3f;

typedef pair<int,int> P;
struct edge{
    int to,cost;
    edge(int a,int b):to(a),cost(b){}
};
vector<edge> 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<P,vector<P>,greater<P> >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]<dist) continue;

        for(int i = 0; i<G[u].size(); i++){
            edge e = G[u][i];
            // d[v] = d2[u]|d[u] + e(u,v), dist = d2[u]|d[u];
            int dv = dist + e.cost;
            // dv是最短距离,更新最短距离
            if( dv < d[e.to]){
                //最短距离d[e.to]更新为dv,dv值变成次短距离(即原来的最短距离)
                swap( dv, d[e.to] );
                //将最短距离放入队列
                pq.push(P(d[e.to], e.to));
            }
            // dv是次短距离,更新次短距离
            if( dv > d[e.to] && dv<d2[e.to]){
                d2[e.to] = dv;
                pq.push(P(d2[e.to], e.to));
            }
        }
    }
    printf("%d\n",d2[V-1]);
}

int main(){
    int u,v,w;
    scanf("%d%d",&V,&R);
    for(int i=0;i<R;i++){
        scanf("%d%d%d",&u,&v,&w);
        G[u-1].push_back(edge(v-1,w));
        G[v-1].push_back(edge(u-1,w));
    }
    dijkstra(0);
    return 0;
}

例2:征兵顺序 POJ3723

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

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

思路:最小生成树裸题。

#include <cstdio>
#include <algorithm>
#include <cstring>
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<e2.cost;
}
edge es[EMAX];
int V,E;

//并查集部分添加:
int fa[VMAX], rk[VMAX];
void init(int V){
    for(int i=0;i<V;i++)fa[i]=i;
    memset(rk,0,sizeof(rk));
}
int findf(int x){
    if(fa[x]==x)return x;
    return fa[x] = findf(fa[x]);
}
void unite(int x, int y){
    x = findf(x);
    y = findf(y);
    if(x==y) return;
    if(rk[x]<rk[y])
        fa[x]=y;
    else{
        fa[y]=x;
        if(rk[x]==rk[y]) rk[x]++;
    }
}
bool same(int x, int y){
    return findf(x)==findf(y);
}

int kruskal(){
    sort(es,es+E,cmp);
    init(V); // 并查集初始化
    int res = 0;
    int totv = V;
    for(int i=0; i< E; i++){
        edge e = es[i];
        if(!same(e.u,e.v)){
            unite(e.u,e.v); //并查集合并
            res+=e.cost;
            totv--;
        }
    }
    res += totv*10000;
    return res;
}

int main(){
    int t,N,M,R;
    scanf("%d",&t);
    while(t--){
        scanf("%d%d%d",&N,&M,&R);
        V = N+M;
        E = 0;
        while(R--){
            scanf("%d%d%d",&es[E].u, &es[E].v, &es[E].cost);
            es[E].cost = 10000-es[E].cost;
            es[E].v += N;
            E++;
        }
        printf("%d\n",kruskal());
    }
}

例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 <cstdio>
#include <algorithm>
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<E; i++){
            edge e = es[i];
            if(d[e.from]!=INF && d[e.from]+e.cost<d[e.to]){
                d[e.to] = d[e.from]+e.cost;
                //第V次仍然更新,说明存在负环。
                if(j==V) return -1;
                update = true;
            }
        }
        if(!update) break;
    }
    if(d[V]==INF) return -2;
    return d[V];
}



int main(){
    int ML,MD;
    scanf("%d%d%d",&V,&ML,&MD);
    E = 0;
    for(int i=2; i<=V; i++){
        es[E].from = i;
        es[E].to = i-1;
        es[E].cost = 0;
        E++;
    }
    while(ML--){
        scanf("%d%d%d",&es[E].from,&es[E].to,&es[E].cost);
        E++;
    }
    while(MD--){
        scanf("%d%d%d",&es[E].to,&es[E].from,&es[E].cost);
        es[E].cost = -es[E].cost;
        E++;
    }
    printf("%d",bellman_ford_negative_loop());
    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值