图论学习笔记

一、图的遍历

P2921 [USACO08DEC]Trick or Treat on the Farm G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

记忆化搜索+有向图

因为一个隔间通向的隔间都是唯一的,不难发现一但来到环上的任何一个房间,必定会绕这个环一圈回到原点。则对于每一个环上的隔间,以其为起点遍历环一圈走的路程是相同的(即环的长度)。所以我们可以根据这个特点,标记环上的每一个房间,一但到达这些隔间就直接返回环的长度。环外的隔间则记录下其到环+环的长度。

int n, d[100008], s[100008], h[100008], flag; 
bool vis[100008];
// d[now]记录now的下一个节点,s[now]记录now到当前路径now到起点的距离,
// h[now]记录以now为起点到该路径终点的距离(记忆化搜索的核心)
// vis[now]记录now是否被遍历过,若遍历过,则说明到达路径终点
int dfs(int now, int nowc) {
    if (h[now]) return nowc + h[now] - 1;  // 记忆化搜索,若已有解直接返回
    if (vis[now] == 1) {
        flag = now;
        h[now] = nowc - s[now];
        return nowc - 1;
    }
    vis[now] = 1;
    s[now] = nowc;
    int ans = dfs(d[now], nowc + 1);
    if (flag != 0) {                      // 点在环上
        if (now == flag) flag = 0;
        else h[now] = h[flag];
    }
    else h[now] = h[d[now]] + 1;          // 点不在环上
    vis[now] = 0;
    return ans;
}

二、最短路径问题

1、Dijkstra算法

P3371 【模板】单源最短路径(弱化版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

Dijkstra是基于一种贪心的策略,首先用数组dis记录起点到每个结点的最短路径,再用一个vis数组保存已经找到最短路径的点。然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点记为已经找到最短路,再从该点出发,看这个点能否到达其它点(记为v)且相比与dis[v]有更短路,将dis[v]的值进行更新,不断重复上述动作,将所有的点都更新到最短路。

小根堆则可以用于取当前最短路的容器,降低时间复杂度。

// 链式前向星存图
struct Edge {
    int u, v, w, next;
}e[maxm];

struct node {
    int w, now;
    inline bool operator <(const node& x)const
    // 实现小根堆
    {
        return w > x.w;
    }
};
// 小根堆,优化取最短边计算最短路径的时间
priority_queue q;

inline void add(int u, int v, int w) {
    e[++cnt].u = u;
    e[cnt].v = v;
    e[cnt].w = w;
    e[cnt].next = head[u]; // next 为以u为起点发散出去的上一条边
    head[u] = cnt;         // head数组记录以u为起点发散出去的最后一条边
}

void dijkstra() {
    for (int i = 1; i <= n; i++) {
        dis[i] = INF;
    }
    dis[s] = 0;
    q.push(node{ 0, s });
    while (!q.empty()) {
        node x = q.top();
        q.pop();
        int  u = x.now;
        if (vis[u]) continue;
        vis[u] = 1;
        for (int i = head[u]; i; i = e[i].next) {
            int v = e[i].v;
            if (dis[v] > dis[u] + e[i].w) {
                dis[v] = dis[u] + e[i].w;
                q.push(node{ dis[v], v });
            }
        }
    }
}

Dijkstra不能处理负边权图!!!

2、spfa算法

原理:每次遍历所有边进行松弛操作,循环n-1次,每次更新起点到其他点的一条最短距离,所以起点到其他点的最短路径只需要n-1次循环即可全部得到。不同于dijkstra,该算法可以处理带负权边的图

用队列优化后,可以去除循环过程中没必要遍历的边。

void spfa(int s) {
    queue q;
    for (int i = 1; i <= n; i++) dis[i] = INF;
    dis[s] = 0, vis[s] = 1;
    q.push(s);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        vis[u] = 0;
        for (int i = head[u]; i; i = e[i].next) {
            int v = e[i].v;
            if (dis[v] > e[i].w + dis[u]) {
                dis[v] = e[i].w + dis[u];
                if (!vis[v]) {
                    vis[v] = 1;
                    q.push(v);    
                }
            }
        }

    }
}

spfa还可用于判断负环

