图论基础
图是若干个顶点和若干条边构成的数据结构,顶点是实际对象的抽象,边是对象之间关系的抽象。可以将图形式化表示为二元组 \(G = (V,E)\),其中,\(V\) 是顶点集,表征数据元素;\(E\) 是边集,表征数据元素之间的关系。信息学竞赛中一般使用 \(n\) 表示图中结点的数量,使用 \(m\) 表示图中边的数量。
图可以分为无向图(undirected graph)、有向图(directed graph)、混合图(mixed graph)。无向图的边集 \(E\) 中的每个元素是一个无序二元组 \((u,v)\),称作无向边(undirected edge),简称边(edge),其中 \(u\) 和 \(v\) 称为端点(endpoint)。有向图的边集 \(E\) 中的每个元素是一个有序二元组 \((u,v)\),称作有向边(directed edge)或弧(arc),其中 \(u\) 称为弧尾,\(v\) 称为弧头。混合图中的边集既有无向边也有有向边。
在无向图中,若任意两个顶点之间都存在边,则该无向图称为完全无向图。\(n\) 个顶点的无向完全图,一共有 \(n(n-1)/2\) 条边。在有向图中,若任意两个顶点 \(x,y\),既存在 \(x\) 到 \(y\) 的弧,也存在 \(y\) 到 \(x\) 的弧,则该有向图称为有向完全图。\(n\) 个顶点的有向完全图,一共有 \(n(n-1)\) 条弧。
在无向图中,若点 \(u\) 与点 \(v\) 存在边 \((u,v)\),则顶点 \(v\) 和顶点 \(u\) 互称为邻接点。在有向图中,若点 \(u\) 与点 \(v\) 之间存在一条点 \(u\) 指向点 \(v\) 的一条弧 \((u,v)\),则称顶点 \(u\) 邻接到顶点 \(v\),顶点 \(v\) 邻接自顶点 \(u\)。
与顶点相关联的边的数目或者弧的数目称为该顶点的度。在无向图中,顶点的度就是其关联的边的数目。在有向图中,由于与顶点关联的弧具有方向性,因此要区分顶点的入度和出度。入度指以该顶点为弧头的弧的数目,而出度指以该顶点为弧尾的弧的数目,入度与出度之和是该顶点的度。
答案
6 号结点,度为 4
例题:P5318 【深基18.例3】查找文献
小 K 喜欢翻看洛谷博客获取知识。每篇文章可能会有若干(也有可能没有)参考文献的链接指向别的博客文章。小 K 求知欲旺盛,如果他看了某篇文章,那么他一定会去看这篇文章的参考文献(如果他之前已经看过这篇参考文献就不用再看它了)。
假设洛谷博客里面一共有 \(n \ (n \le 10^5)\) 篇文章(编号为 \(1\) 到 \(n\))以及 \(m \ (m \le 10^6)\) 条参考文献引用关系。目前小 K 已经打开了编号为 \(1\) 的一篇文章,输出 DFS、BFS 两种遍历方式下看文章的顺序(当有多篇参考文章时,先看编号小的)。
#include <cstdio>
#include <algorithm>
#include <vector>
#include <queue>
using std::sort;
using std::vector;
using std::queue;
using std::pair;
using Edge = pair<int, int>;
const int N = 1e5 + 5;
const int M = 1e6 + 5;
Edge e[M];
vector<int> g[N];
bool vis[N];
void dfs(int u) {
vis[u] = true; printf("%d ", u);
for (int v : g[u]) {
if (!vis[v]) {
dfs(v);
}
}
}
int main()
{
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int x, y; scanf("%d%d", &x, &y);
e[i] = {x, y};
}
sort(e + 1, e + m + 1); // 将输入的边排序后再真正建图
for (int i = 1; i <= m; i++) {
int x = e[i].first, y = e[i].second;
g[x].push_back(y);
}
dfs(1); printf("\n");
for (int i = 1; i <= n; i++) vis[i] = false; // DFS后BFS前清空标记数组
queue<int> q; q.push(1); vis[1] = true;
while (!q.empty()) {
int u = q.front(); printf("%d ", u); q.pop();
for (int v : g[u]) {
if (!vis[v]) {
q.push(v); vis[v] = true;
}
}
}
printf("\n");
return 0;
}
程序阅读题:
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
const int MAXN = 200001;
int main() {
int n, m, l, r, w;
cin >> n >> m;
vector <int> dist(MAXN, -1);
vector <bool> vis(MAXN, false);
vector <vector <pair<int, int> > > go(MAXN);
for (int i = 1; i <= m; i++) {
cin >> l >> r >> w;
go[l].push_back(make_pair(r + 1, w));
go[r + 1].push_back(make_pair(l, -w));
}
queue <int> q;
dist[1] = 0; vis[1] = true;
q.push(1);
while (!q.empty()) {
int x = q.front(); q.pop();
for (auto i : go[x]) {
if (!vis[i.first]) {
vis[i.first] = true;
dist[i.first] = dist[x] + i.second;
q.push(i.first);
}
}
}
if (dist[n + 1] == -1) cout << "sorry" << endl;
else cout << dist[n + 1] << endl;
return 0;
}
假设输入的 \(n,m\) 是不超过 \(200000\) 的正整数,程序第 \(13\) 行每次输入的 \(l,r\) 保证 \(l \le r\)。
判断题
交换程序的第 \(14\) 行与第 \(15\) 行,不影响程序运行的结果。
答案
正确。第 \(14\) 行相当于点 \(l\) 向点 \(r+1\) 连一条权值为 \(w\) 的边,第 \(15\) 行相当于点 \(r+1\) 向点 \(l\) 连一条权值为 \(-w\) 的边。先连哪条边不影响建图的效果。
输入的 \(r\) 的最大值为 \(n\) 时,程序可以正常运行。
答案
错误。数组的大小设定为 \(200001\),可以使用的最大下标是 \(200000\),而当输入的 \(r\) 达到 \(200000\) 时,相当于对应的结点 \(r+1\) 是 \(200001\),下标会越界。
在程序的第 \(17\) 行至第 \(29\) 行,相同的数可能重复进入队列。
答案
错误。进入队列的条件是 !vis[i.first]
,一旦进入队列后 vis[i.first]=true
,因此不可能重复进队。
单选题
当输入的 \(l\) 最小值为 \(x\),输入的 \(r\) 最大值为 \(y\) 时,最多有多少个元素进入过队列?
A. 1 / B. y-x / C. y-x+1 / D. y-x+2
答案
D。这个程序相当于建图后从点 \(1\) 开始进行宽度优先搜索。如果输入的每一对 \(l\) 和 \(r\) 都相同的话,相当于点 \(l\) 和 \(l+1\) 之间连边。所以进队最多的情况是:\(x=1\),点 \(1\) 和点 \(2\) 有连边,点 \(2\) 和点 \(3\) 有连边,以此类推……。那么从 \(1\) 到 \(y+1\) 都进过队列,共有 \(y+1-x+1=y-x+2\) 个元素。
当输入的 \(n\) 为偶数,且 \(r=l+1\) 时,\(m\) 至少为多少时输出不为 sorry
?
A. \(n/2\) / B. \(n/2+1\) / C. \(n/2-1\) / D. \(n\)
答案
A。如果 \(r=l+1\),则每次连边的点之间编号正好差 \(2\)。dist
数组的作用是在宽搜过程中更新从 \(1\) 号点到其他点的距离,根据第 \(30\) 行至第 \(31\) 行,如果最终能够到达 \(n+1\) 号点,则会输出这个距离,到不了(最后 dist[n+1]
等于 -1
)则输出 sorry
。因此要使得 \(m\) 尽可能小也就是输入的边数尽可能少,对应的情况是 \(1\) 和 \(3\) 连边,\(3\) 和 \(5\) 连边,以此类推……。由于输入的 \(n\) 是偶数,则 \(n+1\) 是奇数,需要的边数正好为 \(n/2\)。
当输入为 5 3 1 3 4 3 4 2 4 5 3
时,输出为?
A. 4 / B. 5 / C. 6 / D. 7
答案
D。根据输入数据建的图如下所示:
P2097
#include <cstdio>
#include <vector>
#include <algorithm>
#include <queue>
using std::sort;
using std::vector;
using std::queue;
const int N = 1e5+5;
vector<int> g[N];
bool vis[N]; // 标记i是否被搜到过
void dfs(int u) {
vis[u]=true;
for (int v : g[u]) {
// u->v
if (!vis[v]) {
dfs(v);
}
}
}
int main()
{
int n,m; scanf("%d%d",&n,&m);
for (int i=1;i<=m;i++) {
int u,v; scanf("%d%d",&u,&v);
g[u].push_back(v);
g[v].push_back(u);
}
int ans=0;
for (int i=1;i<=n;i++) {
if (!vis[i]) {
dfs(i);
ans++;
}
}
printf("%d\n",ans);
return 0;
}
P3916
#include <cstdio>
#include <vector>
#include <algorithm>
#include <queue>
using std::sort;
using std::vector;
using std::queue;
const int N = 1e5+5;
int ans[N];
vector<int> g[N];
bool vis[N]; // 标记i是否被搜到过
void dfs(int u, int source) { // 这个搜索是哪个大编号点发起的
vis[u]=true; ans[u]=source;
for (int v : g[u]) {
// u->v
if (!vis[v]) {
dfs(v, source);
}
}
}
int main()
{
int n,m; scanf("%d%d",&n,&m);
for (int i=1;i<=m;i++) {
int u,v; scanf("%d%d",&u,&v);
g[v].push_back(u); // 反向建图
}
for (int i=n;i>=1;i--) { // 优先从编号大的点发起搜索
if (!vis[i]) {
dfs(i, i);
}
}
for (int i=1;i<=n;i++) printf("%d ", ans[i]);
return 0;
}
P2661
#include <cstdio>
#include <algorithm>
using std::min;
const int N = 2e5+5;
int t[N];
bool vis[N];
int ans[N]; // ans[i]表示从i出发最终会遇到的环的长度
int num[N]; // 递归过程中报数
void dfs(int u, int level) { // level是递归层数,报数
if (vis[u]) {
if (ans[u]==0) { // 第一次找到这个环
ans[u]=level-num[u];
}
return;
}
vis[u]=true; num[u]=level;
dfs(t[u],level+1);
// 回溯
ans[u]=ans[t[u]];
}
int main()
{
int n; scanf("%d",&n);
for (int i=1;i<=n;i++) scanf("%d",&t[i]);
for (int i=1;i<=n;i++) {
if (!vis[i]) {
dfs(i,1);
}
}
int r=n;
for (int i=1;i<=n;i++) r=min(r,ans[i]);
printf("%d\n",r);
return 0;
}