C++知识点总结(52):图的搜索

一、概念

1. 数据结构

一般情况下,我们会将数据结构分为逻辑结构和物理结构,其中逻辑结构是我们的逻辑下存储的结构,而物理结构是计算机的逻辑下存储的结构。数据结构的分类可以参见下表:

数据结构
逻辑结构
非线性结构
集合结构
同属一个集合别无其他关系
树状结构
一对多
图状结构
多对多
线性结构
一对一
物理结构
顺序结构
链式结构
单链表
双链表
循环链表
索引结构
CSP-J不涉及
散列结构

2. 图

图(graph)是一种点和点之间多对多关系所组成的数据结构。图也是由顶点的非空集合 V V V 和边的集合 E E E 组成的,表示为 G = ( V , E ) G=(V,E) G=(V,E)。一般地,我们用 V ( G ) V(G) V(G) 表示图 G G G 的顶点集,用 E ( G ) E(G) E(G) 表示图 G G G 的边集。

3. 图的术语

图中的点称为 顶点
图中的点与点所具有的练习称为

点和点的关系称为 邻接
点与边的关系称为 关联

有些图点和点之间的关系是相互的,这种图被称为 无向图,无向图的边一般用 ( x , y ) (x, y) (x,y) 来表示,也可以写作 ( y , x ) (y, x) (y,x)

有些图点和点之间的关系是单向的,这种图被称为 有向图,无向图中由顶点出发的边称为 出边,指向顶点的边称为 入边。有向图的边一般用 < x , y > <x, y> <x,y> 来表示,不可以写作 < y , x > <y, x> <y,x>,因为方向不一样。

图中的边带有某种与之相关的数值,我们称之为 权值,边带有权值的图通常称为 网(加权图)
如果图是有向的,称为 加权有向图
如果图是无向的,称为 加权无向图
没有自环和重边的图就属于 简单图
对于有很少条边的图( e < n log ⁡ 2 n e<n \log_2n e<nlog2n)称为 稀疏图,反之称为 稠密图
一个图从任意一个顶点可以到另外任意一个顶点(不一定直接联通),则称为 连通图
一个图从任意两个顶点可以互相到达(不一定直接联通),则称为 强连通图
从图中提取出的图(可以是空图、一个顶点、图本身)称作 子图

没有重复顶点的一条路径称为 简单路径
起点和终点相同的路径称为 回路(环)

4. 图的公式

如果在无向图中,任意两个顶点都有一条边直接相连,这时就称该图为 无向完全图。具有 n n n 个顶点的无向完全图具有 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n1) 条边。

如果在有向图中,任意两个顶点都有两条边(双向)直接相连,这时就称该图为 有向完全图。具有 n n n 个顶点的无向完全图具有 n ( n − 1 ) n(n-1) n(n1) 条边。

无向图中,顶点 v v v 指的是与该顶点相关联的边的数目。
有向图中,顶点 v v v 分别是 入度出度
一个具有 n n n 个顶点, e e e 条边的图,其所有顶点的度数之和等于边数的 2 2 2 倍。
一个简单无向图中有 x x x 条边,每个顶点的度数都是 y y y,则这个图有 2 x y \frac{2x}{y} y2x 个顶点。

二、图的操作

1. 图的存储

1.1 加权

如果有加权,我们可以使用二维动态(结构体)数组来存储,类似链表的形式来进行作答:

#include <iostream>
#include <vector>
using namespace std;

struct Node
{
    int v, w; // v: 点  w: 权值
};

int n, m;
int a, b, c;
vector <Node> adj[105];

int main()
{
    cin >> n >> m;
    while (m--)
    {
        cin >> a >> b >> c;
        adj[a].push_back({b, c});
    }
    for (int i = 1; i <= n; i++)
    {
        cout << i << ":";
        for (int j = 0; j < adj[i].size(); j++)
            cout << adj[i][j].v << " ";
        cout << endl;
    }
    return 0;
}

1.2 非加权

否则(没有加权)就可以这么写:

#include <iostream>
using namespace std;

int n, m;
int a, b;
int adj[105][105];

int main()
{
    cin >> n >> m;

    for (int i = 1; i <= m; i++)
    {
        cin >> a >> b;
        adj[a][b] = 1;
        adj[b][a] = 1;
    }

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            cout << adj[i][j] << " \n"[j==n];
    return 0;
}

2. 图的搜索

图的搜索就是非常典型的旅行商问题(Travelling Salesman Problem,TSP)。问题描述为:给定一个城市地图和一个旅行商,要求旅行商从某个城市出发,遍历所有城市恰好一次,最后返回起点城市,使得旅行路径的总长度最短。

2.1 DFS

