分类 c语言 下的文章

二分图 当且仅当图中不含奇数环。
二分图就是将图中的点分成左右两个集合,每条边都在两个集合之间
染色法判断是不是二分图
思路:利用dfs将每个点进行染色,将某个点染成白色,则以这个点为中心的其他点染成黑色,用1表示白色,用2表示黑色,0表示没有染过色,可以建立一个数组color[],来存储染色情况,每次dfs先判断是否染色,如果没有就染成和上一个点不一样的颜色,可以用3去减,表示相反的颜色,如果有则判断是不是和上一点的颜色一样,如果一样就是有矛盾,表示出现了奇数环,则返回false,否则返回true。(给dfs传入的参数有两个一个是点,一个是染的颜色,可以设置一个默认值,默认传1)数据结构用邻接表来存图。因为每个点可能不会都是连通的,所以要遍历每一个点,判断是否染色,如果染过则跳过,如果没有则进行一遍dfs。为了判断最后的结构,我们需要一个判断的表示,所以可以新建一个布尔变量,一开始设置为true,之后如果某一次dfs返回值为false则将其设置为false,然后跳出循环。当循环结束之后只要判断这个布尔变量是否为true,为true则说明染色过程很顺利,该图是二分图,否则就不是二分图。
例题:

染色法判定二分图

给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环。
请你判断这个图是否是二分图。
输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含两个整数 u 和 v,表示点 u 和点 v 之间存在一条边。
输出格式
如果给定图是二分图,则输出 Yes,否则输出 No。
数据范围1≤n,m≤1e5
输入样例:

4 4
1 3
1 4
2 3
2 4

输出样例:

Yes

需要注意的是因为存的是无向图,所以边的数量应该是两倍于给的数量(相当于存两个方向相反的有向边)

匈牙利算法求二分图中最大的匹配数
思路:题目会给出一个二分图,先是遍历左边的点,每次遍历都遍历一遍那个点所连的边,让其和右边的点看能不能匹配,如果能那就匹配,如果不能就看一下那个被匹配的点于其匹配的那个点还有没有其他边的点可以与之匹配,如果可以,那就让那个点去匹配其他点,然后让现在这个点和被匹配的点匹配。去匹配其他点的过程是一个迭代的过程。数据结构是用的邻接表来存。对于手写邻接表中e[] , ne[] 的大小,应该是边的大小,因为每一个h都可以引出至多m(边数)条边,就是至多存有m个相连的点。定义一个bool st[],表示被考虑过,每次遍历都要清空一下,表示在后面的迭代过程不会出现重复考虑的情况。定义一个int match[] , 用来存的是被匹配的点去匹配他的点。每次匹配成功就往答案中+1,当循环遍历完之后得到的就是最大匹配数。

例题:

二分图的最大匹配

给定一个二分图,其中左半部包含 n1 个点(编号1∼n1),右半部包含 n2 个点(编号 1∼n2),二分图共包含 m 条边。
数据保证任意一条边的两个端点都不可能在同一部分中。
请你求出二分图的最大匹配数。
二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E} 中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。
二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。
输入格式
第一行包含三个整数 n1、 n2和 m。
接下来 m 行,每行包含两个整数 u 和 v,表示左半部点集中的点 u 和右半部点集中的点 v 之间存在一条边。
输出格式
输出一个整数,表示二分图的最大匹配数。
数据范围 1≤n1 , n2≤500 , 1≤u≤n1 1≤v≤n2 , 1≤m≤1e5
输入样例:

2 2 4
1 1
1 2
2 1
2 2

输出样例:

2

思路:虽然没有说是有向图还是无向图,但是根据匈牙利算法的实现方式,可以当作单向图去存,且必须是去匹配的集合作为去指向的一方。

//find()函数
bool find(int x)
{
    for (int i = h[x]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true;
            if (!match[j] || find(match[j]))
            {
                match[j] = x;
                return true;
            }
        }
    }
    return false;
}

