算法基础课 --- 搜索与图论

目录

代码部分

DFS

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

1_597ec77c49-8-queens.png

现在给定整数 n,请你输出所有的满足条件的棋子摆法。

输入格式

共一行,包含整数 n。

输出格式

每个解决方案占 n 行,每行输出一个长度为 n 的字符串,用来表示完整的棋盘状态。

其中 . 表示某一个位置的方格状态为空,Q 表示某一个位置的方格上摆着皇后。

每个方案输出完成后,输出一个空行。

注意:行末不能有多余空格。

输出方案的顺序任意,只要不重复且没有遗漏即可。

数据范围

1≤n≤9

输入样例:
4
输出样例:
.Q..
...Q
Q...
..Q.

..Q.
Q...
...Q
.Q..
笔记:

dg[N]udg[N] 指的是对角线与副对角线,大小为 2 n − 1 2n - 1 2n1
为什么是 u + in - u + i
从数学角度上考虑,副对角线方程为 y = − x + b y = - x + b y=x+b,所以截距 b = x + y = u + i b = x + y = u + i b=x+y=u+i

#include <iostream>

using namespace std;

const int N = 20;

int n;
char g[N][N];
bool col[N], dg[N], udg[N];

void dfs(int u)
{
    if (u == n)
    {
        for (int i = 0; i < n; i ++ ) puts(g[i]);
        puts("");
        return;
    }
    
    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] = true;
            dfs(u + 1);
            col[i] = dg[u + i] = udg[n - u + i] = false;
            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

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

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

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

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

输入格式

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

接下来 n 行,每行包含 m 个整数(00 或 11),表示完整的二维数组迷宫。

输出格式

输出一个整数,表示从左上角移动至右下角的最少移动次数。

数据范围

1≤n,m≤100

输入样例:
5 5
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0
输出样例:
8
笔记:

需要存储的信息有:

  • 地图;
  • 某点与原点的距离;
  • 队列存储当前状态;

操作有:

  • 每次查看能否往上下左右移动一步,并修改距离
#include <iostream>
#include <cstring>
#include <queue>

using namespace std;

typedef pair<int, int> PII;

const int N = 110;

int n, m;
int g[N][N];
int d[N][N];
queue<PII> q;

int bfs()
{
    memset(d, -1, sizeof d);
    
    q.push({0, 0});
    
    d[0][0] = 0;
    
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    
    while (q.size())
    {
        PII u = q.front();
        q.pop();
        for (int i = 0; i < 4; i ++ )
        {
            int x = u.first + dx[i], y = u.second + dy[i];
            if (x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1)
            {
                d[x][y] = d[u.first][u.second] + 1;
                q.push({x, y});
            }
        }
    }
    
    return d[n - 1][m - 1];
}

int main()
{
    scanf("%d%d", &n, &m);
    
    for (int i = 0; i < n; i ++ )
    {
        for (int j = 0; j < m; j ++ ) scanf("%d", &g[i][j]);
    }
    
    printf("%d", bfs());
    
    return 0;
}

树与图的深度优先遍历

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

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

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

输入格式

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

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

输出格式

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

数据范围

1≤n≤10^5

输入样例
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6
输出样例:
4
笔记:

dfs(u) 表示的是返回以 u 为根结点的子树中结点的数量

#include <iostream>
#include <cstring>

using namespace std;

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

int n, ans = N;
int h[N], e[M], ne[M], idx;
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 sum = 1, res = 0;
    for (int i = h[u]; i != -1; 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(ans, res);
    return sum;
}

int main()
{
    cin >> n;
    
    memset(h, -1, sizeof h);
    
    for (int i = 0; i < n - 1; i ++ )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
        add(b, a);
    }
    
    dfs(1);
    
    cout << ans;
}

树与图的广度优先遍历

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

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

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

输入格式

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

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

输出格式

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

数据范围

1≤n,m≤10^5

输入样例:
4 5
1 2
2 3
3 4
1 3
1 4
输出样例:
1
笔记:

BFS 的核心,就是当队列不为空时不断进出,直到为空
本题只需要每次记录结点的深度即可

#include <iostream>
#include <cstring>
#include <queue>

using namespace std;

const int N = 1e5 + 10;

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

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

int bfs()
{
    memset(d, -1, sizeof d);
    q.push(1);
    d[1] = 0;
    
    while (q.size())
    {
        int u = q.front();
        q.pop();
        for (int i = h[u]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (d[j] == -1)
            {
                d[j] = d[u] + 1;
                q.push(j);
            }
        }
    }
    
    return d[n];
}

