YZhe的头像

图论知识总结

@


声明:部分代码非本人所写

最短路

通常是求图中的最长或最短路径,一般不会考裸题,需要分析图的性质,可能要自行改造图。

任意两点间的最短路

floyd
用于求出任意两点间的最短路
基于dp的思想

void floyd()
{
    for (int i=1;i<=n;i++)
        a[i][i]=0;//很重要
    for (int k=1;k<=n;k++)
        for (int i=1;i<=n;i++)
            for (int j=1;j<=n;j++)
                if (a[i][k]+a[k][j]<a[i][j])
                    a[i][j]=a[i][k]+a[k][j];
}

单源最短路径

dijkstra

朴素版\(O(n^2)\)
适用于稠密图

int d[MAX];//d为从源点到其他所有点的距离
bool vis[MAX];//记录某个点是否已经松弛过
void dijkstra(int s)
{
    for (int i=1;i<=n;i++)    dis[i]=a[s][i];
    memset(vis,false,sizeof(vis));
    for (int i=1;i<=n;i++)
    {
        int minn=MAX;
        int k=0;
        for (int j=1;j<=n;j++)
            if (!vis[j]&&dis[j]<minn)
            {
                minn=dis[j];
                k=j;
            }
        if (k==0)    return;//找不到可松弛的点
        vis[k]=false;
        for (int j=1;j<=n;j++)
            if (!vis[j]&&d[k]+a[k][j]<d[j])
                d[j]=d[k]+a[k][j];
    }
}

堆优化版,适用性很广
\(O(mlogm)\)

#include<utility>
#include<queue>
typedef pair<int,int>    pii;
void dijkstra(int s)
{
    priority_queue <pii,vector<pii>,greater<pii> >    q;
    memset(d,0x3f,sizeof(d));
    d[s]=0;
    memset(vis,false,sizeof(vis));
    q.push(make_pair(d[s],s));
    while (!q.empty())
    {
        pii tmp=q.top();
        q.pop();
        int x=tmp.second;
        if (vis[x])    continue;
        vis[x]=true;
        for (int i=head[x];i;i=edge[x].ne)
            if (d[edge[i].y]>d[x]+edge[i].v)
            {
                d[edge[i].y]=d[x]+edge[i].v;
                q.push(make_pair(d[edge[i].y],edge[i].y));
            }
    }
}

ford

朴素版
\(O(nm)\)

void bellmanford(int s)
{
    memset(dis,0X3f,sizeof(dis));
    dis[s]=0;
    bool rel;
    for (int i=1;i<=n;i++)
    {
        rel=false;
        for (int j=1;j<=len;j++)
            if (dis[edge[j].y]>dis[edge[j].x]+edge[j].v)
            {
                dis[edge[j].y]=dis[edge[j].x]+edge[j].v;
                rel=true;
            }
        if (!rel)    return;
    }
}

队列优化版,又名SPFA
同样很实用,但负责的出题人通常会卡掉这个算法
通常在有负边权、最长路时使用。

int queue[MAX];
void spfa(int s)
{
    int Head=0,tail=1;
    memset(vis,false,sizeof(vis));
    memset(dis,0x3f,sizeof(dis));
    queue[1]=s;    dis[s]=0;    vis[s]=true;
    while (Head<tail)
    {
        int tn=queue[++Head];
        vis[tn]=false;
        int te=head[tn];
        for (int i=te;i;i=edge[i].ne)
        {
            int tmp=edge[i].y;
            if (dis[tmp]>dis[tn]+edge[i].v)
            {
                dis[tmp]=dis[tn]+edge[i].v;
                if (!vis[tmp])
                {
                    vis[tmp]=true;
                    queue[++tail]=tmp;
                }
            }
        }
    }
}

最小生成树

prime

与dijkstra类似,用到了贪心的思想

