ACM - 搜索与图论 - 基础(搜索 + 拓扑 + 最小生成树 + 最短路 + 二分图 + 欧拉路 + 最大流 + 其它)

一、搜索

1、分治

矩阵二分 / 普通二分 模板

原题链接:https://leetcode-cn.com/problems/sorted-matrix-search-lcci/
在这里插入图片描述
① 矩阵二分 :

/*
矩阵二分:从左下角开始二分,如果小了说明需要把列向右移,如果大了,说明需要把行向上移
*/
class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        int r = matrix.size();
        if (r == 0) return false;
        int c = matrix[0].size();
        if (c == 0) return false;
        int i = r - 1, j = 0;
        while (i >= 0 && j < c) {
            if (matrix[i][j] == target) return true;
            else if (matrix[i][j] < target) ++ j;
            else -- i;
        }
        return false;
    }
};

② 暴力二分:

/*
暴力二分:直接二分查每一行,速度慢
*/
class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        int r = matrix.size();
        if (r == 0) return false;
        int c = matrix[0].size();
        if (c == 0) return false;
        for (int i = 0; i < r; ++ i) {
            if (matrix[i][0] <= target && matrix[i][c - 1] >= target) {
                if (judge(matrix, i, target)) return true;
            }
        }
        return false;
    }
    //普通二分模板:在matrix[row]中判断是否存在target
    bool judge(vector<vector<int>>& matrix, int row, int target) {
        int l = 0, r = matrix[row].size();
        while (l <= r) {
            int mid = l + (r - l) / 2;
            if (matrix[row][mid] == target) return true;
            else if (matrix[row][mid] < target) l = mid + 1;
            else r = mid - 1;
        }
        return false;
    }
};

万能二分模板

AcWing 789. 数的范围
原题链接:https://www.acwing.com/problem/content/description/791/
在这里插入图片描述
在这里插入图片描述
思路
在这里插入图片描述

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int a[N], n, m;

int get_l(int num) {
    int l = 0, r = n - 1;
    while (l < r) {
        int mid = l + r >> 1;
        if (a[mid] >= num) r = mid;
        else l = mid + 1;
    }
    //就算return r也是可以的,因为l == r才会跳出循环
    if (l < n && l >= 0 && a[l] == num) return l;
    else return -1;
}

int get_r(int num) {
    int l = 0, r = n - 1;
    while (l < r) {
        //check()为true是l设置为mid,则需要+1防止死循环
        int mid = l + r + 1 >> 1;
        if (a[mid] <= num) l = mid;  
        else r = mid - 1;
    }
    //同上,就算return r也是可以的
    if (l < n && l >= 0 && a[l] == num) return l;
    else return -1;
}

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; ++ i) cin >> a[i];
    
    while (m --) {
        int num;
        cin >> num;
        cout << get_l(num) << " " << get_r(num) << endl;
    }
    
    return 0;
}

2、DFS

例题1、AcWing 842. 排列数字

原题链接:https://www.acwing.com/problem/content/description/844/
在这里插入图片描述

#include<iostream>

using namespace std;

int ans[10], n;
bool st[10];

void dfs(int idx) {  // idx - 第几个数
    if (idx > n) {
        for (int i = 1; i <= n; ++ i) printf("%d ", ans[i]);
        printf("\n");
    }    
    else {
        for (int i = 1; i <= n; ++ i) {
            if (!st[i]) {
                st[i] = true;
                ans[idx] = i;
                dfs(idx + 1);
                st[i] = false;  //还原
            }
        }
    }
}

int main() {
    scanf("%d", &n);
    dfs(1);
    return 0;
}

例题2、AcWing 843. n-皇后问题

原题链接:https://www.acwing.com/problem/content/845/
在这里插入图片描述
在这里插入图片描述
注意
在处理对角线的时候,我们可以借助一次函数,即把两条对角线抽相成 y = ± x + b,移项得到:b = y - x 和 b = y + x,那么这里我们可以用截距 b 去判断两个皇后会不会被放在同一条对角线上。

当然,因为 y - x 可能为负,所以我们可以用 n + y - x 表示该截距 b,因为我们需要的只是判断是否在同一条对角线上,至于 b 的值是正是负不影响结果,而加上 n 后就可以直接用拿 b + n 映射到索引上,方便写代码。

代码

#include<iostream>

using namespace std;

int n;
bool col[10], p[20], unp[20], ans[10][10];