int main()
{
    cin >> n >> m;
    
    memset(h, -1, sizeof h);
    
    while (m -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
    }
    
    printf("%d", bfs());
    
    return 0;
}

拓扑排序

给定一个 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≤10^5

输入样例:
3 3
1 2
2 3
1 3
输出样例:
1 2 3
笔记:

每次 pop() 后,删除该点的出边,并把入度为 0 的点入队
当出队的次数小于点的总数量时,说明有向图带环(因为入度不为 0 的点无法入队)

#include <iostream>
#include <cstring>
#include <queue>

using namespace std;

const int N = 1e5 + 10;

int n, m, k;
int h[N], e[N], ne[N], idx;
int d[N], res[N];
queue<int> q;

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

void bfs()
{
    for (int i = 1; i <= n; i ++ ) if (!d[i]) q.push(i);
    
    while (q.size())
    {
        int u = q.front();
        q.pop();
        res[ ++ k] = u;
        while (h[u] != -1)
        {
            int j = e[h[u]];
            d[j] -- ;
            if (!d[j]) q.push(j);
            h[u] = ne[h[u]];
        }
    }
}

int main()
{
    cin >> n >> m;
    
    memset(h, -1, sizeof h);
    
    while (m -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
        d[b] ++ ;
    }
    
    bfs();
    
    if (k != n) puts("-1");
    else for (int i = 1; i <= n; i ++ ) printf("%d ", res[i]);
    
    return 0;
}

Dijkstra求最短路 1

在这里插入图片描述

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

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

输入格式

第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式

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

如果路径不存在,则输出 −1−1。

数据范围

1≤n≤500,
1≤m≤10^5,
图中涉及边长均不超过10000。

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
笔记:

思路:dist[N] 存储的是所有点到原点的距离,每次遍历数组,找到距离最近的点
然后更新 dist[N]
时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)

#include <iostream>
#include <cstring>

using namespace std;

const int N = 510;

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; i ++ )
    {
        int t = -1;
        for (int j = 1; j <= n; j ++ ) if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j;
        
        st[t] = true;
        
        for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], dist[t] + g[t][j]);
    }
    
    if (dist[n] == 0x3f3f3f3f) return -1;
    else return dist[n];
}

int main()
{
    cin >> n >> m;
    memset(g, 0x3f, sizeof g);
    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        g[a][b] = min(g[a][b], c);
    }
    
    cout << dijkstra();
    
    return 0;
}

Dijkstra求最短路 2

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。

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

输入格式

第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式

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

如果路径不存在,则输出 −1。

数据范围

1≤n,m≤1.5×10^5,
图中涉及边长均不小于 00,且不超过 10000。
数据保证:如果最短路存在,则最短路的长度不超过 10^9。

输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
笔记:

使用堆来存储当前最小的距离,这样可以降低找当前最近结点的开销,总体时间复杂度变为 O ( m log ⁡ n ) O(m\log n) O(mlogn)
在更新距离这一步中,我们直接把新的距离插入堆即可,这样做不会增加时间复杂度,因为在稠密图中, n 2 = m ,   log ⁡ n 2 = 2 log ⁡ n = log ⁡ m n^{2}=m ,\ \log n^{2} = 2\log n = \log m n2=m, logn2=2logn=logm

#include <iostream>
#include <cstring>
#include <queue>

using namespace std;

typedef pair<int, int> PII;

const int N = 1000010;

int n ,m;
int h[N], e[N], w[N], ne[N], idx;
int dist[N];
bool st[N];
priority_queue<PII, vector<PII>, greater<PII>> heap;

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;
    heap.push({0, 1});
    
    while (heap.size())
    {
        PII t = heap.top();
        heap.pop();
        
        int ver = t.second, distance = t.first;
        
        if (st[ver]) continue;
        st[ver] = true;
        
        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[ver] + w[i])
            {
                dist[j] = dist[ver] + w[i];
                heap.push({dist[j], j});
            }
        }
    }
    
    if (dist[n] == 0x3f3f3f3f) return -1;
    else return dist[n];
}

int main()
{
    cin >> n >> m;
    
    memset(h, -1, sizeof h);
    
    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    
    cout << dijkstra();
    
    return 0;
}

Bellmen-ford

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

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

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

输入格式

第一行包含三个整数 n,m,k,。

接下来 m 行,每行包含三个整数 x,y,z,,表示存在一条从点 x 到点 y 的有向边,边长为 z。

点的编号为 1∼n1∼。

输出格式

输出一个整数,表示从 11 号点到 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
笔记:

遍历所有边 n 次,每次更新最短路径,时间复杂度是 O ( m n ) O(mn) O(mn)
n 的含义是从原点出发走了多少步

#include <iostream>
#include <cstring>

using namespace std;

const int N = 510, M = 10010;

int n, m, k;
int dist[N], backup[N];

struct Edge
{
    int a, b, w;
}edges[M];

void bellmen_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    for (int i = 0; i < k; i ++ )
    {
        memcpy(backup, dist, sizeof dist);
        
        for (int j = 0; j < m; j ++ )
        {
            auto e = edges[j];
            dist[e.b] = min(dist[e.b], backup[e.a] + e.w);
        }
    }
}

int main()
{
    scanf("%d%d%d", &n, &m, &k);
    
    for (int i = 0; i < m; i ++ )
    {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        edges[i] = {a, b, w};
    }
    
    bellmen_ford();
    
    if (dist[n] > 0x3f3f3f3f >> 1) puts("impossible");
    else printf("%d", dist[n]);
}

spfa

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

请你求出 11 号点到 n 号点的最短距离,如果无法从 11 号点走到 n 号点,则输出 impossible

数据保证不存在负权回路。

输入格式

第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式

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

如果路径不存在,则输出 impossible

数据范围

1≤n,m≤10^5,
图中涉及边长绝对值均不超过 10000。

输入样例:
3 3
1 2 5
2 3 -3
1 3 4
输出样例:
2
笔记:

对 bellmen-ford 算法的优化。并不是每次所有边都要判断更新,而是上次有变化的点才需要看其边是否需要更新
因此准备一个队列,每次将更新过的边加入队列,弹出时去找这个点出去的边并更新
同时还要有一个标记,避免一个点多次入队
一个点是可以反复入队的,只要有更新就行

#include <iostream>
#include <cstring>
#include <queue>

using namespace std;

const int N = 100010;

int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];
queue<int> q;

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;
    
    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])
                {
                    st[j] = true;
                    q.push(j);
                }
            }
        }
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof h);
    
    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    
    spfa();
    
    if (dist[n] == 0x3f3f3f3f) puts("impossible");
    else printf("%d", dist[n]);
    
    return 0;
}

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^2
1≤m≤20000,
图中涉及边长绝对值均不超过 10000。

输入样例:
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例:
impossible
1
笔记:

基于动态规划的思想
三层循环更新所有的最短距离

#include <iostream>

using namespace std;

const int N = 210, INF = 1e9;

int n, m, Q;
int g[N][N];

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

int main()
{
    scanf("%d%d%d", &n, &m, &Q);
    
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i != j) g[i][j] = INF;
            
    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        g[a][b] = min(g[a][b], c);
    }
    
    floyd();
    
    while (Q -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        if (g[a][b] > INF >> 1) puts("impossible");
        else printf("%d\n", g[a][b]);
    }
    
    return 0;
}

Prim

给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。

求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible

给定一张边带权的无向图 G=(V,E)=(,),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|=||,m=|E|=||。

由 V 中的全部 n 个顶点和 E 中 n−1−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。

输入格式

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

接下来 m 行,每行包含三个整数 u,v,w,,表示点 u 和点 v 之间存在一条权值为 w 的边。

输出格式

共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible

数据范围

1≤n≤500,
1≤m≤10^5,
图中涉及边的边权的绝对值均不超过 1000010000。

输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6
笔记:

跟 dijkstra 很像,不过 dijkstra 的 dist[N] 表示的是到源点的距离,而 prim 是到达集合的距离

#include <iostream>
#include <cstring>

using namespace std;

const int N = 510, INF = 0x3f3f3f3f;

int n, m;
int g[N][N];
int 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];
        
        for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
        
        st[t] = true;
    }
    
    return res;
}

int main()
{
    scanf("%d%d", &n, &m);
    
    memset(g, 0x3f, sizeof g);
    
    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        g[a][b] = g[b][a] = min(g[a][b], c);
    }
    
    int t = prim();
    
    if (t == INF) puts("impossible");
    else printf("%d", t);
    
    return 0;
}

Kruskal

给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。

求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible

给定一张边带权的无向图 G=(V,E)=(,),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|=||,m=|E|=||。

由 V 中的全部 n 个顶点和 E 中 n−1−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。

输入格式

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

接下来 m 行,每行包含三个整数 u,v,w,,表示点 u 和点 v 之间存在一条权值为 w 的边。

输出格式

共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible

数据范围

1≤n≤10^5,
1≤m≤2∗10^5,
图中涉及边的边权的绝对值均不超过 1000。

输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6
笔记:

记得初始化并查集

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 200010;

int n, m;
int p[N];

struct Edge
{
    int a, b, w;
    
    bool operator< (const Edge &W)const
    {
        return w < W.w;
    }
}edges[N];

int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main()
{
    scanf("%d%d", &n, &m);
    
    for (int i = 0; i < n; i ++ ) p[i] = i;
    
    for (int i = 0; i < m; i ++ )
    {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        edges[i] = {a, b, w};
    }
    
    sort(edges, edges + m);
    
    int res = 0, cnt = 0;
    for (int i = 0; i < m; i ++ )
    {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;
        a = find(a), b = find(b);
        if (a != b)
        {
            p[a] = b;
            res += w;
            cnt ++ ;
        }
    }
    
    if (cnt < n - 1) puts("impossible");
    else printf("%d\n", res);
    
    return 0;
}

染色法判定二分图

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

请你判断这个图是否是二分图。

输入格式

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

接下来 m 行,每行包含两个整数 u 和 v,表示点 u 和点 v 之间存在一条边。

输出格式

如果给定图是二分图,则输出 Yes,否则输出 No

数据范围

1≤n,m≤10^5

输入样例:
4 4
1 3
1 4
2 3
2 4
输出样例:
Yes
笔记:

使用 dfs 或者 bfs 都可以实现
dfs(int t, int c) 中的 3 - c 很精髓

#include <iostream>
#include <cstring>

using namespace std;

const int N = 100010, M = 200010;

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

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

bool dfs(int t, int c)
{
    color[t] = c;
    
    for (int i = h[t]; i != -1; 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;
}

int main()
{
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof h);
    
    while (m -- )
    {
        int a, b;
        scanf("%d%d", &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");
    
    return 0;
}

匈牙利算法

给定一个二分图,其中左半部包含 n11 个点(编号 1∼n11∼1),右半部包含 n22 个点(编号 1∼n21∼2),二分图共包含 m 条边。

数据保证任意一条边的两个端点都不可能在同一部分中。

请你求出二分图的最大匹配数。

二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E}{} 中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。

二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。

输入格式

第一行包含三个整数 n11、 n22 和 m。

接下来 m 行,每行包含两个整数 u 和 v,表示左半部点集中的点 u 和右半部点集中的点 v 之间存在一条边。

输出格式

输出一个整数,表示二分图的最大匹配数。

数据范围

1≤n1,n2≤500,
1≤u≤n,
1≤v≤n^2,
1≤m≤10^5

输入样例:
2 2 4
1 1
1 2
2 1
2 2
输出样例:
2
笔记:

匈牙利算法核心:基于贪心,每次看能不能匹配上,如果不能,就看已匹配的这对能不能换

#include <iostream>
#include <cstring>

using namespace std;

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

int n1, n2, m;
int h[N], e[M], ne[M], idx;
int match[N];
bool st[N];

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

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

int main()
{
    scanf("%d%d%d", &n1, &n2, &m);
    
    memset(h, -1, sizeof h);
    
    while (m -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
    }
    
    int res = 0;
    for (int i = 1; i <= n1; i ++ )
    {
        memset(st, 0, sizeof st);
        if (find(i)) res ++ ;
    }
    
    printf("%d", res);
    
    return 0;
}

m 条边。

数据保证任意一条边的两个端点都不可能在同一部分中。

请你求出二分图的最大匹配数。

二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E}{} 中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。

二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。

输入格式

第一行包含三个整数 n11、 n22 和 m。

接下来 m 行,每行包含两个整数 u 和 v,表示左半部点集中的点 u 和右半部点集中的点 v 之间存在一条边。

输出格式

输出一个整数,表示二分图的最大匹配数。

数据范围

1≤n1,n2≤500,
1≤u≤n,
1≤v≤n^2,
1≤m≤10^5

输入样例:
2 2 4
1 1
1 2
2 1
2 2
输出样例:
2
笔记:

匈牙利算法核心:基于贪心,每次看能不能匹配上,如果不能,就看已匹配的这对能不能换

#include <iostream>
#include <cstring>

using namespace std;

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

int n1, n2, m;
int h[N], e[M], ne[M], idx;
int match[N];
bool st[N];

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

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

int main()
{
    scanf("%d%d%d", &n1, &n2, &m);
    
    memset(h, -1, sizeof h);
    
    while (m -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
    }
    
    int res = 0;
    for (int i = 1; i <= n1; i ++ )
    {
        memset(st, 0, sizeof st);
        if (find(i)) res ++ ;
    }
    
    printf("%d", res);
    
    return 0;
}
  • 18
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值