bool spfa(int s) {
    queue q;
    for (int i = 1; i <= n; i++) dis[i] = INF;
    dis[s] = 0, vis[s] = 1;
    q.push(s);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        vis[u] = 0;
        for (int i = head[u]; i; i = e[i].next) {
            int v = e[i].v;
            if (dis[v] > e[i].w + dis[u]) {
                dis[v] = e[i].w + dis[u];
                if (!vis[v]) {
                    vis[v] = 1;
                    q.push(v);  
                    t[v]++;  // t[]记录边被遍历的次数,
                             // 更新的次数大于等于n不就说明图中存在负环
                    if (t[v] == n ) return false;
                }
            }
        }
    }
    return true;
}

3、dijkstra处理带负权边的图

P5905 【模板】Johnson 全源最短路 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

将全部边加上某个增量使全部边变成正权边,在最后计算最短路径时再减去增加量,则可得到原图的最短路径。

增量处理如下:

我们新建一个虚拟节点(在这里我们就设它的编号为 00 )。从这个点向其他所有点连一条边权为 00 的边。

我们新建一个虚拟节点(在这里我们就设它的编号为0)。从这个点向其他所有点连一条边权为 0的边。接下来用 Bellman-Ford 算法求出从0号点到其他所有点的最短路,记为 h_{i}

假如存在一条从 u 点到 v 点,边权为w 的边,则我们将该边的边权重新设置为w+h_{u}-h_{v}

正确性:[洛谷日报#242]Johnson 全源最短路径算法学习笔记 - Studying Father's luogu blog - 洛谷博客

h这个量形如势能。

4、记录最短路径数

P1144 最短路计数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

在最短路径算法中做如下处理:

if (dis[v] > dis[u] + e[i].w) {
     dis[v] = dis[u] + e[i].w;
     ans[v] = ans[u];  // ans[]记录最短路径数
     q.push(node{ dis[v], v });
}
else if (dis[v] == dis[u] + e[i].w) {
     ans[v] = (ans[v] + ans[u]) % mod; // 数目传递
}

5、最短路径+二分

P1462 通往奥格瑞玛的道路 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

求最大值的最小值,且满足单调,可用二分答案求解。

X为当前二分的最大值,所以最短路径要满足经过某节点的费用要小于等于X。

能否到终点则处理为所走路径上收到的伤害值是否小于血量值/dis[n]是否不为INF,即能到终点,以上条件dis[n] <= b即可判断。

bool dijkstra(int X) {
	if (X < f[1]) return 0; // 一开始就不行直接返回
	priority_queue q;
	for (int i = 1; i <= n; i++) {
		vis[i] = 0, dis[i] = INF;
	}
	dis[1] = 0;
	q.push({ dis[1], 1 });
	while (!q.empty()) {
		node x = q.top();
		q.pop();
		int u = x.now;
		if (vis[u]) continue;
		vis[u] = 1;
		for (int i = head[u]; i; i = e[i].next) {
			int v = e[i].v;
			if (f[v] <= X && vis[v] == 0 && dis[v] > dis[u] + e[i].w) {
				dis[v] = dis[u] + e[i].w;
				q.push({ dis[v], v });
			}
		}
	}
	if (dis[n] <= b) {  // 检查能不能到终点
		return 1;
	}
	return 0;
}
	while (l <= r) {
		int mid = (l + r) / 2;
		c = dijkstra(mid);
		if (c) {
			r = mid - 1; // 答案可行,继续看能不能更小
		}
		else {
			l = mid + 1; // 答案不可行,要往大走
		}
	}

6、并查集+最短路径

P1522 [USACO2.4] 牛的旅行 Cow Tours - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

本题目标是在两个独立的连通块,连接各连通块中的一个节点后形成一个大连通块,求这个大连通块中最短路径最大值。

处理方法:

(1)用并查集来区分两个不同的连通块。

(2)由于节点不多,一开始的两个独立连通块中的最短路径可用Floyd-Warshall 算法维护。

    for (int k = 1; k <= n; ++k)  // 注意中间节点在最外面的循环
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j)
                if (dis[i][k] + dis[k][j] < dis[i][j])
                    dis[i][j] = dis[i][k] + dis[k][j];

(3)若节点i属于连通块A,节点J属于连通块B,连接I和j节点后,大连通块的最短路径最大值有三种形况:

A 连通块的最短路径最大值;

B 连通块的最短路径最大值;

A 连通块里从 i 点出发的最远路径 + (i,j) 这一条边 + B 连通块里从j 点出发的最远路径。

你可能感兴趣的:(图论,学习,笔记)