void dfs(int row) {  //递归每一行
    if (row > n) {
        for (int i = 1; i <= n; ++ i) {
            for (int j = 1; j <= n; ++ j) {
                if (ans[i][j]) printf("Q");
                else printf(".");
            }
            printf("\n");
        }
        printf("\n");
    }
    else {
        for (int c = 1; c <= n; ++ c) {   // 循环判断每一列是否能放皇后
        	// 该列以及两个对角线可以放皇后则进入if
            if (!col[c] && !p[n + c - row] && !unp[row + c]) {
                col[c] = p[n + c - row] = unp[row + c] = true;
                ans[row][c] = true;
                dfs(row + 1);
                //记得还原
                col[c] = p[n + c - row] = unp[row + c] = false;
                ans[row][c] = false;
            }
        }
    }
}

int main() {
    scanf("%d", &n);
    dfs(1);
    return 0;
}

3、BFS

例题1、AcWing 844. 走迷宫 (入门经典)

原题链接:https://www.acwing.com/problem/content/846/

在这里插入图片描述
在这里插入图片描述
思路

因为每走一步的权重是 1 ,那么在 bfs 的模式下,最先到达终点的那条路必定是最短路,而且路径的长短 == bfs 的第一个 while 循环次数 - 1。

代码

#include <iostream>

using namespace std;

const int N = 110; 

int n, m, g[N][N], ans;
bool st[N][N];  //判断坐标(x,y)是否已经走过
int hh = 0, tt = -1;  // 模拟队列的头指针和尾指针
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};  //向上下左右走
pair<int, int> q[N * N];  //队列  

void bfs() {
    while (tt - hh + 1 != 0) {
        int length = tt - hh + 1;
    	while (length --) {
    		pair<int, int> temp = q[hh ++];
    		if (temp.first == n && temp.second == m) return;  //找到终点
    		for (int i = 0; i < 4; ++ i) {
    			int x = temp.first + dx[i], y = temp.second + dy[i];
    			if (x > 0 && x <= n && y > 0 && y <= m && !st[x][y] && !g[x][y]) {
    				q[++ tt] = make_pair(x, y);
    				st[x][y] = true;
    			}
    		}
    	}
    	++ ans;
    }
}

int main() {
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; ++ i) {
		for (int j = 1; j <= m; ++ j) {
			scanf("%d", &g[i][j]); 
		}
	} 
	q[++ tt] = make_pair(1, 1);
	bfs();
	printf("%d\n", ans);
	return 0;
}

例题2、到达 “12345678x”:AcWing 845. 八数码

原题链接:https://www.acwing.com/problem/content/847/
在这里插入图片描述
在这里插入图片描述
思路

对于一个初始状态,x 有上下左右四种走法,而这四种走法在当下也只需要交换一次位置即可得到,那么将其中合法的走法入队(如果是曾经走过的状态就不必入队了),再判断下一波,直到走到最终态 “12345678x” 或者队列为空(意味着无路可走了)。

代码

#include<iostream>
#include<string>
#include<queue>
#include<unordered_map>

using namespace std;

string start, ending = "12345678x";
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};

int bfs() {
    queue<string> q;
    unordered_map<string, int> book;
    q.push(start);
    int ans = 0;
    while (!q.empty()) {
        int length = q.size();
        while (length --) {
            string temp = q.front();
            q.pop();
            if (temp == ending) return ans;
            int idx = temp.find('x');  //返回'x'的下标
            int x = idx / 3, y = idx % 3; //获得在3*3的横纵坐标
            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(temp[idx], temp[a * 3 + b]);  //必须先判断a、b,不然a*3+b可能会非法访问
                    // 只有没经历过的字符串才需要添加进去
                    if (book.find(temp) == book.end()) {  
                        book[temp] = 1;
                        q.push(temp);
                    }
                    swap(temp[idx], temp[a * 3 + b]);  //还原
                }
            }
        }
        ++ ans;
    }
    return -1;
}

int main() {
    char op[3];
    for (int i = 0; i < 9; ++ i) {
        scanf("%s", op);
        start += op[0];
    }
    cout << bfs() << endl;
    return 0;
}

二、图论

写在前面

可以说,树是一种特殊的图,而无向图也是一种特殊的有向图,所以对于一些树和无向图的储存和遍历,可以抽象成是对特殊有向图的遍历。

1、图的存储

  1. 邻接矩阵:适合稠密图(m ~ n ^ 2)
int g[N][N];   //g[a][b] 表示从点a出发到点b的边
  1. 邻接表:适合稀疏图(m ~ n )