DFS 的执行逻辑就是:不撞南墙不回头。深度优先遍历的遍历写法是不唯一的。

void dfs(int u) // u: 起点
{
    for (int v : adj[u]) // 遍历可以到达的下一个点
    {
        if (vis[v] == 0) // 未被访问
        {
            vis[v] = 1; // 标记
            dfs(v); // 递归
            vis[v] = 0; // 回溯
        }
    }
}

2.2 BFS

BFS 的执行逻辑就是:层层递进。

void bfs(int u)
{
    cout << u << " ";
    q.push(u);
    vis[u] = 1;
    
    while (!q.empty())
    {
        int f = q.front();
        q.pop();
        
        for (int v : adj[f])
        {
            if (vis[v] == 0)
            {
                vis[v] = 1;
                cout << v << " ";
                q.push(v);
            }
        }
    }
}

2.3 例题

下面简单图,说法正确的是(_____)。
在这里插入图片描述
B B B 的错误: F F F 不能到 I I I
C C C 的错误: D D D 后面要写 I I I
D D D 的错误: A A A 不能到 D D D

故选 A A A

三、例题

1. 查找文献

题目描述

K K K 喜欢翻看洛谷博客获取知识。每篇文章可能会有若干个(也有可能没有)参考文献的链接指向别的博客文章。小 K K K 求知欲旺盛,如果他看了某篇文章,那么他一定会去看这篇文章的参考文献(如果他之前已经看过这篇参考文献的话就不用再看它了)。假设洛谷博客里面一共有 n n n n ≤ 105 n≤105 n105)篇文章(编号为 1 1 1 n n n)以及 m m m m ≤ 106 m≤106 m106)条参考文献引用关系。目前小 K K K 已经打开了编号为 1 1 1 的一篇文章,请帮助小 K K K 设计一种方法,使小 K K K 可以不重复、不遗漏的看完所有他能看到的文章。
这边是已经整理好的参考文献关系图,其中,文献 X → Y X → Y XY 表示文章 X X X 有参考文献 Y Y Y。不保证编号为 1 1 1 的文章没有被其他文章引用。
请对这个图分别进行 DFSBFS,并输出遍历结果。如果有很多篇文章可以参阅,请先看编号较小的那篇(因此你必须要先排序)。

输入格式

第一行两个正整数,表示节点数和边数。
接下来 m m m 行,每行两个整数 u , v u,v u,v,表示点 u u u 到点 v v v 之间有道路相连。

输出格式

输出两行,每行 n n n 个空格分隔的数,表示图的 DFSBFS 顺序。

样例1

输入

8 9
1 2
1 3
1 4
2 5
2 6
3 7
4 7
4 8
7 8

输出

1 2 5 6 3 7 8 4
1 2 3 4 5 6 7 8
#include <queue>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

int n, m;
int u, v;
queue <int> q;
bool vis[100005];
vector <int> adj[100005];

void dfs(int u)
{
    for (int v : adj[u])
    {
        if (vis[v] == 0)
        {
            vis[v] = 1;
            cout << v << " ";
            dfs(v);
        }
    }
}

void bfs(int u)
{
    cout << u << " ";
    q.push(u);
    vis[u] = 1;
    
    while (!q.empty())
    {
        int f = q.front();
        q.pop();
        
        for (int v : adj[f])
        {
            if (vis[v] == 0)
            {
                vis[v] = 1;
                cout << v << " ";
                q.push(v);
            }
        }
    }
}

int main()
{
    cin >> n >> m;
    while (m--)
    {
        cin >> u >> v;
        adj[u].push_back(v);
    }
    
    for (int i = 1; i <= n; i++)
        sort(adj[i].begin(), adj[i].end());
    
    vis[1] = 1;
    cout << "1 ";
    dfs(1);
    cout << endl;
    memset(vis, 0, sizeof(vis));
    bfs(1);
    return 0;
}

2. SPFA 算法

这是一个典型的最短路径问题(BFS:呵呵),因为又融合了图,所以就是一个标准的旅行商问题了(BFS:呵呵呵呵)。所以,你要宜用 BFS 忌用 DFS。

恐怖片开始。胆小者误入。

题目描述

妈妈下班回家,街坊邻居说小明被一群陌生人强行押上了警车!妈妈丰富的经验告诉她小明被带到了 t t t 区,而自己在 s s s 区。该市有 m m m 条大道连接 n n n 个区,一条大道将两个区相连接,每个大道有一个拥挤度。小明的妈妈虽然很着急,但是不愿意拥挤的人潮冲乱了她优雅的步伐。所以请你帮她规划一条从 s s s t t t 的路线,使得经过道路的拥挤度最大值最小。

输入格式

