NOIP图论总结

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/CoolKid_cwm/article/details/53052965

1.图的存储

前向星

struct Edge{
    int to,dist,next;
}edges[MAXM];//边
int head[MAXN],cnt=0;
//加边
void addedge(int from,int to,int dist){
    edges[cnt].to=to;
    edges[cnt].dist=dist;
    edges[cnt].next=head[from];
    head[from]=cnt++;
}

注意:无向图时应调用两遍addedge
上述的head初始值为-1因此遍历以u为from的所有边的时候可以采用下面的代码

for(int i=head[u];~i;i=edges[i].next){
    //do something
}

邻接矩阵

一般跑Floyd的时候这么存。设G[i][j]表示i到j有一条边边权为G[i][j]否则G[i][j]=INF。

邻接表(vector)

刘汝佳推荐的存法,用一个数组edges存边集,G[i][j]存以i为起点的第j条边的编号。

struct Edge{
    int from,to,dist;
    Edge(int f=0,int t=0,int d=0){
        from=f;to=t;dist=d;
    }
};
vector<Edge> edges;
vector<int> G[MAXN];

void addedge(int from,int to,int dist){
    edges.push_back(Edge(from,to,dist));
    int m=edges.size();
    G[from].push_back(m-1);
}//加边

//遍历以u为起点的所有边
void work(int u){
    for(int i=0;i<G[u].size();i++){
        //do something
    }
}

2.最短路

单源最短路

一般来说单源最短路最常用的就是以下两种算法
以下存图方式均为前向星

SPFA

一般来说有负权环时用SFPA。
Code:

queue<int> q;
int d[MAXN];
int cnt[MAXN];
int inq[MAXN];

int SFPA(int S,int T){
    memset(inq,0,sizeof(inq));
    memset(cnt,0,sizeof(cnt));
    for(int i=1;i<=n;i++) d[i]=INF;
    d[S]=0;
    q.push(S);inq[S]=1;cnt[S]++;
    while(!q.empty()){
        int u=q.front();q.pop();inq[u]=0;
        for(int i=head[u];~i;i=edges[i].next){
            int v=edges[i].to;
            if(d[v]>d[u]+edges[i].dist){
                d[v]=d[u]+edges[i].dist;
                if(!inq[v]){
                    q.push(v);inq[v]=1;
                    if(++cnt[v]>n) return -1;//存在负权环
                }
            }
        }
    }
}

Dijkstra

一般来说稀疏图用Dijkstra。
讲道理一般不用Floyd的都用Dijkstra。
Code:

struct Node{
    int d,u;
    bool operator < (const Node &rhs) const{
        return d>rhs.d;
    }
}Hp[MAXM];
int sz=0;

int d[MAXN];
bool done[MAXN];

int Dijsktra(int S,int T){
    memset(done,false,sizeof(done));
    for(int i=1;i<=n;i++) d[i]=INF;
    d[S]=0;
    Hp[sz++]=(Node){0,S};
    while(sz){
        pop_heap(Hp,Hp+sz);sz--;
        int u=Hp[sz].u;
        if(done[u]) continue;
        done[u]=true;
        for(int i=head[u];~i;i=edges[i].nxt)if(!vis[i]){
            Edge &e=edges[i];
            if(d[e.to]>d[u]+e.dist){//松弛
                pre[e.to]=i;//记录路径
                d[e.to]=d[u]+e.dist;
                Hp[sz++]=(Node){d[e.to],e.to};
                push_heap(Hp,Hp+sz);
            }
        }
    }
    return d[T];
}

最短路径的输出

存储:定义一个pre数组然后在松弛操作处进行更新即可。具体代码见Dijkstra。
输出:倒序输出,从to开始一直往前找,直到找到from为止

void printpath(int from,int to){
    int u=to;
    while(from!=u){
        u=edges[pre[u]].from;
        printf("from:%d to:%d\n",u,edges[pre[u]].to);
    }
}

注意这里输出的路径是倒序的,如果要正序输出,将路径暂时存储到栈中即可。

