AcWing.算法基础课-第三章 搜索与图论

DFS

        dfs是一种深度优先搜索算法,他秉持着不装南墙不回头的性格,一直会沿着当前道路走到头才会换道路,对于方案数问题我们一般用dfs的方式去枚举并判断合法性。主要递归实现搜索过程。

        而一般在深度优先搜索算法中往往伴随着两个关键操作,回溯和剪枝;

        回溯就到达终点后一层一层回到上个状态,选择不同的道路,这里我们我进行回复现场的操作,就是把动过的东西还原回去,不影响下次dfs.

        剪枝,顾名思义减去不用的枝条也就是把那些当前看来已经不可能的选项排除掉,剪枝的方式有很多,例如:最优性剪枝(如果当前结果没有之前的优秀那么就直接删除这个道路)....

dfs模板

void 函数名(参数)//回溯算法的参数一般比较多,要根据具体情况进行分析
{
    if(终止条件)
      {
         收集最后节点结果
         return  ;
      }
//单层搜索的逻辑:
    for(集合的元素)//遍历的是集合里的每一个元素,也可能是集合节点的子节点个数
      {
          处理节点
          递归函数
          回溯操作(撤销处理节点的操作)
      }
    return ;

例题讲解

排列数字

//遍历每个数字如果没有用过就添加进答案数组,一组弄完后回复现场即可(取消标记数字)
#include<bits/stdc++.h>
using namespace std;

const int N = 1e6+10;
int n; 
int a[N],v[N];

void dfs(int x){
    if(x == n){
        for(int i = 0; i < n; i++) cout << a[i] << " ";
        cout << endl;
    }
    for(int i = 1; i <= n; i++){
        if(!v[i]){
            v[i] = 1; //标记
            a[x] = i; //加入答案
            dfs(x+1);
            v[i] = 0; //回复现场
        }
    }
}

int main()
{
    cin >> n;
    dfs(0);
    return 0;
}

n-皇后问题

//n皇后问题是dfs的经典问题。解决方法也有很多,这里推荐比较经典的方式
//主要用数学方位标记,判断如果放这个点那么横纵,正对角线,反对角线有没有冲突。
#include<bits/stdc++.h>
using namespace std;

const int N = 1e2+10;
int n;
char g[N][N];
bool col[N],dg[N],udg[N];  //col横纵,dg正对角线,udg反对角线

void dfs(int u){
    if(u == n){
        for(int i = 0; i < n; i++){
            for(int j = 0; j < n; j++) cout << g[i][j];
            cout << endl;
        }
        cout << endl;
    }
    for(int i = 0; i < n; i++){
        if(!col[i] && !dg[u+i] && !udg[n-u+i]){
            //标记
            g[u][i] = 'Q';
            col[i] = dg[u+i] = udg[n-u+i] = 1; 
            dfs(u+1);
            //恢复现场
            col[i] = dg[u+i] = udg[n-u+i] = 0; 
            g[u][i] = '.';
        }
    }
}

int main()
{
    cin >> n;
    for(int i = 0; i < n; i++){
        for(int j = 0; j < n; j++) g[i][j] = '.';
    }
    dfs(0);
    return 0;
}

BFS

        广度优先搜索(BFS),顾名思义一层一层扩散式遍历,主要是队列实现搜索过程,把当前状态下面所以状态全算出来再进行下一层节点遍历,由于这种一层层搜索的过程,所以它常用来解决最短路径问题,可证明这样做第一次遍历到一定是最短的。

bfs模板

Q.push(初始状态); //将初始状态入队
while(!Q.empty()){
    auto u = Q.front(); q.pop();
    for(枚举所有可扩展的点) //找到u所有可达状态v
        if(是合法的) //v需要满足一些条件
            Q.push(v);
            维护其他信息.....
}

例题讲解

走迷宫

#include<bits/stdc++.h>
using namespace std;

typedef pair<int,int> PII;
const int N = 1e3+10;
int n,m;
int g[N][N],dist[N][N]; //g地图,dist步数
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1}; //方向数组,控制每次行走方向