朴素prim(普利姆)算法
一般用这个算法解决稠密图,用邻接矩阵来存图。是无向边所以就是存两条反向的单向边,来表示。
思路和dijkstra算法差不多。
开一个n次遍历,每次遍历都从还没加到集合中的点选一个到集合距离最小的点,dijkstra算法求的是最短路,所以是到原点,prim算法是求一个所有边都是最小的树,即最小生成树,用集合来维持这个树,所以是到集合距离最小的点。
到集合的距离就是说每个点到这个集合中的某一个点的边。之后要求最小生成树一般不是把树给输出而是将其边的合输出。所以要有一个变量去存。一般存的时候先是判断是不是为第一次,因为第一次就是起点,其到集合的距离为0,之后在更新之前存。因为如果存在自环且权重为负,或者权重比本身到集合的距离小,那么在更新之后可能会有disk[t] = d[t] [t] 的这种情况,然后disk[t]的值就会发生变化,所以为了避免这种情况,一般都是将加入答案的操作放在更新之前。
判断有没有最小生成树,一开始将每一个点的距离初始化为无穷,如果就是在某一次遍历(除了第一次)中取得的点的距离为无穷,就是说明当前集合之外的点离集合最短的距离为无穷,也就是集合中没有一个点有和剩余的点向连的边,所以这个图就没有最小生成树。

例题:

Prim求最小生成树

给定一个 n个点 m条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
给定一张边带权的无向图 G=(V,E),其中 VV 表示图中点的集合,E表示图中边的集合,n=|V|,m=|E|。
由 V中的全部 n个顶点和 E中 n−1 条边构成的无向连通子图被称为 G的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G的最小生成树。
输入格式
第一行包含两个整数 n和 m。
接下来 m行,每行包含三个整数u,v,w,表示点 u和点 v之间存在一条权值为 w的边。
输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
数据范围 1≤n≤500, 1≤m≤105, 图中涉及边的边权的绝对值均不超过10000。
输入样例:

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

输出样例:

6

kruskal(克鲁斯卡尔) 算法
利用到并查集的思想。基本思路就是先将每条边按照权重排序,然后维持一个集合,之后枚举每条边,假设枚举到的边的两个点不连通就将他们连起来,就是放到一个集合中,然后将权重加到答案中(因为从小到大枚举边,所以假设存在树结构,那么每条边一定是最小的,也就是实际上构成了最小生成树)。在存储边的时候因为只要能够排序就行,所以可以用结构体来存。
为了在排序的时候实现根据权重来排序,需要重载一下小于号
对于并查集在使用的时候需要初始化每个点的父节点为本身

bool operator < (const edge &W)const
{
    return w < W.w;
}

为了判断是否存在最小生成树,可以再新建一个变量cot,表示集合中的边数,当进行一次连通操作时(也就是并集)cot++,之后如果在所有边都枚举完之后,cot < n - 1 说明有点没有加入集合(一共 n 个点,应该一共 n - 1条边)那么就不存在最小生成树,反之等于 n - 1 时就存在,然后输出res
例题:

Kruskal求最小生成树

给定一个 n个点 m条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
给定一张边带权的无向图 G=(V,E),其中 VV 表示图中点的集合,EE 表示图中边的集合,n=|V|,m=|E|。
由 V中的全部 n个顶点和 E中 n−1 条边构成的无向连通子图被称为 G的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G的最小生成树。
输入格式
第一行包含两个整数 n和 m。
接下来 m行,每行包含三个整数u,v,w,表示点 u和点 v之间存在一条权值为 w的边。
输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
数据范围 1≤n≤105 , 1≤m≤2∗1e5 , 图中涉及边的边权的绝对值均不超过 1000。
输入样例:

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

输出样例:

6

n表示点的数量, m表示边的数量,当m和n^2 相近时是稠密图,当m和n相近时是稀疏图,用的是堆优化版的dijkstra算法

