今天我们来探讨一道来自AtCoder Beginner Contest 245的G题——"Foreign Friends"。这是一道结合了图论和位运算的典型算法题,考察了多源最短路径算法在特定条件下的优化应用。
题目描述如下:给定一个无向图,每个节点有一个颜色属性。我们需要为每个节点找到距离最近的"外国朋友"——即颜色不同的特殊节点。特殊节点由题目给出,数量为L个。
输入包含:
输出要求:
如果没有颜色限制,这个问题就是典型的多源最短路径问题。我们可以采用堆优化的Dijkstra算法:
这种方法的时间复杂度为O(M log N),对于大规模图也是可行的。
当加入颜色限制后(即要求目标特殊节点颜色与当前节点不同),直接套用上述方法就不适用了。最直观的暴力解法是:
对于每个节点i:
这种方法的复杂度高达O(L × M log N),当L很大时(比如L≈N),复杂度将达到O(NM log N),这在N和M都是1e5量级时显然无法接受。
观察到颜色是用整数表示的,我们可以利用位运算的性质进行优化。关键观察点是:
任何两个不同的整数,在它们的二进制表示中至少有一位不同。
这意味着对于任意两个颜色不同的节点,至少存在一个二进制位,使得这两个节点在该位上的值不同。
基于上述观察,我们可以设计如下算法:
这样,对于每个节点i,其颜色A[i]与某个特殊节点颜色不同,必然存在至少一个二进制位b,使得A[i]和该特殊节点在b位上的值不同。因此,正确性得以保证。
总复杂度:O(logV × M log N),其中V是颜色值的上限。对于V=1e5,logV≈17,这在N和M为1e5量级时是可接受的。
cpp复制#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
constexpr i64 inf = 1E18;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, M, K, L;
cin >> N >> M >> K >> L;
vector<int> A(N), B(L);
for (int i = 0; i < N; ++i) cin >> A[i];
for (int i = 0; i < L; ++i) {
cin >> B[i];
--B[i]; // 转换为0-based
}
vector<vector<array<int, 2>>> adj(N);
for (int i = 0; i < M; ++i) {
int u, v, c;
cin >> u >> v >> c;
--u; --v; // 转换为0-based
adj[u].push_back({v, c});
adj[v].push_back({u, c});
}
这部分代码处理输入并构建图的邻接表表示。注意节点编号从1-based转换为0-based的细节处理。
cpp复制 vector<i64> ans(N, inf);
for (int b = 0; b < 18; ++b) {
for (int o = 0; o < 2; ++o) {
priority_queue<pair<i64, int>, vector<pair<i64, int>>, greater<>> q;
vector<i64> dis(N, inf);
// 初始化源点
for (int i = 0; i < L; ++i) {
if ((A[B[i]] >> b & 1) == o) {
q.emplace(0, B[i]);
dis[B[i]] = 0;
}
}
// Dijkstra主循环
while (!q.empty()) {
auto [d, u] = q.top();
q.pop();
if (dis[u] < d) continue;
for (auto &[v, w] : adj[u]) {
if (d + w < dis[v]) {
dis[v] = d + w;
q.emplace(dis[v], v);
}
}
}
// 更新答案
for (int i = 0; i < N; ++i) {
if ((A[i] >> b & 1) != o) {
ans[i] = min(ans[i], dis[i]);
}
}
}
}
这部分是算法的核心:
cpp复制 for (int i = 0; i < N; ++i) {
if (ans[i] == inf) ans[i] = -1;
cout << ans[i] << " \n"[i + 1 == N];
}
return 0;
}
最后处理输出,将未更新的inf值转为-1,并按格式输出结果。
代码中固定枚举了18位(0到17),这是因为题目中颜色值的上限是1e5,而2^16=65536,2^17=131072,所以17位足够覆盖所有可能的颜色值。
使用std::priority_queue配合std::greater实现了最小堆,确保每次取出当前距离最小的节点。这是Dijkstra算法的标准实现方式。
cpp复制if (dis[u] < d) continue;
这一行是Dijkstra算法的经典优化,避免处理已经找到更优解的节点。
可能原因:
解决方案:
std::priority_queuevector<vector<pair<int, int>>>等高效邻接表表示可能原因:
d + w < dis[v])调试建议:
这种基于位运算的分治策略可以应用于其他有"差异"约束的问题,例如:
关键思路是将复杂的差异条件分解为多个独立的二进制位条件,从而降低问题复杂度。
在实际工程中,类似的技巧也常用于:
理解这种按位处理的思维方式,对于解决复杂的算法问题非常有帮助。