7.17 图论 讲义

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int m;
const int n=500;
int a[501][501];
stack<int> st;
void dfs(int id){
    for(int i=1;i<=n;++i){
        if(a[id][i]){
            a[id][i]--; //删边,因为有重边,所以这里用--
            a[i][id]--;
            dfs(i);
        }
    }
    st.push(id); //最后再把这个节点存到栈里
}
int min_node=0x7fffffff; //记录最小的节点
int d[501];
int main(){
    cin>>m;
    for(int i=1;i<=m;++i){
        int s,t;
        cin>>s>>t;
        a[s][t]++; //因为有重边,所以这里用++
        a[t][s]++;
        min_node=min(s,min_node);
        min_node=min(t,min_node);
        d[s]++;d[t]++;
    }
    bool flag=true;//是否有奇点
    /**
    如果有奇点:从奇点开始
    没有:从最小的节点开始
    */
    for(int i=1;i<=n;++i){
        if(d[i]%2==1){
            dfs(i);
            flag=false;
            break;
        }
    }
    if(flag) dfs(min_node);
    while(!st.empty()){ //输出结果
        cout<<st.top()<<endl;
        st.pop();
    }
}

图论模块 链接

存图方法

二维数组存图 / 邻接矩阵

开一个二维数组来存图,假设我们有一个二维数组 bool a[][]a[i][j]==true 就表示 ij 之间是联通的

注意的是如果想建立无向边,那么需要添加 ijji 两条边

该方法示例
bool a[101][101];
int main(){
    int i,j;
    cin>>i>>j;
    a[i][j]=true; //建立一条 i 到 j 的单向边
    cin>>i>>j;
    a[i][j]=true;
    a[j][i]=true; //建立一条 i 到 j 的无向边
    cin>>i>>j;
    if(a[i][j]==true){ //判断 i 和 j 是否联通
        cout<<"Yes"<<endl;
    }
}
//存储边权可以再开一个二维数组 int b[][],其他数据同理

这个方法写起来是很简单的。但是它的缺点是占用的空间很大。比如我有一个 10000 个节点,但是只有 10000 条边的图(稀疏图),这种存图方法的空间占用太大,但是实际上二维数组里大部分地方都是空着的。

vector存图

针对上面那种稀疏图,我们可以发明一种针对边的存图方法。我们把所有边的信息都塞到一个 vector 里,动态地给它们分配内存。

对于一些只需要遍历边就可以求出结果的算法,我们可以直接这样写:

点击查看代码
struct edge{
    int from, //起始节点
        to,   //结束节点
        value;//边权
};
vector<edge> a;
int main(){
    edge m;
    cin>>m.from>>m.to>>m.value;
    a.push_back(m); //存边
    for(edge i:a){  //遍历边
        //...
    }
}

对于一些还跟节点有关的算法时,比如 dfs,我们就可以把边按照起始节点进行归类。也就是开 n 个不同的 vector,第 i 个 vector 存储所有以节点 i 为起点的边

点击查看代码
struct edge{
    int to,   //结束节点
        value;//边权
};
vector<edge> a[101];
int main(){
    int from;
    edge m;
    cin>>from>>m.to>>m.value;
    a[from].push_back(m); //存边
    for(edge i:a[1]){  //遍历以节点 1 为起点的边
        //...
    }
}

当然,这种方法由于使用了 vector 而存在空间不稳定的问题。但是比起二维数组存图,它在节点数较多的图里是值得考虑的。

链式前向星存图 / 邻接表

我们沿用上述思路,刚才我们用的是 vector 存图。那么我们能不能用一种更巧妙的东西来代替 vector?

首先,我们建立一个用来存储边的一维数组。为了像刚才一样,我们可以快速找到从某一个节点开始的所有边,所以我们建立一个 head[] 数组,head[i] 表示 “从 i 开始的第一条边的编号”

现在我们找到了从 i 开始的第一条边,那么剩下的边怎么找?

我们可以在每条边里再维护一个新的数据,它代表“符合要求的下一条边的编号”,这样我们就可以用链式的跳跃来找到所有边。

为了维护这个结构,我们每次直接把新边作为第一条边,剩下的连在第一条边后面。

简单写一下
struct edge{
    int next, //下一条边编号
        to,   //结束节点
        value;//边权
};
edge a[10001]; //存储所有边
int cnt;
int head[101];
int main(){
    cnt=0;
    int from;
    edge m;
    cin>>from>>m.to>>m.value;
    m.next=head[from]; //把当前的第一条边作为新边的下一条边
    cnt++;a[cnt]=m;    //存边
    head[from]=cnt;    //新边成为第一条边

    int s;
    cin>>s;
    int i=head[s];
    while(i!=0){  //最后一条边的 next 是 0,此时结束
        //...
        i=a[i].next;
    }
}

BFS,DFS

image

欧拉路

欧拉路:从任意一个点,走到任意一个点结束,每条边都通过,且每条边只通过一次

