图论全纪录

最短路

图论最基础的想必就是最短路啦

图上的文章(再谈最短路问题)

最短路能解决的问题:

  • 最短路?
    话是这么说没错啦,不过一些题目隐藏的比较深,需要转化一下才能看出最短路的模型:经典例题

  • 差分约束系统

对于差分约束我要说两句了

简述

给出若干形如 xi<=xj+w x i <= x j + w 的限制条件,建图连边 j(w)>i j − ( w ) − > i
上面的三角不等式符合最短路的形式,所以我们用bellman跑一遍最短路,得到每个点的 dis d i s 即为一组可行解(如果存在负环则说明原题无解)

细节一

前辈表示:最短路得到的 dis d i s 最大可行解
这是怎么回事?
观察一下不等式: xi<=xj+w x i <= x j + w
我们如果选择这条边松弛(说明这条边的限制最严格),那么 xi=xj+w x i = x j + w
然而存在情况: xi<xj+w x i < x j + w
所以我们最短路得到的答案是符合限制条件下的上限

同理,如果我们遇到这样的限制条件: xi>=xj+w x i >= x j + w
连边 j(w)>i j − ( w ) − > i ,跑最长路
我们像上文一样分析一下,就会发现:最长路得到最小可行解

所以我们需要根据题设选择把约束条件化为最短路还是最长路

细节二

在细节一中,我们提到了求解可行解

注意初始化(very very important)

最长路: INF − I N F
最短路: INF I N F
判断负环: 0 0

我们在跑Bellman的时候,多半需要建立一个虚拟节点,连向所有结点
而虚拟结点就按照前文的方式初始化

经典例题:bfs(倒水问题)
经典例题:dfs(欧拉路径输出)

经典例题:dijkstra(逆向思维)
经典例题:线段树优化建图+分层图最短路
经典例题:dijkstra+floyed+dp(Mario)

经典例题:floyed负环

经典例题:Bellman+二分(平均权值最小的回路)
经典例题:差分约束+二分(加减边权,使边权最小值非负且尽量大)
经典例题:差分约束
经典例题:差分约束(看似两种未知量+乘除变加减)
经典例题:差分约束+tarjan+floyed

下面就是堆优dijkstra和Bellman的代码啦

dijkstra有一个致命的缺陷:不能对付有负边的图

const int N=10010;
int n,m,st[N],tot=0,dis[N];
bool vis[N];
struct node{
    int y,v,nxt;
}way[N<<1];
struct heapnode{
    int u,d;
    heapnode(int uu=0,int dd=0) {
        u=uu; d=dd;
    }
    bool operator <(const heapnode &a) const {
        return d>a.d;
    }
};

void Dijkstra(int s,int t) {
    priority_queue<heapnode> q;
    memset(dis,0x33,sizeof(dis));
    dis[s]=0;
    q.push(heapnode(s,0));

    while (!q.empty()) {
        heapnode now=q.top(); q.pop();
        int u=now.u,d=now.d;
        if (vis[u]) continue;

        vis[u]=1;

        for (int i=st[u];i;i=way[i].nxt)
            if (dis[way[i].y]>d+way[i].v) {
                dis[way[i].y]=d+way[i].v;
                q.push(heapnode(way[i].y,dis[way[i].y]));
                //每次更新都扔进堆里 
            }
    }
}

Bellman

int cnt[N],dis[N];
bool in[N];

int Bellman() {
    queue<int> q;
    for (int i=1;i<=n;i++) {     //虚拟源点 
        dis[i]=0;                //判断负环
        q.push(i); 
        in[i]=1; cnt[i]=0;
    }
    while (!q.empty()) {
        int now=q.front(); q.pop();

        in[now]=0;

        for (int i=st[now];i;i=way[i].nxt)
            if (dis[way[i].y]>dis[now]+way[i].v) {
                dis[way[i].y]=dis[now]+way[i].v;
                if (!in[way[i].y]) {
                    in[way[i].y]=1;
                    if (++cnt[way[i].y]>n) return 0;   //存在负环 
                    q.push(way[i].y);
                }
            }
    }
    return 1;
}

