图论

发布时间 2023-10-14 10:00:32作者: chfychin

\(DFS\)

\(DFS\) 全称是 \(Depth\ First\ Search\),中文名是深度优先搜索,是一种用于遍历或搜索树或图的算法。所谓深度优先,就是说每次都尝试向更深的节点走。

\(DFS\) 最显著的特征在于其 递归调用自身。同时与 \(BFS\) 类似,\(DFS\) 会对其访问过的点打上访问标记,在遍历图时跳过已打过标记的点,以确保 每个点仅访问一次。符合以上两条规则的函数,便是广义上的 \(DFS\)

该算法通常的时间复杂度为 \(O(n+m)\),空间复杂度为 \(O(n)\),其中 \(n\) 表示点数,\(m\) 表示边数。注意空间复杂度包含了栈空间,栈空间的空间复杂度是 \(O(n)\) 的。在平均 \(O(1)\) 遍历一条边的条件下才能达到此时间复杂度,例如用前向星或邻接表存储图;如果用邻接矩阵则不一定能达到此复杂度。

数组邻接表
void dfs(int u)
{
	st[u] = 1;
	for(int i = h[u]; ~i; i = ne[i])
	{
		int j = e[i];
		if(!st[j])
		{
			dfs(j);
		}
	}
}
vector邻接表
void dfs(int u)
{
	st[u] = 1;
	for(int i = 0; i < e[u].size(); i ++)
	{
		int v = e[u][i], ww = w[u][i];
		if(!st[u])
		{
			dfs(v);
		}
	}
}

\(DFS\)序列

\(DFS\) 序列是指 \(DFS\) 调用过程中访问的节点编号的序列。

每个子树都对应 \(DFS\) 序列中的连续一段(一段区间)。

括号序列

\(DFS\) 进入某个节点的时候记录一个左括号 (,退出某个节点的时候记录一个右括号 )。

每个节点会出现两次。相邻两个节点的深度相差 \(1\)

一般图上 \(DFS\)

对于非连通图,只能访问到起点所在的连通分量。

对于连通图,\(DFS\) 序列通常不唯一。

注:树的 \(DFS\) 序列也是不唯一的。

\(DFS\) 过程中,通过记录每个节点从哪个点访问而来,可以建立一个树结构,称为 \(DFS\) 树。\(DFS\) 树是原图的一个生成树。

例1. 全排列

点击查看代码
#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(false);cin.tie(nullptr),cout.tie(nullptr)

using namespace std;

int s[100];
bool p[100];

void dfs(int u, int fa)
{
	if(u > fa)
	{
		for(int i = 1; i <= fa; i ++)
			cout << s[i] << ' ';
		cout << '\n';
	}
	for(int i = 1; i <= fa; i ++)
	{
		if(!p[i])
		{
			s[u] = i;
			p[i] = true;
			dfs(u + 1, fa);
			p[i] = false;
			s[u] = 0;
		}
	}
}

void solve()
{
	int n;
	cin >> n;
	for(int i = 0; i <= n; i ++)
		s[i] = p[i] = 0;
	dfs(1, n);
}

signed main()
{
	IOS;
	int _ = 1;
	// cin >> _;
	while(_ --)
		solve();
	return 0;
}

例2. 八皇后

点击查看代码
#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(false);cin.tie(nullptr),cout.tie(nullptr)

using namespace std;

int ans, a[100], b[100], n, m;
bool check(int x, int y)
{
	for(int i = 1; i <= x; i ++)
	{
		if(a[i] == y||i + a[i] == x + y||i - a[i] == x - y)
			return false;
	}
	return true;
}

void dfs(int r)
{
	if(r == n +  1)
	{
		ans ++;
		return ;
	}
	for(int i = 1; i <= n; i ++)
		if(check(r, i))
		{
			a[r] = i;
			dfs(r + 1);
			a[r] = 0;
		}
}