说白了就是一笔画

欧拉回路:起点和终点一样的欧拉路

下面说明两个结论:

  1. 是欧拉路的条件为:所有的点中,入边与出边为奇数的点没有或仅有两个
  2. 是欧拉回路的条件为:所有的点中,没有入边与出边为奇数的点

其中,欧拉路的那两个奇点就是起点和终点。

对于中间的所有点,我们肯定要从一条边进去,进去之后,因为不是终点,肯定要再出去,所以中间的点都是偶数点。那么对于起点,我们只能出去,对于终点,我们只能进来。证明出只有两个奇点

但是这并不是一个完全的条件。比如如果是一个断图,它也有可能没有奇点,但是它不是欧拉路。所以我们要做一次 dfs,判断是否能走过所有点。

也就是说:有两个奇点的连通图一定有欧拉路,无奇点的连通图一定有欧拉回路

这是很好判断的,但有些问题会让我们寻找符合条件的欧拉路。这种问题就有一定难度了。

先说结论:dfs 每个节点时在子节点遍历完最后将节点存入栈中,最后再依次输出。

比如:

image

假设我们从 1 开始遍历,那么首先我们会经过 2。这个时候走不动了,记录 2,遍历 3->4->1(为了防止往回走,我们走一条边删一条边),走不动了,记录 2 1 4 3,最后退回主节点,记录 2 1 4 3 1,依次弹出就是路径 1 3 4 1 2

image

1->3->4->1 1 4->2->5->4 1 4 5 2 4 3 1

1 3 4 2 5 4 1

这种办法为什么是对的
分两种情况讨论:
1.整个图是欧拉回路,这个时候一定是成立的。欧拉回路去掉一个小欧拉回路以后还是欧拉回路。
2.整个图是欧拉路但不是欧拉回路,这时图可以被起点分为一个欧拉回路与一个欧拉路。我们需要先遍历欧拉回路再遍历欧拉路,这样来保证是正确的。
如果欧拉回路的优先级低,程序会遍历完整张图才开始从最后一层函数返回,输出顺序是先欧拉回路再欧拉路;
如果欧拉路的遍历优先级低,则会先遍历欧拉路,当被卡住时返回,此时欧拉路上的节点都在栈的最低层,然后开始遍历欧拉回路,此时欧拉回路的节点都在栈顶部,所以输出顺序仍然是正确的欧拉回路再欧拉路。

USECO这个例题挺好的。可以自行练习。

最短路

Floyed

时间复杂度 \(n^3\)

只能用邻接矩阵存储

这个算法的本质:如果从 A 经过 C 到 B 比目前从 A 到 B 的距离更近,那么就更新从 A 到 B 的距离(这个操作我们叫松弛)

我们要做的就是枚举 A,B 和这个中间的 C。三层 for 循环

点击查看代码
int w[][];   //记录 i 到 j 的距离
//输入部分省略
for(int i=1;i<=m;++i){
	for(int j=1;j<=m;++j){
		if(w[i][j]==0){
			w[i][j]=10000000; //初始化,设为无穷大(最好不要太大,下面还有加法环节)。如果是 0 那么没法更新最短路
		}
	}
}
for(k 1-n){ //简写了,k 从 1 到 n 的意思,枚举中间点 注意一定是中间点在最外层。
	for(i 1-n){
		for(j 1-n){
			if(w[i][j]>w[i][k]+w[k][j]){
                //如果从 A 经过 C 到 B 比目前从 A 到 B 的距离更近,那么就更新从 A 到 B 的距离
				w[i][j]=w[i][k]+w[k][j];
			}
		}
	}
}

把中间点放在外层枚举是为了让 w[i][k] w[k][j] 先于 w[i][j] 被更新,可以自己模拟验证一下。

这个算法的时空复杂度可以说很大了,所以一般不太会使用。但是它写起来简单,而且能够一下求出任意两个节点间的最短路,如果在 \(n\leq 400\) 的时候可以选用(这只是个估计值)

#include<bits/stdc++.h>
using namespace std;
int bead[105][105];
int a[105]; //比 i 小的数的数量
int b[105]; //比 i 大的数的数量
int main(){
	int n,m;
	cin>>n>>m;
	int x,y;
	for(int i=1;i<=m;i++){
		cin>>x>>y;
		bead[x][y]=1;
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			for(int k=1;k<=n;k++){
				if(i!=j and j!=k and i!=k and bead[j][i] and bead[i][k]){
					bead[j][k]=1;
				}
			}
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			if(i!=j and bead[i][j]){
				a[i]++;
				b[j]++;
			}
		}
	}
	int cnt=(n+1)/2;int ans=0;
	for(int i=1;i<=n;i++){
		if(a[i]>=cnt||b[i]>=cnt)ans++;
	}
	cout<<ans;
}
posted @ 2026-07-17 08:05  HaneDaniko  阅读(25)  评论(2)    收藏  举报