题意
直角坐标系中有一些点,每个点有个坐标。
一共 m 条边,每条边连接两个点
x
i
,
y
i
x_i, y_i
xi,yi,有权值
w
i
w_i
wi。
问,图中是否存在环,如果存在的话权值最小为多少?
1
≤
T
≤
50.
1 ≤ T ≤ 50.
1≤T≤50.
1
≤
m
≤
4000.
1 ≤ m ≤ 4000.
1≤m≤4000.
−
10000
≤
x
i
,
y
i
≤
10000
,
1
≤
w
≤
1
0
5
−10000 ≤ x_i, y_i ≤ 10000,\ 1 ≤ w ≤ 10^5
−10000≤xi,yi≤10000, 1≤w≤105
思路
在找题解的过程中,发现用 dijkstra 求最小环的做法也是可以解决这道题的。
复杂度不是 O(n*mlogn) 或者 O(m^2logn) 吗?为什么能解决这道题呢?剪枝一下。
这里采用枚举所有边,求一个端点到另一个端点的最短距离+边权的 O(m^2logn) 的做法求解。
需要知道 dijkstra 求解最短路的一个性质:依次出队的节点距离是递增的,按距离递增的顺序更新各个节点。
这就说明,如果发现 当前出队的节点距离+枚举的边权 已经超过之前求得的最小环权值了ans,那么当前边求出的答案一定大于 ans 了,就没有搜下去的必要了,直接 return。
神奇的剪枝!
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define int long long
#define PII pair<int,int>
#define fi first
#define se second
#define endl '\n'
const int N = 200010, mod = 1e9+7;
int T, n, m;
int a[N];
map<PII, int> mp;
struct node{
int x, y;
int w;
}edg[N];
int dist[N], f[N];
vector<PII> e[N];
int mina, fx, fy;
int get(int x, int y)
{
if(!mp.count({x, y})) mp[{x, y}] = ++n;
return mp[{x, y}];
}
void dij(int st, int stw)
{
for(int i=1;i<=n;i++) dist[i] = 1e9, f[i] = 0;
dist[st] = 0;
priority_queue<PII, vector<PII>, greater<PII> > que;
que.push({0, st});
while(que.size())
{
int x = que.top().se, w = que.top().fi;
if(que.top().fi + stw >= mina) return;
que.pop();
if(f[x]) continue;
f[x] = 1;
for(PII it : e[x])
{
int tx = it.fi, w = it.se;
if(x == fx && tx == fy) continue;
if(dist[tx] > dist[x] + w){
dist[tx] = dist[x] + w;
que.push({dist[tx], tx});
}
}
}
}
signed main(){
Ios;
cin >> T;
for(int cs = 1; cs <= T; cs ++)
{
mp.clear();
n = 0;
cin >> m;
for(int i=1;i<=2*m;i++) e[i].clear();
for(int i=1;i<=m;i++)
{
int a1, b1, a2, b2; cin >> a1 >> b1 >> a2 >> b2;
int x = get(a1, b1), y = get(a2, b2);
int w; cin >> w;
edg[i] = {x, y, w};
e[x].push_back({y, w});
e[y].push_back({x, w});
}
mina = 1e9;
for(int i=1;i<=m;i++)
{
fx = edg[i].x, fy = edg[i].y;
int w = edg[i].w;
dij(fx, w);
mina = min(mina, dist[fy] + w);
}
cout << "Case #" << cs << ": ";
if(mina == 1e9) cout << 0 << "\n";
else cout << mina << "\n";
}
return 0;
}
另外一个ac的做法是最小生成树 + LCA,虽然能过掉这个题,但是用该算法处理最小环问题本身就是错误的,可能在这里巧了吧,看看就行了。
具体做法:
首先求出图的最小生成树,然后枚举所有不在树中的边,用其两个端点在树中的距离(LCA求解)再加上该边的权值,便是这个边所在的最小环。
错误原因: 最小生成树会把根据图中所有边权相对最小的边将所有的点连成一棵树,但是两个点之间的最短距离却不一定是从这个树上过来的,那么此时该做法就是错的了。
比如:
这里从1点2点构成的最小环,并不经过最小生成树上的边。
所以这个算法看看就行了。 【更新】
编码中需要特别注意:
- 当给出的图不连通时,在每次 dfs 更新树的深度和倍增数组时,需要搜遍所有的连通块:
for(int i=1;i<=n;i++) //额外注意图不联通的情况,要搜遍整张图
if(!dep[i]) dep[i] = 1, dfs(i, 0);
-
多组用例时,需要把
dist[i], dep[i]
和 倍增数组f[i,j]
都初始化掉。
一开始想的时候觉得,dist[i] 和 dep[i] 都是由节点 1 更新过来的,而每次都没有动节点 1 的 dist 和 dep,所以就都不用初始化。同样 倍增数组 也是这样想的。但是问题出在哪呢?当图不连通的时候,每次连通块都要搜一遍,这样就不是都从 1 节点更新的了,如果最开始搜的节点保存的是上组用例的数据,那么就出现错误了。
所以以后碰见这样的题目就都把数组啥的初始化掉,反正也不会多耗多少时间,养成好习惯,否则就像这道题,一调调半天。。 -
在标记每条边是否在最小生成树中时,直接把这条边输入时的编号标记,而不要把两个端点构成的二元组{x, y}标记。
因为可能存在重边,其中一条边在最小生成树中,完全可以枚举其他边。
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define int long long
#define PII pair<int,int>
#define fi first
#define se second
#define endl '\n'
const int N = 200010, mod = 1e9+7;
int T, n, m;
map<PII, int> mp;
struct node{
int x, y;
int w;
}edg[N];
int dist[N];
vector<PII> e[N];
int mina;
int pre[N];
int f[8010][30];
int k, dep[N];
int stay[N];
void init() //一定记得初始化所有的数组
{
for(int i=1;i<=n;i++){
e[i].clear();
pre[i] = i;
dep[i] = 0;
dist[i] = 0;
for(int j=0;j<=k;j++) f[i][j] = 0;
}
for(int i=1;i<=m;i++) stay[i] = 0;
}
int get(int x, int y)
{
if(!mp.count({x, y})) mp[{x, y}] = ++n;
return mp[{x, y}];
}
bool cmp(node a, node b){
return a.w < b.w;
}
int find(int x){
if(pre[x] != x) pre[x] = find(pre[x]);
return pre[x];
}
void dfs(int x, int fa)
{
for(PII it : e[x])
{
int tx = it.fi, w = it.se;
if(tx == fa) continue;
dist[tx] = dist[x] + w;
dep[tx] = dep[x] + 1;
f[tx][0] = x;
for(int i=1;i<=k;i++)
f[tx][i] = f[f[tx][i-1]][i-1];
dfs(tx, x);
}
}
int lca(int x, int y)
{
if(dep[x] < dep[y]) swap(x, y);
for(int i=k;i>=0;i--)
{
if(dep[f[x][i]] >= dep[y]) x = f[x][i];
}
if(x == y) return x;
for(int i=k;i>=0;i--)
{
if(f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
}
return f[x][0];
}
signed main(){
Ios;
cin >> T;
for(int cs = 1; cs <= T; cs ++)
{
mp.clear();
n = 0;
cin >> m;
for(int i=1;i<=m;i++)
{
int a1, b1, a2, b2; cin >> a1 >> b1 >> a2 >> b2;
int x = get(a1, b1), y = get(a2, b2);
int w; cin >> w;
edg[i] = {x, y, w};
}
k = log(n)/log(2);
init();
sort(edg+1, edg+m+1, cmp);
for(int i=1;i<=m;i++)
{
int x = edg[i].x, y = edg[i].y, w = edg[i].w;
if(find(x) != find(y))
{
e[x].push_back({y, w});
e[y].push_back({x, w});
pre[find(x)] = find(y);
stay[i] = 1; //注意可能有重边,所以不能直接标记{x, y}
}
}
for(int i=1;i<=n;i++) //注意图不连通的情况
if(!dep[i]) dep[i] = 1, dfs(i, 0);
mina = 1e9;
for(int i=1;i<=m;i++)
{
int x = edg[i].x, y = edg[i].y, w = edg[i].w;
if(stay[i]) continue;
int Lca = lca(x, y);
int dis = dist[x] + dist[y] - 2*dist[Lca];
mina = min(mina, dis + w);
}
cout << "Case #" << cs << ": ";
if(mina == 1e9) cout << 0 << "\n";
else cout << mina << "\n";
}
return 0;
}
如果用这种做法处理经典的问题,求至少有三个点的最小环,在枚举所有边的时候就需要特别判断是否两个端点在最小生成树中直接相连,要避免这种情况。
此时,就需要标记 {x, y} 二元组了。
(一开始想的是,如果两个端点的 lca 是其中一个的话,就说在最小生成树中是直接相连的,后面发现不一定)
这道题写的时间长,调的时间长,最后想的时间也长。
细节真的很多!
以后碰见图论题一定要考虑图是不是一定连通!!
多组样例把所有数组都给初始化掉!!