// 对于每个点k,开一个单链表,存储k所有可以走到的点
//h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx = 1;

// 添加一条边a->b
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; 
    //ne[idx] == 0 表示第idx个结点的next指向null
}

2、图的类型

3、基于DFS的图算法

模板框架

//时间复杂度 O(n+m)O(n+m), n 表示点数,m 表示边数
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);
    }
}

树与图的深度优先遍历 AcWing846. 树的重心

原题链接:https://www.acwing.com/problem/content/description/848/

在这里插入图片描述
在这里插入图片描述
思路

因为该图是无向图,而且没有说明一定是以 1 为根节点,所以换句话说,把任一个结点拎起来,都可以自然垂下成为一棵树。换而言之,在 main 函数里面,dfs()里面传的参数可以是 1 到 n 之间的任意一个,得到的 ans 也会是一样的。
在这里插入图片描述
因为我们最终想要的 ans 是去掉树的重心后,数量最少的所有连通分支中最多结点的连通分支。比如说,假如上面以 1 为根的树的重心是 4,那么去掉重心后就有 3 个连通分支如下图:
在这里插入图片描述
而此时,连通分支结点数 res 最多是 5 个结点,所以 ans 就是要求最小的 res。

分析完题目要求,那么这道题的做法就是去 dfs,把每一个结点都先假设成是树的重心,进而去求去掉这个重心的数量最多的连通分支,最后对每一个 res 求 min 就得到了 ans

所以这道题的关键就在于怎么求在重心 u 的前提下数量最多的连通分支。最直接的做法就是去求 max(所有连通分支的结点数),换句话说,如果知道所有连通分支的结点数,直接求 max 即可。

比如说,现在要来求以 4 为重心的各个连通分支的结点数,那么分别为:dfs(3)、dfs(6)、n - dfs(3) - dfs(6),而所谓的 n - dfs(3) - dfs(6)虽然结点 1 也是 4 的一个分支,但是因为是从 1 走向 4 ,所以 1 一定在 4 之前被遍历到,所以 1 一定会在 if ( ! st [ j ] ) 时排除在 if 之外,不会重复 dfs (1),这符合了 dfs 时每个点一般只走一次的原则。

综上。

代码

#include<iostream>

using namespace std;

const int N = 100010, M = 200010;  //边数最大可达到 2N

int h[N], e[M], ne[M], idx = 1;  //邻接表表示树
bool st[N];  //判断点是否已经遍历过
int ans = N, n; // ans最多也只能达到n,所以初始化为大于等于n即可

//增加a到b的边
void add(int a, int b) {
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx ++;
}

//对点u深搜,返回以u为根的树的结点总数
int dfs(int u) {
    st[u] = true;  //标记点u已经走过
    //res - 假设u为重心,结点数最多的连通分量的结点数目
    //sum - 以u为根节点的树的结点总数
    int res = 0, sum = 1; 
    for (int i = h[u]; i != 0; i = ne[i]) {  //迭代得到所有和u相连的结点
        int j = e[i];  //获得在树中的编号
        if (!st[j]) {  //如果该结点还没走过
            int s = dfs(j); //获得该连通分量的结点总数
            sum += s;    //累加以u为根结点的结点数
            res = max(res, s);  //求最多结点数的连通分量
        }
    }
    res = max(res, n - sum);
    ans = min(ans, res);
    return sum;
}

int main() {
    scanf("%d", &n);
    int a, b;
    for (int i = 1; i < n; ++ i) {
        scanf("%d%d", &a, &b);
        add(a, b);  //无向图,所以相当于两条边
        add(b, a);
    }
    dfs(4);  // 1 ~ n皆可,这表示这棵树以哪一个为根节点
    printf("%d\n", ans);
    return 0;
}

4、基于BFS的图算法

模板框架

//树与图的广度优先遍历

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);
        }
    }
}

bfs求无权最短路:AcWing 847. 图中点的层次

原题链接:https://www.acwing.com/problem/content/submission/849/

在这里插入图片描述
在这里插入图片描述
思路
一圈圈往外走。

代码

#include<iostream>
#include<queue>

using namespace std;

const int N = 100010;

int g[N], e[N], ne[N], idx = 1;  //邻接表
bool book[N];  //判断该点是否已经走过

//插入从a走到b的边
void insert(int a, int b) {
    e[idx] = b;
    ne[idx] = g[a];
    g[a] = idx ++;
}

