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 就表示 i 与 j 之间是联通的
注意的是如果想建立无向边,那么需要添加 i 到 j 和 j 到 i 两条边
该方法示例
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

欧拉路
欧拉路:从任意一个点,走到任意一个点结束,每条边都通过,且每条边只通过一次
说白了就是一笔画
欧拉回路:起点和终点一样的欧拉路
下面说明两个结论:
- 是欧拉路的条件为:所有的点中,入边与出边为奇数的点没有或仅有两个
- 是欧拉回路的条件为:所有的点中,没有入边与出边为奇数的点
其中,欧拉路的那两个奇点就是起点和终点。
对于中间的所有点,我们肯定要从一条边进去,进去之后,因为不是终点,肯定要再出去,所以中间的点都是偶数点。那么对于起点,我们只能出去,对于终点,我们只能进来。证明出只有两个奇点
但是这并不是一个完全的条件。比如如果是一个断图,它也有可能没有奇点,但是它不是欧拉路。所以我们要做一次 dfs,判断是否能走过所有点。
也就是说:有两个奇点的连通图一定有欧拉路,无奇点的连通图一定有欧拉回路
这是很好判断的,但有些问题会让我们寻找符合条件的欧拉路。这种问题就有一定难度了。
先说结论:dfs 每个节点时在子节点遍历完最后将节点存入栈中,最后再依次输出。
比如:

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

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;
}

浙公网安备 33010602011771号