acwing算法基础课第三讲搜索与图论复习总结

最近备考蓝桥杯,准备把之前算法基础课题目都复习一遍,冲(*^▽^*)

1.排列数字

这里的排列数字排列的是组合数

通过深度优先搜索来做

st[]数组判断每个数字是否使用过(因为组合数排列,每个数字只能用一次)

深度优先搜索的题目可以先画出递归搜索树

acwing算法基础课第三讲搜索与图论复习总结_第1张图片

依据枚举到了哪个位置来进行搜索dfs(u)

如果u从0开始搜索,当u==n时说明0-n-1这n个位置已经搜索完了,输出一组排列,return回上一层递归,进行下一次搜索

#include 
#include 
#include 
using namespace std;
const int N = 10;
int n;
int s[N];//输出最后的序列
bool st[N];//判断每个数是否已选过
void dfs(int u)
{
    if(u==n)//当到第n个位置,说明0-n-1层已经枚举好了,所以输出结果并返回上一层即可
    {
        for(int i=0;i>n;
    dfs(0);//从第0个位置开始搜
    return 0;
}

2.n-皇后问题

 这题非常经典,初学dfs的必做题

题目概述:(不浪费时间,复制粘贴一下拉倒了,不清楚题目的可以搜索一下)

n−皇后问题是指将n个皇后放在n×n的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。

这题有两种解题策略,对应了两种搜索的顺序

顺序1:

依据每一行来搜索

假设搜索到了第u行,u从0开始,那么退出循环,递归return返回上一层的条件应该是u==n,即行数超出了棋盘

枚举这行的每一列,看看哪个列是可以放棋子的,如果可以放,在该位置放下棋子,再递归到下一层,不可以放就看下一个位置,如果该行所有位置都不能放棋子,就说明失败,上一层就出了问题,返回上一层

相当于一个多叉搜索树,每一行有n中选择,排除掉不可行的即可。

第u行第i列可以放棋子的条件:

第i列无棋子,左斜右斜对角线无棋子

(1)数组col[i]表示第i列上是否已经有了棋子

(2)数组dg[i]表示(i,j)所在的左斜对角线是否可以放棋子,递归时候是第u行,第i列,y=-x+b,带入(u,i),得到截距b=i+u,这里截距必大于0,用udg[i+u]来表示第u行第i列左斜对角线上是否有棋子

(3)数组udg[i]表示(i,j)所在的右斜对角线是否可以放棋子,由于递归时候是第u行,第i列,y=x+b,带入(u,i),得到对应的截距,b=i-u,为了确保截距>0,所以用dg[i-u+n]来表示第u行第i列右斜对角线上是否有棋子

对角线数组大小开2*N比较合理

#include 
#include 
#include 
using namespace std;
const int N = 10;
char g[N][N];
int n;
bool col[N],dg[2*N],udg[2*N];

void dfs(int u)
{
    if(u==n)
    {
        for(int i=0;i>n;
    for(int i=0;i

顺序2:

dfs(x,y,s)

依次枚举格子上的每一个点(x,y)放棋子或者不放棋子s是已经放的棋子的个数,当s>n,return

相当于一个二叉搜索树

当y==n,右端点出界,则行数x++,列数y=0,

当x=n时输出最终的结果,输出完返回上一个点,继续判断上一个位置的另一种情况(放/不放)

当棋子个数s>n的时候也返回上一层

判断棋子能不能放的条件这里会多一个行row[],判断该行有没有放过棋子

同样的在判断左右斜对角线时y=x+b;b=y-x,为了保证b>0,b=y-x+n

y=-x+b;b=x+y  

#include 
#include 
#include 
using namespace std;
const int N = 10;
char g[N][N];
bool col[N],row[N],dg[2*N],udg[2*N];
int n;

void dfs(int x,int y,int s)//(x,y)当前搜索到的点,s是已经放了几个棋子
{
    if(y==n) x++,y=0;//如果列数出界,那么到下一行第一个
    if(x==n)//到了最后一行,看看棋子个数是不是符合条件n
    {
        if(s==n)
        {
            for(int i=0;i>n;
    for(int i=0;i

3.走迷宫

题目描述:

给定一个n*m的二维整数数组,用来表示一个迷宫,数组中只包含0或1,其中0表示可以走的路,1表示不可通过的墙壁。

最初,有一个人位于左上角(1, 1)处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。

请问,该人从左上角移动至右下角(n, m)处,至少需要移动多少次。

数据保证(1, 1)处和(n, m)处的数字为0,且一定至少存在一条通路。

bfs入门题:从(1,1)开始走,走到(n,m)

将起点(1,1)放入队列中,每次遍历上下左右四个方向,将遍历到的点放入队列,然后下次遍历弹出队列第一个点,再用这个点遍历上下左右四个方向即可

队列queue,可以数组模拟(见基础课的第二讲),也可以STLqueue

#include 
#include 
#include 
#include 
using namespace std;
const int N = 110;
int n,m;
int dist[N][N];//存储每个点到起点的距离
int g[N][N];//存储地图

typedef pair PII;

int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};//定义四个方向

void bfs(int start,int end)
{
    queue q;
    
    memset(dist,-1,sizeof dist);//每个点到起点距离初始化为-1,-1代表没走过
    dist[start][end]=0;
    q.push({start,end});
    
    while(q.size())
    {
        PII t=q.front();
        q.pop();
        for(int i=0;i<4;i++)
        {
            int x=t.first+dx[i],y=t.second+dy[i];
            if(x>=0&&x=0&&y>n>>m;
    for(int i=0;i>g[i][j];
        }
    }
    bfs(0,0);
    cout<

4.八数码

在一个3×3的网格中,1~8这8个数字和一个“X”恰好不重不漏地分布在这3×3的网格中。

例如:

1 2 3
X 4 6
7 5 8
在游戏过程中,可以把“X”与其上、下、左、右四个方向之一的数字交换(如果存在)。

我们的目的是通过交换,使得网格变为如下排列(称为正确排列):

1 2 3
4 5 6
7 8 X
例如,示例中图形就可以通过让“X”先后与右、下、右三个方向的数字交换成功得到正确排列。

交换过程如下:

1 2 3      1 2 3      1 2 3       1 2 3
X 4 6  ->4 X 6  -> 4 5 6  -> 4 5 6
7 5 8      7 5 8      7 X 8      7 8 X
现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。

与走迷宫问题一样:这题相当于传递给定一个start的状态,如s=“123x46758”,最终变为状态"12345678x"

先找到x的位置,每次可以与上下左右四个方向的数交换

注意点:

(1)但是string中的一维坐标得转化为二维坐标  如x初始的位置为下标3 在3*3方格中的坐标为

(3/3,3%3)  所以一维中的下标若为k 在二维中的位置就为(k/n,k%n)

(2)用unordered_map  dist来存储达到每个string状态需要的最小距离

  1. #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    unordered_map dist;
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    
    int bfs(string start)
    {
        queue q;
        dist[start]=0;
        q.push(start);
        
        while(q.size())
        {
            string t=q.front();
            q.pop();
            int d=dist[t];
            if(t=="12345678x") return d;//当已经获取最终状态的距离时候,就返回距离
            
            int k=t.find('x');//找到x的下标
            int x=k/3,y=k%3;//求得x在3*3网格中的二维下标
            
            for(int i=0;i<4;i++)
            {
                int a=dx[i]+x,b=dy[i]+y;
                if(a>=0&&a<3&&b>=0&&b<3)//不出界
                {
                    swap(t[k],t[a*3+b]);
                    if(!dist.count(t))//如果t这个状态是第一次出现
                    {
                        dist[t]=d+1;
                        q.push(t);
                    }
                    swap(t[k],t[a*3+b]);
                    //由于这里直接改变了原来的状态,下一次枚举上下左右的另一种状态时要用到原状态
                    //所以要恢复原状态
                }
            }
        }
        return -1;//无法达到最终状态,返回-1
    }
    int main()
    {
        string s;
        for(int i=0;i<9;i++)
        {
            char c;
            cin>>c;
            s+=c;
        }
        cout<

5.树的重心

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

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

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

acwing算法基础课第三讲搜索与图论复习总结_第2张图片

任选一个节点u作为dfs的入口,由于是无向图,会遍历整棵树的每个节点

对于每个节点u,都求一下其所各个子树的节点个数s1,s2,s3,...,这些节点个数和+1就是以该点为根的树的节点个数Su

那么最后删掉该点后的节点个数的最大值会在res=max(s1,s2,s3,...)和n-Su(删掉这棵树剩下的节点个数)中出现

在dfs的过程最后对每个点都求一次ans=min(res,n-Su),由于会遍历到所有的节点,所以最后的结果就是需要的删掉某个点后剩余连通块中节点个数最大值最小的方案

#include 
#include 
#include 
using namespace std;
const int N = 1e5+10,M = 2 * N;
int h[N],e[M],ne[M],idx;
bool st[N];//判断每个点是否被遍历过
int ans=N;//初始化为最大值
int 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 Su=1;//当前该树就自己一个结点
    int res=0;//存子树中节点最多多少
    for(int i=h[u];i!=-1;i=ne[i])
    {
        int j=e[i];
        if(!st[j])
        {
            int s=dfs(j);//递归以j为根的子树的节点个数
            Su+=s;//累加到Su上
            res=max(s,res);
        }
    }
    res=max(res,n-Su);//删除u点后的最大连通块的最大值
    ans=min(ans,res);//看看能否更新ans
    return Su;
}
int main()
{
    memset(h, -1, sizeof h);
    cin>>n;
    for(int i=0;i>a>>b;
        add(a,b);
        add(b,a);
    }
    dfs(1);
    cout<

6.图中点的层次

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

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

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

输入格式

第一行包含两个整数 n 和 m。

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

输出格式

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

遍历图的模板题,当每条边的权值为1,bfs有能够求最短距离的特性,因为当权值为1时,第一次遍历到一个点距离的一定是起点到该点距离的最小值

acwing算法基础课第三讲搜索与图论复习总结_第3张图片

#include 
#include 
#include 
#include 

using namespace std;
int n,m;
const int N = 1e5+10;//有向图,N个点N-1条边
int h[N],e[N],ne[N],idx;
int dist[N];

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

void bfs()
{
    queue q;
    
    memset(dist,-1,sizeof dist);
    dist[1]=0;
    q.push(1);
    
    while(q.size())
    {
        int t=q.front();
        q.pop();
        
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(dist[j]==-1)
            {
                dist[j]=dist[t]+1;
                q.push(j);
            }
        }
    }
}
int main()
{
    memset(h, -1, sizeof h);
    cin>>n>>m;
    for(int i=0;i>a>>b;
        add(a, b);
    }
    bfs();
    cout<

 7.有向图的拓扑排序

给定一个 n 个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。

请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1。

若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。

本题是求拓扑序列的一个模板题

小性质:一个有向无环图一定存在拓扑序列,被称为拓扑图

设置一个数组d[N],用来存储每个节点的入度

根据拓扑序列的性质,入度为0的点一定可以排在前面

1.先将所有入度为0的点依次入队列q[]

2.取队头的值,然后将所有其指向的节点的入度-1

循环上面过程,直到hh>tt,说明所有点入队

当出循环时,如果队列中数的个数不足n个,说明有环,不能构成拓扑序列

本题用数组模拟队列比较好,因为数组模拟并不是真的弹出数据,只是下标向后移动,最后拓扑序列就保存在数组里,输出队列即可

#include 
#include 
#include 
#include 
using namespace std;
int n,m;
const int N = 1e5+10;
int q[N];
int h[N], e[N], ne[N], idx;
int 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;
}
int main()
{
    cin>>n>>m;
    memset(h, -1, sizeof h);
    for(int i=0;i>a>>b;
        add(a, b);
        d[b]++;
    }
    if(topsort())
    {
        for(int i=0;i

8.朴素版Dijkstra--适合稠密图(n小m大)O(n~2+m)

acwing算法基础课第三讲搜索与图论复习总结_第4张图片

acwing算法基础课第三讲搜索与图论复习总结_第5张图片

 注意:若要求任意点i到任意个点j的最短距离,只需修改dijkstra方法中的起源位置dist[i] = 0,以及返回为dist[j]

#include 
#include 
#include 
using namespace std;

const int N = 510,INF = 0X3f3f3f3f;
int n,m;
int g[N][N];//领接矩阵存图
int dist[N];//记录每个节点到源点的距离
bool st[N];//判断当前节点是否已经确定到起点的最小距离,已确定的话会放入集合中

int dijkstra()
{
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    for(int i=0;i>n>>m;
    memset(g,0x3f,sizeof g);
    for(int i=0;i>a>>b>>c;
        g[a][b]=min(g[a][b],c);
    }
    cout<

 9.堆优化版Dijkstra--O(mlog(n))

每次找当前未确定的点中到起点距离最小的点时候,从小根堆中取出第一个数即可

且用领接表存储,在遍历一个点所有相连的点时候,不需要遍历所有点,只要遍历领接表上连接的点即可

#include 
#include 
#include 
#include 
using namespace std;
const int N = 150010,M = 2 * N;
int n,m;
int h[N],e[M],ne[M],w[M],idx;
int dist[N];
bool st[N];
typedef pair PII;

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

int dijkstra()
{
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    
    priority_queue,greater> heap;//小根堆
    //first为到起点的距离,second为点的序号
    heap.push({0,1});
    
    while(heap.size())
    {
        PII t=heap.top();
        heap.pop();
        
        int ver=t.second,distance=t.first;
        if(st[ver]==true) continue;//找的是未确定最小距离中离起点距离最小的点
        st[ver]=true;
        
        for(int i=h[ver];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(dist[j]>distance+w[i])
            {
                dist[j]=distance+w[i];
                heap.push({dist[j],j});
            }
        }
    }
    if(dist[n]==0x3f3f3f3f) return -1;
    return dist[n];
}
int main()
{
    memset(h, -1, sizeof h);
    cin>>n>>m;
    for(int i=0;i>a>>b>>c;
        add(a, b, c);
    }
    cout<

10.有边数限制的最短路(Bellman-ford算法)

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。(dijkstra得没有负权边)

请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible

注意:图中可能 存在负权回路 。

acwing算法基础课第三讲搜索与图论复习总结_第6张图片

 Bellman - ford 算法是求含负权图的单源最短路径的一种算法,效率较低,代码难度较小。其原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在 n-1 次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。
(通俗的来讲就是:假设 1 号点到 n 号点是可达的,每一个点同时向指向的方向出发,更新相邻的点的最短距离,通过循环 n-1 次操作,若图中不存在负环,则 1 号点一定会到达 n 号点,若图中存在负环,则在 n-1 次松弛后一定还会更新)

bellman - ford算法的具体步骤
for n次
for 所有边 a,b,w (松弛操作)
dist[b] = min(dist[b],back[a] + w)

注意:back[] 数组是上一次迭代后 dist[] 数组的备份,若不进行备份会因此发生串联效应,影响到下一个点。

什么是串联效应:

acwing算法基础课第三讲搜索与图论复习总结_第7张图片

如图所示:每次迭代枚举每条边,

如果没有备份数组

第一次迭代,边1->2 dist[2]=1,边1->3 dist[3]=3   *注意:边2->3,将dist[3]更新成了2

但是我们第k次迭代要求的是经过最多k条边走到该点的最小值

这里dist[3]在第一次迭代中就得到了经过两条边走到该点的最小值

显然是不合题意的

原因就在于没有使用备份数组,备份上一次迭代的dist结果

导致本次迭代刚刚更新完的结果又去更新下面的点

如果题目没有最多经过k条边的限制,那么无所谓用不用备份数组

因为假如走到n点,不用备份数组的话,第k次迭代就是经过不少于k条边走到n点的最短距离,但最终如果提前达到最小值,会不再更新,所以最终还是经历了n-1条边到达n点的最小值。

但如果存在负环,就会在n-1次迭代后还在更新

#include 
#include 
#include 
using namespace std;
const int N = 510,M = 10010;
int n,m,k;//n个点,m条边,经过不超过k条边
struct Edge{
    int a,b,w;
}edge[M];
int dist[N],backup[N];

void bellman_ford()
{
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    for(int i=0;i>n>>m>>k;
    for(int i=1;i<=m;i++)//输入m条边
    {
        int a,b,w;
        cin>>a>>b>>w;
        edge[i]={a,b,w};
    }
    bellman_ford();
    if(dist[n]>0x3f3f3f3f/2) cout<<"impossible"<

11.spfa求最短路(对Bellman_ford做优化)

acwing算法基础课第三讲搜索与图论复习总结_第8张图片

bellman-ford算法是每次迭代将所有的边都枚举一遍

事实上:

再看此图:dist[b]如果变小了,因为a--->b的边的权值是不可能变小的,那么dist[a]一定变小了

只有dist[a]变小的点才能去更新dist[b],所以没必要枚举所有的边,只要枚举所有dist[a]变小的边即可

acwing算法基础课第三讲搜索与图论复习总结_第9张图片

 所以用一个队列来存

每次取出队头,枚举其所有出边,如果出边能被更新,放入队列,它有更新其他点的价值

这里要用一个st数组来判断每个点是否在队列当中,因为如果在已经在队列里面了,没有重复放进去的必要

*注意:dijkstra算法中的st数组是来判断是否一个点的最短距离已经确定,dijkstra每次循环都选择当前到起点距离最短且还未确定最短距离的点,放入st中,表明其已经确定最短距离,与spfa是不同的,一旦放入st数组,就不能再次被更新,spfa每个点可以被重复更新

#include 
#include 
#include 
#include 
using namespace std;
int n,m;
const int N = 1e5+10,M = 2 * N;
int dist[N];
bool st[N];//判断每个点在不在队列里
int h[N], e[M], w[M], ne[M], idx;

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

void spfa()
{
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    
    queue q;
    q.push(1);
    st[1]=true;
    
    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])//j不在队列中才将其放入队列
                {
                    q.push(j);
                    st[j]=true;
                }
            }
        }
    }
}
int main()
{
    memset(h, -1, sizeof h);
    cin>>n>>m;
    for(int i=0;i>a>>b>>c;
        add(a, b, c);
    }
    spfa();
    if(dist[n]==0x3f3f3f3f) cout<<"impossible"<

12.spfa判断负环

852. spfa判断负环 - AcWing题库

判断负环:
这里的dist[N]数组的含义已经完全改变,是从负边出现才开始能够更新dist变得更小,表示了出现负
边的位置
设置一个新的数组cnt[N],从第一次出现负边的点x开始,该负边延伸的最大边长为cnt[x],
cnt等于负环上的结点数,没有负环的话,结点数会 做多再往后延伸到一共n-1个点,如果有负环的话,则结点数会一直变多到无穷,所以只要判断
cnt[]是否>=n即可判断负环

注意:由于不知道是负环是会经过哪些节点,所以像spfa求最短路一样,把1点放进去是不对的,
因为可能1到不了负环那个入口,所以一开始要把所有点放入队列

#include 
#include 
#include 
#include 
using namespace std;
const int N = 2010,M = 10010;
int n,m;
int h[N],e[M],ne[M],w[M],idx;
int dist[N];
int cnt[N];
bool st[N];

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

bool spfa()
{
    queue q;
    for(int i=1;i<=n;i++) {
        q.push(i);
        st[i]=true;
    }
    
    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];
                cnt[j]=cnt[t]+1;
                
                if(cnt[j]>=n) return true;
                
                if(!st[j]){
                    q.push(j);
                    st[j]=true;
                }
            }
        }
    }
    return false;
}

int main()
{
    memset(h,-1,sizeof h);
    cin>>n>>m;
    for(int i=0;i>a>>b>>c;
        add(a,b,c);
    }
    if(spfa()) cout<<"Yes"<

你可能感兴趣的:(算法,图论,深度优先)