int bfs(int n) {
    queue<int> q;
    q.push(1);  //从1出发
    book[1] = true;  //标记已经走过点1
    int ans = 0;
    while (!q.empty()) {
        int length = q.size();
        ++ ans;  //走了几层就说明最短路是几
        while (length --) {
            int a = q.front();
            q.pop();
            int aa = g[a];
            while (aa != 0) {
                if (e[aa] == n) return ans;  //走到n,此时路程最短
                else {
                    if (!book[e[aa]]) {  //没走过的点才放进队列
                        book[e[aa]] = true;
                        q.push(e[aa]);
                    }
                }
                aa = ne[aa];
            }
        }
    }
    return -1;  //走不到n
}

int main() {
    int n, m, a, b;
    scanf("%d%d", &n, &m);
    for (int i = 0; i < m; ++ i) {
        scanf("%d%d", &a, &b);
        insert(a, b);
    }
    printf("%d\n", bfs(n));
    return 0;
}

求拓扑序列 :AcWing 848. 有向图的拓扑序列

有向无环图必定可为拓扑图。
有环不可能是拓扑图(因为删减到最后是一个环,环没有一个入度为0的结点可以突破)。
无向图不可能是拓扑图(无向相当于有环)。

在这里插入图片描述在这里插入图片描述

/*
入度为0才会被放进ans,如果搜索时出现a指向b,而b放进过ans里面,这是不可能存在的情况。
因为ans里面都是入度为0的点,为a指向b说明b入度大于0,自相矛盾
所以其实不用判断放入队列时,该点是否走过
*/
#include <bits/stdc++.h> 

using namespace std;

#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define ull unsigned long long
#define PII pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rim int m; scanf("%d", &m)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 100010; 
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};

int ans[N], d[N], k = 0;
int h[N], ne[N], e[N], idx = 1;

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

void bfs(int n) {
	queue<int> q;
	//预先把入度为0的放进去
	for (int i = 1; i <= n; ++ i) {
		if (!d[i]) {
			ans[k ++] = i;
			q.push(i);
		}
	}
	//一圈一圈往下走
	while (!q.empty()) {
		int num = q.front();
		q.pop();
		for (int i = h[num]; i != -1; i = ne[i]) {
			int a = e[i];
			-- d[a];
    		if (!d[a]) {
    			q.push(a);
    			ans[k ++] = a;
    		}
		}
	}
}

int main() {
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	rin;
	rim;
	MEM(h, -1);
	for (int i = 0; i < m; ++ i) {
		rit;
		ria;
		insert(t, a);
		++ d[a];
	}
	
	bfs(n);
	
	if (k == n) {
		for (int i = 0; i < n; ++ i) {
			cout << ans[i] << " ";
		}
	}
	else cout << -1;
	return 0;
}

5、最小生成树

在这里插入图片描述

(1)朴素版 prim 算法 - 稠密图

AcWing 858. Prim算法求最小生成树

原题链接:https://www.acwing.com/problem/content/description/860/
在这里插入图片描述
在这里插入图片描述

#include<bits/stdc++.h>

using namespace std;

const int N = 510;
const int INF = 0x3f3f3f3f;

int n, m;
//dis[i]表示i点所能确定的长度最小的边,不同于最短路里到起始点的距离
int dis[N], g[N][N];  
bool st[N];

int prim() {
    dis[1] = 0;  //一个点本身没有距离
    int ans = 0;  //最终生成树的权重和
    for (int i = 0; i < n; ++ i) {
        int t = -1;  //每一次找到还未确定的点里面权重最小的点
        for (int j = 1; j <= n; ++ j) {
            if (!st[j] && (t == -1 || dis[j] < dis[t])) {
                t = j;
            }
        }
        if (dis[t] == INF) return INF;  //说明不是连通图
        st[t] = true;  //标记为已访问
        ans += dis[t];
        //更新点集
        for (int j = 1; j <= n; ++ j) {
            dis[j] = min(dis[j], g[t][j]);
        }
    }
    return ans;
}

int main() {
    cin >> n >> m;
    //初始化
    memset(dis, INF, sizeof dis);
    memset(g, INF, sizeof g);
    for (int i = 0; i < m; ++ i) {
        int a, b, w;
        cin >> a >> b >> w;
        g[a][b] = g[b][a] = min(w, g[a][b]);  //无向图 + 有重边
    }
    
    int ans = prim();
    if (ans == INF) cout << "impossible";
    else cout << ans;
    
    return 0;
}