最短路算法的难点在建图

无向图是特殊的有向图,所以在最短路的算法当中,可以用有向图的算法来直接解决无向图的最短路问题

朴素dijkstra基本思路
定义一个int数组dist[],表示每个点到原点的距离,定义一个bool数组st[],表示是否确定为最短路,初始化先让每个点的距离为正无穷,原点例如为1的距离为0,

memset(dist, 0x3f, sizeof dist);
dist[1] = 0

迭代 n 次表示遍历每一个点,之后每次迭代从1开始到n,让每一个还没有确定过最短距离,到达原点距离最小的点的距离确定,然后用它来更新其他点的距离,找这个点可以先定义一个中间变量 int t = -1;然后循环每次用if判断更新,如果t == -1 说明点还没找到让第一个点赋给t,之后每次比较disk[t]和disk[j],如果后者比前者小则将更小的 j 赋值给 t ,最后当他跑完了之后留下来的那个点就是需要的点。

for (int j = 1; j <= n; j ++)
{
    dist[j] = min(disk[j], disk[t] + d[t][j]);
}

先是因为稠密图,所以用邻接矩阵来存,就是二维数组,d [N] [N].

例题:

Dijkstra求最短路I

给定一个 n个点 m条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 1号点到 n号点的最短距离,如果无法从 1号点走到 n号点,则输出 −1。
输入格式
第一行包含整数 n和 m。
接下来 m行每行包含三个整数 x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。
输出格式
输出一个整数,表示 11 号点到 nn 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围 1≤n≤500,1≤m≤1e5, 图中涉及边长均不超过10000。
输入样例:

3 3
1 2 2
2 3 1
1 3 4

输出样例:

3

思路:其中存在重边和自环,自环因为在更新距离的时候没有用到所以不用管,而重边,因为是求最短路,所以我们每次只要保存最短的一条就行,这就要求我门在一开始初始化d每条边的距离为无限大,之后每次存距离时用min函数更新,对于路径不存在的意思,就是一直到循环结束n号点的距离还是为0x3f3f3f3f,所以就是没有边可以将其连到原点,所以不存在,此时输出-1.

堆优化的dijkstra算法

优化在求每个没确定过的最短距离的点的步骤上,使用堆,可以手写也可以用优先队列,用优先队列可能会有数据的冗余,最后时间复杂度到mlogm,但是其等价于mlogn,(m近似于n^2,mlogm - > 2mlogn).因为是稀疏表所以用邻接表来存,用邻接表来存的时候就不用关注重边,因为用到优先队列,虽然可能抽到两个一样的点,但是可以判断是否加过,加过了就跳过这个循环(用的是while循环,一直到最后没有新的点加入优先队列中,且原有的节点都遍历过了时循环结束,虽然有重复,但是重复的会continue,这和队列的循环判断一样,就是队列为空时循环结束),所以就不用在意(邻接矩阵在输入的时候要判断是因为他存不了重边)。需要注意的是和bfs求最短路的区别,就是他的权重不全为1,这意味着需要再来一个数组来存放权重,假设我们叫他w[].

例题:

Dijkstra求最短路II

给定一个 n个点 m条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出 1号点到 n号点的最短距离,如果无法从 1号点走到 n号点,则输出 −1。
输入格式
第一行包含整数 n和 m。
接下来 m行每行包含三个整数x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1号点到 n号点的最短距离。
如果路径不存在,则输出−1。
数据范围 1≤n,m≤1.5×1e5, 图中涉及边长均不小于 0,且不超过10000。
数据保证:如果最短路存在,则最短路的长度不超过 1e9。
输入样例:

3 3
1 2 2
2 3 1
1 3 4

输出样例:

3

思路:定义一个小根堆

priority_queue<pair<int, int>, vector<pair<int, int> > , greater<pair<int, int> > > heap;

