【算法】图论

【网络流与二分图】专题链接

【图论】

图论-刘汝佳

完全三部图:图G可被分为三个顶点集,点集内的点相互均没有连边,不同点集的点之间相互均有连边。完全三部图的三元环个数是三点集点数的乘积。

无向无环图就是树。有向无环图DAG方便操作。

有环图可以tarjan缩点。

哈密顿回路(路径):每个点只经过一次的回路,此时显然每条边也只经过一次,是NPC难题。

欧拉回路(一笔画):每条边只经过一次的回路。

【无向图存在欧拉路径】所有点度数均为偶数,或只有两个点的度数是奇数。

【无向图存在欧拉回路】所有点度数均为偶数。

【有向图存在欧拉回路】所有点入度=出度。

欧几里得距离:两个点之间的距离,n维空间中的欧式距离的计算公式为:

曼哈顿距离:两个点在标准坐标系上的绝对轴距总和,在2维空间中的计算公式为:

(摘自欧几里得距离、曼哈顿距离和切比雪夫距离

【最短路】Dijkstra+Heap比SPFA稳得多!

floyd O(n3)

SPFA 虽然理论上界O(nm),但是实际运行效果不错。

SLF优化:设要加入的节点是j,队首元素为i,若dist(j)<dist(i),则将j插入队首,否则插入队尾。

循环队列大小至少开到点数那么大。

bool spfa()
{
    memset(vis,0,sizeof(vis));
    memset(d,0x3f,sizeof(d));
    int head=0,tail=1;q[0]=S;vis[S]=1;d[S]=0;
    while(head!=tail)
     {
         int x=q[head++];if(head>3000)head=0;
         for(int i=first[x];i;i=e[i].from)
          if(d[x]+e[i].w<d[e[i].v])
           {
               int y=e[i].v;
               d[y]=d[x]+e[i].w;
               if(!vis[y])
                {
                    if(d[y]<d[q[head]]){head--;if(head<0)head=3000;q[head]=y;}
                     else{q[tail++]=y;if(tail>3000)tail=0;}
                    vis[y]=1;
                }
           }
         vis[x]=0;
     }
    return d[T];
}
SPFA+SLF优化

spfa判负环(SLF把负权边放队头,优化显著):进队次数>n就有负环,或开数组记录步数(最短路路径条数),步数>n即有负环(两种方法一起用保证速度)

退出来后不一定是环上的点,但一定受环的影响才会走多次,所以沿着pre并记录vis直到找到重复访问即读出了环。

二分+DFS版SPFA判负环【BZOJ】1690: [Usaco2007 Dec]奶牛的旅行(玄学复杂度,速度优秀)

dijkstra+Heap 理论上界O(mlog n),一般会十分接近上界,但是很稳

只适用于无负权边的图。

类似BFS,每次找到当前距离最近的点(即不会再被更新)进行更新,这样每个点只会被更新一次(但是堆中的点在被访问前会被频繁修改)

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int maxn=100010,maxm=500010,inf=0x3f3f3f3f;
struct edge{int from,v,w;}e[maxm];
struct Node{
    int x,d;
    bool operator <(const Node &b)const{
        return d>b.d;
    }
}cyc;
int n,m,first[maxn],tot,d[maxn],s,t;
priority_queue<Node>q;
void insert(int u,int v,int w)
{tot++;e[tot].v=v;e[tot].w=w;e[tot].from=first[u];first[u]=tot;}
int dijkstra()
{
    memset(d,0x3f,sizeof(d));
    d[s]=0;q.push((Node){s,d[s]});
    while(!q.empty())
     {
        cyc=q.top();q.pop();
        if(cyc.d!=d[cyc.x])continue;
        int x=cyc.x;
        for(int i=first[x];i;i=e[i].from)
         if(d[e[i].v]>d[x]+e[i].w)
          {
              d[e[i].v]=d[x]+e[i].w;
              q.push((Node){e[i].v,d[e[i].v]});
          }
     }
    return d[t];
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
     {
         int u,v,w;
         scanf("%d%d%d",&u,&v,&w);
         insert(u,v,w);
         insert(v,u,w);
     }
    s=1;t=n;
    printf("%d",dijkstra());
    return 0; 
}
View Code

技巧:

★dijkstra和spfa可以求多源最短路,只要d[x]=0就是起点集,然后对于每个点x,d[x]min就是到起点集的最短路。

1.分别从S、T做一遍最短路,然后枚举边+两端最短路。

2.求路径可以用DAG上的DP做。

3.求最短路树:d[i]==d[j]+w(i,j)时为最短路树的边,在最短路中记录父亲就可以顺便求出(通常不管相同长度路径,建成树方便后续操作),这样树上有n-1条边。

4.注意重边和自环。

5.所有边反向重做。

6.有时说不定可以走回头路。

7.求最短/最长回路可以把所有指向S的边复制一份指向新的点S',然后跑最短/长路即可。

8.判断是否在最短路上的常见套路:判断两端最短路(正反向)+边权是否等于整体最短路。如“第二短路”。

最短路算法都是一样的,关键在建图

明确状态表示(每个状态是一个点)

编号:int& x=id[...];if(x==0)x==++n;

预处理cango(能否走这一步),处理原点决策,处理每点的决策(明确状态表示,double表示已经加倍过了),跑最短路,处理终点。

9.平面图转对偶图:浅析最大最小定理在信息学竞赛中的应用

蓝书(《算法竞赛入门经典训练指南》)最短路最后一题。

就是对于s-t平面图(边不相交,s,t都在外界面的边界上),可以从s-t拉一条边,那么以s-t内的边界边为起点,以s-t外的边界边为终点跑最短路就可以得到最小割。

【差分约束】线性不等式组与图论最短路联系的桥梁

参考资料:《浅析差分约束系统》    夜深人静写算法(四) - 差分约束

约束条件:不等式组左侧为同数组两点差,右侧为常数。(约束条件一定要考虑全面细致)

转化模型:对于约束条件xj-xi≤bk(j和i之间的最短路<=连边),新建边i--->j,权值为bk。加入一个源点s,从s出发与所有点相连,权值为0,跑最短路。

1.如果要求两个变量差的最大值,那么将所有不等式转变成"<="的形式(极端约束条件,要尽可能大但必须≤...),则最短路就是满足所有约束的最大值

2.相反,如果需要求的是两个变量差的最小值,那么需要将所有不等式转化成">=",建图后求最长路

最短路:"<=的最短路"和">=的最长路“就是在现有约束下得出对于起点和终点的直接约束(搭一条边),即x[T]-x[S]≤d,所以d就是满足所有约束的最值

解:<有解>  <无解:存在负环,d无穷小>  <无穷多解:s和t不连通,d无穷大,没有约束>

两边夹定理:A-B=C  <=>  A-B>=C&&A-B<=C

设num[ i ]为i时刻能够开始工作的人数,x[ i ]为实际雇佣的人数,那么x[ I ]<=num[ I ]。 
设r[ i ]为i时刻至少需要工作的人数,于是有如下关系: 
    x[ I-7 ]+x[ I-6 ]+x[ I-5 ]+x[ I-4 ]+x[ I-3 ]+x[ I-2 ]+x[ I-1 ]+x[ I ]>=r[ I ] 
设s[ I ]=x[ 1 ]+x[ 2 ]…+x[ I ],得到 
    0<=s[ I ]-s[ I-1 ]<=num[ I ], 0<=I<=23 
    s[ I ]-s[ I-8 ]>=r[ I ], 8<=I<=23 
    s[ 23 ]+s[ I ]-s[ I+16 ]>=r[ I ], 0<=I<=7

这个问题比较特殊,我们还少了一个条件,即:s[23] = T,它并不是一个不等式,我们需要将它也转化成不等式,由于设定s[-1] = 0,所以 s[23] - s[-1] = T,它可以转化成两个不等式:

      s[23] - s[-1] >= T
      s[-1] - s[23] >= -T
将这两条边补到原图中,求出的最长路s[23]等于T,表示T就是满足条件的一个解,由于T的值时从小到大枚举的(T的范围为0到N),所以第一个满足条件的解就是答案。
zju1420

一定要注意题目中的所有约束条件,例如结点编号顺序xi-xi-1>=0,千万不要漏掉。

变量式出现三个变量时,考虑枚举其中一个变量。

【最小生成树】MST

【1】切割性质:如果某条边是跨越集合且边权最小的边,那么所有最小生成树一定经过它。(假设边权不等)

理解:根据MST的定义,两个集合要连通必然是连最小的边,其中一个例子是每个点一定通过最小的邻边连接,MST不存在所谓决策!

【2】回路性质:回路上边权最大的边,所有最小生成树一定不经过它。(假设边权不等)

要连接回路上的n个点只需要n-1条边,因此最大的边一定不会连接,MST不存在所谓决策。

【3】实现算法

kruskal算法:根据切割性质,每次找到全图最小的跨集合边连接直到形成MST,复杂度O(m log m)。

prim算法:维护构成MST的点集S,每次找到距离S最近的点加入点集直至形成MST,原理是最近的点不可能通过其它点中转使距离更小,需要邻接矩阵,O(n^2)。

Boruvka算法:连接图上所有生成子树的最小邻边,然后再次进行直至完成,可证至多log n次,复杂度O(m log n)。

常用kruskal算法。

#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=310;
struct cyc{int from,to,pre,k;}e[100010];
int fa[maxn],head[maxn],n,m,cnt,tot,maxs;
bool cmp(cyc a,cyc b){return a.k<b.k;}
void insert(int u,int v,int k)
{cnt++;e[cnt].from=u;e[cnt].to=v;e[cnt].pre=head[u];head[u]=cnt;e[cnt].k=k;}
int getfa(int x)
{return fa[x]==x?x:(fa[x]=getfa(fa[x]));}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
     {
         int u,v,c;
         scanf("%d%d%d",&u,&v,&c);
         insert(u,v,c);
         insert(v,u,c);
     }
    sort(e+1,e+cnt+1,cmp);
    for(int i=1;i<=n;i++)fa[i]=i;
    tot=0;
    for(int i=1;i<=cnt;i++)
     {
         int u=e[i].from,v=e[i].to;
         if(getfa(u)!=getfa(v))
          {
              fa[fa[u]]=fa[v];
              tot++;maxs=e[i].k;
          }
         if(tot>=n-1)break;
     }
    printf("%d %d",tot,maxs);
    return 0;
}
View Code