(2)Kruskal 算法 - 稀疏图

AcWing 859. Kruskal算法求最小生成树

原题链接:https://www.acwing.com/problem/content/description/861/
在这里插入图片描述
在这里插入图片描述

#include<bits/stdc++.h>

using namespace std;

const int N = 100010, M = 200010;

//表示从a到b权重为w的边
struct edge{
    int a, b, w;
}es[M];

int n, m;
int p[N];  //表示图中节点根属于哪一个集合,其实就是并查集里的father

//找到根节点
int find(int a) {
    if (p[a] != a) return p[a] = find(p[a]);
    else return a;
}

bool cmp(edge a, edge b) {
    return a.w < b.w;
}

void kruskal() {
    for (int i = 1; i <= n; ++ i) p[i] = i; //初始化根节点
    sort(es, es + m, cmp);  //按照权重从小到大排序
    
    int ans = 0, cnt = 1;  //ans - 生成树权重和, cnt - 已经加入几个结点
    
    for (int i = 0; i < m; ++ i) {
        int a = es[i].a, b = es[i].b, w = es[i].w;
        int x = find(a), y = find(b);
        if (x != y) {
            ++ cnt;
            ans += w;
            p[x] = y;  // 不可以是p[a] = y
        }
    }
    if (cnt < n) cout << "impossible";
    else cout << ans;
}

int main() {
    cin >> n >> m;
    for (int i = 0; i < m; ++ i) {
        int a, b, w;
        cin >> a >> b >> w;
        es[i] = {a, b, w};
    }
    
    kruskal();
    
    return 0;
}

6、最短路算法

在这里插入图片描述

(1) 单源最短路

① 所有边权重都是正数
Ⅰ、 朴素 Dijkstra

AcWing 849. Dijkstra求最短路 I

原题链接 : https://www.acwing.com/problem/content/851/

在这里插入图片描述

#include <bits/stdc++.h> 

using namespace std;

#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define ull unsigned long long
#define PII pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rim int m; scanf("%d", &m)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 510; 
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};

//dis 为各个点到起点的最短距离
int g[N][N], dis[N]; 
bool st[N];  //判断是否已经更新到最短距离

int dijkstra(int n) {
	dis[1] = 0;
	//迭代n次
	for (int i = 0; i < n; ++ i) {
		int t = -1;
		//找到当前还没更新成最短距离且距离起点最近的点
		for (int j = 1; j <= n; ++ j) {
			if (!st[j] && (t == -1 || dis[j] < dis[t]))
				t = j;
		}
		//标记为已访问
		st[t] = true;
		//用贪心的思想更新所有点的距离
		for (int j = 1; j <= n; ++ j) {
			dis[j] = min(dis[j], dis[t] + g[t][j]);
		}
	}
	
	if (dis[n] == INF) return -1;  //不存在从起点到终点的路径
	else return dis[n];
}

int main() {
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	rin;
	rim;
	MEM(g, INF);
	MEM(dis, INF);
	for (int i = 0; i < m; ++ i) {
		int a, b, c;
		sc("%d%d%d", &a, &b, &c);
		g[a][b] = min(g[a][b], c);  //因为有重边,所以需要取min
	}
	
	pr("%d", dijkstra(n));
	return 0;
}
Ⅱ、 堆优化版 Dijkstra

AcWing 850. Dijkstra求最短路 II

原题链接:https://www.acwing.com/problem/content/submission/code_detail/6234491/
在这里插入图片描述

#include <bits/stdc++.h> 

using namespace std;

#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define ull unsigned long long
#define PII pair<int, int>
#define PLL pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rim int m; scanf("%d", &m)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 200000; 
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};


// w存边的权重,dis为各个点到起点的最短距离
int h[N], e[N], ne[N], w[N], dis[N];
int idx = 1;
bool st[N];  //判断是否已经遍历过


//插入从a到b权重为c的边
void insert(int a, int b, int c) {
	e[idx] = b;
	ne[idx] = h[a];
	w[idx] = c;
	h[a] = idx ++; 
}

int dijkstra(int n) {
	dis[1] = 0;
	priority_queue<PII, vector<PII>, greater<PII> > heap;  //小根堆
	heap.push({0, 1});
	while (!heap.empty()) {
	    // 获得当前堆内距离起点最近的点
		PII p = heap.top();
		heap.pop();
		int distance = p.first, var = p.second;
		if (st[var]) continue;	//如果该点已有最短距离过便跳过
		st[var] = true;  //标记为已最短
		//遍历与该点存在连边的点
		for (int i = h[var]; i != -1; i = ne[i]) {
			int j = e[i];
			//更新距离且放在堆内
			if (dis[j] > distance + w[i]) {
				dis[j] = distance + w[i];
				heap.push({dis[j], j});
			}
		}
	}	
	if (dis[n] == INF) return -1;  //不存在从起点到终点的路径
	else return dis[n];
}