来维持还没确定的距离的点,因为这个点是要包含距离的,所以存的是pair,先将pair<int, int> {1,0} 存进去表示的是第一个点(原点)的距离为0,这是初始化,然后每次都将堆头取出,然后再弹出堆头,表示的是这个最小的已经确定了,之后在更新的时候只要判断某点原来的距离和这个点到那个点的距离的大小(不存在就是无穷大),然后如果比原来的小就加入到堆中。
后面就是一样的,当循环结束后判断是否有路径,有路径输出路径,没路径输出-1。

bellman_ford 算法基本思路
求权重为负的最短路问题。先是遍历n遍,每次遍历都遍历所有的边,然后更新所有的边。需要的数据结构只要是支持将边全部遍历就行,所以可以用结构体。
这样循环结束之后会使每一个点都会小于等于上一个点加上两点之间的权重。也叫做三角不等式

// 松弛操作
for (int i = 0; i < n; i ++)
{
    for (auto t : edges)
    {
        disk[b] = disk[a] + w
    }
}

//三角不等式
disk[b] <= disk[a] + w;

若图中存在负权回路,则有可能不存在最短路径,所以可以求出最短路的图一般情况下是不存在负权回路,负权回路就是一个环上的边权重总和是负的
松弛操作的n是有意义的,是说在不超过n条边的情况下最短路径,假设不是n是k就是在不超过k条边的最短路径。假如在第n次有更新,因为有n条边所以应该是n + 1个点,但是一共就是n个点,根据抽屉原理,可以知道两个点是一样的,所以就是成环了,因为有更新,所以值就是变小,所以就是负环。

例题:

有边数限制的最短路

给定一个 n个点 m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从 1号点到 n号点的最多经过 k条边的最短距离,如果无法从 1号点走到 n号点,输出 impossible。
注意:图中可能 存在负权回路 。
输入格式
第一行包含三个整数 n,m,k。
接下来 m行,每行包含三个整数x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。
点的编号为 1∼n。
输出格式
输出一个整数,表示从 1号点到 n号点的最多经过 k条边的最短距离。
如果不存在满足条件的路径,则输出 impossible。
数据范围 1≤n,k≤500 , 1≤m≤10000 , 1≤x,y≤n,
任意边长的绝对值不超过10000。
输入样例:

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

输出样例:

3

思路:为了避免串边所以需要新建一个数组backup[],为了在更新的时候使用的上一次备份还没有操作过的点的距离。

memcpy(backup, disk, sizeof disk);

如果不备份的话假如k为1,就是不超过1条边,然后遍历所有的边,可能会出现3被更新成两条边的情况(如果这条边的出点被更新过)。因为可能有负环,所以最后在判断是否不存在的时候只要是路径长度大于一个很大的数就当作不存在,输出 -1 。(因为正无穷加上一个负数还是不存在,但是在判断的时候就不会等于我们设定的最大值)。

spfa算法思路
spfa有一个限制就是如果所要求最短路的图中有负环就不能用,但是一般情况下都没有。
spfa是以bellman_ford 算法为基础进行优化,优化的方向是更新的那部分,原因是当第一次更新之后如果下次再更新的时候如果a没有变小b也不会进行更新(每次都会把每一条边遍历一遍)。
所以我们新建一个队列,来保持一个变小的a,然后当这个队列不空的时候对b进行更新,之后将b加入到队列中变成了新的a,去更新其他边。这里有一个问题就是a可能会出现重复元素,因为每个元素都有可能有多个入度,所以为了避免这个情况在加入的时候要进行判断。这个其实就是基于bfs。数据结构可以使用邻接表
要记得在更新的时候是disk[j] > disk[t] + w 才更新,表示的是从上一个点走过来,比原来的路径要短,更新为最短路径。

例题:

spfa求最短路