#include<iostream>
#define INF 10000
using namespace std;
const int maxn = 6;
bool vis[maxn];
int dist[maxn];
int graph[maxn][maxn];	//graph[u][v]表示从u到v的权值,INF代表两点之间不可达 
int prim(int cur)
{
    int index = cur;
    int sum = 0;
    cout<<index<<" ";	//输出路径 
    memset(vis, false, sizeof(vis));
    vis[cur] = true;
    for(int i=0; i<maxn; i++)
        dist[i] = graph[cur][i];    //初始化,每个与cur邻接的点的距离存入dist
    for(int i=1; i<maxn; i++)
    {
        int minor = INF;
        for(int j=0; j<maxn; j++)
        {
            if(!vis[j]&&dist[j]<minor)     //找到未访问的点中,距离当前最小生成树距离最小的点
            {
                minor = dist[j];
                index = j;
            }
        }
        vis[index] = true;
        cout << index << " ";	//输出路径 
        sum += minor;
        for(int j=0; j<maxn; j++)
        {
            if(!vis[j]&&dist[j]>graph[index][j])//执行更新,如果点距离当前点的距离更近
                                                //就更新dist
            {
                dist[j] = graph[index][j];
            }
        }
    }
    cout<<endl;
    return sum;               //返回最小生成树的总路径值
}

kruskal

对边排序,用并查集维护图的连通性

#include<iostream>
using namespace std;
const int maxn = 7;
typedef struct _node
{
    int val;
    int start;
    int end;
}Node;
Node V[maxn];
int cmp(const void *a, const void *b)
{
    return(*(Node *)a).val - (*(Node *)b).val;
}
int fa[maxn];
int cap[maxn];
 
void make_set()              //初始化集合,让所有的点都各成一个集合,每个集合都只包含自己
{
    for(int i=0; i<maxn; i++)
    {
        fa[i] = i;
        cap[i] = 1;
    }
}
 
int Find(int x)              //判断一个点属于哪个集合,点如果都有着共同的祖先结点,就可以说他们属于一个集合
{
    if(x != fa[x])
     {                             
        fa[x] = Find(fa[x]);
    }    
    return fa[x];
}                                 
 
void Union(int x, int y)         //将x,y合并到同一个集合
{
    rootx = Find(x);
    rooty = Find(y);
    if(x == y)
        return;
    if(cap[x]<cap[y])
        fa[x] = rooty;
    else
    {
        if(cap[x]==cap[y])
            cap[x]++;
        fa[y] = rootx;
    }
}
 
int Kruskal(int n)
{
	qsort(V, maxn, sizeof(V[0]), cmp);
    int sum = 0;
    make_set();
    for(int i=0; i<maxn; i++)//将边的顺序按从小到大取出来
    {
        if(Find(V[i].start) != Find(V[i].end))     //如果改变的两个顶点还不在一个集合中,就并到一个集合里,生成树的长度加上这条边的长度
        {
            Union(V[i].start, V[i].end);  //合并两个顶点到一个集合
            sum += V[i].val;
        }
    }
    return sum;
}

基环树

给一颗树加上一条边,它就不在是一颗严格意义上的树了,但同样具备树的某些性质,我们称它为基环树。
基环树通常作为一些经典模型的扩展,增加题目难度。

基环树没有固定的套路,通常需要自己研究基环树在一道题中的性质。

不过有一点是确定的,一道基环树的题目的突破口一定是在它的环上。

找环

void mark(int root,int leaf){
	memset(vis,0,sizeof(vis));
	ext=true;
	l[root]=leaf;
	r[leaf]=root;
	circle[root]=circle[leaf]=true;
	while(1){
		l[leaf]=fa[leaf];
		r[fa[leaf]]=leaf;
		leaf=fa[leaf];
		circle[leaf]=true;
		if(leaf==root) break;
	}
}
void find_circle(int u){
	if(ext) return;
	vis[u]=true;
	int lim=go[u].size();
	for(int i=0;i<lim;++i){
		int v=go[u][i];
		if(!vis[v]){
			vis[v]=true;
			fa[v]=u;
			dep[v]=dep[u]+1;
			find_circle(v);
		}
		else if(vis[v]&&dep[v]-dep[u]>1) mark(u,v);
		if(ext) return;
	}
	if(ext) return;
}

