NOIP 图论[ZHX]

\(G\) 是一个有序二元组 \((V,E)\),其中 \(V\) 成为点集(\(Vertices\) \(Set\)),\(E\) 称为边集(\(Edges\) \(set\))。

  • 有向边、无向边

    如果边有方向,那么得到的图称为有向图。在有向图中,与一个节点相关联的有出边和入边之分。

    相反,边没有方向的图称为无向图,即所有边都是无向边的图称为无向图。

  • 度(\(Degree\)

    一个顶点的度是指与该顶点相关联的边的条数,顶点 \(v\) 的度数记作 \(d_v\)

  • 入度(\(In-degree\))和出度(\(Out-degree\)

    对于有向图来说,以该点为终点的边的数量为入度,以该点为起点的边的数量为出度。

  • 自环(\(Loop\)

    一条边的起点和终点为同一顶点。

  • 路径(\(Path\)

    从任意一点出发,在图上走过的过程的序列,称为路径。

    简单路径:每个点最多走了一次的路径,即点不能够重复。

  • 环(\(Ring\)

    出发点和结束点一样的路径称为环。

    简单环:去掉起点后,会变成简单路径的环。

特殊的图

  • 树(\(Tree\)

    无环无向的连通图,\(n\) 个点的树有 \(n-1\) 条边。

    • 森林

      无环无向图。

    • 有向图的树

      外向树:边从根指向叶子。

      内向树:边从叶子指向根。

    • 章鱼图/基环树

      只有一个环的无向连通图,环上的点伸展出去为一棵树。\(n\)\(n\) 边,删掉环上的任意一条边就会变成树(同理,随便加一条边即可使树变为章鱼图)。

      树形 \(DP\) \(+\) 环形 \(DP\)

    • 仙人掌图

      把树的每一个点变成一个环,环叫做边仙人掌的叶片。

      边仙人掌:用边连接不同的环。

      点仙人掌:用点连接不同的环。

      缩点之后的树进行树形 \(DP\),环进行环形 \(DP\)

    • \(DAG\)\(Directed\) \(Acyclic\) \(Graph\)

      有向无环图。

      \(DP\) 的状态看做点,转移看成边,所有的 \(DP\) 都是一个 \(DAG\)

    • 二分图

      把图上的点分为左右两部分,所有的边都是由左边/右边连向右边/左边。

      树是二分图,把深度为奇数的点放在左边,深度为偶数的点放在右边。

      有奇环的图一定不是二分图,没有奇环的图一定是二分图。

      判断二分图用 \(DFS\)\(BFS\) 染色的方法。

图的存储方法

邻接矩阵

开一个 \(n \times m\) 大小的数组 \(a\)\(a_{i,j}\) 表示从 \(i\) 号点到 \(j\) 号点的边的长度。

好处:速度快,好写。

坏处:空间过大,没有办法处理重边。

边表(链式前向星)

对于每个点建立一个链表,共 \(n\) 个链表,把从同一个点出发的点串在一起,存着从这个点出发的所有边。

本质:用 \(n\) 个链表存储所有的边。

struct edge {
	int e;//当前边终点
	int nxt;//下一条边的编号 
}ed[MAXN];

int cnt;
int fir[MAXN];//每个链表的第一条边的编号 

void add_edge(int s,int e){
	ed[++cnt].nxt=fir[s];
	fir[s]=cnt;
	ed[cnt].e=e;
}

for(int s=1;s<=n;s++)
	for(int i=fir[s];i!=0;i=ed[i].nxt)
		ed[i].e;//s -> e 

最短路

多源最短路:\(Floyd\)
单源最短路:
无负边权:\(Dijkstra + Heap\)
有负边权:\(SPFA\)

\(Floyd\)

\(dist_{i,j,k}\) 代表从 \(j\) 走到 \(k\) 且走过的点的编号都 \(\leq i\) 的最短路。最后求出 \(dist_{n,j,k}\) 来作为我们的答案。

如果 \(j\)\(k\) 有边,\(dist_{0,j,k}=d_{j,k}\)
如果 \(j\)\(k\) 无边,\(dist_{0,j,k}=∞\)

\(dist_{i,j,k}=min(dist_{i-1,j,k},dist_{i-1,j,i}+dist_{i-1,i,k})\)

因为每个 \(i\) 都可以从 \(i-1\) 推来,一旦把 \(i\) 算出后,\(i-1\) 已经没用了,根据滚动数组的思想,我们就可以把这一维度删掉,以达到空间为 \(n^2\) 级别。

memset(dist,0x3f,sizeof(dist));//最短路赋值为无穷大
for(int i=1;i<=n;i++)dist[i][i]=0;
for(int i=1;i<=n;i++)
	for(int j=1;j<=n;j++)
		for(int k=1;k<=n;k++)
			dist[j][k]=std::min(dist[j][k]/*等于 i*/,dist[j][i]+dist[i][k]/*小于 i*/);

\(Dijkstra\)

限制:边的权值必须都是正数。

每次选取 dist 值最小的值,也就是选取已经求出最短路的点(因为边的权值都是正数,所以它当前是最小值,那后面也不会有),然后对其进行松弛操作(用自己的最短路去更新其他点的最短路)。

bool done[MAXN];//是否已经求出最短路
void Dijkstra(int s){//计算 s 到其他所有点的最短路
	memset(dist,0x3f,sizeof(dist));
	dist[s]=0;

	for(int i=1;i<=N;i++){
		//找还没有求出最短的 dist 值最小的那个点
		int p=0;
		for(int j=1;j<=N;j++)
			if(!done[j]&&(p==0||dist[j]<dist[p]))p=j;
		done[p]=1;

		//松弛操作
		for(int j=0;j<g[p].size();j++){
			int q=g[p][j].first,d=g[p][j].second;
			//这是一条从 p 到 q 长度为 d 的边
			dist[q]=std::min(dist[q],dist[p]+d);
		}
	}
}

\(Dijkstra + Heap\)

dist 值最小的点都是一个 \(O(n)\) 的循环,维护一个堆来存储 dist 值的信息。

bool done[MAXN];//是否求过
void Dijkstra(int s){//计算 s 到其他所有点的最短路
	memset(dist,0x3f,sizeof(dist));
	dist[s]=0;
	std::priority_queue<std::pair<int,int> > heap;
	//first 为最短路的相反数,second 为点的编号
	for(int i=1;i<=N;i++)
		heap.push(std::make_pair(-dist[i],i));

	for(int i=1;i<=N;i++){
		while(done[heap.top().second])heap.pop();

		//找还没有求出最短的 dist 值最小的那个点
		int p=heap.top().second;
		heap.pop();
		done[p]=1;

		//松弛操作
		for(int j=0;j<g[p].size();j++){
			int q=g[p][j].first,d=g[p][j].second;
			//这是一条从 p 到 q 长度为 d 的边
			if(dist[q]>dist[p]+d){
				dist[q]=dist[p]+d;
				heap.push(std::make_pair(-dist[q],q));
			}
		}
	}
}

\(Bellman\)_\(ford\)

任意两点间的最短路路径上边的数量一定不会超过 \(n-1\)

证明:如果超过,则说明有一个点经过了两次,也就是有环,那么删去环的路径一定更优。

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]]=std::min(dist[e[j]],dist[s[j]]+d[j]);

\(SPFA\)

维护一个队列,表示可能改变其他点最短路的点。不断向队列中加入可能改变其他点的最短路,然后再把新的点加入队列中,直至队列为空。

//最坏 O(nm) 平均 O(km) k<20
bool inque[MAXN];//i 点是否在队列中
void SPFA(int S){//计算 s 到其他所有点的最短路
	memset(dist,0x3f,sizeof(dist));
	dist[S]=0;
	std::queue<int> q;//用来存储可能改变其他点最短路的点
	q.push(S);
	inque[S]=true;

	while(q.size()){//队列不为空
		int s=q.front();
		q.pop();
		inque[s]=false;

		for(int i=0;i<g[s].size();i++){
			int e=g[s][i].first,d=g[s][i].second;
			if(dist[e]>dist[s]+d){
				dist[e]=dist[s]+d;
				if(!inque[e])inque[e]=true,q.push(e);//有可能更新其他点
			}
		}
	}
}

应用

负环判定

负环:边权总和为负数的环。

  1. 最短路经过边数超过 \(n-1\) 条。
    最短路上存在环,因为已经是最短路,所以该环一定是负环。
  2. \(SPFA\) 一个点的入队次数超过了 \(n\)
    有至少一个点对该点有两次贡献,则代表出现负环。

差分约束

Luogu P5960 【模板】差分约束

给定 \(n\) 个变量和 \(m\) 个不等式 \(x_i - x_j \le a_k\),求 \(x_n - x_1\) 的最大值。

对于求 \(x_n - x_1\) 的最大值,我们将原式子转化为 \(x_i \le x_j + a_k\) 的形式,此时创建一条从 \(j\)\(i\) 长度为 \(a_k\) 的边,最终答案则为最短路答案。

相反,对于求最小值则转化为 \(x_i \ge x_j +a_k\) 的形式,同样建边,但是跑最长路。

  • 扩展

很多时候差分约束的条件并不是简单的小于等于号,这时候我们需要稍微做点变形。

当不等号与我们所需相反时(即求最大值时的式子为 \(x_i - x_j \ge a_k\)),让等式两边同乘 \(-1\),将等号翻转过来。

当出现 \(x_i - x_j = a_k\) 时,则可以将式子转化为 \(x_i - x_j \le a_k\)\(x_i - x_j \ge a_k\) 两个条件。

Code
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
const int MAXN=5e3+5;

int n,m;
std::vector<std::pair<int,int> > g[MAXN];
void add(int s,int e,int d){g[s].push_back({e,d});}
int dist[MAXN],tot[MAXN];
bool inq[MAXN];

bool SPFA(){
	memset(dist,0x7f,sizeof(dist));
	std::queue<int> q;
	q.push(0);
	inq[0]=1;
	dist[0]=0;

	while(q.size()){
		int s=q.front();
		q.pop();
		inq[s]=0;

		for(int i=0;i<g[s].size();i++){
			int e=g[s][i].first;
			int d=g[s][i].second;
			if(dist[e]>dist[s]+d){
				dist[e]=dist[s]+d;
				if(inq[e]==0){
					inq[e]=1;
					tot[e]++;
					if(tot[e]>n+1)return false;
					q.push(e);
				}
			}
		}
	}
	return true;
}

signed main(){
	std::ios::sync_with_stdio(false);
	std::cin.tie(0);

	std::cin>>n>>m;
	for(int i=1;i<=n;i++)add(0,i,0);
	while(m--){
		int s,e,d;
		std::cin>>e>>s>>d;
		add(s,e,d);
	}

	if(SPFA()==0)std::cout<<"NO\n";
	else for(int i=1;i<=n;i++)std::cout<<dist[i]<<" \n"[i==n];

	return 0;
}

HDU 3592 World Exhibition

一些牛站在数轴上,给定两种限制:

  1. \(i\) 头牛和第 \(j\) 头牛满足 \(|x_i-x_j| \le a_k\)
  2. \(i\) 头牛和第 \(j\) 头牛满足 \(|x_i-x_j| \ge a_k\)

求满足限定条件时的 \(1\) 号牛和 \(n\) 号牛之间的最长距离。

  • 思路

求最大距离,我们需要跑最短路,且要将原式子转化为 \(x_i \le x_j + a_k\) 的形式。对于 \(|x_i-x_j| \le a_k\),我们转化为 \(x_i \le x_j + a_k\),并从 \(j\)\(i\) 连一条长度为 \(a_k\) 的边;对于 \(|x_i-x_j| \ge a_k\),我们转化为 \(x_j \le x_i - a_k\),并从 \(i\)\(j\) 连一条长度为 \(-a_k\) 的边。

对于题目假定的条件 \(x_1 \le x_2 \le \dots \le x_n\),我们将其看为 \(x_i \le x_{i+1} + 0\),从 \(i\)\(i+1\) 连一条长度为 \(0\) 的边。

HDU 3666 THE MATRIX PROBLEM

给定一个 \(n \times n\) 的矩阵,每次可以将任意一行或任意一列乘任意一个数,问能否使所有的数都在 \([l,r]\) 内。

  • 思路

我们设置 \(2 \times n\) 个变量,\(a_i\)\(b_j\) 分别代表将第 \(i\) 行和第 \(j\) 列所乘的数,\(c_{i,j}\) 为原先的数,则有 \(l \le c_{i,j} \times a_i \times b_j \le r\)

因为差分约束需要 \(x_i - x_j \le c\) 的形式,那么我们将原式子转化为 \(\log\frac{l}{c_{i,j}} \le \log a_i - (- \log b_j) \le \log\frac{r}{c_{i,j}}\),由此建边判解即可。

【写出每个位置的约束条件 -> 取 \(\log\) 把两个变量拆开 -> 令一个变量取负数】

树上序列

\(LCA\) 问题

Luogu P3379 【模板】最近公共祖先(LCA)

暴力做法:调整深度一致 -> 同时向上跳直至到达同一个点,复杂度 \(O(n)\)

考虑优——倍增求 \(LCA\)

提前进行预处理计算 \(f\) 数组,\(f_{i,j}\) 表示从点 \(i\) 向上跳 \(2^j\) 到达的点。

初始化 \(f_{i,0} = fa[i]\),转移式子为 \(f_{i,j} = f_{f_{i,j-1},j-1}\)

void dfs(int now){
	for(int i=0;i<v[now].size();i++){
		int p=v[now][i];
		if(p!=f[now][0]){//不是父亲
			dep[p]=dep[now]+1;
			f[p][0]=now;
			for(int x=1;x<=20;x++)//枚举 2^x
				f[p][x]=f[f[p][x-1]][x-1];//2^x=2^(x-1)+2^(x-1)
			dfs(p);
		}
	}
}
posted @ 2024-02-04 17:37  CheZiHe929  阅读(26)  评论(0编辑  收藏  举报