int main() {
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	rin;
	rim;
	MEM(h, -1);
	MEM(dis, INF); 
	while (m --) {
		int a, b, c;
		sc("%d%d%d", &a, &b, &c);
		insert(a, b, c);
	}
	
	pr("%d", dijkstra(n));
	return 0;
}
② 存在负权边
Ⅰ、 bellman-ford(有边数限制的最短路)

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

原题链接:https://www.acwing.com/problem/content/description/855/
在这里插入图片描述

#include<bits/stdc++.h>

using namespace std;

const int N = 510, M = 10010;
const int INF = 0x3f3f3f3f;

//从a到b权重为w的边
struct edge{
    int a, b, w;
};

int n, m, k;
int dis[N], cpy[N];  //距离数组和备份数组
edge es[M];  //边集

int bellman_ford() {
    memset(dis, INF, sizeof dis);
    dis[1] = 0;
    for (int i = 0; i < k; ++ i) {   //最多走k次
        memcpy(cpy, dis, sizeof dis);  //用备份数组去更新距离,防止更新时二次更新
        for (int j = 0; j < m; ++ j) {
            int a = es[j].a, b = es[j].b, w = es[j].w;
            dis[b] = min(dis[b], cpy[a] + w);
        }
    }
    //INF / 2是为了防止类似于1到a和1到n都是正无穷,但是a到n是负距离,
    //导致从1到a再到n比1直接到n小,而更新了dis[n]
    if (dis[n] > INF / 2) return -1;
    else return dis[n];
}

int main() {
    cin >> n >> m >> k;
    for (int i = 0; i < m; ++ i) {
        int a, b, w;
        cin >> a >> b >> w;
        es[i] = {a, b, w};
    }
    
    int ans = bellman_ford();
    if (ans == -1) cout << "impossible";
    else cout << ans;
    
    return 0;
}
Ⅱ、spfa
AcWing 851. spfa求最短路

原题链接:https://www.acwing.com/problem/content/853/
在这里插入图片描述

#include<bits/stdc++.h>

using namespace std;

const int N = 100010;
const int INF = 0x3f3f3f3f;

int n, m;
int h[N], e[N], ne[N], w[N], dis[N];
int idx = 1;

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

int spfa() {
    dis[1] = 0;
    queue<int> q;
    q.push(1);
    while (!q.empty()) {
        int t = q.front();
        q.pop();
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dis[j] > dis[t] + w[i]) {
                dis[j] = dis[t] + w[i];
                q.push(j);  //和bellman—ford不同的是,只将有变化的边放进队列
            }
        }
    }
    //和bellmam-ford同理
    if (dis[n] > INF / 2) return -1;
    else return dis[n];
}

int main() {
    memset(h, -1, sizeof h);
    memset(dis, INF, sizeof dis);
    cin >> n >> m;
    for (int i = 0; i < m; ++ i) {
        int a, b, c;
        cin >> a >> b >> c;
        insert(a, b, c);
    }
    int ans = spfa();
    if (ans == -1) cout << "impossible";
    else cout << ans;
    
    return 0;
} 
AcWing 852. spfa判断负环

原题链接:https://www.acwing.com/problem/content/854/
在这里插入图片描述

#include<bits/stdc++.h>

using namespace std;

const int N = 100010;
const int INF = 0x3f3f3f3f;

int n, m;
int h[N], e[N], ne[N], w[N], dis[N], cnt[N];
int idx = 1;

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

bool spfa() {
    dis[1] = 0;
    queue<int> q;
    for (int i = 1; i <= n; ++ i) {
        q.push(i);  //将所有点放入,因为可能从1出发到不了负环
        cnt[i] = 1;
    }
    while (!q.empty()) {
        int t = q.front();
        q.pop();
        if (cnt[t] > n) return true;  //存在负环
        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dis[j] > dis[t] + w[i]) {
                dis[j] = dis[t] + w[i];
                q.push(j);  //只将有变化的边放进队列
                cnt[j] = cnt[t] + 1;
            }
        }
    }
    return false;
}

