题单
最小生成树学习参考笔记
P3366 【模板】最小生成树
最小生成树的定义
在一给定的无向图G = (V, E) 中,(u, v) 代表连接顶点 u 与顶点 v 的边,w(u, v) 代表此边的权重,若存在 T 为 E 的子集且为无循环图(即没有环),使得联通所有结点的的 w(T) 最小,则此 T 为 G 的最小生成树。
Prim算法思路
我们易得对于最小生成树的一个连通子图A,从A外部的点连向A的最短的边一定属于最小生成树。若不然,则在最小生成树中至少存在相邻的两点,其之间的边不是最短的,这与最小生成树的定义矛盾,故得证。于是我们每次把这个最短的边的在A外部的端点加入到A中,在加入(n - 1)次后便得到了最小生成树。那为什么是(n-1)呢?因为我们刚开始选取了一个点作为最小生成树的根节点。但是这个题还说,可能这n个点并不全联通,这个的判断方法详见代码注释。时间复杂度为O(n^2)。
Prim算法代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5010, maxm = 2e5 + 100;
int n, m;
int head[maxn], cnt;
struct edge
{
int v, w, next;
} e[maxm << 1]; // 不要忘了这是个无向图
void add (int u, int v, int w)
{
e[++ cnt] = (edge){v, w, head[u]};
head[u] = cnt;
}
int vis[maxn], dis[maxn], tot, now = 1;
// 把1当作最小生成树的根节点
// dis[i]记录的是目前的最小生成树的连通子图中的点于i之间的最短边权,而不是i到根节点的距离!
// tot记录加点的次数
int prim ()
{
int ans = 0;
memset (dis, 0x7f7f7f7f, sizeof (dis));
dis[1] = 0; // 一定要初始化dis[1]
while ((++ tot) < n)
{
int mindis = 2147483647;
vis[now] = 1; // 标记,表示now已进入目前的联通子图
int v, w;
for (int i = head[now]; i; i = e[i].next)
{
v = e[i].v, w = e[i].w;
// 更新未在连通子图中的v的dis[v]
if (!vis[v] && dis[v] > w)
dis[v] = w;
}
// 寻找下一个now
for (int i = 1; i <= n; i++)
{
if (!vis[i] && dis[i] < mindis)
mindis = dis[i],
now = i;
}
// 若n个点无法构成连通图,则肯定至少有一个点在能联通的点全联通后,其dis一直为0x7f7f7f7f不变,故可以把这个当作判断依据。
if (mindis == 0x7f7f7f7f)
{
printf ("orz");
exit (0);
}
ans += mindis;
}
return ans;
}
int main ()
{
scanf ("%d %d", &n, &m);
int u, v, w;
for (int i = 1; i <= m; i++)
{
scanf ("%d %d %d", &u, &v, &w);
add (u, v, w), add (v, u, w);
}
printf ("%d", prim ());
return 0;
}
Prim算法收获
①一定要注意这里面的dis与最短路中的dis的含义是不同的。
②知道了如何判断这n个点无法构成连通图。
③要深刻理解为什么是while((++ tot) < n)
Kruskal算法思路
与Prim算法相同,也是利用的贪心的思想。不同的是,Prim算法是逐步向最小生成树的子连通图中加点;而Kruskal是向最小生成树的子连通图中加边,而且使用了并查集。具体的实现方法是,先对所有的边按其边权由小到大排序;然后从新的编号为1的便开始一步步加边:如果说这条边的两个端点已经联通了,那说明他们属于一个集了(即在并查集中有相同的祖先),则直接跳过;如果说这两个点还没有联通,那么就把这条边加进去(不要忘了更新fa);得到最小生成树的标志是加入的边的个数等于(n - 1)。时间复杂度为O(mlogn)。
Kruskal算法代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5100, maxm = 2e5 + 100;
int n, m;
int cnt;
struct edge
{
int u, v, w;
} e[maxm << 1];
void add (int u, int v, int w)
{
e[++ cnt] = (edge) {u, v, w};
}
bool cmp (edge a, edge b)
{
return a.w < b.w;
}
int fa[maxn];
void Init ()
{
for (int i = 1; i <= n; i++)
fa[i] = i;
sort (e + 1, e + m + 1, cmp);
}
int find (int x)
{
while (x != fa[x]) x = fa[x] = fa[fa[x]]; // 循环找祖先,同时实现了路径压缩
return x;
}
int ans = 0, tot;
int kruskal ()
{
for (int i = 1; i <= m; i++)
{
int fu = find (e[i].u),
fv = find (e[i].v);
if (fu == fv) continue;
ans += e[i].w;
fa[fu] = fv;
if ((++tot) == n - 1)
return ans;
}
// m个边走完了,还没有return ans,说明无法完全联通
return -1;
}
int main ()
{
scanf ("%d %d", &n, &m);
for (int i = 1; i <= m; i++)
{
int u, v, w;
scanf ("%d %d %d", &u, &v, &w);
add (u, v, w); // 这里不再是链式前向星存图,一定不要再写add (v, u, w)
}
Init ();
if (kruskal () == -1) printf ("orz");
else printf ("%d", ans);
return 0;
}
Kruskal算法收获
①浅浅地复习了以下并查集。
②一定要谨慎不再是链式前向星存图,故一定不要双向建边。
Prim算法与Kruskal算法的比较
①Prim算法是不断加点;Kruskal算法是不断加边。
②Prim算法在稠密图中更有,时间复杂度是O(
n
2
n^2
n2);Kruskal算法在稀疏图中更优,时间复杂度是
O
(
m
l
o
g
n
)
O(mlogn)
O(mlogn)。
③核心思想都是贪心。
P4180 [BJWC2010] 严格次小生成树
思路
- 容易想到严格次小生成树一定是将最小生成树中的某一边换成权值更大的一边且换边后仍然使这n个点连通。于是先用 Kruskal 或 Prim 构造最小生成树。
- 那怎么找边?删除最小生成树中的一个边后枚举没用过的边加上去,然后计算新的生成树的总权值?时间复杂度高达 O ( n m 2 ) \mathcal O(nm^2) O(nm2),不可行。
- 我们可以使用 Kruskal 剩下的那些边。具体操作是:在构建最小生成树时,我们把最小生成的用边打上标记。然后枚举剩下的边:如果这条边加入原最小生成树后成为重边,那么直接比较这条边与最小生成树中相应边的权值即可;如果这条边加入后形成了一个环(如下图),假设 3 与 4 之间的边是新加入的这条边,那么我们需要删除环中的小于这条边的边权的最大边权。
- 那么找小于这条边权的最大边权呢?根据上图,我们发现可以用 LCA。设这条边的两个端点是 u u u、 v v v,如果这条边是重边,那么一定有 a n c [ u ] [ 0 ] = v anc[u][0]=v anc[u][0]=v 或 a n c [ v ] [ 0 ] = u anc[v][0] = u anc[v][0]=u,然后再判断最小生成树中的边权是否小于这条边的边权即可;如果这条边加入后构成环,则在最小生成树中找这两个点的最近公共祖先的过程中遍历可能要被替换的边,找这些边的边权小于新加边权的最大值。
- 那怎么找这些边的边权小于新加边权 w w w 的最大值?只用 m x mx mx 记录这些边中的最大边权是不可以的,因为 m x mx mx 可能等于 w w w,于是我们需要记录严格次小边权 s m x smx smx,当 m x = = w mx == w mx==w时,就需要 s m x smx smx 的值,而不是 m x mx mx 的值。
- 但是 LCA 中有初始化、跳到同一高度、一起往上跳三个过程,其中最大边权好更新,但严格次大边权不好更新。那我们用 v a l [ i ] [ j ] val[i][j] val[i][j] 记录从点 i 到其 2 j 2^j 2j 祖先的路径上的最大边权,用 s v a l [ i ] [ j ] sval[i][j] sval[i][j] 记录从点 i 到 2 j 2^j 2j 祖先的路径上的严格次大边权,然后我们需要特别注意 s m x smx smx 与 s v a l sval sval 的更新方式,具体更新方式见代码。
- 时间复杂度为 O ( m ( l o g m + l o g n ) ) \mathcal O(m(logm+logn)) O(m(logm+logn))
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 100, maxm = 3e5 + 100;
const int maxlg = 20;
int n, m, mx, smx;
long long ans;
// 最小生成树,链式前向行
int head[maxn], cnt;
struct edge
{
int v, w, next;
} e[maxn << 1];
// 题给的图
int cnti;
struct edgei
{
int u, v, w;
} ei[maxm];
bool cmp (edgei a, edgei b) {return a.w < b.w;}
void add (int u, int v, int w, int tp)
{
if (tp == 0)
ei[++ cnti] = (edgei) {u, v, w};
else if (tp == 1)
e[++ cnt] = (edge) {v, w, head[u]},
head[u] = cnt;
}
int fa[maxn], vis[maxm], tot;
int find (int x)
{
while (x != fa[x]) x = fa[x] = fa[fa[x]];
return x;
}
void Init ()
{
for (int i = 1; i <= n; i++)
fa[i] = i;
sort (ei + 1, ei + cnti + 1, cmp);
}
void Kruskal ()
{
Init ();
for (int i = 1; i <= cnti; i++)
{
int u = ei[i].u, v = ei[i].v,
w = ei[i].w;
int x = find (u), y = find (v);
if (x == y) continue;
fa[x] = y;
// 构建最小生成树的图
add (u, v, w, 1), add (v, u, w, 1);
vis[i] = 1, ans += w;
if ((++ tot) == n - 1) break;
}
}
int sval[maxn][maxlg + 5], val[maxn][maxlg + 5], anc[maxn][maxlg + 5], depth[maxn];
void dfs (int u, int fa, int d)
{
anc[u][0] = fa, depth[u] = d;
for (int i = head[u]; i; i = e[i].next)
{
int v = e[i].v, w = e[i].w;
if (v == fa) continue;
val[v][0] = w;
dfs (v, u, d + 1);
}
}
void Initi ()
{
for (int j = 1; j <= maxlg; j++)
for (int i = 1; i <= n; i++)
{
// 更新 anc、val、sval
anc[i][j] = anc[anc[i][j - 1]][j - 1],
val[i][j] = max (val[i][j - 1], val[anc[i][j - 1]][j - 1]);
if (val[i][j - 1] < val[anc[i][j - 1]][j - 1])
sval[i][j] = max (val[i][j - 1], sval[anc[i][j - 1]][j - 1]);
else if (val[i][j - 1] > val[anc[i][j - 1]][j - 1])
sval[i][j] = max (val[anc[i][j - 1]][j - 1], sval[i][j - 1]);
else
sval[i][j] = max (sval[i][j - 1], sval[anc[i][j - 1]][j - 1]);
}
}
void swim (int &x, int h)
{
for (int i = 0; h; i++)
{
if (h & 1)
{
//跳到同意高度时更新 smx、mx
if (mx < val[x][i])
smx = max (smx, max (mx, sval[x][i]));
else if (mx > val[x][i])
smx = max (smx, val[x][i]);
else smx = max (smx, sval[x][i]);
mx = max (mx, val[x][i]);
x = anc[x][i];
}
h >>= 1;
}
}
void lca (int x, int y)
{
if (depth[x] < depth[y]) swap (x, y);
swim (x, depth[x] - depth[y]);
if (x == y) return; // 写里面要写的第一个地方
// 更新 smx、mx
for (int i = maxlg; i >= 0 && anc[x][0] != anc[y][0]; i--)
{
if (anc[x][i] != anc[y][i])
{
if (mx > max (val[x][i], val[y][i]))
smx = max (smx, max (val[x][i], val[y][i]));
else if (mx < max (val[x][i], val[y][i]))
{
if (val[x][i] < val[y][i])
smx = max (smx, max (mx, max (val[x][i], sval[y][i])));
else if (val[x][i] > val[y][i])
smx = max (smx, max (mx, max (val[y][i], sval[x][i])));
else
smx = max (smx, max (mx, max (sval[x][i], sval[y][i])));
}
else
smx = max (smx, max (sval[x][i], sval[y][i]));
mx = max (mx, max (val[x][i], val[y][i]));
x = anc[x][i], y = anc[y][i];
}
}
if (mx > max (val[x][0], val[y][0]))
smx = max (smx, max (val[x][0], val[y][0]));
else if (mx < max (val[x][0], val[y][0]))
{
if (val[x][0] < val[y][0])
smx = max (smx, max (mx, max (val[x][0], sval[y][0])));
else if (val[x][0] > val[y][0])
smx = max (smx, max (mx, max (val[y][0], sval[x][0])));
else
smx = max (smx, max (mx, max (sval[x][0], sval[y][0])));
}
else
smx = max (smx, max (sval[x][0], sval[y][0]));
mx = max (mx, max (val[x][0], val[y][0]));
// 第二个地方
}
int main ()
{
scanf ("%d %d", &n, &m);
for (int i = 1; i <= m; i++)
{
int u, v, w;
scanf ("%d %d %d", &u, &v, &w);
if (u == v) continue;
add (u, v, w, 0);
}
Kruskal ();
dfs (1, 0, 1), Initi ();
int ansi = 2147483647;
for (int i = 1; i <= cnti; i++)
{
if (vis[i]) continue;
int u = ei[i].u, v = ei[i].v, w = ei[i].w;
// 重边的情况
if (anc[u][0] == v)
{
if (w > val[u][0]) ansi = min (ansi, w - val[u][0]);
continue;
}
if (anc[v][0] == u)
{
if (w > val[v][0]) ansi = min (ansi, w - val[v][0]);
continue;
}
// 环的情况
mx = smx = -1;
lca (u, v);
// mx与w比较这一步最好放在主函数中,因为放在 LCA 中需要在两个地方都写一次。
if (mx == w) {if (smx != 0) ansi = min (ansi, w - smx);}
else ansi = min (ansi, w - mx);
}
printf ("%lld", ans + 1ll * ansi);
return 0;
}
收获
- 知道了 LCA 的一种用法,学会了如何更新 s v a l sval sval 与 s m x smx smx。
P2872 [USACO07DEC]Building Roads S
思路
- 有一些边不花费权值直接给我们了,于是我们先把这些可以连通的点连通起来,然后我们再用 Kruskal 把最小生成树中剩下的边补齐就可以了。
- 不过 e d g e edge edge 数组要开到 m a x n 2 maxn^2 maxn2,因为任意两点之间是可以有一条边的,边权用两点之间距离公式算。
- 时间复杂度为 O ( m l o g m ) \mathcal O(mlogm) O(mlogm)。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 100;
int n, m, cnt, x[maxn], y[maxn];
struct edge
{
int u, v;
double w;
} e[maxn * maxn];
void add (int u, int v, double w)
{
e[++ cnt] = (edge) {u, v, w};
}
bool cmp (edge a, edge b)
{
return a.w < b.w;
}
int fa[maxn], tot;
double ans;
int find (int x)
{
while (x != fa[x]) x = fa[x] = fa[fa[x]];
return x;
}
void Init ()
{
for (int i = 1; i <= n; i++)
{
fa[i] = i;
}
for (int i = 1; i <= m; i++)
{
int x, y;
scanf ("%d %d", &x, &y);
x = find (x), y = find (y);
if (x == y) continue;
fa[x] = y;
}
sort (e + 1, e + cnt + 1, cmp);
}
void Kruskal ()
{
for (int i = 1; i <= cnt; i++)
{
int x = find (e[i].u), y = find (e[i].v);
if (x == y) continue;
ans += e[i].w;
fa[x] = y;
if ((++ tot) == n - m - 1) return;
}
return;
}
int main ()
{
scanf ("%d %d", &n, &m);
for (int i = 1; i <= n; i++)
scanf ("%d %d", &x[i], &y[i]);
for (int i = 1; i < n; i++)
for (int j = i + 1; j <= n; j++)
{
long long delx = x[i] - x[j],
dely = y[i] - y[j];
double l = (double) sqrt (delx * delx + dely * dely);
add (i, j, l);
}
Init (), Kruskal ();
printf ("%.2lf", ans);
return 0;
}
收获
- 数组开的大小不要想当然。
P1991 无线通讯网
思路 1 最小生成树法
- 我们有 s s s 个卫星城市,所以我们可以无限制地连 s − 1 s-1 s−1 条边使得这 s s s 个城市形成一棵生成树。
- 于是我们用 Kruskal 先使得 n − s + 1 n-s+1 n−s+1 个城市形成这些城市的最小生成树,然后我们以这些城市中的某一个成为卫星城市,与剩下的 s − 1 s-1 s−1 城市构成 1 中的那棵生成树。于是这 n n n 个点便联通了。
- 再kruskal中被加进去的第 n − s n-s n−s 条边即为答案。
- 时间复杂度为 O ( p 2 l o g p ) \mathcal O(p^2logp) O(p2logp)。
代码 1
#include <bits/stdc++.h>
using namespace std;
const int maxp = 510;
int s, p, cnt, fa[maxp], x[maxp], y[maxp], tot;
struct edge
{
int u, v;
double w;
} e[maxp * maxp];
void add (int u, int v, double w)
{
e[++ cnt] = (edge) {u, v, w};
}
bool cmp (edge a, edge b) {return a.w < b.w;}
int find (int x)
{
while (x != fa[x]) x = fa[x] = fa[fa[x]];
return x;
}
void Init ()
{
for (int i = 1; i <= p; i ++)
fa[i] = i;
sort (e + 1, e + cnt + 1, cmp);
}
double Kruskal ()
{
for (int i = 1; i <= cnt; i++)
{
int x = find (e[i].u),
y = find (e[i].v);
if (x == y) continue;
fa[x] = y;
if ((++ tot) == p - s)
return e[i].w;
}
}
int main ()
{
scanf ("%d %d", &s, &p);
for (int i = 1; i <= p; i++)
scanf ("%d %d", &x[i], &y[i]);
for (int i = 1; i < p; i++)
for (int j = i + 1; j <= p; j++)
{
long long delx = x[i] - x[j],
dely = y[i] - y[j];
double w = sqrt (delx * delx + dely * dely);
add (i, j, w);
}
Init ();
printf ("%.2lf", Kruskal ());
return 0;
}
收获 1
- 这个方法灵活运用了最小生成树,值得再观。
思路 2 二分+并查集
- 我们发现 D D D 具有单调性与有界性。单调性,即若存在 D 1 D_1 D1 不符合要求,则 ∀ D < D 1 \forall D < D_1 ∀D<D1 不符合要求。有界性: D ∈ [ m i n ( w ) , m a x ( w ) ] D \in[min(w),max(w)] D∈[min(w),max(w)],其中 w w w 为边权。而且我们可以得出答案要求的 D D D 一定是边权中的某一个。于是可以对 D D D 进行二分答案。
- 那么什么样子的 D D D 是符合要求的。我们设连通块的数量为 t o t tot tot,则 t o t tot tot 初始化为 n n n。枚举所有可能的边,如果某条边的边权小于等于 D D D 且 两个端点不在同一连通块中,则将两个点联通同一联通块中,同时 t o t − 1 tot-1 tot−1。若最后的 t o t ≤ s tot \leq s tot≤s ,则这个 D D D 符合要求。
- 时间复杂度为 O ( p 2 l o g p ) \mathcal O(p^2logp) O(p2logp)。
代码 2
#include <bits/stdc++.h>
using namespace std;
const int maxp = 510;
int cnt, s, p, x[maxp], y[maxp];
double w[maxp][maxp], ans;
int fa[maxp], vis[maxp], tot;
struct edge
{
int u, v;
double w;
} e[maxp * maxp];
void add (int u, int v, double w)
{
e[++ cnt] = (edge) {u, v, w};
}
bool cmp (edge a, edge b) {return a.w < b.w;}
int find (int x)
{
while (x != fa[x]) x = fa[x] = fa[fa[x]];
return x;
}
int main ()
{
scanf ("%d %d", &s, &p);
for (int i = 1; i <= p; i++)
scanf ("%d %d", &x[i], &y[i]);
for (int i = 1; i < p; i++)
for (int j = i + 1; j <= p; j++)
{
long long delx = x[i] - x[j],
dely = y[i] - y[j];
w[i][j] = sqrt (delx * delx + dely * dely);
add (i, j, w[i][j]);
}
sort (e + 1, e + cnt + 1, cmp);
int l = 1, r = cnt;
while (l <= r)
{
int mid = (l + r) >> 1;
double d = e[mid].w;
for (int i = 1; i <= p; i++)
vis[i] = 0, fa[i] = i;
tot = p;
for (int i = 1; i < p; i++)
for (int j = i + 1; j <= p; j++)
{
int x = find (i), y = find (j);
if (w[i][j] <= d && x != y)
{
fa[x] = y;
tot --;
}
}
if (tot <= s) ans = d, r = mid - 1;
else l = mid + 1;
}
printf ("%.2lf", ans);
return 0;
}
收获
- 最大距离的最小值问题,二分答案首选。
P1967 [NOIP2013 提高组] 货车运输
思路
- 为了让两点之间的最小承重尽可能大,我们可以用 Kruskal 尽可能地构成最大生成树,即使构不成最大生成树也可以构成若干个连通块,每个连通块都可以看作一棵最大生成树。
- 对于两点,若其不在同一连通块中,则不可以相互到达;若其在同一连通块中,则需要得到这两点间的最小承重。
- 最小承重可以用 LCA 得到。
- 时间复杂度为 O ( m l o g + q l o g n ) \mathcal O(mlog+qlogn) O(mlog+qlogn)
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e4 + 100, maxm = 5e4 + 100;
const int maxlg = 15;
int n, m, q;
// 最大生成树,链式前向星存图
int head[maxn], cnt;
struct edge
{
int v, w, next;
} e[maxn << 1];
// 输入的边,存图
int cnti;
struct edgei
{
int u, v, w;
} ei[maxm];
bool cmp (edgei a, edgei b) {return a.w > b.w;}
// 加边操作
void add (int u, int v, int w, int tp)
{
if (tp == 0)
ei[++ cnti] = (edgei) {u, v, w};
else if (tp == 1)
e[++ cnt] = (edge) {v, w, head[u]},
head[u] = cnt;
}
int fa[maxn], tot;
void Init ()
{
for (int i = 1; i <= n; i++)
fa[i] = i;
sort (ei + 1, ei + cnti + 1, cmp);
}
int find (int x)
{
while (x != fa[x]) x = fa[x] = fa[fa[x]];
return x;
}
void Kruskal ()
{
Init ();
for (int i = 1; i <= cnti; i++)
{
int x = find (ei[i].u), y = find (ei[i].v);
if (x == y) continue;
fa[x] = fa[y];
add (ei[i].u, ei[i].v, ei[i].w, 1);
add (ei[i].v, ei[i].u, ei[i].w, 1);
if ((++ tot) == n - 1) break;
}
}
int val[maxn][maxlg + 5], anc[maxn][maxlg + 5], depth[maxn];
void dfs (int u, int fa, int d)
{
anc[u][0] = fa, depth[u] = d;
for (int i = head[u]; i; i = e[i].next)
{
int v = e[i].v, w = e[i].w;
if (v == fa) continue;
val[v][0] = w;
dfs (v, u, d + 1);
}
}
void Initi ()
{
for (int j = 1; j <= maxlg; j++)
for (int i = 1; i <= n; i++)
anc[i][j] = anc[anc[i][j - 1]][j - 1],
val[i][j] = min (val[i][j - 1], val[anc[i][j - 1]][j - 1]);
}
int ans;
void swim (int &x, int h)
{
for (int i = 0; h; i++)
{
if (h & 1)
ans = min (ans, val[x][i]), x = anc[x][i];
h >>= 1;
}
}
int lca (int x, int y)
{
ans = 2147483647;
if (find (x) != find (y)) return -1;
if (depth[x] < depth[y]) swap (x, y);
swim (x, depth[x] - depth[y]);
if (x == y) return ans;
for (int i = maxlg; i >= 0 && anc[x][0] != anc[y][0]; i--)
if (anc[x][i] != anc[y][i])
ans = min (ans, min (val[x][i], val[y][i])),
x = anc[x][i], y = anc[y][i];
ans = min (ans, min (val[x][0], val[y][0]));
return ans;
}
int main ()
{
scanf ("%d %d", &n, &m);
for (int i = 1; i <= m; i++)
{
int u, v, w;
scanf ("%d %d %d", &u, &v, &w);
add (u, v, w, 0);
}
Kruskal ();
dfs (1, 0, 1);
Initi ();
scanf ("%d", &q);
for (int i = 1; i <= q; i++)
{
int u, v;
scanf ("%d %d", &u, &v);
printf ("%d\n", lca (u, v));
}
return 0;
}
收获
- 随机应变,用Kruskal得最大生成树。
- 学会用 LCA 找两点之间的最小承重等量。
P4047 [JSOI2010]部落划分
思路
- 设两个部落之间的最小距离为 D D D,则 D D D 具有有界性与单调性。单调性,即若存在 D 1 D_1 D1 不符合要求,则 ∀ D > D 1 \forall D>D_1 ∀D>D1,不符合要求。有界性,即 D ∈ [ m i n ( w ) , m a x ( w ) ] D \in [min(w),max(w)] D∈[min(w),max(w)],其中 w w w 是边权。且最后的答案一定是边权中的某一个。若不然,若最后的答案略小于某一个边权,则取这个边权更优;若最后的答案略大于某一个边权,则比这个边权大的最小的那个边权更优。
- 什么样的 D D D 符合要求。根据 D D D 的定义,我们知道边权小于 D D D 的两个点一定属于同一部落;反之则不一定属于同一个部落。我们设至多有连通块的数量为 t o t tot tot,则 t o t tot tot 初始化为 n n n。枚举所有可能的边,如果某条边的边权小于 D D D 且 两个端点不在同一连通块中,则将两个点联通同一联通块中,同时 t o t − 1 tot-1 tot−1。若最后的 t o t ≥ k tot \geq k tot≥k ,则这个 D D D 符合要求。
- 时间复杂度为 O ( n 2 l o g n ) \mathcal O(n^2logn) O(n2logn)。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1010;
int k, n, x[maxn], y[maxn], cnt, fa[maxn], tot;
double ans, w[maxn][maxn];
struct edge
{
int u, v;
double w;
} e[maxn * maxn];
void add (int u, int v, double w)
{
e[++ cnt] = (edge) {u, v, w};
}
bool cmp (edge a, edge b) {return a.w < b.w;}
int find (int x)
{
while (x != fa[x]) x = fa[x] = fa[fa[x]];
return x;
}
void Init ()
{
for (int i = 1; i <= n; i++)
fa[i] = i;
tot = n;
}
int check (double d)
{
Init ();
for (int i = 1; i < n; i++)
{
for (int j = i + 1; j <= n; j++)
{
int fx = find (i), fy = find (j);
if (fx != fy && w[i][j] < d)
fa[fx] = fy, tot --;
}
}
return tot;
}
int main ()
{
scanf ("%d %d", &n, &k);
for (int i = 1; i <= n; i++)
scanf ("%d %d", &x[i], &y[i]);
for (int i = 1; i < n; i++)
for (int j = i + 1; j <= n; j++)
{
int delx = x[i] - x[j],
dely = y[i] - y[j];
w[i][j] = sqrt (delx * delx + dely * dely);
add (i, j, w[i][j]);
}
sort (e + 1, e + cnt + 1, cmp);
int l = 1, r = cnt;
while (l <= r)
{
int mid = (l + r) >> 1;
double d = e[mid].w;
int sum = check (d);
if (sum < k) r = mid - 1;
if (sum >= k) ans = d, l = mid + 1;
}
printf ("%.2lf", ans);
return 0;
}
收获
- 又是一题二分加并查集。