给定一个 n个点 m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出 1号点到 n号点的最短距离,如果无法从 1号点走到 n号点,则输出 impossible。
数据保证不存在负权回路。
输入格式
第一行包含整数 n和 m。
接下来 m行每行包含三个整数x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。
输出格式
输出一个整数,表示 1号点到 n号点的最短距离。
如果路径不存在,则输出 impossible。
数据范围 1≤n,m≤1e5,
图中涉及边长绝对值均不超过10000。
输入样例:

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

输出样例:

2

spfa算法判断负环
和bellman_ford的思路一样只要边>=n,(可能不止有1一个点相等),就说明有负环。
所以我们可以新建一个数组cot[], 来维护每个点当前最短路径的边数,用到的等式是当前点的变数等于上个点的路径边数加1,当其上个点为原点时原点边数为0,其边数就为1 。
因为是判断图中有没有负环,并不是最短路径上有没有负环,所以如果负环没有在最短路径中就不会被查到,所以就是要在初始化的时候将每一个点都加入队列,让每一个点都为原点就不会漏环。

例题:

spfa判断负环

给定一个 n个点 m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你判断图中是否存在负权回路。
输入格式
第一行包含整数 n和 m。
接下来 m行每行包含三个整数 x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。
输出格式
如果图中存在负权回路,则输出 Yes,否则输出 No。
数据范围 1≤n≤2000 , 1≤m≤10000 , 图中涉及边长绝对值均不超过 10000。
输入样例:

3 3
1 2 -1
2 3 4
3 1 -4

输出样例:

Yes

Floyd算法求多源汇最短路
用到三重循环,用邻接矩阵来存所有的点,原理是基于动态规划。循环的时候必须先循环k,之后的i和j顺序可以颠倒,循环结束之后d从表示边权重的一个矩阵变成了每个点最短路径的一个矩阵,d[i] [j] 表示的就是从i到j的最短路径长度。floyd算法也可以求有负权的图,但是和spfa算法一样都是不能存在负环。

for (int k = 0; k < n; k ++)
    for (int i = 0; i < n; i ++)
        for (int j = 0; j < n; j ++)
            d[i][j] = min(d[i][j], d[i][k] + d[k][j])

例题:

Floyd求最短路

给定一个 n个点 m条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定 k个询问,每个询问包含两个整数 x 和 y,表示查询从点 x到点 y的最短距离,如果路径不存在,则输出 impossible。
数据保证图中不存在负权回路。
输入格式
第一行包含三个整数 n,m,k。
接下来 m行,每行包含三个整数x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。
接下来 k 行,每行包含两个整数 x,y,表示询问点 x到点 y的最短距离。
输出格式
共 k行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible。
数据范围 1≤n≤200 , 1≤k≤n,1≤m≤20000, 图中涉及边长绝对值均不超过10000。
输入样例:

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

输出样例:

impossible
1

思路:当判断不存在的时候和bellman_ford算法相似,因为有负权边的存在,最后正无穷可能不会是我们设置的最大值,所以只要让他大于某个很大的数就行,例如我们设置的值除以2.在初始化的时候由于不存在负环所以自环一定是正的,所以可以选择保留或者删去,保留的话就是在更新的时候因为不会变小所以没关系,当然要是删去也行。

//保留自环的初始化
memset(d, 0x3f, sizeof d);
//删去自环的初始化
const int INF = 1e9; //在下面判断的时候就是用 d[i][j] > IOF / 2 来判断是否存在
for (int i = 1; i <= n; i ++)
    for (int j = 1; j <=n; j++)
        if (i == j) d[i][j] =0;
        else d[i][j] = INF;

树是无环连通图
图分为两种,一种是有向图例如a -> b 另外一种是无向图,a-b。
有向图就是记录从a到b的路径,无向图就是从a 到b可以,b到a也行,所以在存储无向图的时候就是存两个有向图,分别是a -> b 和 b -> a。
存储有向图的方法有两个一个是邻接矩阵,就是开一个二维数组,表示从a 到 b的边,如果存在存1,不存在存0,如果有权重就存权重。这种适合存比较稠密的图,比较稀疏的适合用邻接表