最短路延伸:次短路

次短路(解法一)
最短路,次短路计数(解法二)

次短路有两种解法:

  • 跑一边最短路,将最短路中的边扔到一个集合中
    每次删除集合中的一条边(边权设为INF),再次跑一边最短路,得到大于的最短路的最短路径即为次短路

  • 对于每一个结点记录f[i],g[i],本别表示最短路和次短路
    每次松弛的时候,分别转移一下两者即可(此种方法可以进行最短路和次短路计数)

    给出解法二的代码

    int dis[N][2],cnt[N][2];
    bool vis[N][2];
    
    void solve(int s,int t) {
        for (int i=1;i<=n;i++) {
            dis[i][0]=dis[i][1]=INF;
            vis[i][0]=vis[i][1]=0;
            cnt[i][0]=cnt[i][1]=0;
        }
        dis[s][0]=0;   //最短路
        cnt[s][0]=1; 
    
        for (int T=1;T<2*n;T++)        //n^2的dijksta  当然也可以用nlogn的方法 
        {
            int p=0,q=0,mn=INF;
            for (int i=1;i<=n;i++)
                if (!vis[i][0]&&dis[i][0]<mn) {
                    mn=dis[i][0];
                    p=i; q=0;
                }
                else if (!vis[i][1]&&dis[i][1]<mn) {
                    mn=dis[i][1];
                    p=i; q=1;
                }
            if (mn==INF) break;
    
            vis[p][q]=0;
    
            for (int i=st[p];i;i=way[i].nxt) {
                int w=dis[p][q]+way[i].v;
                int y=way[i].y;
                if (w<dis[y][0]) {
                    dis[y][1]=dis[y][0];    //不要忘了转移次短路 
                    cnt[y][1]=cnt[y][0];
                    dis[y][0]=w;
                    cnt[y][0]=cnt[p][q];
                }
                else if (w==dis[y][0])
                {
                    cnt[y][0]+=cnt[p][q];
                }
                else if (w<dis[y][1]) {
                    dis[y][1]=w;
                    cnt[y][1]=cnt[p][q];
                }
                else if (w==dis[y][0]) {
                    cnt[y][1]+=cnt[p][q];
                }
            }
        } 
    }

    割点+桥+双连通分量

    图上的文章:割点和桥

    连通分量真的不是很会
    对于这一部分,应该还是比较重要的
    给出相关定义吧(三个概念都是在无向图的基础上):

    割点

    如果将连通图G中的某个点及和这个点相关的边删除后,将使连通分量数量增加,那么这个点就称为图G的割点
    【性质一】
    如果深度优先搜索树的根节点至少有两个以上的子节点,则根节点是割点。显然去掉根节点后将得到以子节点为根结点的森林
    【性质二】
    在深度优先搜索树中,v存在一个子节点不能通过后向边到达v的祖先节点,则节点v是割点
    也就是说从v的子节点开始没有一条边能够回到v的祖先节点,那么当去掉v时将会使得v的子孙节点与v的祖先节点之间失去联系,必定会使得图不再连通

    如果将连通图G中的某条边删除后,将使连通分量数量增加,那么这条边就称为图G的割点

    双连通分量

    对于一个连通图,如果任意两点至少存在两条“点不重复”的路径,则说这个图是点-双连通的(一般简称双连通)
    这就相当于任意两条边都在同一个简单环中,即内部无割点
    类似的,如果任意两个点至少存在“边不重复”的路径,我们说这个图是边-双连通
    即所有边都不是桥

    其实这三个东西的求法大同小异:维护low和dfn
    注意一下这句话:if (dfn[y]<dfn[now]&&y!=fa)

    在求 BCC B C C 时,我们维护一个栈,里面放我们已经走过的边
    (因为点双不能重复经过点,实际上就是不能重复经过边)
    找到一个割顶后,弹栈,栈中边的两个端点属于一个 BCC B C C

    割顶+桥

    const int N=100010;
    int low[N],dfn[N],clo=0;
    bool bridge[N],iscut[N];
    
    void dfs(int now,int fa) {
        dfn[now]=low[now]=++clo;
        int ch=0;
        for (int i=st[now];i;i=way[i].y) {
            int y=way[i].y;
            if (!dfn[y]) {
                ch++;                           //子结点 
                dfs(y,now);
                low[now]=min(low[now],low[y]);
                if (low[y]>=dfn[now])           //存在即合理 
                    iscut[now]=1;               //割点 
                if (low[y]>dfn[now])            //该边为桥 
                    bridge[i]=1;
            }
            else if (dfn[y]<dfn[now]&&y!=fa) {   //dfn[y]<dfn[now]
                low[now]=min(low[now],dfn[y]);
            }
        }
        if (fa<0&&ch==1) iscut[now]=0;          //性质一验证 
    }

    BCC

    struct node{
        int x,y,nxt;
    }way[N<<1];
    int dfn[N],low[N],iscut[N],belong[N],clo,bcc_cnt;
    vector bcc[N];
    stack<node> S;
    
    void dfs(int now,int fa) {
        dfn[now]=low[now]=++clo;
        int ch=0;
        for (int i=st[now];i;i=way[i].nxt) {
            node e=way[i];
            int y=way[i].y;
            if (!dfn[y]) {
                S.push(e);                               //边入栈 
                ch++;
                dfs(y,now);
                low[now]=min(low[now],low[y]);
                if (low[y]>=dfn[now]) {
                    iscut[now]=1;
                    bcc_cnt++;
                    bcc[bcc_cnt].clear();
                    for (;;) {
                        node x=S.top(); S.pop();
                        if (belong[x.x]!=bcc_cnt) {
                            bcc[bcc_cnt].push_back(x.x);
                            belong[x.x]=bcc_cnt;
                        }
                        if (belong[x.y]!=bcc_cnt) {
                            bcc[bcc_cnt].push_back(x.y);
                            belong[x.y]=bcc_cnt;
                        }
                        if (x.x==now&&x.y==y) break;
                    }
                }
            }
            else if (dfn[y]<dfn[now]&&y!=fa) {
                S.push(e);                             //边入栈 
                low[now]=min(low[now],dfn[y]);
            }
        }
        if (fa<0&&ch==0) iscut[now]=0;
    }
    
    void find_bcc() {
        //调用结束后S保证为空 
        memset(dfn,0,sizeof(dfn));
        memset(iscut,0,sizeof(iscut));
        memset(belong,0,sizeof(belong));
        clo=bcc_cnt=0;
        for (int i=1;i<=n;i++)                         //forest 
            if (!dfn[i])
                dfs(i,-1);
    }

    强连通分量

    强连通分量讲解

    强连通分量的应用:2_SAT
    经典例题:tarjan+dp
    经典例题:tarjan+拓扑+概率期望+gauss

    注意代码中一定要判断是否在栈内
    int S[N],top=0,dfn[N],low[N],clo=0,belong[N],cnt=0;
    bool in[N];
    
    void tarjan(int now) {
        dfn[now]=low[now]=++clo;
        S[++top]=now;
        in[now]=1;                 //入栈 
        for (int i=st[now];i;i=way[i].nxt)
            if (!dfn[way[i].y]) {
                tarjan(way[i].y);
                low[now]=min(low[now],low[way[i].y]);
            }
            else if (in[way[i].y]) {
                low[now]=min(low[now,dfn[way[i].y]);
            }
        if (low[now]==dfs[now]) {
            cnt++;
            int x=-1;
            while (x!=now) {
                x=S[top--];
                belong[x]=cnt;
                in[x]=0;          //出栈 
            }
        }
    }
    
    void solve() {
        memset(dfn,0,sizeof(dfn));
        for (int i=1;i<=n;i++)
            if (!dfn[i])
                tarjan(i);
    }

    拓扑

    可以解决有阶梯性的问题(2_SAT解得构造)

    经典例题:拓扑(一)
    经典例题:拓扑(二)

    拓扑有两种写法:依赖队列,依赖栈
    依赖队列就相当于bfs
    依赖栈就相当于dfs
    都比较好写,视情况选择

    int S[N],top,ans[N],cnt;
    
    void TOP() {
        top=0;
        cnt=0;
        for (int i=1;i<=n;i++)
            if (!du[i]) 
                S[++top]=i;
        while (top) {
            int now=S[top--];
            ans[++cnt]=now;
            for (int i=st[now];i;i=way[i].nxt)
            {
                du[way[i].y]--;
                if (!du[way[i].y])
                    S[++top]=way[i].y;
            }
        }
    }

    生成树

    当图变成了一棵数(纠结的生成树)
    喜闻乐见最小生成树有两种写法,一般我使用的时Kruskal算法
    但是面对稠密图(边数过多),我们需要最小生成树解法二:Prim算法

    Kruskal

    const int N=100010;
    int fa[N],n,m,dep[N];
    struct node{
        int x,y,v;
    }e[N];
    
    int cmp(const node &a,const node &b) {
        return a.v<b.v;
    }
    
    int find(int x) {              //路径压缩
        if (fa[x]!=x) fa[x]=find(fa[x]);
        return fa[x]; 
    }
    
    void unionn(int f1,int f2) {   //按秩合并 
        if (dep[f1]<dep[f1]) swap(f1,f2);    //f2->f1
        fa[f2]=f1;
        dep[f1]=max(dep[f1],dep[f2]+1);
    }
    
    void Kruskal() {
        int ans=0;                 //权值和 
        for (int i=1;i<=n;i++) fa[i]=i,dep[i]=1;
        sort(e+1,e+1+m,cmp);
        int cnt=0;
        for (int i=1;i<=m;i++) {
            int x=e[i].x;
            int y=e[i].y;
            int f1=find(x);
            int f2=find(y);
            if (f1==f2) continue;
            unionn(f1,f2);
            cnt++;
            ans+=e[i].v;
            if (cnt==n-1) break;
        }
    }

    Prim
    (实质上就是dijkstra)
    v v 数组记录与每个点相连的最短边

    int dis[N][N],v[N];
    bool vis[N];
    
    void Prim() {
        vis[1]=1;
        v[1]=0;
        int cur=1,ans=0;
        for (int i=2;i<=n;i++) 
            vis[i]=0,v[i]=INF;
        for (int T=1;T<n;T++) {
            int k=0,mn=INF;
            for (int i=1;i<=n;i++)
                if (!vis[i]) {
                    if (v[i]>d[cur][i]) v[i]=d[cur][i];
                    if (v[i]<mn) mn=v[i],k=i;
                }
            vis[k]=1;
            cur=k;
            ans+=mn;
        }
    }

    最小生成树是最小网络的一种,求解联通所有点且不增加其他点的最小网络
    这里简单提一下最小生成树的兄弟:斯坦纳树
    斯坦纳树求解联通部分点且可以增加其他点的最小网络

    什么?你想求解图的所有生成树数量?
    矩阵树定理讲解
    首先构造度数矩阵D和邻接矩阵 A A ,得到矩阵K=DA
    方便起见,我们去掉的第n行和第n列,得到一个新矩阵
    用高斯消元(取模情况下要使用辗转相除的高斯消元)得到新矩阵的上三角形
    对角线乘积的绝对值就是生成树数量


    KM算法

    图上的文章续(KM算法)

    求解完全二分图的最大完美匹配
    划重点:完美匹配,最大
    如果我们图两部的大小不相等,也不是完全图
    那么我们就可以加点加边使其变成完全图

    经典例题:KM
    经典例题:KM+双元限制

    const int N=305;
    int W[N][N],n;
    int Lx[N],Ly[N],belong[N],slack[N]; 
    bool L[N],R[N];
    
    int match(int i) {
        L[i]=1;
        for (int j=1;j<=n;j++)
            if (!R[j])
            {
                int v=Lx[i]+Ly[j]-W[i][j];
                if (!v) {
                    R[j]=1;
                    if (!belong[j]||match(belong[j])) {
                        belong[j]=i;
                        return 1;
                    }
                }
                else slack[j]=min(slack[j],v);
            }
        return 0;
    }
    
    int KM() {
        memset(belong,0,sizeof(belong));            //Y部在X部的匹配元素 
        for (int i=1;i<=n;i++) {                    //顶标
            Ly[i]=0;
            Lx[i]=W[i][1];
            for (int j=2;j<=n;j++) Lx[i]=max(Lx[i],W[i][j]);
        }
        for (int i=1;i<=n;i++) {
            for (int j=1;j<=n;j++) slack[j]=INF;    //(德尔塔)顶标
            while (1) {
                memset(L,0,sizeof(L));
                memset(R,0,sizeof(R));
                if (match(i)) break;
                int a=INF;
                for (int j=1;j<=n;j++) 
                    if (!R[j]) a=min(a,slack[j]);
                //寻找新顶标,新顶标只与没有参与匹配的Y点有关 
                for (int j=1;j<=n;j++)
                    if (L[j]) Lx[j]-=a;
                for (int j=1;j<=n;j++)
                    if (R[j]) Ly[j]+=a;
                    else slack[j]-=a;
            }
        }
        int ans=0;
        for (int i=1;i<=n;i++)
            ans+=W[belong[i]][i];
        return ans;
    }

    匈牙利算法

    图上的文章再续(二分图)
    blog上说的已经很详细了
    需要注意的一点:
    匈牙利算法(包括ta的特殊情况:KM算法)都已一个一个点以此匹配

    每次 match m a t c h 之前, L L R数组都要清零

    经典例题:匈牙利算法
    经典例题:匈牙利算法(方案)

    给出匈牙利算法如何计算最小顶点覆盖

    const int N=101;
    int n,m;
    struct node{
        int y,nxt;
    }way[N<<1];
    int st[N],tot=0,ans[N],cnt=0;
    int cx[N],cy[N];
    bool R[N],L[N];
    
    int match(int x) {
        L[x]=1;
        for (int i=st[x];i;i=way[i].nxt) {
            int y=way[i].y;
            if (!R[y]) {
                R[y]=1;
                if (cy[y]==-1||match(cy[y])) {
                    cy[y]=x;
                    cx[x]=y;
                    return 1;
                }
            }
        }
        return 0;
    }
    
    void method() {                        //最小顶点覆盖
        memset(L,0,sizeof(L));
        memset(R,0,sizeof(R));
        for (int i=1;i<=n;i++)
            if (cx[i]==-1)                 //寻找X部未匹配点 
                match(i);                  
        for (int i=1;i<=n;i++)
            if (!L[i]) ans[++cnt]=i;       //X部仍未匹配的点 
        for (int i=1;i<=m;i++)             
            if (R[i]) ans[++cnt]=i+n;      //Y部匹配点 
    }
    
    int XYL() {
        int ans=0;
        memset(cx,-1,sizeof(cx));
        memset(cy,-1,sizeof(cy));
        for (int i=1;i<=n;i++)
            if (cx[i]==-1) {
                memset(L,0,sizeof(L));
                memset(R,0,sizeof(R));
                ans+=match(i);
            }
        return ans;
    }

    其他

    对偶图
    平面图的最小割转换成对偶图的最短路
    化面为点,规定一个方向,每条边相邻的面连边

    n点无向连通图

    f[n]=2C(n,2)i=1n1C(n1,i1)f[i]2C(ni,2) f [ n ] = 2 C ( n , 2 ) − ∑ i = 1 n − 1 C ( n − 1 , i − 1 ) f [ i ] ∗ 2 C ( n − i , 2 )

    f[n]=i=1n1f[i]f[ni]C(n2,i1)(2i1) f [ n ] = ∑ i = 1 n − 1 f [ i ] ∗ f [ n − i ] ∗ C ( n − 2 , i − 1 ) ∗ ( 2 i − 1 )

    经典例题:双栈排序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值