全源最短路

Dijkstra SPFA

如果图是稀疏图的话,对每个节点跑一边单源最短路即可得到全源最短路。

Floyd

三重for循环

void floyd(){
    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++) if(d[i][j]>d[i][k]+d[k][j]) d[i][j]=d[i]k]+d[k][j];//松弛
}

3.最小生成树

Kruscal

Kruscal的算法基本思想就是贪心,然后用并查集去维护。
稀疏图的情况下Kruscal跑的飞快,算法复杂度O(nlog2n)

int fa[MAXN];
void initUFS(){
    for(int i=1;i<=n;i++) fa[i]=i;
}
int getfather(int x){
    return fa[x]==x?x:fa[x]=getfather(fa[x]);
}
//UFS
int Chosen[MAXN];
void Kruscal(){
    initUFS();
    memset(Chosen,0,sizeof(Chosen));
    sort(edges,edges+cnt);//将边按照从小到大的顺序排序
    for(int i=0;i<cnt;i++){
        int fax=getfather(edges[i].from);
        int fay=getfather(edges[i].to);
        if(fax==fay) continue;//已经在同一个联通块中
        //若不在同一个联通块中,加边合并
        fa[fax]=fay;//合并
        Chosen[i]=1;//加边
    }
}

讲道理这之后应该还有一个Prim算法
但是我并没有用过,而且两个算法作用几乎一样,并且大部分题目都是给的稀疏图,因此Kruscal就完全足够了

4.强连通分量

强连通分量简单说就是在有向图中一个点经过若干个其他点再回到本身的一个点集。

Tarjan

Tarjan的思想就是用一个dfs_clock对每个点进行遍历,找到自己可以到达的dfs_clock最小的值的节点。
Code:

stack<int> S;
int DFN[MAXN],LOW[MAXN],inS[MAXN];
vector<int> G;
int sccno;

void Tarjan(int u){
    DFN[u]=LOW[u]=++T;
    S.push(u);
    inS[u]=true;

    for(int i=head[u];~i;i=edges[i].next){
        int v=edges[i].to;
        if(!DFN[v]){
            Tarjan(v);
            LOW[u]=min(LOW[u],LOW[v]);
        }else if(inS[v]) LOW[u]=min(LOW[u],DFN[v]);
    }
    if(DFN[u]==LOW[u]){
        int x;
        while(x!=u){
            x=S.top();S.pop();inS[x]=false;
            G[sccno].push_back(x);
        }
        sccno++;
    }
}

还有一种算法是Kosaraju算法但是它的常数要比Tarjan大,所以并没有什么卵用。

5.二分图染色

二分图就是对于一张无向图可以分成两个点集使得两个点的集合内没有边相连。
我们可以利用二分图染色进行二分图的判断,即把两个集合的点分别进行染色,如果出现冲突,那就不是二分图,否则为二分图。
下面代码中col初始值为0表示未被访问,1和-1表示两种相反的颜色。

int col[MAXN];
bool Color(int wanted,int u){
    col[u]=wanted;
    for(int i=head[u];~i;i=edges[i].next){
        int v=edges[i].to;
        if(col[v]==c) return false;
        if(col[v]==0&&!Color(-wanted,v)) return false;
    }
    return true;
}

6.二分图的最大匹配

讲道理这里应该有两种方法,一种是匈牙利算法,一种是最大流算法。
但是我并不会匈牙利,因此这里只讨论最大流求二分图的最大匹配

最大流求二分图的最大匹配

大致思路是对于一个二分图,我们把二分图的边从一个点集V1指向另一个V2,流量设为1,之后建立一个超级源点S指向V1的所有节点,流量设为1,设一个超级汇点T使V2所有的节点都指向汇点,流量设为1。
之后跑一遍最大流Dinic。

7.其他

FloodFill

FloodFill就是通过DFS或者BFS对图进行遍历填充。

EulerPath

欧拉道路

对于一张图可以从一个点出发,遍历整张图的,且条道路都只访问一遍的路径称为欧拉道路。
对于无向图的欧拉道路