例题

小 Y 是一个爱好旅行的 OIer。她来到 X 国,打算将各个城市都玩一遍。

小Y了解到, X国的 n 个城市之间有 m 条双向道路。每条双向道路连接两个城市。
不存在两条连接同一对城市的道路,也不存在一条连接一个城市和它本身的道路。并且,
从任意一个城市出发,通过这些道路都可以到达任意一个其他城市。小 Y 只能通过这些 道路从一个城市前往另一个城市。

小 Y 的旅行方案是这样的:任意选定一个城市作为起点,然后从起点开始,每次可
以选择一条与当前城市相连的道路,走向一个没有去过的城市,或者沿着第一次访问该 城市时经过的道路后退到上一个城市。当小 Y
回到起点时,她可以选择结束这次旅行或 继续旅行。需要注意的是,小 Y 要求在旅行方案中,每个城市都被访问到。

为了让自己的旅行更有意义,小 Y 决定在每到达一个新的城市(包括起点)时,将 它的编号记录下来。她知道这样会形成一个长度为 n
的序列。她希望这个序列的字典序 最小,你能帮帮她吗?

对于两个长度均为 n 的序列 A 和 B,当且仅当存在一个正整数 x,满足以下条件时, 我们说序列 A 的字典序小于 B。 对于任意正整数
1 ≤ i < x1≤i<x,序列 A 的第 i 个元素 A_i 和序列 B 的第 i 个元素 B_i 相同,序列 A 的第 x
个元素的值小于序列 B 的第 x 个元素的值。

输入格式 输入文件共 m + 1 行。第一行包含两个整数 n,m(m ≤ n),中间用一个空格分隔。

接下来 m 行,每行包含两个整数 u,v (1 ≤ u,v ≤ n),表示编号为 u 和 v 的城市之
间有一条道路,两个整数之间用一个空格分隔。

输出格式 输出文件包含一行,n 个整数,表示字典序最小的序列。相邻两个整数之间用一个 空格分隔。

NOIP2018Day2T1

当时我就是被这个环困扰,所以只拿了形状为树的分。

现在看来,对于当时的题目,只需要暴力断边,\(O(n^2)\)即可通过原题。

现在考虑下当\(n<=500000\)时的做法。

按套路找到环之后,我们从1开始贪心地向编号小的点走,当第一次走环上时,一定会贪心地走向更小的点走,在环上还是要贪心地走,但是这时的贪心策略有所不同。

这便是这道题的难点。考虑到在环上,我们可以随时在走向一个环上的点时回溯,那么什么时候回溯合适呢?又因为我们一定会贪心地走完更大的不在环上的点才会走向环上的下一个点,所以我们就记录一下回溯时要走向的第一个点,如果回溯的这个点的编号小于环上的下一个点,就回溯。

题目就迎刃而解了。