void init()
{
	for(n = 1; n <= 10; n ++)
	{
		ans = 0;
		dfs(1);
		b[n] = ans;
	}
}
void solve()
{
	while(cin >> m, m)
		cout << b[m] << '\n';
}

signed main()
{
	IOS; init();
	int _ = 1;
	// cin >> _;
	while(_ --)
		solve();
	return 0;
}

\(BFS\)

\(BFS\) 全称是 \(Breadth\ First\ Search\),中文名是宽度优先搜索,也叫广度优先搜索。

是图上最基础、最重要的搜索算法之一。

所谓宽度优先。就是每次都尝试访问同一层的节点。 如果同一层都访问完了,再访问下一层。

这样做的结果是,\(BFS\) 算法找到的路径是从起点开始的 最短 合法路径。换言之,这条路径所包含的边数最小。

\(BFS\) 结束时,每个节点都是通过从起点到该点的最短路径访问的。

时间复杂度: \(O(n + m)\)

空间复杂度: \(O(n)\)\(vis\) 数组和队列)

数组邻接表
void bfs(int u)
{
	int hh = 0, tt = -1;
	q[++ tt] = u;
	st[u] = true;
	while(hh <= tt)
	{
		int t = q[hh ++];
		for(int i = h[t]; ~i; i = ne[i])
		{
			int j = e[i];
			if(!st[j])
			{
				st[j] = true;
				q[++ tt] = j;
			}
		}
	}
}
vector邻接表
void bfs(int u)
{
	queue<int> q;
	q.push(u);
	st[u] = true;
	while(!q.empty())
	{
		int t = q.front();
		q.pop();
		for(int i = 0; i < e[t].size(); i ++)
		{
			int j = e[t][i];
			if(!st[j])
			{
				st[j] = true;
				q.push(j);
			}
		}
	}
}

树上问题

树的直径

树上任意两节点之间最长的简单路径即为树的「直径」。

最近公共祖先LCA

最近公共祖先简称 \(LCA(Lowest Common Ancestor)\)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。 为了方便,我们记某点集 \(S=\{v_1,v_2,\ldots,v_n\}\) 的最近公共祖先为 \(\text{LCA}(v_1,v_2,\ldots,v_n)\)\(\text{LCA}(S)\)

例. 距离

题目描述:

给出 \(n\) 个点的一棵树,多次询问两点之间的最短距离。

注意:边是无向的。所有节点的编号是 \(1,2,…,n\)

输入格式:

第一行为两个整数 \(n\)\(m\)\(n\) 表示点数,\(m\) 表示询问次数;

下来 \(n−1\) 行,每行三个整数 \(x,y,k\),表示点 \(x\) 和点 \(y\) 之间存在一条边长度为 \(k\)

再接下来 \(m\) 行,每行两个整数 \(x,y\),表示询问点 \(x\) 到点 \(y\) 的最短距离。

树中结点编号从 \(1\)\(n\)

输出格式:

\(m\) 行,对于每次询问,输出一行询问结果。

输入1

2 2
1 2 100
1 2
2 1

输出1

100
100

输入2

3 2
1 2 10
3 1 15
1 2
3 2

输出2

10
25

数据范围

\(2≤n≤10^4,1≤m≤2×10^4,0<k≤100,1≤x,y≤n\)

LCA模板
#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(false);cin.tie(nullptr),cout.tie(nullptr)

using namespace std;

const int N = 1e5 + 10, M = N * 2, S = 16;

int h[N], w[M], e[M], ne[M], idx;
int fa[N][S], ce[N];
int q[N], dist[N];
int n, m, root;

void add(int a, int b, int c)
{
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}