int bfs(){
    memset(dist,-1,sizeof dist); //初始化,用于判断此点是不是第一次到
    dist[1][1] = 0;
    queue<PII> q;
    q.push({1,1});
    while(!q.empty()){
        auto t = q.front(); q.pop();
        for(int i = 0; i < 4; i++){
            //找到扩展新点
            int sx = t.first+dx[i];
            int sy = t.second+dy[i];
            if(sx >= 1 && sy >= 1 && sx <= n && sy <= m && g[sx][sy] == 0 && dist[sx][sy] == -1){
                dist[sx][sy] = dist[t.first][t.second]+1;
                q.push({sx,sy});
            }
        }
    }
    cout << dist[n][m] << endl; //输出答案即可
}




int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++) cin >> g[i][j];
    }
    bfs();
    return 0;
}

八数码

        这道题的思路分析比较难,就是用二维数组和一维数组来回对应,其余还像找最短路那样,方向数组不断扩展,直到找到正确答案。

#include<bits/stdc++.h>
using namespace std;

int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};


int bfs(string start){
    string end = "12345678x"; //记录一下答案如果找到就停止
    queue<string> q;
    unordered_map<string,int> d;
    q.push(start);
    d[start] = 0;
    while(!q.empty()){
        auto t = q.front(); q.pop();
        int dis = d[t];
        if(t == end) return dis;
        //状态转移
        int k = t.find('x');
        int x = k/3,y = k%3; //转换二维数组下标即xy
        for(int i = 0; i < 4; i++){
            int a = x+dx[i],b  = y+dy[i];
            if(a >= 0 && a < 3 && b >= 0 && b < 3){
                swap(t[k],t[a*3+b]); //t的a*3+b是一维数组的下标
                if(!d.count(t)){
                    d[t] = dis+1;
                    q.push(t);
                }
                swap(t[k],t[a*3+b]); //恢复现场
            }
        }
    }
    return -1;
}

int main()
{
    string start;
    for(int i = 0; i < 9; i++){
        char c; cin >> c;
        start += c;
    }
    cout << bfs(start) << endl;
    return 0;
}

树和图的遍历与储存

树与图的存储

树是一种特殊的图,与图的存储方式相同。
对于无向图中的边ab,存储两条有向边a->b, b->a。
因此我们可以只考虑有向图的存储。

(1) 邻接矩阵:g[a][b] 存储边a->b

(2) 邻接表:

// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx;

// 添加一条边a->b
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

// 初始化
idx = 0;
memset(h, -1, sizeof h);

树与图的遍历

时间复杂度 O(n+m), n 表示点数,m表示边数

(1) 深度优先遍历 —— 模板题 AcWing 846. 树的重心

int dfs(int u)
{
    st[u] = true; // st[u] 表示点u已经被遍历过

    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j]) dfs(j);
    }
}

(2) 宽度优先遍历 —— 模板题 AcWing 847. 图中点的层次

queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
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 (!st[j])
        {
            st[j] = true; // 表示点j已经被遍历过
            q.push(j);
        }
    }
}

拓扑排序

时间复杂度 O(n+m), n 表示点数,m 表示边数

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

const int N = 1e5;
int e[N],ne[N],h[N],idx,d[N],v[N],st[N],pre[N],cnt = 0;
int n,m; 

long long sum = 0;

void topsort(){
    queue<int> q;
    for(int i = 1; i <= n; i++){
        if(d[i] == 0){ //统计出入度为0的点
            pre[++cnt] = i;
            q.push(i);
            
        }
    }
    while(!q.empty()){
        auto t = q.front(); q.pop();
        for(int i = h[t]; i != -1; i = ne[i]){
            int j = e[i];
            d[j]--;
            if(d[j] == 0){
            	q.push(j);
            	pre[++cnt] = j;
			}
        }
    }
    //如果还有一个点入度大于0表示没有构成topsort
    for(int i = 1; i <= n; i++){
        if(d[i] > 0){
            cout << -1 << endl;
            return ;
        }
    }
    for(int i =  1; i <= n; i++) cout << pre[i] << " ";
    cout << endl;
}

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

int main(){
	memset(h,-1,sizeof h);
	memset(st,0,sizeof st);
	cin >> n >> m;
	for(int i = 1; i <= m; i++){
		int a,b; cin >> a >> b;
		add(a,b); 
		d[b]++; //标注入度
	}
	topsort();
}