#include<cstdio>
#include<vector>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=500007;
int n,m,tot=0,no1,no2,ot,ans[N],dep[N],fa[N],l[N],r[N];
bool vis[N],circle[N],ext,fir=true,al;
vector<int> go[N];
template<class T>inline void read(T &res){
	T flag=1;static char ch;
	while((ch=getchar())<'0'||ch>'9') if(ch=='-') flag=-1;res=ch-48;
	while((ch=getchar())>='0'&&ch<='9') res=res*10+ch-48;
	res*=flag;
} 
void dfs(int u){
	ans[++tot]=u;
	vis[u]=true;
	int lim=go[u].size();
	for(int i=0;i<lim;++i){
		int v=go[u][i];
		if(vis[v]) continue;
		dfs(v);
	}
}
void dfs1(int u,int last){
	ans[++tot]=u;
	vis[u]=true;
	int lim=go[u].size();
	if(circle[u]){
		bool out=false;
		for(int i=0;i<lim;++i){
			int v=go[u][i];
			if(vis[v]) continue;
			if(out){
				last=v;
				break;
			}
			if(circle[v]) out=true;
		}
	}
	for(int i=0;i<lim;++i){
		int v=go[u][i];
		if(vis[v]) continue;
		if(!circle[v]||fir||al){
			if(circle[v]) fir=false;
			dfs1(v,last);
		}
		else{
			if(v>last){
				al=true;
				continue;
			}
			dfs1(v,last);
		}
	}
}
void mark(int root,int leaf){
	memset(vis,0,sizeof(vis));
	ext=true;
	l[root]=leaf;
	r[leaf]=root;
	circle[root]=circle[leaf]=true;
	while(1){
		l[leaf]=fa[leaf];
		r[fa[leaf]]=leaf;
		leaf=fa[leaf];
		circle[leaf]=true;
		if(leaf==root) break;
	}
}
void find_circle(int u){
	if(ext) return;
	vis[u]=true;
	int lim=go[u].size();
	for(int i=0;i<lim;++i){
		int v=go[u][i];
		if(!vis[v]){
			vis[v]=true;
			fa[v]=u;
			dep[v]=dep[u]+1;
			find_circle(v);
		}
		else if(vis[v]&&dep[v]-dep[u]>1) mark(u,v);
		if(ext) return;
	}
	if(ext) return;
}
int main()
{
//	freopen("travel.out","w",stdout);
	read(n);read(m);
	for(int i=1;i<=m;++i){
		int u,v;
		read(u);read(v);
		go[u].push_back(v);
		go[v].push_back(u);
	}
	for(int i=1;i<=n;++i) sort(go[i].begin(),go[i].end());
	if(n==m){
		dep[1]=1;fa[1]=0;
		find_circle(1);
		memset(vis,0,sizeof(vis));
		dfs1(1,0);
	}
	else dfs(1);
	for(int i=1;i<=n;++i) printf("%d ",ans[i]);
    return 0;
}

差分约束

题目给定一些要求,这些要求在转化后可以表示成形如:A - B ≥ val或者A - B ≤ val的不等式组。
这个不等式的的一边,一般是未知(待求)的两个状态(或者两个待求的数值),而另一边是一个定值(通常是已知值)。这种问题通常可转化成最长路或者最短路求解。

有解,无解,有无数解的 条件分别是什么?

有解----->有最短路(求最大值)/有最长路(求最小值)
无解----->有负环(求最大值)/有正环(求最小值)
无数解 ----->不连通,无限制关系

例题

为了庆祝大佬 wxh 的生日,众人决定为他准备礼物。现在有 n 个礼品盒排成一行,从 1
到 n 编号,每个礼品盒中可能有 1 个或 0 个礼品。大佬 wxh 提出了 m 个要求,形如“第 l[i] 到第
r[i]个礼品盒当中至少有 c[i]个礼品”。现在众人想知道,为了满足这些要求,所需准备 的最少礼品数。

首先考虑把区间的约束转化为点的约束,很明显是前缀和。

\(那么再根据题目描述就可以写出3个约束条件\)

\(设前缀和为f_i,那么有\)

\[f_a-f_b>=c_i \]

\[0<=f_i-f_{i-1}<=1 \]

根据这个条件建图,每个点表示前缀和

\(b-1向a建一条c边\)

\(i向i-1建一条-1边,i-1向i建一条0边\)

然后跑最长路

例题

[SCOI2011]糖果

一道比较综合的题。

\(当op为一的时候,建一个双向边权值为0;\)
\(为2的时候,建立从u到v权值为1的边;\)
\(为3的时候,建立从v到u权值为0的边;\)
\(为4的时候建立从v到u权值为1的边;\)
\(为5的时候建立从u到v权值为0的边;\)
一共五种情况。

强连通分量(SCC)与连通性

拓扑排序

非常重要的算法,通常用于在DAG上dp。(其实dp的本质就是DAG啊)