应用:

(1)增量最小生成树:每次加边构成一个环,根据回路性质,只需把原u,v路径上最大的一条边与新边比较即可。

(2)最小瓶颈生成树:最小生成树一定是最小瓶颈生成树

最小瓶颈路:起点和终点在最小生成树上的唯一路径就是最小瓶颈路。

(3)次小生成树:根据枚举每条非MST边加入后构成的环的边交换代价,找到最小的即可。

边交换实际上就是所有最小瓶颈路,预处理即可O(n^2)。

(4)最小有向生成树(最小树形图):朱刘算法

朱刘算法

坑:《生成树的计数及其应用》

【拓扑排序】不断寻找入度为0的点入队处理,处理时顺便把它能到达的点in-1,新产生的in=0入队。

拓扑序可以保证DAG无后效性。若树边有向时可用于把递归(自上而下)改为拓扑+递推(自下而上),不过也可以BFS记录深度然后从最大深度往小扫,这样就保证叶子节点先扫到。

有向图找简单环:拓扑排序后剩余环+环内DAG,DFS将DAG上的点(不能继续访问的点)在回溯时叉掉,然后访问到访问过的点就是环。

【无向图的双连通分量】

边双连通分量:任意两点之间至少存在两条边不重复的路径。要求每条边都至少在一个简单环中,即所有边都不是桥。