朴素dijkstra算法

时间复杂是 O(n2+m), n 表示点数,m 表示边数

int g[N][N];  // 存储每条边
int dist[N];  // 存储1号点到每个点的最短距离
bool st[N];   // 存储每个点的最短路是否已经确定

// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    for (int i = 0; i < n - 1; i ++ )
    {
        int t = -1;     // 在还未确定最短路的点中,寻找距离最小的点
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        // 用t更新其他点的距离
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);

        st[t] = true;
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

堆优化版dijkstra

void dijkstra(){
    memset(st, 0, sizeof st);
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;
    priority_queue<PII,vector<PII>,greater<PII> > q;
    q.push({dist[1],1});
    while (!q.empty()){
        auto t = q.top(); q.pop();
        int k = t.second,dis = t.first;
        if(st[k]) continue;
        st[k] = 1;
        for(int i = h[k]; i != -1; i = ne[i]){
            int j = e[i];
            if(dist[j] > dis+w[i]){
                dist[j] = dis+w[i];
                q.push({dist[j],j});
            }
        }
    }
}

Bellman-Ford算法

时间复杂度 O(nm), n 表示点数,m 表示边数.

int n, m;       // n表示点数,m表示边数
int dist[N];        // dist[x]存储1到x的最短路距离

struct Edge     // 边,a表示出点,b表示入点,w表示边的权重
{
    int a, b, w;
}edges[M];

// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
    for (int i = 0; i < n; i ++ )
    {
        for (int j = 0; j < m; j ++ )
        {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            if (dist[b] > dist[a] + w)
                dist[b] = dist[a] + w;
        }
    }

    if (dist[n] > 0x3f3f3f3f / 2) return -1;
    return dist[n];
}

模板题 AcWing 853. 有边数限制的最短路

#include<iostream>
#include<cstring>
using namespace std;
 
const int N = 1e5*5;
int x,m,n,dist[N],last[N];
 
struct Node{
	int u,v,w;
} a[N];
 
void bellman_ford(int st,int k){
	memset(dist,0x3f,sizeof(dist));
	dist[st] = 0;
	for(int i = 0; i < k; i++){ //松弛k次就是1-n经过k条边的最短路
	    memcpy(last, dist, sizeof dist); //这个数组是为了避免这一层数据覆盖了上一层的
		for(int j = 0; j < m; j++){
			auto e = a[j];
			dist[e.v] = min(dist[e.v],last[e.u]+e.w);
		} 
	}
}
 
int main(){
    int k;
	cin >> n >> m >> k;
	for(int i = 0; i < m; i++){
		int aa,b,c; cin >> aa >> b >> c; 
		a[i] = {aa,b,c};
	}
	bellman_ford(1,k);
	if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
	else cout << dist[n] << endl;
	return 0;
}

spfa 算法

        时间复杂度 平均情况下 O(m),最坏情况下 O(nm), n 表示点数,m 表示边数

        算是Bellman-Ford的一个优化。但是最坏情况下会退化到Bellman-Ford的时间复杂度,被菊花图卡。

求最短路

int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储每个点到1号点的最短距离
bool st[N];     // 存储每个点是否在队列中

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;

    while (q.size())
    {
        auto 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,则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

判负环

bool spfa(){
    memset(dist,0x3f,sizeof dist);
    queue<int> q;
    for (int i = 1; i <= n; i ++ )
    {
        st[i] = true;
        q.push(i);
    }
    while(!q.empty()){
        auto t = q.front(); q.pop();
        st[t] = 0;
        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 1;
                if(!st[j]){
                    st[j] = 1;
                    q.push(j);
                }
            }
        }
    }
    return 0;
}

floyd算法

时间复杂度是 O(n3), n 表示点数。

//初始化:
    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;

// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

最小生成树

Prim算法

算法流程如下:

 Prim算法(普里姆算法)一般是从图中一个点出发,让后找到与他相邻且点到集合边权最小,去联通,如此重复直到覆盖整个图为止。需要注意的是更新顺序,如果统计边权的话,需要先统计,在更新,不然在某种情况下更新会出错(如有负环)

        prim判断是否可以构成一颗最小生成树的方法是如果找到的最小点为我当前声明的最大值,则表明他与其他点不连通,自然构不成一颗最小生成树。

        当然他的时间复杂度也不是很优秀,O(n^2),但是我们可以用堆优化查找最小值那一步,但是这样做有些复杂,这里不推荐。如果遇到稀疏图就用Kruskal算法,稠密图用Prim朴素版就好了。