inline void tpsort(){
	int l = 1,r = 0;
	for( ri i = 1 ; i <= ndnum ; ++i )
	  if( !rd[ i ] )
	    Q[ ++r ] = i;
	while( r >= l ){
		int u = Q[ r-- ];
		last = u;
		for( ri i = head2[ u ] ; i ; i = e2[ i ].next ){
			int v = e2[ i ].to;
			--rd[ v ];
			if( !rd[ v ] )
			  Q[ ++r ] = v;
		}
	}
	printf( "%d\n" , size[ last ] );
}

tarjan算法

这是用于找SCC的模板

void tarjan( int x ){
	q.push( x );vf[ x ] = low[ x ] = ++cnt;est[ x ] = true;
    for( register int i = head[ x ] ; i ; i = e[ i ].next ){
    	int v = e[ i ].to;
    	if( !vf[ v ] ){tarjan( v );low[ x ] = min( low[ x ] , low[ v ] );}
    	else if( est[ v ] ) low[ x ] = min( low[ x  ] , vf[ v ] ); 
    }
    if( vf[ x ] == low[ x ] ){
    	int v;tot++;
    	while( v = q.top() ){
    		bel[ v ] = tot;np[ tot ] += w[ v ];
    		est[ v ] = false;q.pop();
    		if( v == x ) break;
    	}
    }
}

tarjan算法与图的连通性有着密不可分的联系,它的用处非常多。

缩点

有向图强连通分量:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。

缩点就是指把SCC合并成一个点。

很多题在有环的情况下非常难写,但是如果能把它变成有向无环图就很好做了,一般就是tarjan缩点+拓扑排序解决。

【模板】缩点

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<stack>
using namespace std;
#define oo 0x3f3f3f3f
#define N 10010
typedef long long ll;
int ans = 0,n,m,tot = 0,cnt= 0,vf[ N ],low[ N ],w[ N ],head[ N ],nhead[ N ],bel[ N ],np[ N ],g[ N ];
bool est[ N ];
stack<int> q;

struct Edge{
	int to,next,from;
}e[ N * 10 ],ne[ N * 10 ];

template<class T>
inline void read(T &res){
	static char ch;T flag = 1;
	while( ( ch = getchar() ) < '0' || ch > '9' )
	  if( ch == '-' ) flag = -1;
	res = ch - 48;
	while( ( ch = getchar() ) >= '0' && ch <= '9' )
	  res = res * 10 + ch - 48;
	res *= flag;
}

inline void add( int from , int to ){
	e[ ++tot ].to = to;
	e[ tot ].next = head[ from ];
	e[ tot ].from = from;
	head[ from ] = tot;
}

inline void nadd( int from , int to ){
	ne[ ++tot ].to = to;
	ne[ tot ].next = nhead[ from ];
	ne[ tot ].from = from;
	nhead[ from ] = tot;
}

void tarjan( int x ){
	q.push( x );vf[ x ] = low[ x ] = ++cnt;est[ x ] = true;
    for( register int i = head[ x ] ; i ; i = e[ i ].next ){
    	int v = e[ i ].to;
    	if( !vf[ v ] ){tarjan( v );low[ x ] = min( low[ x ] , low[ v ] );}//没被访问 
    	else if( est[ v ] ) low[ x ] = min( low[ x  ] , vf[ v ] ); //在栈中
    }
    if( vf[ x ] == low[ x ] ){
    	int v;tot++;
    	while( v = q.top() ){
    		bel[ v ] = tot;np[ tot ] += w[ v ];//缩点 
//    		printf( "%d " , v );
    		est[ v ] = false;q.pop();
    		if( v == x ) break;
    	}
//    	printf( "\n" );
    }//弹栈 
}

void search( int x ){
    if( g[ x ] ) return;
    g[ x ] = np[ x ];
    int maxn = 0;
    for( int i = nhead[ x ] ; i ; i = ne[ i ].next ){
        if( !g[ ne[ i ].to ] ) search( ne[ i ].to );
        maxn = max( maxn , g[ ne[ i ].to ] );
    }
    g[ x ] += maxn;
}//记忆化搜索 