void LCA_init()
{
	int hh = 0, tt = -1;
	ce[root] = 1;
	q[++ tt] = root;
	while(hh <= tt)
	{
		int t = q[hh ++];
		for(int i = h[t]; ~i; i = ne[i])
		{
			int j = e[i];
			if(ce[j] == 0)
			{
				ce[j] = ce[t] + 1;
				dist[j] = dist[t] + w[i];
				q[++ tt] = j;
				fa[j][0] = t;
				for(int k = 1; k < 15; k ++)
					fa[j][k] = fa[fa[j][k - 1]][k - 1];
			}
		}
	}
}

int query(int a, int b)
{
	if(ce[a] < ce[b]) swap(a, b);
	for(int i = 14; i >= 0; i --)
		if(ce[fa[a][i]] >= ce[b])
			a = fa[a][i];
	if(a == b) return a;
	for(int i = 14; i >= 0; i --)
		if(fa[a][i] != fa[b][i])
		{
			a = fa[a][i];
			b = fa[b][i];
		}
	return fa[a][0];
}

void solve()
{
	memset(h, -1, sizeof h);
	root = 1;
	cin >> n >> m;
	int a, b, c;
	for(int i = 1; i < n; i ++)
	{
		cin >> a >> b >> c;
		add(a, b, c), add(b, a, c);
	}
	LCA_init();
	int x, y, ans;
	while(m --)
	{
		cin >> x >> y;
		ans = dist[x] + dist[y] - 2 * dist[query(x, y)];
		cout << ans << "\n";
	}
}

signed main()
{
	IOS;
	int _ = 1;
//	cin >> _;
	while(_ --)
		solve();
	return _ ^ _;
}

树链剖分

树的层次

题目描述:

给定一个 \(n\) 个点 \(m\) 条边的有向图,图中可能存在重边和自环。

所有边的长度都是 \(1\),点的编号为 \(1∼n\)

请你求出 \(1\) 号点到 \(n\) 号点的最短距离,如果从 \(1\) 号点无法走到 \(n\) 号点,输出 \(−1\)

输入格式:

第一行包含两个整数 \(n\)\(m\)

接下来 \(m\) 行,每行包含两个整数 \(a\)\(b\),表示存在一条从 \(a\) 走到 \(b\) 的长度为 \(1\) 的边。

输出格式:

输出一个整数,表示 \(1\) 号点到 \(n\) 号点的最短距离。

输入

4 5
1 2
2 3
3 4
1 3
1 4

输出

1

数据范围

\(1≤n,m≤10^5\)

点击查看代码
#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(false);cin.tie(nullptr),cout.tie(nullptr)

using namespace std;

const int N = 1e5 + 10;

int h[N], e[N], ne[N], idx;
int q[N], d[N], n, m;

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

int bfs()
{
    memset(d, -1, sizeof(d));
    int tt = 0, hh = 0;
    q[0] = 1, d[1] = 0;
    while(hh <= tt)
    {
        int t = q[hh ++];
        for(int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if(d[j] == -1)
            {
                d[j] = d[t] + 1;
                q[++ tt] = j;
            }
        }
    }
    return d[n];
}

void solve()
{
    memset(h, -1, sizeof(h));
    cin >> n >> m;
    for(int i = 0; i < m; i ++)
    {
        int a, b;
        cin >> a >> b;
        add(a, b);
    }
    cout << bfs() << endl;
}

signed main()
{
	IOS;
	int _ = 1;
	// cin >> _;
	while(_ --)
		solve();
	return 0;
}

树的重心

例. 树的重心

题目描述

给定一颗树,树中包含 \(n\) 个结点(编号 \(1∼n\))和 \(n−1\) 条无向边。

请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。

重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。

输入格式:

第一行包含整数 \(n\),表示树的结点数。

接下来 \(n−1\) 行,每行包含两个整数 \(a\)\(b\),表示点 \(a\) 和点 \(b\) 之间存在一条边。

输出格式:

输出一个整数 \(m\),表示将重心删除后,剩余各个连通块中点数的最大值。

输入

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

输出

4

数据范围
$1≤n≤10^5$11