模板代码如下:

#include<bits/stdc++.h>
using namespace std;
 
const int N = 1e3+10,INF = 0x3f3f3f3f;
 
int n,m; 
int g[N][N],dist[N];
bool st[N];
 
int prim(){
    memset(dist,0x3f,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];
        //因为最小生成树是看当前的点到集合的距离而不是源点距离所以是min(dist[j],g[t][j])比较
        for(int j = 1; j <= n; j++) dist[j] = min(dist[j],g[t][j]);
        st[t] = true;
    }
    return res;
}
 
int main()
{
    cin >> n >> m;
    memset(g,0x3f,sizeof g);
    while(m--){
        int a,b,c; cin >> a >>b >> c;
        g[a][b] = g[b][a] = min(g[a][b],c); //重边保留较小的一条
    }
    int t = prim();
    if(t == INF) cout << "impossible" << endl;
    else cout << t << endl;
    return 0;
}

Kruskal算法

        Kruskal(克鲁斯卡尔)算法,他是先进行排序,让后在从小到大去选择,如果当前元素a和b已经连通就不去管他,如果不连通就直接连通,并且把权值放入总值中。也是很好实现。因为从小到大排序,所以说每次选择一定是最优。

        证明:

        在这里我们如果不选这条边,那么最后的生成树一定包含一条不小于这条边的边,那么我们把这条边替换上去,结果不会变差,可证这是最优解。

#include<bits/stdc++.h>
using namespace std;
 
const int N = 1e6+10;
 
int n,m,res,cnt; 
int p[N];
 
struct Node{
    int a,b,w;
} a[N];
 
bool cmp(Node a, Node b){
    return a.w < b.w;
}
 
int find(int x){
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}
 
int main(){
    cin >> n >> m;
    for(int i = 1; i <= n; i++) p[i] = i;
    for(int i = 0; i < m; i++){
        int aa,b,w;
        cin >> aa >> b >> w;
        a[i] = {aa,b,w};
    }
    sort(a,a+m,cmp);
    for(int i = 0; i < m; i++){
        int aa = a[i].a,b = a[i].b,w = a[i].w;
        aa = find(aa),b = find(b);
        if(aa != b){  //判断是否连通 
            p[aa] = b;
            res += w;
            cnt++;
        }
    }
    if(cnt < n-1) cout << "impossible" << endl; //判断是否能组成最小生成树 
    else cout << res << endl;
    return 0;   
}

二分图

染色法判定二分图

#include<bits/stdc++.h>
using namespace std;

const int N = 1e6+10;
int h[N],ne[N],e[N],color[N],idx;
int n,m; 

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

bool dfs(int u,int c){
    color[u] = c;
    for(int i = h[u]; i != -1; i = ne[i]){
        int j = e[i];
        if(!color[j]){
            if(!dfs(j,3-c)) return false; //如果是1那么染成2,是2染成1,就是3-c
        }
        //如果已经染色,但是与我当前这个颜色发生冲突,那么说明不是二分图
        else if(color[j] == c) return false; 
    }
    return true;
}

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

匈牙利算法求二分图的最大匹配

#include<bits/stdc++.h>
using namespace std;

const int N = 1e6+10;
int n1,n2,m; 
int h[N],ne[N],e[N],idx;
int match[N]; //表示现在n2的点和那个n1的点匹配,y总:找这个妹子和哪个男生在一起
bool st[N];

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] == 0 || find(match[j])){
                match[j] = x;
                return true;
            }
        }
    }
    return false;
}

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

int main()
{
    memset(h, -1, sizeof h);
    cin >> n1 >> n2 >> m;
    for(int i = 1; i <= m; i++){
        int a,b; cin >> a >> b;
        add(a, b);
    }
    int res = 0;
    for(int i = 1; i <= n1; i++){
        memset(st, 0, sizeof st);
        if(find(i)) res++;
    }
    cout << res << endl;
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值