一、图的遍历
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号点到其他所有点的最短路,记为 。
假如存在一条从 u 点到 v 点,边权为w 的边,则我们将该边的边权重新设置为。
正确性:[洛谷日报#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 点出发的最远路径。