int main()
{
	read( n );read( m );
	for( register int i = 1 ; i <= n ; i++ ) read( w[ i ] );
	//读入点权 
	for( register int i = 1 ; i <= m ; i++ ){
		int x,y;read( x );read( y );
		add( x , y );
	}tot = 0;//建边
	for( register int i = 1 ; i <= n ; i++ )
	  if( !vf[ i ] ) tarjan( i );//tarjan找强连通分量 
	for( register int i = 1 ; i <= m ; i++ ){//枚举每条边 
		int x = e[ i ].from,y = e[ i ].to;
		if( bel[ x ] == bel[ y ] ) continue;
		nadd( bel[ x ] , bel[ y ] );//建新图
	}
//	for( register int i = 1 ; i <= tot ; i++ )
//	  printf( "%d " , np[ i ] );
    for( register int i = 1 ; i <= tot ; i++ )
      if( !g[ i ] ){
      	  search( i );
      	  ans = max( ans , g[ i ] );
      }
    printf( "%d" , ans );
	return 0;
}

例题

[HAOI2006]受欢迎的牛

因为关系可以传递,所以考虑缩点把关系合并,然后关系网变成了一张DAG,最后只需要看DAG中是不是只有一个没有出度的SCC。

#include<cstdio>
#include<algorithm>
using namespace std;
#define ri register int 
const int N = 50007,M = 10007;
int n,m,tot,top,ndnum,last = 0,head[M],dfn[M],low[M],q[M],pos[M],head2[M],rd[M],size[M],Q[M],cd[M];
bool IN[M],vis[M];
struct Edge{
	int from,to,next;
}e[N],e2[N];

inline void add( int from , int to );

inline void add2( int from , int to );

template<class T>inline void read(T &res);

void tarjan( int u );

inline void tpsort();

int main()
{
    read( n );read( m );
    for( ri i = 1 ; i <= m ; ++i ){
    	int u,v;
    	read( u );read( v );
    	add( u , v );
    }
    tot = 0;
    for( ri i = 1 ; i <= n ; ++i )
      if( !dfn[ i ] )
        tarjan( i );
    tot = 0;
    for( ri i = 1 ; i <= m ; ++i ){
    	int u = e[ i ].from,v = e[ i ].to;
    	if( pos[ u ] != pos[ v ] )
    	  add2( pos[ u ] , pos[ v ] );
    }
    int c = 0;
    for( ri i = 1 ; i <= ndnum ; ++i )
      if( !cd[ i ] )
        ++c;
    if( c > 1 ){
    	printf( "0\n" );
    	return 0;
    }
    tot = 0;
    tpsort();
	return 0;
}

inline void add( int from , int to ){
	e[ ++tot ].to = to;
	e[ tot ].from = from;
	e[ tot ].next = head[ from ];
	head[ from ] = tot;
}

inline void add2( int from , int to ){
	e2[ ++tot ].to = to;
	e2[ tot ].next = head2[ from ];
	head2[ from ] = tot;
	++rd[ to ];
	++cd[ from ];
}

void tarjan( int u ){
	low[ u ] = dfn[ u ] = ++tot;
	q[ ++top ] = u;
	IN[ u ] = true;
	for( ri i = head[ u ] ; i ; i = e[ i ].next ){
		int v = e[ i ].to;
		if( !dfn[ v ] ){
			tarjan( v );
			low[ u ] = min( low[ u ] , low[ v ] );
		}
		else if( IN[ v ] )
		  low[ u ] = min( low[ u ] , dfn[ v ] );
	}
	if( low[ u ] == dfn[ u ] ){
		++ndnum;
		while( top >= 1 ){
			++size[ ndnum ];
			pos[ q[ top ] ] = ndnum;
			IN[ q[ top ] ] = false;
			if( q[ top-- ] == u )
			  break;
		}
	}
}

template<class T>inline void read(T &res){
	static char ch;T flag = 1;
	while( ( ch = getchar() ) < '0' || ch > '9' ) if( ch == '-' ) flag = -1;
    res = ch - 48;
	while( ( ch = getchar() ) >= '0' && ch <= '9' ) res = res * 10 + ch - 48;
    res *= flag;
}