另外一种方法是邻接表,就是每个点都开一个单链表,用h[],表示表头,初始化的时候指向-1,数量级就是N(全部数据的个数)

memset(h, -1, sizeof h); //在头文件cstring中

之后每次存一个边,例如a - > b, 就在表头为a的单链表上从表头处插入b,单链表中存的值就是表头指向的那个点,其中每个单链表都是无序的,这个顺序也没有意义,因为当遍历的时候每次都将遍历到的e[],值作为一个头节点去遍历,表示的是遍历到的这个点之后指向的其他点,所以单链表是不存路径的,只是存单向边。

插入表头的方法,定义一个idx,表示当前可以用的下标,e[], 表示值,ne[],表示指向的值(即指向下一个数的指针),e[idx] = x, ne[idx] = h[表头], h[当前的表头] = idx++. 其中e[], ne[] 数量级可以开两倍的N,防止越界

深度优先遍历(即深搜dfs)

因为一般的题目每个点只会遍历一次,所以要开一个布尔数组st[N], 来存放是否遍历过

void dfs(int u ) // u 表示开始的点
{
    st[u] = true ; // 标记u以及遍历过
    for (int i = h[u]; i != -1; i = ne[i]) // u开始所以作为头节点,ne和h都是表示的是指向下一个数的指针
    {
        j = e[i] // 将值赋给j,j表明下一个遍历到的点,然后判断是否遍历过,若没有则递归dfs
        if (!st[j]) dfs(j);
    }
    
}

例题:

树的重心

给定一颗树,树中包含 n个结点(编号 1∼n)和 n−1 条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
输入格式
第一行包含整数 n,表示树的结点数。
接下来 n−1行,每行包含两个整数 a和 b,表示点 a和点 b之间存在一条边。
输出格式
输出一个整数 m,表示将重心删除后,剩余各个连通块中点数的最大值。
数据范围 1≤n≤1e5

输入样例:

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

输出样例:

4

思路:
利用dfs,每次都将遍历到的点,删除后,连通块的最大值求出,之后只要是将所有点代表的最大值的最小值求出就行。
如何求每个点删除之后的连通块的最大值,这里先是新建一个变量sum,表示的是这个点所连通的所有点的数量,一开始让 sum = 1, 表示只有一个点,之后让每次dfs完成之后返回一个sum,表示的是从某个点开始到所有可以遍历的点的数量,理论上,一开始是只有一条线,但是由于是有迭代,每个点的sum都加等于那个点之后每条线的点的数量,所以最后返回的sum就是那个点往下所连的点的数量(包括他本身,以及因为他不能向上回溯,只有下面的点回溯到他,所以sum是只包括了他本身,以及之后所连所有点的数量)。再新建一个变量res,这个变量要放在dfs函数中,表示的是每一个有一个单独的res(如果是放到外面,当迭代回溯的时候原来的那个res会变,而res不止比一次,而是有多少个子节点就比几次,所以在内存中这个正在遍历的点的res应该要被保留),当下一次dfs开始时 = 0初始化,表示的是这个点删除之后,最大的联通块所包含的点的数量,很显然,这个连通块分为两个部分,一个是这个点之后的连通块,以及这个点上面的节点所连接的连通块,之后的连通块其实就是dfs的返回值,所以用res = max{res, s},就是用当前的res和后面返回的s进行比较然后更新。
上面的连通块其实就是用全部个个数减去这个节点连接的后面的节点的个数(包括他本身,因为他被删除了),所以用n - sum ,之后就是更新res。完成之后res存的就是当前这个节点的最大连通块的值,之后更新答案只要是比原来的ans小就保存。ans = min(ans, res) // min和max在头文件algorithm中