除起点和终点外其他节点的“进出”次数应该是相等的,换句话说,除起点和终点外,其他点的度数(degree)应该是偶数。

如果一个无向图是连通的,且最多只有两个奇点,则一定存在欧拉道路。如果有两个奇点,则必须从其中一个奇点出发,另一个奇点终止。

以上引自刘汝佳入门经典

Code:

void Euler(int u){
    for(int v=0;v<n;v++) if(G[u][v]&&!vis[u][v]){
        vis[u][v]=vis[v][u]=1;
        Euler(v);
        printf("%d %d\n",u,v);
    }
}

对于有向图来说把上面的vis[u][v]=vis[v][u]=1改为vis[u][v]=1即可。

欧拉回路

欧拉回路就是在欧拉道路遍历完所有边之后,在回到起点本身。

欧拉回路中如果奇点不存在,则可以从任意点出发,最终一定会回到该点。

TopoSort

求有向图的拓扑序,以及判断是否存在有向环。

int c[MAXN];
int topo[MAXN],t=n;
bool dfs(int u){
    c[u]=-1;//-1表示灰色节点即在系统栈内
    for(int i=head[u];~i;i=edges[i].next){
        int v=edges[i].to;
        if(~c[v]) return false;//存在有向环
        else if(!c[v]&&!dfs(v)) return false;
    }
    c[u]=1;topo[t--]=u;
    return true;
}

bool TopoSort(){
    memset(c,0,sizeof(c));
    for(int i=1;i<=n;i++) if(!c[i] && !dfs(i)) return false;
    return true;
}//拓扑序存在数组topo中

8.例题

最短路

差分约束系统经常会用到最短路中的SPFA
例题:POJ1201
题解:若设Si为集合Z在区间[1,i]的元素个数,我们可以直接从题意中得到一个不等式Sbi+1SaiCi 并且天然存在两个等式即Si+1Si0(表示节点i不选取的情况)和SiSi+11(表示i选取的情况)这样我们可以建立一张有向图,跑一遍最长路即可。

删边求最长/短路
例题:玛丽卡
题解:利用pre数组记录最短路所经过的节点然后枚举删除最短路上的节点跑Dijkstra求出最长时间。

Floyd传递闭包
例题:[USACO Jan08] 奶牛的比赛
题解:我们把A比B能力强抽象为A到B的一条有向边然后用下面的代码求出他们的相对关系。

for(int k=1;k<=n;k++)
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++) if(G[i][k]&&G[k][j]) G[i][j]=1;

上述代码其实就是Floyd的一个变形。之后直接统计即可。

最小生成树

最小生成树的题目是要进行建模。
例题:USACO Oct08挖水井
题解:我们可以把水管费用抽象成边权,建立一个超级源点,把第一个水井的费用也设为边权,之后就是直接跑Kruscal即可。

最小生成树还可以和树形数据结构一起用
例题:[NOIP2013]货车运输
题解:题目要求最大载重,我们可以注意到,答案取决于所经过路径的最小边权,因此我们可以通过最大生成树来使边权尽可能大(可以通过交换法证明正确性,若把最大生成树上的任意一条边换为一个边权较小的边,那么经过该该条边的路径可能就不是最优的,因此我们可证得答案一定在最大生成树上。)
然后我们可以通过LCA求解路径上的最大值即可。

FloodFill

例题:[NOIP2010]引水入城
题解:待更

搜索

例题:[CTSC1999]拯救大兵瑞恩
题解:状态压缩记录经过的每个节点和获得的钥匙,然后BFS找最优值。思路简单,但是实现起来有些复杂。
Code:待更

二分图染色

例题:[NOIP2010] 关押罪犯
题解:因为题目中要求最大值最小,因此我们可以二分答案。二分冲突值,判断函数中把小于所传的值的遍都删去,然后用二分图染色剩下的边判断能否形成一个二分图,得到答案。

展开阅读全文

没有更多推荐了,返回首页