点击查看代码
#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(false);cin.tie(nullptr),cout.tie(nullptr)

using namespace std;

const int N = 1e5 + 10, M = 2 * N;

int h[N], e[M], ne[M], idx;
int n, ans = N;
bool st[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

int dfs(int u)
{
    st[u] = true;
    int res = 0, sum = 1;
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if(!st[j])
        {
            int s = dfs(j);
            res = max(res, s);
            sum += s;
        }
    }
    res = max(res, n - sum);
    ans = min(res, ans);
    return sum;
}

void solve()
{
    memset(h, -1, sizeof(h));
    cin >> n;
    for(int i = 0; i < n - 1; i ++)
    {
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a);
    }
    dfs(1);
    cout << ans << endl;
}

signed main()
{
	IOS;
	int _ = 1;
	// cin >> _;
	while(_ --)
		solve();
	return 0;
}

最短路

多源汇\(Floyd\)

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

Floyd
void floyd()
{
    for(int k = 1; k <= n; k ++)
        for(int i = 1; i <= n; i ++)
            for(int j = 1; j <= n; j ++)
                p[i][j] = min(p[i][j], p[i][k] + p[k][j]);
}

单源朴素\(Dijkstra\)

时间复杂度:\(O(nm)\)

朴素Dijkstra
void Dijkstra()
{
    memset(dist, 0x3f, sizeof(dist));
    dist[1] = 0;
    for(int i = 0; i < n; i ++)
    {
        int t = -1;
        for(int j = 1; j <= n; j ++)
            if(!st[j]&&(t == -1||dist[j] < dist[t]))
                t = j;
        st[t] = 1;
        for(int j = h[t]; j != -1; j = ne[j])
        {
            int i = e[j];
            dist[i] = min(dist[i], dist[t] + w[j]);
        }
    }
}

单源堆优化\(Dijkstra\)

时间复杂度:\(O(mlog(n))\)

堆优化Dijkstra
int dijkstra()
{
    memset(dist, 0x3f, sizeof(dist));
    priority_queue<pii, vector<pii>, greater<pii> > heap;
    dist[1] = 0;
    heap.push({0, 1});
    while(heap.size())
    {
        auto t = heap.top();
        heap.pop();
        int y = t.second;
        if(st[y]) continue;
        st[y] = true;
        for(int i = h[y]; ~i; i = ne[i])
        {
            int j = e[i];
            if(dist[j] > dist[y] + w[i])
            {
                dist[j] = dist[y] + w[i];
                heap.push({dist[j], j});
            }
        }
    }
    if(dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

单源\(Spfa\)

时间复杂度:\(O(nm)\)

Spfa
int spfa()
{
    memset(dist, 0x3f, sizeof(dist));
    queue<int> q;
    dist[1] = 0; st[1] = true;
    q.push(1);
    while(q.size())
    {
        int t = q.front(); q.pop();
        st[t] = false;
        for(int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if(dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if(!st[j])
                {
                    st[j] = true;
                    q.push(j);
                }
            }
        }
    }
    return dist[n];
}

最小生成树

\(Prim\)

时间复杂度:\(O(nm)\)

Prim
const int N = 510, inf = 0x3f3f3f3f;

int n, m, dist[N], g[N][N];
bool st[N];

int prim()
{
    memset(dist, inf, sizeof(dist));
    int res = 0;
    for(int i = 0; i < n; i ++)
    {
        int t = -1;
        for(int j = 1; j <= n; j ++)
            if(!st[j]&&(t == -1||dist[t] > dist[j]))
                t = j;
        if(i&&dist[t] == inf) return inf;
        if(i) res += dist[t];
        st[t] = true;
        for(int j = 1; j <= n; j ++)
            dist[j] = min(dist[j], g[t][j]);
    }
    return res;
}

Kruskal

时间复杂度:\(O(nlogm)\)

Kruskal
const int N = 1e5 + 10, inf = 0x3f3f3f3f;

struct node
{
	int a, b, w;
}s[N * 2];

int fa[N], n, m;

int cmp(node a, node b)
{
	return a.w < b.w;
}

void init()
{
	for(int i = 1; i <= n; i ++)
		fa[i] = i;
}

int find(int x)
{
	return fa[x] == x ? x : fa[x] = find(fa[x]);
}

int kruskal()
{
	sort(s + 1,s + m + 1, cmp);
	int res = 0, cnt = 0;
	for(int i = 1; i <= m; i ++)
	{
		int a = s[i].a, b = s[i].b, w = s[i].w;
		if(find(a) != find(b))
		{
			fa[find(a)] = find(b);
			cnt ++;
			res += w;
		}
	}
	if(cnt == n - 1) return res;
	return inf;
}

二分图

染色法判断二分图

染色法判断二分图
#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(false);cin.tie(nullptr),cout.tie(nullptr)

using namespace std;

const int N = 1e5 + 10, M = 2 * N;

int e[M], ne[M], h[N], color[N], idx;
int n, m;

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

bool dfs(int u, int c)
{
    color[u] = c;
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if(!color[j])
        {
            if(!dfs(j, 3 - c)) return false;
        }
        else if(color[j] == c)
            return false;
    }
    return true;
}

void solve()
{
    memset(h, -1, sizeof(h));
    cin >> n >> m;
    for(int i = 0; i < m; i ++)
    {
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a);
    }
    bool flag = true;
    for(int i = 1; i <= n; i ++)
    {
        if(!color[i])
        {
            if(!dfs(i, 1))
            {
                flag = false;
                break;
            }
        }
    }
    if(flag) puts("Yes");
    else puts("No");
}