inline void tpsort(){
	int l = 1,r = 0;
	for( ri i = 1 ; i <= ndnum ; ++i )
	  if( !rd[ i ] )
	    Q[ ++r ] = i;
	while( r >= l ){
		int u = Q[ r-- ];
		last = u;
		for( ri i = head2[ u ] ; i ; i = e2[ i ].next ){
			int v = e2[ i ].to;
			--rd[ v ];
			if( !rd[ v ] )
			  Q[ ++r ] = v;
		}
	}
	printf( "%d\n" , size[ last ] );
}

割点

在一个无向图中,如果有一个顶点集合,删除这个顶点集合以及这个集合中所有顶点相关联的边以后,图的连通分量增多,就称这个点集为割点集合。
如果某个割点集合只含有一个顶点X(也即{X}是一个割点集合),那么X称为一个割点。

tarjan算法同样可以求出无向图中的割点

inline void tarjan( int u , int root ){
    int son = 0;
    dfn[ u ] = low[ u ] = ++tot;
    for( ri i = head[ u ] ; i ; i = e[ i ].next ){
        int v = e[ i ].to;
        if( !dfn[ v ] ){
            tarjan( v , root );
            low[ u ] = min( low[ u ] , low[ v ] );
            if( root == u ) ++son;//记录根结点的儿子 
            if( low[ v ] >= dfn[ u ] && u != root )
              cp[ u ] = true;//当v的追溯值大于u时,u就是割点 
        }
        else low[ u ] = min( low[ u ] , dfn[ v ] );
    }
    if( u == root && son >= 2 )
      cp[ u ] = true;
}

DAG的必经点

考到了一次这个知识点,思路是把正反图都建出来,然后对其进行拓扑排序,算出起点和终点到达每个点的路径条数\(fs_i\)\(ft_i\),如果\(fs_i*ft_i=fs_t\)那么根据乘法原理,\(i\)就是从起点到终点的必经点。

二分图

二分图又称作二部图,是图论中的一种特殊模型。 设\(G=(V,E)\)是一个无向图,如果顶点\(V\)可分割为两个互不相交的子集\((A,B)\),并且图中的每条边\((i,j)\)所关联的两个顶点 \(i\)\(j\) 分别属于这两个不同的顶点集\((i in A,j in B)\),则称图\(G\)为一个二分图。

简而言之,就是顶点集\(V\)可分割为两个互不相交的子集,并且图中每条边依附的两个顶点都分属于这两个互不相交的子集,两个子集内的顶点不相邻。

匈牙利算法(增广路算法)

bool dfs( int x , int timer ){
    if( vis[ x ] == timer ) return false;//有环 
    vis[ x ] = timer;
    for( ri j = head[ x ] ; j ; j = e[ j ].next )
      if( !match[ e[ j ].to ] || dfs( match[ e[ j ].to ] , timer ) ){
      	  match[ e[ j ].to ] = x;
      	  return true;
      }
    return false;
}

二分图匹配的模型有两个要素:
1.结点能分成两个独立的集合,每个集合内部有0条边。
2.每个节点只能与一条匹配边相连。

解题时,我们就要分析问题,找到两个要素,建立合适的图,然后用匈牙利算法解决。

例题

小QQ是一个非常聪明的孩子,除了国际象棋,他还很喜欢玩一个电脑益智游戏――矩阵游戏。矩阵游戏在一个 N×N 黑白方阵进行(如同国际象棋一般,只是颜色是随意的)。每次可以对该矩阵进行两种操作:

行交换操作:选择矩阵的任意两行,交换这两行(即交换对应格子的颜色)

列交换操作:选择矩阵的任意两列,交换这两列(即交换对应格子的颜色)

游戏的目标,即通过若干次操作,使得方阵的主对角线(左上角到右下角的连线)上的格子均为黑色。