int main() {
    memset(h, -1, sizeof h);
    memset(dis, INF, sizeof dis);
    cin >> n >> m;
    for (int i = 0; i < m; ++ i) {
        int a, b, c;
        cin >> a >> b >> c;
        insert(a, b, c);
    }
    if (spfa()) cout << "Yes";
    else cout << "No";
    
    return 0;
} 

(2) 多源汇最短路 - Floyd

AcWing 854. Floyd求最短路

原题链接:https://www.acwing.com/problem/content/856/

#include <bits/stdc++.h> 

using namespace std;

#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define ull unsigned long long
#define PII pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rim int m; scanf("%d", &m)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 210; 
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};

int n, m, k;
int dis[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) {
				dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
			}
		}
	}
}

int main() {
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	
	cin >> n >> m >> k;
	//初始化
	for (int i  = 1; i <= n; ++ i) {
		for (int j = 1; j <= n; ++ j) {
			if (i == j) dis[i][j] = 0;
			else dis[i][j] = INF;
		}
	}
	//输入每条边
	for (int i = 0; i < m; ++ i) {
		int a, b, w;
		cin >> a >> b >> w;
		dis[a][b] = min(dis[a][b], w);
	}
	//寻找最短路
	floyd();
	//输出每组询问
	while (k --) {
		int a, b;
		cin  >> a >> b;
		if (dis[a][b] > INF / 2) cout << "impossible" << endl;
		else cout << dis[a][b] << endl;
	}
	
	return 0;
}

7、二分图

在这里插入图片描述

二分图:当且仅当图中不含奇数环。
奇数环:边的数量是奇数的环。

HDU 2458 Kindergarten :二分图用匈牙利求最大独立集

(1) 判定二分图 : 染色法

AcWing 860. 染色法判定二分图

原题链接:https://www.acwing.com/problem/content/submission/code_detail/6332360/
在这里插入图片描述

#include <bits/stdc++.h> 

using namespace std;

#define MEM(x, y) memset(x, y, sizeof x)

const int INF = 0x3f3f3f3f;
const int N = 200010; 

int n, m;
int color[N];  // 0-未染色  1-白色  2-黑色
int h[N], e[N], ne[N], idx = 1;

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

bool dfs(int a, int b) {
	color[a] = b;  //染色
	for (int i = h[a]; i != -1; i = ne[i]) {
		int j = e[i];
		if (!color[j]) {  //还没染过色
			if (!dfs(j, 3 - b)) return false;
		}
		else if (color[j] == b)  //已经染过色,但是出现矛盾
		    return false;
	} 
	return true;
} 


int main() {
	cin >> n >> m;
	MEM(h, -1);
	for (int i = 0; i < m; ++ i) {
		int a, b;
		cin >> a >> b;
        // if (a == b) continue;  //自环也是奇数环,不可以continue
		insert(a, b);
		insert(b, a);
	}
	bool ans = true;
	//可能不是连通图,所以需要逐一判断逐块染色
	for (int i = 1; i <= n; ++ i) {
		if (!color[i]) {
			if (!dfs(i, 1)) {
				ans = false;
				break;
			}
		}
	}
	
	if (ans) cout << "Yes";
	else cout << "No";
	
	return 0;
}

(2) 求最大匹配 : 匈牙利算法

看似时间复杂度为O(nm),实际远远小于。

AcWing 861. 二分图的最大匹配

原题链接:https://www.acwing.com/problem/content/description/863/
在这里插入图片描述
在这里插入图片描述

#include<bits/stdc++.h>

using namespace std;

const int N = 510, M = 100010;

int n1, n2, m;
int h[N], e[M], ne[M], idx = 1;
int match[N];   //记录右边都是和左边的谁匹配了
bool st[N];   //每一趟,都需要确保每一个女生只被访问一次,相当于剪枝

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

//查询是否能找到与其匹配的
bool find (int a) {
    for (int i = h[a]; i != -1; i = ne[i]) {
        int j = e[i];
        if (!st[j]) {
            st[j] = true;  //标记已访问
            //要么从未匹配,要么可以通过易主原先的达到匹配
            if (!match[j] || find(match[j])) {
                match[j] = a;
                return true;
            }
        }
    }
    return false;
}

int main() {
    memset(h, -1, sizeof h);
    cin >> n1 >> n2 >> m;
    for (int i = 0; i < m; ++ i) {
        int a, b;
        cin >> a >> b;
        insert(a, b);
    }
    int cnt = 0;
    for (int i = 1; i <= n1; ++ i) {
        //记得初始化,st只用于每一趟,而不是全局
        memset(st, 0, sizeof st);  
        if (find(i)) ++ cnt;
    }
    cout << cnt;
    
    return 0;
}