第一行有四个用空格隔开的 n , m , s , t n,m,s,t n,m,s,t。接下来 m m m 行,每行三个整数 u , v , w u,v,w u,v,w,表示有一条大道连接区 u u u 和区 v v v,且拥挤度为 w w w注意!!两个区之间可能存在多条大道。

输出格式

输出一行一个整数,代表最大的拥挤度。

样例1

输入

3 3 1 3
1 2 2
2 3 1
1 3 3

输出

2

提示

1 ≤ s ≤ t ≤ n ≤ 1 0 4 1\le s\le t \le n\le10^4 1stn104 1 ≤ m ≤ 2 × 1 0 4 1\le m\le 2\times10^4 1m2×104 w ≤ 1 0 4 w\le10^4 w104

#include <queue>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

struct Node
{
    int v, w;
};

int n, m;
int s, t;
int u, v, w;
bool vis[10005];
vector <Node> adj[10005];

bool check(int mid)
{
    queue <int> q;
    memset(vis, 0, sizeof(vis));

    q.push(s);
    vis[s] = 1;

    while (!q.empty())
    {
        int f = q.front();
        q.pop();

        if (f == t) return true;

        for (auto node : adj[f])
        {
            int fv = node.v;
            int fw = node.w;
            if (!vis[fv] && fw<=mid)
            {
                vis[fv] = 1;
                q.push(fv);
            }
        }
    }
    return false;
}

int main()
{
    cin >> n >> m >> s >> t;
    for (int i = 1; i <= m; i++)
    {
        cin >> u >> v >> w;
        adj[u].push_back({v, w});
        adj[v].push_back({u, w});
    }

    int ans;
    int l = 0, r = 10000;
    while (l <= r)
    {
        int mid = (l+r)/2;
        memset(vis, 0, sizeof(vis));
        if (check(mid))
        {
            ans = mid;
            r = mid-1;
        }
        else
        {
            l = mid+1;
        }
    }
    
    cout << ans;
    return 0;
}

3. [USACO19OPEN] Milk Factory B

题目描述

牛奶生意正红红火火!农夫 John 的牛奶加工厂内有 N N N 个加工站,编号为 1 , 2 , ⋯   , N − 1 , N 1,2,\cdots,N-1,N 1,2,,N1,N 1 ≤ N ≤ 100 1≤N≤100 1N100),以及 N − 1 N-1 N1 条通道,每条连接某两个加工站。(通道建设很昂贵,所以农夫 John 选择使用了最小数量的通道,使得从每个加工站出发都可以到达所有其他加工站)。
为了创新和提升效率,农夫 John 在每条通道上安装了了传送带。不幸的是,当他意识到传送带是单向的已经太晚了,现在每条通道只能沿着一个方向通行了!所以现在的情况不再是从每个加工站出发都能够到达其他加工站了。
然而,农夫 John 认为事情可能还不算完全失败,只要至少还存在一个加工站 i i i 满足从其他每个加工站出发都可以到达加工站 i i i。注意从其他任意一个加工站 j j j 前往加工站 i i i 可能会经过 i i i j j j 之间的一些中间站点。请帮助农夫 John 求出是否存在这样的加工站 i i i

输入描述

输入的第一行包含一个整数 N N N,为加工站的数量。以下 N − 1 N-1 N1 行每行包含两个空格分隔的整数 a i a_i ai b i b_i bi,满足 1 ≤ a i 1≤ai 1ai b i ≤ N bi≤N biN 以及 a i ≠ b i ai≠bi ai=bi。这表示有一条从加工站 a i a_i ai 向加工站 b i b_i bi 移动的传送带,仅允许沿从 a i a_i ai b i b_i bi 的方向移动。

输出描述

如果存在加工站i满足可以从任意其他加工站出发都可以到达加工站 i i i,输出最小的满足条件的 i i i。否则,输出 -1

样例1

输入

3
1 2
3 2

输出

2

这里有一个非常离谱的思路:把要求的点当成起点而不是终点。这样 while n-- 的时候要 adj[b].push_back a; 了。

#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

int cnt;
int n, m;
int a, b;
bool vis[105];
vector <int> adj[105];

void dfs(int u)
{
    for (int v : adj[u])
    {
        if (vis[v] == 0)
        {
            vis[v] = 1;
            cnt++;
            dfs(v);
        }
    }
}

int main()
{
    cin >> n;
    for (int i = 1; i < n; i++)
    {
        cin >> a >> b;
        adj[b].push_back(a);
    }
    
    for (int i = 1; i <= n; i++)
    {
        memset(vis, 0, sizeof(vis));
        cnt = 0;
        vis[i] = 1;
        dfs(i);
        if (cnt == n-1)
        {
            cout << i;
            return 0;
        }
    }
    cout << -1;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值