对于某些关卡,小QQ百思不得其解,以致他开始怀疑这些关卡是不是根本就是无解的!于是小QQ决定写一个程序来判断这些关卡是否有解。

分析发现,一个点可以将无论怎么移动,一个点都只能把一行一列联系起来,所以把每个点所在的行和列建边,然后跑匈牙利算法,只要每一行都可以找到一条匹配边就可以。

例题

lxhgww最近迷上了一款游戏,在游戏里,他拥有很多的装备,每种装备都有2个属性,这些属性的值用[1,10000]之间的数表示。当他使用某种装备时,他只能使用该装备的某一个属性。并且每种装备最多只能使用一次。游戏进行到最后,lxhgww遇到了终极boss,这个终极boss很奇怪,攻击他的装备所使用的属性值必须从1开始连续递增地攻击,才能对boss产生伤害。也就是说一开始的时候,lxhgww只能使用某个属性值为1的装备攻击boss,然后只能使用某个属性值为2的装备攻击boss,然后只能使用某个属性值为3的装备攻击boss……以此类推。现在lxhgww想知道他最多能连续攻击boss多少次?

第一眼看到这个题想到的是将某个物品的两个属性分成左右部点,但是很难解决本题,尤其是在处理一个物品只能用一种属性的时候。所以我们不妨换一种思路,对于物品 i 的属性 a,b ,分别从 a 和 b 向 i 连一条有向边。将物品的属性当做左部点,编号当做右部点,求最大匹配即可。

这样为什么是正确的呢?我们可以考虑匈牙利算法的具体过程:在匹配值为 i 的技能时,那么 1 到 i-1 的属性肯定已经匹配完成,所以如果 i 对应的编号 j 被匹配了的话,那么就让匹配 j 的那个属性 p 再去找别的物品标号匹配,形象地说,就是用别的物品来释放攻击力为 p 的这个技能,用 j 这个物品释放攻击力为 i 的技能。如果找到这样一条增广路,那么就说明当前可以匹配,ans++。

总结

这两天考了图论,我发现了自己知识上的一些漏洞,也趁这几天补了一些坑,把图论的知识简单地写了一个总结。

第一套

四道题拿了260分,分数还算看得过去,但发现对于高级一点的知识就不是很熟悉。
T1很快发现是一道差分约束的模板题,切掉了。
T2是一道带权并查集的模板题,幸好以前写过几次,印象还比较深,于是花了20分钟写,20分钟查错,感觉不会错就去做下一道题了。
T3看了一会,竟然没怎么读懂题,于是先去做了下一题,最后30分钟才读懂,发现是个线段树优化建边的题,但因为不会所以只打了暴力分。
T4让我很困惑,开始以为是数学题,拿了30的暴力分。听了评讲才发现不是那么难,我自己还写了一个独特一点的dp做法,非常好写,可惜当时脑子没放开,考图论专题,怎么也想不到这是一道dp题,还是我自己没有学得那么通透。
收题时我竟然把目录写错了,给我了一个大教训,以后一定要认真检查。

第二套

这套题明显要难写一些。
开始把比较擅长的二分图的T2写了,然后看到T4可做,花了2h才敲完,缩点的题都是码量巨大啊。虽然写了,最后还是被题目的自环给坑死了,一分也没拿到。最后感觉T1可以二分,但细节没处理好,也没拿到分。至于T3是什么神仙人工智能算法我现在也没搞懂。
启发我还是要读好题,注意毒瘤数据。

第三套

一来又切掉了二分图的T3,然后T2、T4没有思路,就企图打表找规律把T1数学题打出来,找了2h规律也没找到,最后只有100+30。今天把T2的分层图的思想搞懂了,T1也自己手推出了通项公式。

图论就此结束了,自己薄弱的地方就是细节的处理,经常跳到题目的坑里,以后有时间还要把高级一点的图论知识搞懂。

posted @ 2019-10-12 08:22  YZhe  阅读(358)  评论(0编辑  收藏  举报
ヾ(≧O≦)〃嗷~