分层图
分层图
前言
在一次模拟赛中,我遇到了 [USACO15JAN] Grass Cownoisseur G 这道题,当时不知如何下手,和边上的同学偷偷讨论,听别人说是分层图,建两份图然后连一层反向边即可,当时对这个图论建模大为惊叹(不亚于我在学网络流时学到拆点拆边),故专门写一篇博客记录之。
算法思想
分层图是图论建模的一个巧妙的工具,主要用来解决类似以下形式的问题:
在图中求最短路,但可以做不多于 \(k\) 次的特殊转移,求最短路。
一般题目中的 \(k\) 不会较大
对于这种题目,可以使用分层图解决。
方法是建立 \(k + 1\) 层架构与原图相同的图,然后将特殊转移用有向边的形式连接第 \(i\) 层和第 \(i + 1\) 层节点,则可以使用图论建模来描述原图的 \(k\) 次决策。
一些 \(\text{tricks}\):
- 如果层间的转移比较显然,我们无需真的把多层图建立出来,我们只需要“魔改”最短路算法,对松弛操作做一些修改即可,这也是许多题解的做法。
这种思想可以类比 \(\text{DP}\) 套 \(\text{DP}\) 时,不必把内层自动机建立出来,二者是一个道理。
但我认为,作为图论建模,应该要把逻辑问题抽象为图论问题,因此我更倾向于把分层图完整的建立出来,就像网络流一样。
也许在一些极限卡常的题目,这种方法空间常数更小?
例题
[USACO15JAN] Grass Cownoisseur G
题意:给定一个 \(n\) 点 \(m\) 边的有向图,节点编号 \(1 \dots n\)。
定义一条路径的长度为该路径上经过的所有不同的节点的总数,求 \(1\) 到 \(1\) 的最长路。
特殊转移:可以在任意时刻走最多一次反向边。
这道题是我在考场上遇到的题目,当时并未场切,丢人丢大了。
首先,建两份与原图相同的图,对于原图节点 \(i\),其在第二层图的对应节点为 \(i + n\),用反向边连接两层图。
for (int i = 0; i < m; i++)
{
read(u), read(v);
add_edge(v, u + n); // 跨层级反向边
add_edge(u, v), add_edge(u + n, v + n); // 建两份图
}
考虑到原题要求最长路,而原图全都是正权环,因此要先 \(\text{Tarjan}\) 缩点再求最长路。
同时,注意到 \(\text{Tarjan}\) 缩点具有性质:\(\text{SCC}\) 编号恰为拓扑序,则可以考虑直接逆拓扑序 \(\text{DP}\),显然有转移:
其中 \(E\) 为边集,\(d_u\) 为缩点 \(u\) 到缩点 \(1\) 的距离,\(w_u\) 为缩点 \(u\) 包含的节点数量。
for (int u = id[1]; u; u--)
for (auto &&v : DAG[u])
chkmax(dis[v], dis[u] + cnt[v]);
则答案为 \(d_{1 + n}\)。
有的题解会说答案为 \(\max{d_1, d_{1 + n}}\),其实不然。
以 \(\text{DP}\) 的形式计算贡献默认了 \(d_1\) 为 \(0\),统计 \(d_1\) 无意义。
而且经过反向边的最长路一定不会比不经过反向边要劣,因为可以经过反向边后立即经过正向边。
因此直接统计 \(d_{1 + n}\) 即可。
总代码:
#include <bits/extc++.h>
#define inline __always_inline
template <typename T> inline void chkmin(T &x, const T &y) { if (x > y) x = y; }
template <typename T> inline void chkmax(T &x, const T &y) { if (x < y) x = y; }
template <typename T> inline void read(T &x)
{
char ch;
for (ch = getchar(); !isdigit(ch); ch = getchar());
for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
}
const int MaxN = 2e5 + 5;
int n, m, u, v;
std::vector<int> graph[MaxN], DAG[MaxN];
inline void add_edge(int u, int v) { graph[u].push_back(v); }
char visit[MaxN];
int dis[MaxN], cnt[MaxN], id[MaxN], low[MaxN], dfn[MaxN], cur = 1, scc = 1;
int queue[MaxN], *head = queue, *tail = queue;
void tarjan(int u)
{
low[u] = dfn[u] = cur++;
visit[*tail++ = u] = 1;
for (auto &&v : graph[u])
if (!visit[v])
tarjan(v), chkmin(low[u], low[v]);
else if (visit[v] == 1)
chkmin(low[u], dfn[v]);
if (low[u] == dfn[u])
{
while (*tail != u)
cnt[id[*--tail] = scc]++, visit[*tail] = 2;
scc++;
}
}
int main()
{
read(n), read(m);
for (int i = 0; i < m; i++)
read(u), read(v), add_edge(v, u + n), add_edge(u, v), add_edge(u + n, v + n);
n <<= 1;
for (int u = 1; u <= n; u++)
if (!dfn[u]) tarjan(u);
for (int u = 1; u <= n; u++)
for (auto &&v : graph[u])
if (id[u] != id[v])
DAG[id[u]].push_back(id[v]);
for (int u = id[1]; u; u--)
for (auto &&v : DAG[u])
chkmax(dis[v], dis[u] + cnt[v]);
printf("%d", dis[id[1 + (n >> 1)]]);
return 0;
}
[USACO09FEB] Revamping Trails G
这道题其实非常的板,只需要建立 \(k + 1\) 层图,对于一条原边 \((u, v, w)\),层间连上有向边 \((u, v', 0)\) 和 \((v, u', 0)\) 即可。
不知读者会不会有这样的疑问:为什么直接建立一条 \(0\) 的跨层单向边就可以了呢?高速公路的建立难道是一次性的吗?
事实上,高速公路的建立与使用就是一次性的,因为我们要走最短路,所以我们走重复的路得到的答案一定不优。
总代码:
#include <bits/extc++.h>
#define inline __always_inline
#define Node(u, k) ((u) + (k) * (n))
template <typename T> inline void chkmax(T &x, const T &y) { if (x < y) x = y; }
template <typename T> inline void read(T &x)
{
char ch;
for (ch = getchar(); !isdigit(ch); ch = getchar());
for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
}
const int MaxN = 1e4 + 5, MaxK = 21, MaxV = MaxK * MaxN;
using pii = std::pair<int, int>;
using priority_queue = __gnu_pbds::priority_queue<pii, std::greater<pii>>;
int n, m, k, u, v, w;
struct edge_t { int v, w; };
std::vector<edge_t> graph[MaxV];
inline void add_edge(int u, int v, int w) { graph[u].push_back({v, w}); }
int dis[MaxV]; char visit[MaxV]; priority_queue pq;
decltype(pq)::point_iterator its[MaxV];
int main()
{
read(n), read(m), read(k);
for (int i = 0; i < m; i++)
{
read(u), read(v), read(w), add_edge(u, v, w), add_edge(v, u, w);
for (int i = 1; i <= k; i++)
{
add_edge(Node(u, i), Node(v, i), w), add_edge(Node(v, i), Node(u, i), w);
add_edge(Node(u, i - 1), Node(v, i), 0), add_edge(Node(v, i - 1), Node(u, i), 0);
}
}
memset(dis, 0x3f, sizeof(dis)), dis[1] = 0;
for (int i = 1; i <= Node(n, k); i++) its[i] = pq.push({dis[i], i});
while (!pq.empty())
{
auto u = pq.top().second; pq.pop();
visit[u] = 1;
for (auto &&[v, w] : graph[u])
if (!visit[v] && dis[v] - dis[u] > w)
pq.modify(its[v], {dis[v] = dis[u] + w, v});
}
printf("%d", dis[Node(n, k)]);
return 0;
}
多倍经验:
- [USACO08JAN] Telephone Lines S:注意路径长度的定义为路径上边权的最大值,不是边权和。
- [JLOI2011] 飞行路线:注意注意:可能没有走完 \(k\) 条边就走到了终点,因此要对每一层都取一遍最小值。之前的几道题目数据太水没有测出来!!!
- [BJWC2012] 冻结:注意此题跨层边权为 \(\frac{w}{2}\)。