8、欧拉路

9、最大流

10、其他

(1) 树的直径 - 树上距离最远的两点间的距离

一般地,距离最远的这两点均为叶节点。特殊地,一棵仅含两个点的树,此时的直径为根节点与另一个节点的距离,直径为1;仅含一个根节点的树的直径为0。

模板题 : AcWing 1207. 大臣的旅费

( 原题是第四届蓝桥杯省赛C++A组 / 第四届蓝桥杯省赛JAVAA组 )

原题链接:https://www.acwing.com/problem/content/1209/
在这里插入图片描述
在这里插入图片描述
做法
先从任意一点a进入,dfs更新其余结点到这个结点的距离(加上权重),再遍历一下dis数组找到和a距离最远的结点b,再从结点b进入去同样dfs一次更新其余结点到b的距离(加上权重),最后遍历dis数组,此时数组中的最大值就是树的直径.

证明
主要反证法.
上面的做法其实换句话说就是,虽然 a 是任意选的, 但是 y 必然是直径的一个端点.
此时再从 y 进入寻找距离最远的结点, 那么此时 dis 里面保存的数值最大的点必然是直径的另一个点.
现反证如下:
在这里插入图片描述
xy 不是真正的直径, 设真正的直径是 uv , 且 xp 为 1, vp为 2, 其他依次类推.

情况一: uv 与 xy 有直接交点.
由于 y 是距离 x 最远的结点, 所以有 1 + 4 >= 1 + 3, 所以 4 >= 3
故而 2 + 4 >= 2 + 3, 所以当 2 + 4 == 2 + 3, 那么有多条直径, 如果 2 + 4 > 2 + 3, 那么存在比预设的直径长的路径, 故而预设有误. 但无论哪种情况都无法否认 y 是直径的一个端点.

情况二: uv 和 xy 没有直接相交.
但是由于这是一棵树,所以从一个结点出发,可以到达任意一个结点,所以必然存在图中的 pq, 而且由于 1 + 4 >= 1 + 3 + 5 , 所以 4 >= 3 + 5, 所以 4 >= 5.
故而 2 + 3 + 4 >= 2 + 5。换句话说,以 y 为直径的端点才符合直径的定义。

当然, x 并不一定就是直径的另一个端点, 有可能存在另外比 xy 长的.

所以这个时候就需要再从 y 出发, 更新除 y 的所有结点到 y 的距离, 此时的那个最大距离就是直径.

#include <iostream>
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <vector>
#include <map>
#include <set>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <climits>
#include <unordered_set>
#include <unordered_map>

using namespace std;

#define getlen(array) {return (sizeof(array) / sizeof(array[0]));}
#define ll long long 
#define ull unsigned long long
#define PII pair<ll, ll>
#define MEM(x, y) memset(x, y, sizeof x)
#define rin int n; scanf("%d", &n)
#define rln ll n; scanf("%lld", &n)
#define rit int t; scanf("%d", &t)
#define ria int a; scanf("%d", &a)
#define sc scanf
#define pr printf

const int INF = 0x3f3f3f3f;
const int N = 100010;

struct edge {
	int id, weight;
};

vector<edge> e[N]; 
int dis[N];

void dfs (int idx, int fa, int d) {
    dis[idx] = d;
    int length = e[idx].size();
    for (int i = 0; i < length; ++ i) {
        edge ed = e[idx][i];
        if (ed.id != fa) {
            dfs(ed.id, idx, d + ed.weight);
        }
    }
}

int main() {
	//freopen("D:\\in.txt", "r", stdin); 
	//freopen("D:\\out.txt", "w", stdout);
	rin;
	int a, b, c;
	for (int i = 1; i < n; ++ i) {
		sc("%d%d%d", &a, &b, &c);
		e[a].push_back({b, c});
		e[b].push_back({a, c});
	}
	dfs(1, -1, 0);
	int u = 1;
	for (int i = 2; i <= n; ++ i) {
		if (dis[i] > dis[u]) u = i;
	}
	dfs(u, -1, 0);
	u = 1;
	for (int i = 2; i <= n; ++ i) {
		if (dis[i] > dis[u]) u = i;
	}
	u = dis[u];
	cout << u * 10 + (u + 1ll) * u / 2;
	return 0;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值