二进制最短路的完整详解
C++ 二进制最短路详解
目录
- 引言与基本概念
- 0-1 BFS:边权为0或1的最短路
- 2.1 算法原理与正确性证明
- 2.2 与Dijkstra算法的关系
- 2.3 算法实现
- 2.4 时间复杂度分析
- 2.5 路径还原
- 二进制分组最短路
- 3.1 问题模型
- 3.2 核心思想
- 3.3 正确性证明
- 3.4 算法实现
- 3.5 复杂度分析
- 3.6 经典例题:旅行者问题
- 二进制优化建图
- 4.1 问题背景:完全图中的异或边
- 4.2 核心观察与数学原理
- 4.3 建图优化方法
- 4.4 算法实现
- 4.5 复杂度分析
- 4.6 经典例题:[Code+#4] 最短路
- 二进制状态最短路(状压最短路)
- 5.1 问题模型:状态压缩
- 5.2 状态表示与转移
- 5.3 算法实现
- 5.4 经典例题:软件补丁问题
- 综合应用与变形
- 6.1 二进制分组求点集内两两最短路最小值
- 6.2 二进制分组与环问题
- 6.3 二进制优化与分层图结合
- 6.4 bitset优化最短路
- 总结与备忘
1. 引言与基本概念
二进制最短路并非一个单一的算法,而是一类利用二进制(bit)性质优化最短路问题的算法与技巧的总称。它主要包含以下几个分支:
| 分支 | 核心思想 | 适用场景 |
|---|---|---|
| 0-1 BFS | 利用双端队列,边权仅0或1时达到\(O(E)\)线性复杂度 | 边权为0或1的最短路 |
| 二进制分组 | 按二进制位分组,多次跑最短路覆盖所有情况 | 求点集内两两最短路最小值 |
| 二进制优化建图 | 利用异或性质,仅连接二进制相差一位的点 | 完全图异或边权最短路 |
| 二进制状态最短路 | 用二进制掩码表示状态,状态数\(2^k\) | 带状态限制的最短路 |
这些技巧的共同特点是利用二进制的数学性质来降低时间复杂度或简化问题。当边权属于集合 {0, X}(X为任意非负实数)时,这类算法被称为二进制广度优先搜索(Binary BFS) 。
1.1 符号说明
- G = (V, E):有向图或无向图,V为顶点集,E为边集
- n = |V|:顶点数
- m = |E|:边数
- dist[v]:从源点到顶点v的最短距离
- w(u, v):边(u, v)的权重
- ⊕:按位异或(XOR)运算
- S:特殊点集
- k:二进制位数或状态数
2. 0-1 BFS:边权为0或1的最短路
2.1 算法原理与正确性证明
0-1 BFS(也称双端队列BFS)是解决边权只有0和1的单源最短路径(SSSP)问题的线性时间算法。
2.1.1 问题定义
给定一个有向图或无向图 \(G = (V, E)\),每条边 e 的权重 \(w(e) ∈ {0, 1}\)。求从源点 s 到所有其他顶点的最短距离。
2.1.2 核心思想
0-1 BFS的核心洞察来源于对Dijkstra算法的观察。在Dijkstra算法中,优先队列中的顶点距离值相差至多1:
其中Q是优先队列,v是当前距离最小的顶点。这意味着队列中的距离分布形式为:
既然队列中只有两种距离值(当前值和当前值+1),我们不需要优先队列——双端队列(deque)就足够了:
- 0权边扩展的顶点:距离不变,放入队首(立即处理,与当前顶点同优先级)
- 1权边扩展的顶点:距离+1,放入队尾(稍后处理)
2.1.3 正确性证明
引理1(队列距离单调性) :在0-1 BFS的任意时刻,队列中的距离从队首到队尾非递减,且任意两个元素的距离差不超过1。
证明(归纳法):
- 基础:初始时队列只有源点s,\(dist[s]=0\),单调性成立。
- 归纳步骤:假设队列满足单调性。从队首取出顶点u(距离为d)。遍历u的所有出边:
- 若边权为0,新距离为d,放入队首,队列变为
[新顶点, 原队列],仍保持单调。 - 若边权为1,新距离为d+1,放入队尾,队列变为
[原队列, 新顶点],仍保持单调且距离差不超过1。
- 若边权为0,新距离为d,放入队首,队列变为
因此归纳成立。
定理1(0-1 BFS正确性) :0-1 BFS算法正确计算从源点到所有顶点的最短距离。
证明:由引理1,队列始终保持距离非递减。每次从队首取出的顶点具有当前全局最小距离,这与Dijkstra算法每次从优先队列取出最小距离顶点等价。因此,0-1 BFS是Dijkstra算法在边权集合{0,1}下的退化实现,正确性由Dijkstra算法的正确性保证。
2.1.4 与普通BFS的区别
| 特性 | 普通BFS | 0-1 BFS |
|---|---|---|
| 适用边权 | 全部相等(通常为1) | 0或1 |
| 数据结构 | 普通队列(FIFO) | 双端队列(deque) |
| 入队策略 | 全部队尾 | 0权边→队首,1权边→队尾 |
| 时间复杂度 | \(O(V + E)\) | \(O(V + E)\) |
普通BFS假设所有边代价相同。当存在0权边时,普通BFS会先处理代价更大的路径,导致错误。
2.2 与Dijkstra算法的关系
0-1 BFS可以看作是Dijkstra算法的特化版本:
Dijkstra算法 0-1 BFS
─────────────────────────────────────────────────
优先队列(最小堆) → 双端队列
取堆顶(最小距离顶点) → 取队首
松弛边,更新距离 → 松弛边,更新距离
按距离插入堆 → 0权→队首,1权→队尾
因为边权只有0和1,优先队列中永远只有两种不同的距离值,堆结构退化为双端队列。
2.3 算法实现
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
struct Edge {
int to,w;
};
vector<Edge> g[N];
int n,m;
// 0-1 BFS 求单源最短路
// 返回true表示成功,dist存储最短距离
bool zero_one_bfs(int s, vector<int>& dist) {
dist.assign(n+1, inf);
vector<bool> inq(n+1, false);
deque<int> dq;
dist[s]=0;
dq.push_front(s);
inq[s]=true;
while(!dq.empty()) {
int u=dq.front();
dq.pop_front();
inq[u]=false;
for(auto e:g[u]) {
int v=e.to;
int w=e.w;
if(dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
if(!inq[v]) {
if(w == 0) {
dq.push_front(v); // 0权边放队首
} else {
dq.push_back(v); // 1权边放队尾
}
inq[v]=true;
}
}
}
}
return true;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=0;i<m;i++) {
int u,v,w;
cin>>u>>v>>w;
// w 必须为 0 或 1
g[u].push_back({v,w});
// 无向图需要再加一条反向边
// g[v].push_back({u,w});
}
vector<int> dist;
int s;
cin>>s;
zero_one_bfs(s, dist);
for(int i=1;i<=n;i++) {
cout<<"dist["<<i<<"] = "<<dist[i]<<"\n";
}
return 0;
}
2.4 时间复杂度分析
- 时间复杂度:\(O(|V| + |E|)\)
- 每个顶点最多入队出队一次(在距离被优化时可能多次,但每个边最多被松弛一次)
- 每条边被检查常数次
- 空间复杂度:\(O(|V| + |E|)\)
2.5 路径还原
有时我们需要输出具体的路径而不仅仅是距离。
// 在0-1 BFS中同时记录前驱节点
bool zero_one_bfs_with_path(int s, vector<int>& dist, vector<int>& pre) {
dist.assign(n+1, inf);
pre.assign(n+1, -1);
vector<bool> inq(n+1, false);
deque<int> dq;
dist[s]=0;
dq.push_front(s);
inq[s]=true;
while(!dq.empty()) {
int u=dq.front();
dq.pop_front();
inq[u]=false;
for(auto e:g[u]) {
int v=e.to;
int w=e.w;
if(dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pre[v] = u; // 记录前驱
if(!inq[v]) {
if(w == 0) dq.push_front(v);
else dq.push_back(v);
inq[v]=true;
}
}
}
}
return true;
}
// 还原从s到t的路径
vector<int> get_path(int s, int t, const vector<int>& pre) {
vector<int> path;
for(int v=t; v!=-1; v=pre[v]) {
path.push_back(v);
if(v==s) break;
}
reverse(path.be, path.en);
return path;
}
3. 二进制分组最短路
3.1 问题模型
二进制分组最短路用于解决以下经典问题:
给定一个图 G 和一个特殊点集 S(\(|S| = k\)),求 S 中任意两个点之间的最短距离的最小值。
即求:
朴素做法是对每个特殊点跑一次最短路,复杂度 \(O(k · (V+E)\log V)\),当k较大时不可接受。
3.2 核心思想
二进制分组的思想基于一个简单的事实:
任意两个不同的整数,它们的二进制表示中至少有一位不同。
因此,我们可以枚举每个二进制位(0到\(⌈log_₂k⌉-1\)),按照该位是0还是1将特殊点分成两组:
- 集合A:该二进制位为1的特殊点
- 集合B:该二进制位为0的特殊点
对于每次分组,我们添加超级源点S连接到集合A(边权0),集合B连接到超级汇点T(边权0),然后跑一次从S到T的最短路。
由于任意两个特殊点至少有一个二进制位不同,它们必然在某次分组中被分到不同的集合,因此它们之间的最短路必然会被某次最短路计算覆盖。
3.3 正确性证明
定理2(二进制分组正确性) :对特殊点集S按二进制位进行\(⌈log_₂k⌉\)次分组并求最短路,所得最小值即为S中任意两点的最短距离最小值。
证明:
- 设 \(x, y ∈ S\) 是距离最近的两个特殊点,\(dist(x, y) = d_{min}\)
- 因为 \(x ≠ y\),它们的二进制表示至少有一位不同
- 设第 p 位不同,则在枚举第 p 位时,x 和 y 被分到不同的集合
- 此时超级源点到x(或y)的路径和另一个到超级汇点的路径构成了从x到y的一条路径,其长度 \(≤ d_{min}\)
- 由于 \(d_{min}\) 是全局最小值,该路径长度 \(≥ d_{min}\)
- 因此该次最短路的结果恰好等于 \(d_{min}\)
3.4 算法实现
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
struct Edge {
int to,w;
};
vector<Edge> g[N], temp[N];
int n,m,k;
vector<int> special; // 特殊点集
void dijkstra(int s, vector<int>& dist) {
dist.assign(n+2, inf);
vector<bool> vis(n+2, false);
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;
dist[s]=0;
pq.push({0,s});
while(!pq.empty()) {
auto [d,u]=pq.top();
pq.pop();
if(vis[u]) continue;
vis[u]=true;
for(auto e:g[u]) {
int v=e.to;
int w=e.w;
if(dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
}
// 二进制分组求特殊点集内两两最短路最小值
int binary_grouping_shortest() {
int max_bit = 0;
int max_val = 0;
for(int x:special) max_val = max(max_val, x);
while((1LL << max_bit) <= max_val) max_bit++;
max_bit = max(max_bit, 1LL);
int ans = inf;
int S = n + 1; // 超级源点
int T = n + 2; // 超级汇点
for(int bit=0; bit<max_bit; bit++) {
// 备份原图
for(int i=0; i<=n+2; i++) temp[i] = g[i];
// 清空超级源点和汇点的边
g[S].clear();
g[T].clear();
vector<int> group0, group1;
for(int x:special) {
if((x >> bit) & 1) group1.push_back(x);
else group0.push_back(x);
}
if(group0.empty() || group1.empty()) {
// 恢复原图
for(int i=0; i<=n+2; i++) g[i] = temp[i];
continue;
}
// 情况1:group1 -> S, group0 -> T
for(int x:group1) g[S].push_back({x, 0});
for(int x:group0) g[x].push_back({T, 0});
vector<int> dist;
dijkstra(S, dist);
ans = min(ans, dist[T]);
// 恢复
for(int i=0; i<=n+2; i++) g[i] = temp[i];
// 情况2:group0 -> S, group1 -> T(对称情况,保证正确性)
for(int x:group0) g[S].push_back({x, 0});
for(int x:group1) g[x].push_back({T, 0});
dijkstra(S, dist);
ans = min(ans, dist[T]);
// 恢复原图
for(int i=0; i<=n+2; i++) g[i] = temp[i];
}
return ans;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>k;
for(int i=0;i<m;i++) {
int u,v,w;
cin>>u>>v>>w;
g[u].push_back({v,w});
}
special.resize(k);
for(int i=0;i<k;i++) cin>>special[i];
int ans = binary_grouping_shortest();
cout<<ans<<"\n";
return 0;
}
3.5 复杂度分析
- 时间复杂度:\(O(\log k · (V+E)\log V)\)
- 共进行 \(2·⌈log_₂k⌉\) 次最短路计算(每个二进制位两种方向)
- 每次Dijkstra复杂度 \(O((V+E)\log V)\)
- 空间复杂度:\(O(V + E)\)
3.6 经典例题:旅行者问题(Luogu P5304)
题目:给定一个 n 个点 m 条边的有向图,以及 k 个特殊点,求这些特殊点之间两两最短路的最小值。
解法:直接应用二进制分组,对每个二进制位将特殊点分成两组,分别从一组到另一组跑最短路。
关键观察:任意两个特殊点至少有一个二进制位不同,因此它们一定会在某次分组中被分到不同集合。
4. 二进制优化建图
4.1 问题背景:完全图中的异或边
考虑以下问题:
给定一个包含 n 个点的完全图,任意两点 i, j 之间的边权为 (i ⊕ j) × C,其中 ⊕ 是按位异或,C 是常数。求从 s 到 t 的最短路。
朴素建图需要 $O(n²) $条边,当 n 达到 1e5 时不可行。
4.2 核心观察与数学原理
关键观察:异或运算具有可分解性。
对于任意两个数 a 和 b,设它们的异或值为 \(x = a ⊕ b\)。x 的二进制表示可以分解为若干单个二进制位的和:
其中 \(p_i\) 是 a 和 b 二进制不同的位。
例如:1010 ⊕ 0101 = 1111,可以分解为:
这意味着从 a 到 b 的路径可以通过一系列只改变一个二进制位的中间节点来实现。
因此:我们不需要在每对点之间都连边,只需要连接二进制表示相差一位的点对即可。
4.3 建图优化方法
对于每个节点 \(i\)(\(0 ≤ i ≤ n\)),连接所有 i ⊕ 2^k(\(0 ≤ k ≤ ⌈log₂n⌉\))且不超过 n 的节点,边权为 \(2^k × C\)。
原理:
- 从 i 到 j 的最短路径可以通过一系列"翻转单个二进制位"的操作实现
- 每次翻转第 k 位的代价为 \(2^k × C\)
- 总的代价恰好等于$ (i ⊕ j) × C$
边的数量从 $O(n²) $降为 \(O(n \log n)\)。
4.4 算法实现
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
struct Edge {
int to,w;
};
vector<Edge> g[N];
int n,m,C,s,t;
void dijkstra(int s, vector<int>& dist) {
dist.assign(n+1, inf);
vector<bool> vis(n+1, false);
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;
dist[s]=0;
pq.push({0,s});
while(!pq.empty()) {
auto [d,u]=pq.top();
pq.pop();
if(vis[u]) continue;
vis[u]=true;
for(auto e:g[u]) {
int v=e.to;
int w=e.w;
if(dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>C;
// 读入原有的边
for(int i=0;i<m;i++) {
int u,v,w;
cin>>u>>v>>w;
g[u].push_back({v,w});
}
// 二进制优化建图:连接二进制仅一位不同的点
int max_bit = 0;
while((1LL << max_bit) <= n) max_bit++;
for(int i=0;i<=n;i++) {
for(int bit=0;bit<max_bit;bit++) {
int j = i ^ (1LL << bit);
if(j <= n) {
int w = (1LL << bit) * C;
g[i].push_back({j, w});
// 由于对称性,反向边会自动添加(当循环到j时)
}
}
}
cin>>s>>t;
vector<int> dist;
dijkstra(s, dist);
cout<<dist[t]<<"\n";
return 0;
}
4.5 复杂度分析
- 建图复杂度:\(O(n \log n)\)
- 边数:\(m + n·⌈log_₂n⌉\)
- 最短路复杂度:\(O((m + n \log n) \log n)\)
4.6 经典例题:[Code+#4] 最短路(P4366)
题目:给定 n 个点,任意两点 i, j 之间有边权为 \((i ⊕ j) × C\) 的边,另外还有 m 条给定的边。求 s 到 t 的最短路。
解法:利用二进制优化建图,只连接 \(i\) 和 \(i ⊕ 2^k\)。建图后直接跑 Dijkstra。
代码要点:
for(int i=0;i<=n;i++)
for(int k=0;k<=20;k++)
if((i^(1<<k))<=n)
add(i, i^(1<<k), (1<<k)*C);
5. 二进制状态最短路(状压最短路)
5.1 问题模型:状态压缩
在某些最短路问题中,每个节点或边具有状态属性(如是否拥有某把钥匙、是否修复了某个bug等),路径的可行性取决于当前累积的状态。
状态压缩(状压) 用二进制掩码(bitmask)表示状态集合:
- 第 i 位为 1:拥有/完成了第 i 个属性
- 第 i 位为 0:没有/未完成第 i 个属性
总状态数为 \(2^k\),其中 k 是属性种类数。
5.2 状态表示与转移
将原图扩展为分层图,每一层对应一个状态:
- 节点:(u, mask) 表示在顶点 u 且状态为 mask
- 边:从 (u, mask) 到 (v, new_mask),代价为 w
- 状态转移通过位运算实现:
mask | (1 << key):获得第 key 个属性mask & ~(1 << key):失去第 key 个属性mask & (1 << key):检查是否拥有第 key 个属性
5.3 算法实现
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=105;
const int mod=998244353;
const int inf=1e18;
struct Edge {
int to,w;
};
vector<Edge> g[N];
int n,m,k; // k为属性种类数
// 状态最短路:dist[u][mask] 表示在顶点u、状态为mask时的最短距离
int state_shortest(int s, int t) {
int total_states = 1 << k;
vector<vector<int>> dist(n+1, vector<int>(total_states, inf));
vector<vector<bool>> vis(n+1, vector<bool>(total_states, false));
// 优先队列:{距离, 顶点, 状态}
priority_queue<tuple<int,int,int>, vector<tuple<int,int,int>>, greater<tuple<int,int,int>>> pq;
dist[s][0] = 0;
pq.push({0, s, 0});
while(!pq.empty()) {
auto [d,u,mask] = pq.top();
pq.pop();
if(vis[u][mask]) continue;
vis[u][mask] = true;
if(u == t) return d;
for(auto e:g[u]) {
int v=e.to;
int w=e.w;
// 假设每条边有一个属性要求 need,通过后获得属性 gain
// 这里简化:边需要属性need才能通过
int need = 0; // 需要拥有的属性掩码
int gain = 0; // 通过后获得的属性掩码
if((mask & need) != need) continue; // 不满足条件
int new_mask = mask | gain;
if(dist[v][new_mask] > dist[u][mask] + w) {
dist[v][new_mask] = dist[u][mask] + w;
pq.push({dist[v][new_mask], v, new_mask});
}
}
}
return -1;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>k;
for(int i=0;i<m;i++) {
int u,v,w;
cin>>u>>v>>w;
g[u].push_back({v,w});
}
int s,t;
cin>>s>>t;
int ans = state_shortest(s, t);
cout<<ans<<"\n";
return 0;
}
5.4 经典例题:软件补丁问题(Luogu P2761)
题目:有 n 个bug,m 个补丁。每个补丁可以修复某些bug,但也可能引入新的bug。求从全bug状态到无bug状态的最少补丁数。
解法:将bug状态用二进制掩码表示(\(2^n\) 种状态)。每个补丁对应一条从当前状态到新状态的边,跑最短路。
6. 综合应用与变形
6.1 二进制分组求点集内两两最短路最小值
这是二进制分组最典型的应用。除了前面介绍的基本形式外,还有一些变形:
变形1:求点集内两两最短路的最大值
- 可以二分答案,然后用二进制分组判断是否存在距离 ≤ mid 的点对
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
int n,m,k;
vector<vector<pair<int,int>>> g;
vector<int> interest;
bool check(int mid,vector<vector<pair<int,int>>>& graph,vector<int>& nodes) {
int bit=0;
while((1<<bit)<=k) bit++;
for(int i=0;i<bit;i++) {
vector<vector<pair<int,int>>> temp=graph;
vector<int> e;
for(int j=0;j<k;j++) {
int l=nodes[j];
if((j>>i)&1) temp[0].push_back({l,0});
else {
temp[l].push_back({n+1,0});
e.push_back(l);
}
}
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<>> pq;
vector<int> dist(n+2,inf);
vector<bool> vis(n+2,false);
dist[0]=0;
pq.push({0,0});
while(!pq.empty()) {
int u=pq.top().se;
pq.pop();
if(vis[u]) continue;
vis[u]=true;
if(u==n+1) break;
for(auto edge:temp[u]) {
int v=edge.fi;
int w=edge.se;
if(dist[v]>dist[u]+w) {
dist[v]=dist[u]+w;
pq.push({dist[v],v});
}
}
}
if(dist[n+1]<=mid) return true;
temp[0].clear();
for(auto l:e) temp[l].pop_back();
e.clear();
for(int j=0;j<k;j++) {
int l=nodes[j];
if(!((j>>i)&1)) temp[0].push_back({l,0});
else {
temp[l].push_back({n+1,0});
e.push_back(l);
}
}
dist.assign(n+2,inf);
vis.assign(n+2,false);
dist[0]=0;
pq.push({0,0});
while(!pq.empty()) {
int u=pq.top().se;
pq.pop();
if(vis[u]) continue;
vis[u]=true;
if(u==n+1) break;
for(auto edge:temp[u]) {
int v=edge.fi;
int w=edge.se;
if(dist[v]>dist[u]+w) {
dist[v]=dist[u]+w;
pq.push({dist[v],v});
}
}
}
if(dist[n+1]<=mid) return true;
}
return false;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
cin>>T;
while(T--) {
cin>>n>>m>>k;
g.assign(n+2,{});
for(int i=1;i<=m;i++) {
int a,b,c;
cin>>a>>b>>c;
g[a].push_back({b,c});
}
interest.clear();
vector<int> vis(n+1,0);
for(int i=1;i<=k;i++) {
int a;
cin>>a;
if(!vis[a]) {
vis[a]=1;
interest.push_back(a);
}
}
k=interest.size();
if(k<=1) {
cout<<-1<<"\n";
continue;
}
int l=0,r=inf,ans=-1;
while(l<=r) {
int mid=(l+r)/2;
if(check(mid,g,interest)) {
ans=mid;
r=mid-1;
}
else l=mid+1;
}
cout<<ans<<"\n";
}
return 0;
}
变形2:求点集内每个点到最近特殊点的距离
- 多源BFS/Dijkstra即可,不需要二进制分组
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244998;
const int inf=1e18;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
cin>>T;
while(T--) {
int n,m,k;
cin>>n>>m>>k;
vector<vector<pair<int,int>>> g(n+2);
for(int i=1;i<=m;i++) {
int a,b,c;
cin>>a>>b>>c;
g[a].push_back({b,c});
}
vector<int> interest;
vector<int> special(n+1,0);
for(int i=1;i<=k;i++) {
int a;
cin>>a;
if(!special[a]) {
special[a]=1;
interest.push_back(a);
}
}
k=interest.size();
vector<int> dist(n+1,inf);
vector<int> from(n+1,-1);
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<>> pq;
for(int i=0;i<k;i++) {
int node=interest[i];
dist[node]=0;
from[node]=node;
pq.push({0,node});
}
while(!pq.empty()) {
int u=pq.top().se;
int d=pq.top().fi;
pq.pop();
if(d!=dist[u]) continue;
for(auto edge:g[u]) {
int v=edge.fi;
int w=edge.se;
if(dist[v]>dist[u]+w) {
dist[v]=dist[u]+w;
from[v]=from[u];
pq.push({dist[v],v});
}
}
}
for(int i=1;i<=n;i++) {
if(special[i]) cout<<0<<" ";
else if(dist[i]==inf) cout<<-1<<" ";
else cout<<dist[i]<<" ";
}
cout<<"\n";
}
return 0;
}
变形3:带权点集
- 每个特殊点有权值,求 \(min(dist(u,v) + val[u] + val[v])\)
- 可以在超级源点连接时边权设为$ val[u]$
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244998;
const int inf=1e18;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
cin>>T;
while(T--) {
int n,m,k;
cin>>n>>m>>k;
vector<vector<pair<int,int>>> g(n+2);
for(int i=1;i<=m;i++) {
int a,b,c;
cin>>a>>b>>c;
g[a].push_back({b,c});
}
vector<int> interest;
vector<int> val(n+1,0);
vector<int> special(n+1,0);
for(int i=1;i<=k;i++) {
int a;
cin>>a;
cin>>val[a];
if(!special[a]) {
special[a]=1;
interest.push_back(a);
}
}
k=interest.size();
if(k<=1) {
cout<<-1<<"\n";
continue;
}
int bit=0;
while((1<<bit)<=k) bit++;
int ans=inf;
for(int i=0;i<bit;i++) {
vector<vector<pair<int,int>>> temp=g;
vector<int> e;
for(int j=0;j<k;j++) {
int l=interest[j];
if((j>>i)&1) temp[0].push_back({l,val[l]});
else {
temp[l].push_back({n+1,val[l]});
e.push_back(l);
}
}
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<>> pq;
vector<int> dist(n+2,inf);
vector<bool> vis(n+2,false);
dist[0]=0;
pq.push({0,0});
while(!pq.empty()) {
int u=pq.top().se;
pq.pop();
if(vis[u]) continue;
vis[u]=true;
if(u==n+1) break;
for(auto edge:temp[u]) {
int v=edge.fi;
int w=edge.se;
if(dist[v]>dist[u]+w) {
dist[v]=dist[u]+w;
pq.push({dist[v],v});
}
}
}
ans=min(ans,dist[n+1]);
temp[0].clear();
for(auto l:e) temp[l].pop_back();
e.clear();
for(int j=0;j<k;j++) {
int l=interest[j];
if(!((j>>i)&1)) temp[0].push_back({l,val[l]});
else {
temp[l].push_back({n+1,val[l]});
e.push_back(l);
}
}
dist.assign(n+2,inf);
vis.assign(n+2,false);
dist[0]=0;
pq.push({0,0});
while(!pq.empty()) {
int u=pq.top().se;
pq.pop();
if(vis[u]) continue;
vis[u]=true;
if(u==n+1) break;
for(auto edge:temp[u]) {
int v=edge.fi;
int w=edge.se;
if(dist[v]>dist[u]+w) {
dist[v]=dist[u]+w;
pq.push({dist[v],v});
}
}
}
ans=min(ans,dist[n+1]);
}
cout<<ans<<"\n";
}
return 0;
}
6.2 二进制分组与环问题
在环问题中(如从1出发回到1,要求两条边不同),二进制分组也很有用。
问题:求从1出发回到1的最短环,要求出边和入边不同。
思路:将与1相邻的边按二进制位分组:
- 某次分组中,出边在集合A,入边在集合B
- 跑从1到1的最短路(经过集合A的边出去,经过集合B的边回来)
- 任意两条不同的边至少有一个二进制位不同,因此会被某次分组覆盖
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244998;
const int inf=1e18;
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
cin>>T;
while(T--)
{
int n,m;
cin>>n>>m;
vector<vector<pair<int,int>>> g(n+2);
vector<pair<int,pair<int,int>>> edges;
for(int i=1;i<=m;i++)
{
int a,b,c;
cin>>a>>b>>c;
g[a].push_back({b,c});
if(a==1) edges.push_back({b,{c,i}});
}
int k=edges.size();
if(k<=1)
{
cout<<-1<<"\n";
continue;
}
int bit=0;
while((1<<bit)<=k) bit++;
int ans=inf;
for(int i=0;i<bit;i++)
{
vector<vector<pair<int,int>>> temp=g;
for(int j=0;j<k;j++)
{
int v=edges[j].fi;
int w=edges[j].se.fi;
int id=edges[j].se.se;
if((j>>i)&1) temp[0].push_back({v,w});
else temp[v].push_back({n+1,w});
}
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<>> pq;
vector<int> dist(n+2,inf);
vector<bool> vis(n+2,false);
dist[0]=0;
pq.push({0,0});
while(!pq.empty())
{
int u=pq.top().se;
pq.pop();
if(vis[u]) continue;
vis[u]=true;
if(u==n+1) break;
for(auto edge:temp[u])
{
int v=edge.fi;
int w=edge.se;
if(dist[v]>dist[u]+w)
{
dist[v]=dist[u]+w;
pq.push({dist[v],v});
}
}
}
ans=min(ans,dist[n+1]);
temp[0].clear();
for(int j=0;j<k;j++)
{
int v=edges[j].fi;
int w=edges[j].se.fi;
int id=edges[j].se.se;
if(!((j>>i)&1)) temp[0].push_back({v,w});
else temp[v].push_back({n+1,w});
}
dist.assign(n+2,inf);
vis.assign(n+2,false);
dist[0]=0;
pq.push({0,0});
while(!pq.empty())
{
int u=pq.top().se;
pq.pop();
if(vis[u]) continue;
vis[u]=true;
if(u==n+1) break;
for(auto edge:temp[u])
{
int v=edge.fi;
int w=edge.se;
if(dist[v]>dist[u]+w)
{
dist[v]=dist[u]+w;
pq.push({dist[v],v});
}
}
}
ans=min(ans,dist[n+1]);
}
cout<<(ans==inf?-1:ans)<<"\n";
}
return 0;
}
6.3 二进制优化与分层图结合
当问题同时具有状态和大图两个维度时,可以将二进制优化建图和分层图结合:
- 外层:二进制优化建图处理完全图异或边
- 内层:分层图处理状态限制
这种结合可以处理诸如"在异或边权的图上,带钥匙限制的最短路"等复杂问题。
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244998;
const int inf=1e18;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
cin>>T;
while(T--) {
int n,m,k;
cin>>n>>m>>k;
vector<vector<pair<int,int>>> g(n+2);
vector<int> key(n+1,0);
for(int i=1;i<=n;i++) {
cin>>key[i];
}
for(int i=1;i<=m;i++) {
int a,b,c;
cin>>a>>b>>c;
g[a].push_back({b,c});
}
int bit=0;
while((1<<bit)<=k) bit++;
vector<vector<pair<int,int>>> temp(n+2);
for(int i=0;i<bit;i++) {
temp.assign(n+2,{});
vector<int> group0,group1;
for(int j=1;j<=n;j++) {
if(key[j]&(1<<i)) group1.push_back(j);
else group0.push_back(j);
}
for(int j=0;j<(int)group0.size();j++) {
temp[0].push_back({group0[j],0});
}
for(int j=0;j<(int)group1.size();j++) {
temp[group1[j]].push_back({n+1,0});
}
for(int u=1;u<=n;u++) {
for(auto edge:g[u]) {
temp[u].push_back({edge.fi,edge.se});
}
}
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<>> pq;
vector<int> dist(n+2,inf);
vector<bool> vis(n+2,false);
dist[0]=0;
pq.push({0,0});
while(!pq.empty()) {
int u=pq.top().se;
pq.pop();
if(vis[u]) continue;
vis[u]=true;
if(u==n+1) break;
for(auto edge:temp[u]) {
int v=edge.fi;
int w=edge.se;
if(dist[v]>dist[u]+w) {
dist[v]=dist[u]+w;
pq.push({dist[v],v});
}
}
}
int ans=dist[n+1];
if(ans<inf) {
cout<<ans<<"\n";
break;
}
}
}
return 0;
}
6.4 bitset优化最短路
在某些场景中,可以用 bitset 来优化最短路算法:
应用1:传递闭包
- 用 bitset 存储每个节点能到达的节点集合
- Floyd-Warshall 的 bitset 优化:
reach[i] |= reach[k]当reach[i][k]为真
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2005;
const int mod=998244998;
const int inf=1e18;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
cin>>T;
while(T--) {
int n,m;
cin>>n>>m;
bitset<N> reach[N];
for(int i=1;i<=n;i++) {
reach[i].reset();
reach[i][i]=1;
}
for(int i=1;i<=m;i++) {
int a,b;
cin>>a>>b;
reach[a][b]=1;
}
for(int k=1;k<=n;k++) {
for(int i=1;i<=n;i++) {
if(reach[i][k]) {
reach[i]|=reach[k];
}
}
}
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++) {
cout<<(reach[i][j]?1:0)<<" ";
}
cout<<"\n";
}
cout<<"\n";
}
return 0;
}
应用2:最短路图中的路径计数
- 在 Dijkstra 过程中用 bitset 维护经过的节点集合
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
cin>>T;
while(T--) {
int n,m,s,t;
cin>>n>>m>>s>>t;
vector<vector<pair<int,int>>> g(n+2);
for(int i=1;i<=m;i++) {
int a,b,c;
cin>>a>>b>>c;
g[a].push_back({b,c});
}
vector<int> dist(n+1,inf);
vector<int> cnt(n+1,0);
vector<bool> vis(n+1,false);
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<>> pq;
dist[s]=0;
cnt[s]=1;
pq.push({0,s});
while(!pq.empty()) {
int u=pq.top().se;
pq.pop();
if(vis[u]) continue;
vis[u]=true;
for(auto edge:g[u]) {
int v=edge.fi;
int w=edge.se;
if(dist[v]>dist[u]+w) {
dist[v]=dist[u]+w;
cnt[v]=cnt[u];
pq.push({dist[v],v});
}
else if(dist[v]==dist[u]+w) {
cnt[v]=(cnt[v]+cnt[u])%mod;
}
}
}
cout<<dist[t]<<" "<<cnt[t]%mod<<"\n";
}
return 0;
}
应用3:稠密图的BFS优化
- 用 bitset 存储邻接矩阵,利用位运算加速BFS
// bitset优化BFS示例(稠密图)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=2005;
bitset<N> adj[N]; // 邻接矩阵
bitset<N> unvisited; // 未访问节点
void bfs_bitset(int s, int n) {
queue<int> q;
q.push(s);
unvisited.reset(s);
while(!q.empty()) {
int u=q.front();
q.pop();
// 找出所有未访问且与u相邻的节点
bitset<N> reachable = adj[u] & unvisited;
for(int v=reachable._Find_first(); v<n; v=reachable._Find_next(v)) {
unvisited.reset(v);
q.push(v);
}
}
}
7. 总结与备忘
7.1 各技术对比
| 技术 | 核心数据结构 | 时间复杂度 | 适用场景 |
|---|---|---|---|
| 0-1 BFS | 双端队列 | \(O(V+E)\) | 边权仅0/1 |
| 二进制分组 | 优先队列(Dijkstra) | \(O(\log k·(V+E)\log V)\) | 点集内两两最短路 |
| 二进制优化建图 | 优先队列(Dijkstra) | \(O((V+E)\log V)\) | 异或边权完全图 |
| 二进制状态最短路 | 优先队列(Dijkstra) | \(O(2^k·(V+E)\log V)\) | 带状态限制 |
7.2 选择指南
- 边权只有0和1 → 使用 0-1 BFS
- 求特殊点集内两两最短路最小值 → 使用 二进制分组
- 完全图边权为异或值 → 使用 二进制优化建图
- 路径受状态/属性限制 → 使用 二进制状态最短路
7.3 常见陷阱
- 0-1 BFS的权值检查:确保所有边权确实是0或1,否则算法错误
- 二进制分组的位数:需要覆盖所有特殊点的最大编号,位数 = \(⌈log_₂(max_{id})⌉\)
- 二进制优化建图的节点范围:注意 0 号节点和 n 号节点的处理
- 状态最短路的状态数:$2^k $可能很大,注意 k 的范围(通常 \(k ≤ 20\))
7.4 核心公式速查
0-1 BFS入队规则:
二进制分组轮数:
二进制优化建图边:
状态压缩位运算:
- 设置第k位:
mask |= (1 << k) - 清除第k位:
mask &= ~(1 << k) - 检查第k位:
mask & (1 << k)
以上是二进制最短路的完整详解,涵盖了0-1 BFS、二进制分组、二进制优化建图、二进制状态最短路四大技术分支及其变形与应用。
本文来自博客园,作者:Aries2011,转载请注明原文链接:https://www.cnblogs.com/Aries2011/p/23051207

浙公网安备 33010602011771号