点双连通分量:任意两点之间至少存在两条点不重复的路径。要求任意两条边都在同一个简单环中(一条边可能属于多个简单环),即内部无割顶。

特别的,一条边相连的两个点视为一个点双

一个图如果是点双连通分量(更严格),它也一定是边双连通分量(范围更大)。

所以,双连通分量其实是一堆简单环组成的图,边双实际上是要求所有点和边都必须在环中,点双则更加严格。

点双:一条边恰好属于一个点双连通分量,因为若属于两个,这两个分量间有两个公共点(边的两个端点),则实际上是同一个分量(两点保证了点不重复)。

由此也可知两个点双间可能且至多有一个公共点,即割顶。反之任意割顶都是至少两个分量的公共点(点双tarjan算法的理论基础)

边双:除了桥不属于任何边双连通分量之外,每条边恰好属于一个边双连通分量。两个分量之间不可能有公共点(否则为同一个),只能有桥。

★Tarjan算法(边双连通分量)

tarjan算法是图连通问题的通用算法,其核心思想是找连通块断点

令dfn[x]为x的时间戳,low[x]为x的往上走最远能到达的位置。

未访问,low[x]=min(low[x],low[y])

low[x]的值要根据x的子树的low值计算完毕后才可得知。

已访问,low[x]=min(low[x],dfn[y])