重边 指的是有两个或多个重复的边,例如a -> b 有两个
自环 指的是一个元素的一条边指向自己,例如 a -> a
所有边的长度都是1,指的就是可以用宽搜来求最短路,否则可能要用动态规划(DP)

广度优先遍历(bfs)

和之前的bfs思路一致,用到的数据结构是队列,只不过是在图中进行的。
先是用邻接表来存图,

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

bfs中队列还是用数组来模拟

int d[N]; //表示的是起始点到某个点的距离
bool st[N]; //第一次遍历为最小值,所有要加一个判断
int q[N]; //模拟的队列

int bfs()
{
    int hh = 0, tt = -1;
    q[++tt] = 1; //看题目要求加入起始点
    st[1] = true;
    memset(d, -1, sizeof d) //初始化d,表示还没开始计算距离
    d[1] = 0; //初始化
    while(hh <= tt)
    {
        int t = q[hh++]; //取队头
        // 这边如果是矩阵的话就是要遍历四个点,但这个用的是邻接表,所以也要用邻接表的遍历方法来遍历,判断条件一样也是在遍历之后,如果符合条件就放到队列中,之后就是循环继续从上一个最先放进去的点开始,表示进行到下一层
        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if(!st[j])
            {
                st[j] = true;
                d[j] = d[t] + 1;
                q[++tt] = j;
            }
        }
    }
    return d[n]
}

例题:

图中点的层次

给定一个 n个点 m条边的有向图,图中可能存在重边和自环。
所有边的长度都是 1,点的编号为 1∼n。
请你求出 1号点到 n号点的最短距离,如果从 1号点无法走到 n号点,输出 −1。
输入格式
第一行包含两个整数 n和 m。
接下来 m行,每行包含两个整数 a和 b,表示存在一条从 a走到 b的长度为 1 的边。
输出格式
输出一个整数,表示 1号点到 n号点的最短距离。
数据范围 1≤n,m≤1e5
输入样例:

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

输出样例:

1

思路:求最短距离,然后所有边的长度为1,所以可以用广度优先遍历bfs。在输入的时候要记得,读入的是边,不是点。

在读双边的时候数据大小应该差不多是4N,因为单边是2N,双边差不多有两倍所以占4N.

拓扑序列,针对有向图,定义是每一条边x, y ,x都在y前面,就是说对于一个拓扑序列来讲,每一条边都是从前指向后的 ,例如1 -> 2, 1 -> 3, 2 -> 3 ,则说1 -> 2 -> 3 是一个拓扑序列
可以证明一个有向无环图一定存在一个拓扑序列(拓扑序一定是全部的点参与,只是排列的顺序或许会不同),所以有向无环图又被称为拓扑图

度数 , 一个数的入度指的是有几条边指向自己,出度指的是有几条指向其他点的边

一个有向无环图一定至少存在一个入度为0的点,一个有向无环图的拓扑序不一定是唯一的,拓扑序不一定就是值是从小到大排序,也可能是乱序,关键是从第一个头节点开始都是指向后,中间没有一个节点指向前,例如: 1 -> 2 ,1 - > 3 在这个图中 1 -> 2 -> 3 是拓扑序 1 - > 3 - > 2 也是拓扑序,他和上面那个例子的区别是上面有 2 -> 3,根据拓扑排序的定义,2因为指向3,所以他必须排在3之前,所以,只能是 1, 2, 3 ,而在这个例子中 2没有指向3,所以只要1同时满足在2和3之前就是拓扑序