signed main()
{
	IOS;
	int _ = 1;
	// cin >> _;
	while(_ --)
		solve();
	return 0;
}

匈牙利算法求最大匹配数

匹配:在图论中,一个「匹配」是一个边的集合,其中任意两条边都没有公共顶点。

最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。

完美匹配:如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。

交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。

增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替 路称为增广路(\(agumenting\ path\))。

匈牙利算法模板
#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(false);cin.tie(nullptr),cout.tie(nullptr)

using namespace std;

const int N = 410, M = 1e5 + 10;

int h[N], e[M], ne[M], idx;
int a[N], b[N];
int n, m, q, ans;
bool st[N];

void add(int a, int b)
{
	e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

bool find(int x)
{
	for(int i = h[x]; ~i; i = ne[i])
	{
		int j = e[i];
		if(!st[j])
		{
			st[j] = true;
			if(!b[j]||find(b[j]))
			{
				b[j] = x;
				return true;
			}
		}
	}
	return false;
}

void solve()
{
	memset(h,  -1, sizeof h);
	cin >> n >> m >> q;
	for(int i = 1; i <= q; i ++)
	{
		int a, b, c;
		cin >> a >> b;
		add(a, b);
	}
	for(int i = 1; i <= n; i ++)
	{
		memset(st, false, sizeof st);
		if(find(i)) ans ++;
	}
	cout << ans << "\n";
}

signed main()
{
	IOS;
	int _ = 1;
	// cin >> _;
	while(_ --)
		solve();
	return 0;
}

拓扑排序

拓扑排序模板
const int N = 100010;

int h[N], e[N], ne[N], idx;
int  n, m, q[N], d[N];

void add(int a, int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx ++;
}

bool topsort()
{
    int hh = 0, tt = -1;
    for(int i = 1; i <= n; i ++)
        if(!d[i])
            q[++ tt] = i;
    while(hh <= tt)
    {
        int t = q[hh ++];
        for(int i = h[t]; i != -1; i =ne[i])
        {
            int j = e[i];
            d[j] --;
            if(d[j] == 0)
                q[++ tt] = j;
        }
    }
    return tt == n - 1;
}