zoukankan      html  css  js  c++  java
  • 图论笔记

    图论笔记

    图的定义

    图G是一个有序二元组(V,E),其中V称为点集,E称为边集

    无向图、有向图:

    无向图:所有边都是无向边

    有向图:存在一条边是有向的图

    (上文为简单理解)

    图的概念

    度:一个顶点的度指的是与该顶点相关联的边的边的条数,顶点v的度称为d(v).

    入度、出度:对于有向图来说,一个顶点的度可以细分为入度和出度。一个顶点的入度指的是与其相关联的各边中,以其为终点的边数;出度则是相对的概念,指以该顶点为起点的边数。

    自环:如果一条边的两个顶点为同一顶点,那么这个边就称为自环

    路径:在一个图上遍历,所遍历的点叫路径

    环:如果路径的出发点和路径的结束点一样,那么这个路径就叫做环

    简单路径:每个点最多只走一次的路径,在简单路径中,点是不可以重复的

    简单环:去掉起点之后,是一个简单路径,这样的环叫做简单环

    特殊的图

    没有环的无向图(无环无向连通图):

    n个点的树有n-1条边

    --->无环无向图:森林

    --->无环连通图:外向树、内向树

    外向树:树上的的所有边的方向都是向叶子节点指的

    内向树:树上的所有边的方向都是向根节点指的

    树的拓展:

    章鱼图:中间只有一个环,周边伸展出去都是一棵树的图,又称“基环树”

    怎么把这个图变成一棵树呢?

    将环断掉,删去环上的任意一条边就可以变成一棵树了。

    如何把一棵树变成一个章鱼图呢?

    随便加一条边即可。

    章鱼图Dp的做法:两个DP,一个树形DP,一个环形DP,先树形Dp,再环形DP

    如何在章鱼图上找环?

    深度优先搜索,一直,直到走到了它的某一个祖先,这样它和它的祖先之间就形成了一个环

    仙人掌图:边仙人掌和点仙人掌

    边仙人掌:有很多个环,每个环对应树的一个边

    点仙人掌:有很多个环,每个环对应树的一个节点

    DAG:有向无环图(多半是一个Dp的题目)

    二分图:如果一个图有奇环,是二分图,如果没有奇环,那一定是二分图

    如果两个相邻的两个点通过染不同的颜色,只有一种染色方法,那么这就是二分图

    图的储存方法

    邻接矩阵:

    好处:非常非常快,判断边的存在只需要O(1)的时间

    坏处:空间过大

    边表:

    用链表来存储所有的边

    struct edge
    {
    	int e,next;//next代表下一条边的边号是多少 、e表示终点 
    }ed[maxm];
    
    int en,first[maxn];//en代表边的数量 first从i出发的第一个边的编号 
    
    void add_edge(int s,int e)
    {
    	en++;
    	ed[en].next=first[s];
    	first[s]=en;
    	ed[en].e=e;
    }
    
    for(int p=first[1];p!=0;p=ed[p].next)
    ed[p].e; 
    

    最短路问题

    单源最短路径:求一点到所有点的最短路

    多源最短路径:求多点到所有点的最短路

    FLOYD

    for(int i=1;i<=n;i++)
    {
    	for(int j=1;j<=n;j++)
    	{
    		for(int k=1;k<=n;k++)
    		f[j][k]=min(f[j][k],f[j][i]+f[i][k]); 
    	}
    }
    

    Dijkstra

    void dijkstra()
    {
    	memset(dist,0x3f,sizeof(dist));
    	dist[s]=0;
    	for(inr i=1;i<=n;i++)
    	{
    		for(int j=1;j<=n;j++)
    		if(!right[j]&&(p==-1||dist[p]>dist[j])) p=j;
    		right[p]=true;
    		for(int j=first[p];j;j=ed[j].next)
           dist[ed[j].e]=min(dist[ed[j].e],dist[p]+ed[j].d);
    	}
    }
    

    堆优化

    //dijkstra 的堆优化 
    #include <queue>
    using namespace std;
    struct point{
    	int p,d;
    	point(){p=d=0;}
    	point(int a,int b){p=a;d=b;}
    };
    bool operator<(const point &a,const point &b)
    {
    	retuern a.d>b.d;
    }
    priority_queue<point> heap;
    
    void dijkstra(int s)
    {
    	memset(dist,0x3f,sizeof(dist));
    	dist[s]=0;
    	for(int i=1;i<=n;i++)
    	heap.push(point(i,dist[i]));
    	for(int i=1;i<=n;i++)
    	{
    		while(right[heap.top().p])
    		heap.pop();
    		point now=heap.top();
    		heap.pop();
    		int p=now.p,d=now.d;
    		right[p]=true;
    		for(int j=first[p];j!=0;j=ed[j].next)
    		{
    			int e=ed[j].e;
    			int d=dist[p]+ed[j].d;
    			if(dist[e]>d)
    			{
    				dist[e]=d;
    				heap.push(point(e,d));
    			}
    		}
    	}
    }
    

    Bellman-Ford

    for(int i=1;i<=m;i++)
    {
    	cin>>s[i]>>e[i]>>d[i];
    }
    memset(dist,0x3f,sizeof(dist));
    dist[1]=0;
    for(int i=1;i<n;i++)
    for(int j=1;j<=m;j++)
    dist[e[j]]=min(dist[e[j]],dist[s[j]]);
    

    SPFA

    void spfa(int s)
    {
    	memset(dist,0x3f,sizeof(dist));
    	dist[s]=0;
    	queue<int> q;
    	q.push(s);
    	while(q.size())
    	{
    		now=q.top;
    		q.pop();
    		
    		for(int i=first[now];i;i=ed[i].next)
    		{
    			int e=ed[i].e;
    			int d=dist[now]+ed[i].d;
    			if(dist[e]>d)
    			{
    				dist[e]=d;
    				if(!=inque[e])
    				inque[e]=true,q.push(e);  
    			}
    		}
    	}
    }
    

    负环的判定

    1.最短路经过边数不能大于n-1

    2. SPFA 任何一个点入队次数不能超过n次

    差分约束

    给定n个变量和m个不等式

    (x_i-x_j leq a_k)

    (x_n-x_1)的最大值

    解法:

    (x_i-x_j leq a ,x_j-x_k leq b ightarrow x_i-x_k le a+b)

    记录(y_i)表示到第i个点的最短路

    则有

    (y_i le y_j+a,y_j le y_k+b ightarrow y_i le y_k +a+b)

    最大值对应最短路,最小值对应最长路

    树相关

    树的储存方式:当做一个无向图去存

    树最上面的节点叫根,最下面的节点叫做叶子

    最近公共祖先问题(LCA):

    给你两个点,这两个点都有他们的祖先,他们的公共祖先是这两个点祖先的交集,所有公共祖先最靠下的一点叫做最近公共祖先。

    作用

    确定树上一条路径

    LCA的求法

    暴力

    两个点往上跳,找出各自的祖先,得到它们最深的一个交点就是最近公共祖先

    如果两个点深度相等

    可以倍增跳;

    ----->倍增求LCA

    倍增求LCA

    设立状态

    F i j 表示i这个点向上跳(2^j)步能到的点

    可以看做先跳(2^{j-1}),再跳(2^{j-1})步,这样这个问题就可以解决了

    F i j=F [F i j-1] j-1

    步骤:

    1.调整深度

    2.一起跳

    //预处理 
    void dfs(int now)
    {
    	for(int p=first[now];i;i=ed[i].next)
    	if(ed[i].e!=f[now][0])
    	{
    		int p=ed[i].e;
    		depth[p]=depth[now]+1; 
    		int f[p][0]=now;
    		for(int x=1;x<=18;x++)
    		f[p][x]=f[f[p][x-1]][x-1];
    		dfs(p);
    	}
    }
    int get_lca(int p1,int p2)
    {
    	if(depth[p1]<depth[p2])
    	for(int x=18;x>=0;x--) 
    	{
    		int p=f[p1][x];
    		if(depth[p]>=depth[p2]) p1=p;
    	}
    	if(p1==p2) return p1;
    	for(int x=18;x>=0;x--)
    	if(f[p1][x]!=f[p2][x])
    	p1=f[p1][x],p2[p2][x];
    	return f[p1][0];
    }
    

    树的序列化

    dfs序

    按照深度优先搜索的顺序遍历的点所组成的序列叫做dfs序

    预处理dfs序

    void dfs(int now,int f)
    {
    	q[l[now]]=now;//q表示dfs序列 
    	int x=l[now]+1;
    	size[now]=1;
    	for(int i=first[now];i;i=ed[i].next )
    	if(ed[i].e!=f)
    	{
    		l[e->e]=x;
    		dfs(ed[i].e);
    		x=r[ed[i].e]+1;
    		dfs(ed[i].e);
    		size[now]+=size[ed[i].e];
    	}
    	r[now]=l[now]+size[now]-1;
    }
    
    l[1]=1,r[1]=n;
    

    bfs求dfs序

    int front=1,tail=1;
    q[1]=1;
    for( ;front<=tail;)
    {
    	int now=q[front++];
    	for(int i=first[now];i;i=ed[i].next)
    	if(!vis[ed[i].e])
    	{
    		q[++tail]=ed[i].e;
    		vis[ed[i].e]=true;
    	}
    }
    for(int i=n;i>=1;i--)
    {
    	int now=q[i];
    	size[now]=1;
    	for(int i=first[now];i;i=ed[i].next )
    	if(size[ed[i].e])
    	size[now]+=size[ed[i].e];
    }
    l[1]=1;r[1]=n;
    for(int i=1;i<=n;i++)
    {
    	int now=q[i];
    	int x=l[now]+1;
    	for(int i=first[now];i;i=ed[i].next)
    	if(size[ed[i].e]<size[now])
    	{
    		l[ed[i].e]=x;
    		r[ed[i].e]=l[ed[i].e]+size[ed[i].e]
    		x=r[ed[i].e]+1;
    	}
    }
    for(int i=1;i<=n;i++)
    q[l[i]]=i;
    

    括号序列

    生成树

    一个图变成一个树

    最小生成树

    找到一棵生成树,使得这些边的边权总和最小

    Prim

    void prim(int s)
    {
    	memset(dist,0x3f,sizeof(dist));
    	dist[s]=0;
    	for(int i=1;i<=n;i++)
    	{
    		int p=-1;
    		for(int j=1;j<=n;j++)
    		if(!right[j]&&(p==-1||dist[p]>dist[j])) p=j;
    		right[p]=true;
    		for(int j=first[p];j;j=ed[j].next)
    		dist[ed[j].e]=min(dist[ed[j].e],ed[j].d);
    	}
    }
    

    优化和Dijkstra 非常相似

    Kruscal

    使用并查集来做

    求次小生成树

    可以由最小生成树替换一条边得到

    //并查集求最大链
    int get_max(int p1,int p2)
    {
    	p1=get_f(p1);
    	p2=get_f(p2);
    	int ans=-23333;
    	while(p1!=p2)
    	{
    		if(depth[p1]<depth[p2])
    		swap(p1,p2);
    		ans=max(ans,fd[p1]);//fd代表到父亲的长度
    		merge(p1,f[p1]);
    		p1=get_f(p1);
    	}
    	return ans; 
    }
    

    连通分量

    如果一个子图,从其中任意一个点出发,可以到达这个图的任意一个点,这个连通分量就叫做强连通分量

    求强连通分量

    Tarjan求强连通分量

    void dfs(int now)
    {
    	t++;//找到了一个新的点 
    	dfn[now]=low[now]=t;//dfn代表dfs的顺序,low表示从i点出发,可以向下走或者走回边
    	//能走到的所有点中dfn值最小是多少 ,赋值为t是因为它最先走到它自己 
    	s[++size]=now;//储存当前点的祖先或在环里面且为确定为连通分量的点 
    	instack[now]=true;
    	for(int i=first[now];i;i=ed[i].next)
    	{
    		int e=ed[i].e;
    		if(!dfn[e])
    		{
    			dfs(e);
    			low[now]=min(low[now],low[e]);//e能走到的所有点now也可以走到 
    		}
    		else
    		{
    			if(instack[e])
    			low[now]=min(low[now],dfn[e]);
    			//还没有从栈里面弹出来,所以他的祖先在栈里面,这是一条回边 
    		}
    	 }
    	 if(dfn[now]==low[now])//如果自己dfn值和low值一样,要取出这个点了 
    	 {
    	 	cnt++;
    	 	belong[now]=cnt;//now 这个节点属于新的连通分量 
    	 	while(s[size]!=now)//在栈now后面的都属于和now的同一个强连通分量 
    	 	{
    	 		belong[s[size]]=now;
    	 		instack[s[size]]=false;
    	 		size--;
    		 }
    		 size--;
    		 instack[now]=false;
    	 }
    }
    

    作用

    缩点,然后变成有向无环图(DAG),然后做dp,点权和边权规定到相应的强连通分量中

    for(int i=1;i<=n;i++)
    if(!dfn[i]) dfs(i);
    
    for(int i=1;i<=n;i++)
    	for(int j=first[i];j;j=ed[j].next)
    	if(belong[i]!=belong[ed[j].e])
    	add_edge2(belong[i],belong[ed[j].e]);
    

    拓扑排序

    目的:使转移始终从左向右进行

    方法:每次找到一个入度为0的点,然后删去,不断重复这一过程,被删去点的先后顺序即为拓扑排序的顺序

    int size=0;
    for(int i=1;i<=n;i++)
    if(!in[i])
    {
    	size++;
    	q[size]=i;
    }
    
    for(int i=1;i<=n;i++)
    {
    	int now=q[i];
    	for(int j=first[now];j;j=ed[j].next)
    	{
    		int e=ed[j].e;
    		in[e]--;
    		if(!in[e])
    		{
    			size++;
    			q[size]=e;
    		} 
    	}
    }
    

    2 SAT问题(判定性问题)

    无向图中的问题

    桥:在无向图中,删掉这一条边,使这个无向图不连通,那么这一条边叫做桥

    割点:删掉这个点,使这个图不连通,那么这个点叫做割点

    边双连通分量:这个子图尽量大,且这个图里面没有桥

    点双连通分量:这个子图尽量大,且这个图里面啊没有割点

    如果一个图里面有k个桥,那么这个图里面有k+1个边双连通分量

    边双连通分量,缩点后可以看成一个树

    求边双连通分量

    dfn[2333],low[2333];//和tarjan的相同 
    void dfs(int now,int from)//from指的是当前从那一条边走过来的 
    {
    	t++;
    	dfn[now]=low[now]=t;
    	vis[now]=true;
    	for(int i=first[now];i;i=ed[i].next)//一条无向边会被拆为两条有向边形成死循环 
    	if((i^1)!=from)
    	{
    		int e=ed[i].e;
    		if(!dfb[e])
    		{
    			dfs(e,i);
    			low[now]=min(low[now],low[e]);
    		}
    		else
    		{
    			if(vis[e])
    			low[now]=min(low[now],dfn[e]);
    		}
    	}
    	vis[e]=false;
    }
    

    二分图问题

    回顾二分图:左边有n个点,右边有m个点,左边内部没有边,右边内部也没有

    匹配:如果说左边的i和右边的j连接了一条边,那么这两个点可以匹配,每个点最多只能和一个点匹配

    匈牙利算法求最大二分图匹配

    int n,m;//左边有n个人,右边有m个人 
    
    bool dfs(int i)//让左边第i个人尝试匹配,看是否成功 
    {
    	for(int j=1;j<=m;j++)
    	if(match[i][j]&&!use[j])
    	{
    		use[j]=true;
    		if(!result[j]||dfs(result[j]))//result[i]表示右边的i和左边的某个数匹配 
    		{
    			result[j]=i;
    			return true;
    		}
    	}
    	return false;
    }
    int xiongyali()
    {
    	int ans=0;
    	for(int i=1;i<=n;i++)
    	{
    		memset(use,false,sizeof(use));
    		if(dfs(i)) ans++;//匹配成功就把答案加一 
    	}
    	return ans;
    } 
    

    最大独立集

    选择尽量多的点,使这几个点之间都没有边相连,这就是最大独立集

    最大团

    从一个图中选择尽量多的点,每个点之间都必须连边,这就是最大团

    最大团与最大独立集的关系

    最大团等于补图的最大独立集

    它们都是NPC的问题

    在二分图上我们可以求最大独立集

    最大独立集( ightarrow)二分图

    最大团( ightarrow)二分图的补图

    最大独立集和最大匹配之和等于点数

    类似于最大流等于最小割

    本博文为wweiyi原创,若想转载请联系作者,qq:2844938982
  • 相关阅读:
    使用layer.tips实现鼠标悬浮时触发事件提示消息实现
    鼠标移入、移出触发事件实现
    vue组件独享守卫钩子函数参数详解(beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave)
    vscode 向下复制当前行(即visual studio 中的Ctrl + D)功能快捷键
    vue UI可视化窗口修改为显示中文
    js中的匿名函数
    NPM install -save 和 -save-dev 傻傻分不清
    Node.js中package.json中库的版本号详解(^和~区别)
    RTX管理器里怎么建群
    Mysql的timestamp(时间戳)详解以及2038问题的解决方案
  • 原文地址:https://www.cnblogs.com/wweiyi2004/p/14469759.html
Copyright © 2011-2022 走看看