当检测到反向边(x,y)时,y一定是x的祖先(无向图中,若y不是x的祖先,则dfs到y时一定沿(x,y)先访问到x了,故不可能是横叉边)

此时说明x能到达y的位置dfn[y](此时low[y]的值仍未得知,我们只需要先到达dfn[y],如果y能到更高,在回到y的时候再由low[y]去就行了)

未访问,if(low[y]>dfn[x])iscut[i]=1

找桥,在访问x的时候判断x-y是否桥。

★<边双连通分量>【POJ】3177 Redundant Paths

边双连通分量更加常用。注意有重边时就不能禁止走向父亲而要禁止走原边。

void tarjan(int x,int fa){
    dfn[x]=low[x]=++dfsnum;
    for(int i=first[x];i;i=e[i].from)if((i^1)!=fa){
        if(!dfn[e[i].v]){
            tarjan(e[i].v,i);
            low[x]=min(low[x],low[e[i].v]);
            if(low[e[i].v]>dfn[x])iscut[i]=iscut[i^1]=1;
        }else low[x]=min(low[x],dfn[e[i].v]);
    }
}
void dfs(int x){
    col[x]=colnum;
    for(int i=first[x];i;i=e[i].from)if(!col[e[i].v]&&!iscut[i])dfs(e[i].v);
}
View Code

<点双连通分量>割顶low[y]>=dfn[x],且需要特殊处理根节点。

割顶判断