求拓扑序的思路:
新建一个数组d[],在读入的时候,对于指向的边例如b,让其d[b] ++,之后bfs中先是把入度为0的边加入到队列中,然后就是和正常的dfs差不多了,遍历拓展的节点,然后让其入度减1,之后判断入度是否为0如果为0则加入到队列中,之后在这个头节点的子节点都拓展之后,就取下一个头节点(上一个被弹出)。之后在循环跑完之后判断tt == n - 1,tt初始化是为 - 1,一共n个点,如果n个点都加入,则说明没有环,(环形图没有突破口使其入度减一,环上的所有点最后的结果一定是形成孤立的环),所以输出拓扑序,否则输出-1,
输出拓扑序的方式,因为队列是只有在入度为0的时候才会加入,所以队列中从前往后的顺序一定是一个拓扑序,假如某个点在入度为0时被加入队列,则他有可能会有出度,然后指向后方,所以入队顺序就是一个拓扑序,因为是用数组来模拟,所以在输出的时候可以从0开始遍历到n - 1.

例题:

拓扑排序

给定一个 n个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。
请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1。
若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y之前,则称 A是该图的一个拓扑序列。
输入格式
第一行包含两个整数 n和 m。
接下来 m 行,每行包含两个整数 x 和 y,表示存在一条从点 x 到点 y 的有向边 (x,y)。
输出格式
共一行,如果存在拓扑序列,则输出任意一个合法的拓扑序列即可。
否则输出 −1。
数据范围 1≤n,m≤1e5
输入样例:

3 3
1 2
2 3
1 3

输出样例:

1 2 3

bfs就是宽度优先搜索,用到队列来保持一个先入先出的结构,队列开的长度应该是边长的平方表示的是全部点的个数,存的是位置,所以可以是用pair<int, int> 来存,

其中用组数模拟队列的方法就是定义两个指针,hh、tt ,hh = 0, tt = -1,存入元素时,++tt ,表示向后退一格,弹出队头时hh++,表示同时退后一个元素,同时原来的索引就不用管了,为的是节约时间,但是可以浪费一点内存

这样是为了,在后面加入的元素时可以优先把前面先加入的元素进行宽搜,为了标记位置是否已经来过,需要一个bool二维数组来储存,同时用一个int类型的二维数组来储存每一个点是第几个经过的点,当这个点是终点时,也就是成了从初始点到终点的距离,所以显然,初始点的值为0,表示还没有开始走,也就是头节点。因为是对没有个节点进行标记,所以第一次到达终点时,就是最短的路径,因为有队列在保持后面相同同一层的点,假如某一点要再走两格才可以到达,但是第一个点,就是我们拿出来的头节点可以在下一次的遍历中就到达终点,并给终点上了个标记,所以很显然就是,这个第一次经过的终点距离是最短的。

用来模拟向四个方向走可以新建两个数组,表示偏移量,例如dx = {-1,0,1,0},dy = {0,1,0,-1},分别表示(-1,0)、(0,1)、(1,0)、(0,-1),即向上,向右,向下,向左
之后只要for循环4次,表示四个坐标,同时加上限制条件(边界或其他),如果满足条件,则那个点的路径数为前面的路径数 + 1,然后将其放到对列中

所以为了保持这个最短的性质,每次再加入队列之前先进行判断是不是已经经过了,如果已经经过了就没有必要再经过一遍,所以就不用加入队列,从而有了一个循环结束的标志,就是当队列中没有元素时,即没有新的元素加入,然后老元素也已经全部走完时,就是表示表格中的所有元素都被走过了,此时输出终点的路径数就是最短路径数

关于输入以及边界判断,如果主要是看题目,来看是从1开始还是从0开始。以及对应的终点的坐标是否要减1

关于如何输出路径,可以再新建一个类型为pair<int, int> 类型的二维数组pre,来存放每个点之前的坐标,然后在输出的时候,可以用一个while循环来表示,while(x || y) 表示当x或y有一个不为0时,循环就继续,即如果x和y都为0时循环就结束,在逻辑上就是起始点没有前一个点,然后在每次进行扩张的时候都将当前点的上一个存入pre。

思考:这个时候如果已经知道了终点的坐标,而且只有一个询问,是不是可以在当循环遍历到终点时就return,但是如果是有多组询问,或许应该就是要让他bfs跑完,这样的话每次查询的时间复杂度就是O(1)的了。