#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=100000,maxm=100000;
struct edge{int u,v,from;}e[maxm*3];
int first[maxn],mark,dfn[maxn],low[maxn],iscut[maxn],tot,n,m;
void insert(int u,int v)
{tot++;e[tot].v=v;e[tot].u=u;e[tot].from=first[u];first[u]=tot;}
void dfs(int x,int fa)
{
    dfn[x]=low[x]=++mark;
    int child=0;
    for(int i=first[x];i;i=e[i].from)
     if(e[i].v!=fa)
     {
         int y=e[i].v;
         if(!dfn[y])
          {
              child++;//important
              dfs(y,x);
              low[x]=min(low[x],low[y]);
              if(low[y]>=dfn[x])iscut[x]=1;
          }
         else /*if(dfn[y]<dfn[x])*/low[x]=min(low[x],dfn[y]);
     }
    if(fa<0&&child<=1)iscut[x]=0;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
     {
         int u,v;
         scanf("%d%d",&u,&v);
         insert(u,v);insert(v,u);
     }
    for(int i=1;i<=n;i++)if(!dfn[i])dfs(i,-1);
    for(int i=1;i<=n;i++)if(iscut[i])printf("%d ",i);printf("
");
    return 0;
}
View Code

点双连通分量

#include<cstdio>
#include<algorithm>
#include<vector>
#include<stack>
#include<cstring>
using namespace std;
const int maxm=50010;
struct edge{int u,v,from;}e[maxm*3];
int first[maxm],iscut[maxm],dfn[maxm],low[maxm],bccno[maxm],bcc_cnt,dfsnum,tot,n,m,kase;
vector<int>bcc[maxm];
stack<int>s;
void insert(int u,int v)
{tot++;e[tot].u=u;e[tot].v=v;e[tot].from=first[u];first[u]=tot;}
void tarjan(int x,int fa)
{
    dfn[x]=low[x]=++dfsnum;
    int child=0;
    for(int i=first[x];i;i=e[i].from)
     if(e[i].v!=fa)
      {
          int y=e[i].v;
//          s.push(i);
          if(!dfn[y])
           {
              s.push(i);//
              child++; //important
               tarjan(y,x);
               low[x]=min(low[x],low[y]);
               if(low[y]>=dfn[x])
                {
                    iscut[x]=1;
                    bcc_cnt++;
                    bcc[bcc_cnt].clear();
                    for(;;)
                     {
                         int now=s.top();s.pop();
                         int u=e[now].u,v=e[now].v;
                         if(bccno[u]!=bcc_cnt){bcc[bcc_cnt].push_back(u);bccno[u]=bcc_cnt;}
                         if(bccno[v]!=bcc_cnt){bcc[bcc_cnt].push_back(v);bccno[v]=bcc_cnt;}
//                         printf("%d
",bcc_cnt);
                         if(u==x&&v==y)break;
                     }
                }
           }
          else s.push(i),low[x]=min(low[x],dfn[y]);//important
      }
    if(fa<0&&child==1)iscut[x]=0;//根节点特判!!! 
}
int main()
{
    scanf("%d%d",&n,&m);
    while(m!=0)
     {
         memset(first,0,sizeof(first));
         tot=0;bcc_cnt=0;
         for(int i=1;i<=m;i++)
          {
              int u,v;
              scanf("%d%d",&u,&v);
              insert(u,v);
              insert(v,u);
          }
         memset(dfn,0,sizeof(dfn));
         memset(low,0,sizeof(low));
         memset(iscut,0,sizeof(iscut));
         memset(bccno,0,sizeof(bccno));
         dfsnum=0;
         while(!s.empty())s.pop();
         for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,-1);
         for(int i=1;i<=n;i++)printf("%d ",iscut[i]);printf("
");
         for(int i=1;i<=bcc_cnt;i++)
          {
              printf("#%d: ",i);
              for(int j=0;j<bcc[i].size();j++)
               printf("%d ",bcc[i][j]);
              printf("
");
          }
         scanf("%d%d",&n,&m);
     }
    return 0;
}
View Code

缩点:简单环就是双连通分量,所以双连通缩点后就是树(无向无环连通图)。

【有向图的强连通分量】

定义有向图的极大连通子图(相互可达)为强连通分量(SCC)。

经常将SCC缩点使图变为DAG(有向无环图)。(也就是极端情况下,每个点自成一个SCC)

Tarjan算法(强连通分量)

核心思想是通过dfn[x]=low[x]找到SCC。

low[x]=min(low[x],low[y])

传递

if(!col[y])low[x]=min(low[x],dfn[y])

阻止连向SCC,因为已经形成了的SCC不会再往外连边,此时往内连边没用且使用其dfn会导致后面和low判等出错。

if(dfn[x]==low[x])

DFS过程中用栈记录点,lack记录每个点在栈中的位置。

一旦能形成SCC就出栈到lack[x],用col染色。

补充:原理是一旦下面的点都能到达这里就形成SCC,而下面有一些不能到达的已经自成SCC了,是典型的子过程思想。

缩点

读每条边,当color不同时建新边,color就是天然的新图节点标号,留下来的信息只有点权(SCC内节点数量)。

参考:https://www.byvoid.com/blog/scc-tarjan/

http://www.cnblogs.com/saltless/archive/2010/11/08/1871430.html

void tarjan(int x)
{
    dfn[x]=low[x]=++mark;
    s[++top]=x;lack[x]=top;
    for(int i=first[x];i;i=e[i].from)
     {
         int y=e[i].v;
         if(!dfn[y])
          {
              tarjan(y);
              low[x]=min(low[x],low[y]);
          }
         else if(!col[y])low[x]=min(low[x],dfn[y]);
     }
    if(dfn[x]==low[x])
     {
         color++;
         for(int i=lack[x];i<=top;i++)col[s[i]]=color;
         num[color]=top-lack[x]+1;
         top=lack[x]-1;
     }
}
for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i);
View Code

经典问题:【BZOJ】2208 [Jsoi2010]连通数

【无向连通图计数】带标号

引用自:图论--连通图计数模板 by 奚政

问题:含有n个不同的点的连通图有多少种(同构的图算多个,即带标号)。

题解:

记 含有n个点的连通图个数为 F[n] ;

  含有n个点的不连通图个数为 G[n] ;

  含有n个点的图的个数为 H[n] = 2^(n*(n-1)/2) ;

  有  F[n] + G[n] = H[n] ;  F[n] = H[n] - G[n] ;

因此要求 F[n] 只需求 G[n] ;

求 G[n] :枚举 1 号点所在的连通块大小为 k ( 1 ~ n-1 ) ;

     当 1 号点所在连通块大小为 k 时: k个点选取情况为组合数  C[n-1][k-1]

                     该连通块的种类数确定 k 个点后,连通块的种类数为 F[k] 

                     剩余 n-k 个点组成的图的种类数 H[n-k]

                     因此情况数为 C[n-1][k-1] * F[k] * H[n-k]

     G[n]为所有枚举的情况数总和  Σ( k : 1 ~ n-1 ) ( C[n-1][k-1] * F[k] * H[n-k] )

定初始值 F[1] = 1 , G[1] = 1

BZOJ3456 多项式求逆!

【最短路顶点计数】

题意:给定带权无向图,定义顶点为删去影响两两最短路的点,求顶点数。

结论:以每个点为根做dijkstra,若某个点只有一条路径到达,则该点的前继为顶点,因为删去前继后会使根到该点断路。(不能将路径上的点都标识,只能标识前继)

数据小所以可以用n^2的dijkstra。

【2-sat】你以为你会2-sat了,其实你只知道怎么用……

【描述】给定n个布尔变量,m个对两个变量的限制条件(与或非),求可行解,最小字典序解等问题。

【建模】将第i个布尔变量拆成2*i-1和2*i两个点分别表示真和假,对限制条件连接“推导出”边。

【算法】O(m)的判可行性的dfs算法:对每个未决策的点假设为假,然后dfs,遇到标记过却不符合返回矛盾。矛盾则假设为真再dfs,再矛盾则全图无解(不回溯)。若不矛盾则过程中标记的点都决策完毕。

【原理】开始阐述2-sat算法原理。

对称性:2-sat最重要的特点就是建模具有对称性,i->j'必然伴随j->i',因此点x如果被y强制选,点x'一定会强制选y‘

连通块理论:为什么不用回溯?

连通块:也就是一个变量确定决策后,该点能到的地方都确定了决策,我们把这些一起确定了决策的点称为一个连通块(为了方便)。

假设连通块内点x决策为假,外界如果有边连向x假,这条边没有意义。外界如果有边连向x真,由对称性x假会连向外界,则连通块会拓展,故外界无边连向连通块内。

综上,连通块十分独立,它已经和剩下未决策的点无关,故改变前面的决策也不会对结果产生影响,所以不用回溯,所以矛盾了就全图无解(改变之前的决策也没用)。

矛盾判定理论:为什么真假都矛盾才算矛盾?

由①可知连通块独立,所以一个点dfs不可能遇到之前由其它点确定决策的点。

故一个点dfs遇到已标记的点只会是dfs中前后遇到,只有两种情况:

两次遇到x,构成环,正常返回。

两次遇到x和x’,矛盾。(可能是环,也可能不是)

对于第二种情况,如果真假都是如此,就说明有环包含了x和x‘,则全图矛盾(从而推出2-sat的定理:无解当且仅当存在环包含x和x’)。

不回溯dfs算法就由“连通块”和“矛盾判定”两部分构成,可以O(m)地求解2-sat问题,包括但不限于可行性、可行解、最小字典序解(按顺序安排)等。

所以为什么网上流行缩点+拓排,没人看紫书吗……?

另外有拓扑排序做法的一些资料:kuangbin 详细介绍 论文 对称性论文,以后发现了dfs做法的缺陷再来补。

缺陷:虽然复杂度正确,但是有爆栈风险!解决方法是倒过来处理,或把处理序列随机化

复杂度不对……不好意思,已经改写法了233

经典问题:三元环计数

1.暴力分块

将点分成点度小于$sqrt m$的点和大于等于$sqrt m$的点。

对于第一类点,暴力枚举每个点的两条边搭配,极限情况是√m个点,每个点由√m条边,复杂度为O(m√m)。

对于第二类点,暴力枚举三个点判断是否三元环,复杂度O(m√m)。

暴力分块的特点是:一类块多块小,针对块内容设计算法。一类块少块大,针对块数量设计算法。

2.点度

枚举每条边,枚举该边点度小的一个端点的邻接边来判断,复杂度O(m√m)。原因不明。

3.定向

每条边指向点度大的点,同点度指向编号小的点,这样一定是一个DAG,且每个点的出度<√n。

然后枚举每条边,枚举度数小的点的出度,复杂度O(m√n)。

原文地址:https://www.cnblogs.com/onioncyc/p/6617915.html