「学习笔记」严格次小生成树算法(倍增)
看到标题,你可能会有疑问。次小生成树?那是个什么东东?我只会求最小生成树
我只能说,会求最小生成树就够了,最小生成树如果不会可以先去补好基础再来(bye~bye)
次小分为严格次小和非严格次小(应该是这么叫),严格次小一定会比最小的要小,说白了就是第二小,非严格次小可能会等于最小的
一般都是严格次小
OK,进入正题
严格次小生成树其实就与最小生成树差了一条边,否则就是你最小生成树求错了 去补基础
所以,我们先求出最小生成树,prim 或 kruskal,看情况而定
接下来就是加边了,我们已经生成了最小生成树,如果加任意一条边,都会构成环,所以我们在加边的同时,要删去一条边。
那么,问题来了,我们要删哪一条边呢?
我们想一下,想让他严格次小,是不是删去原树中最大的边,再加上新边就可以了?
为什么呢?
我们任意一条新边,都是大于等于最小生成树中最大的边的,如果不是,你最小生成树求错了,去补基础。
要想让我们的答案做到第二小,是不是我们要让它的增值最小。
增值怎么求?你新加的边的权值-你删去边的权值
已知新加的边比任意一条原边大于或等于,所以,替换最大边即可
我们每加入一条边,都要连接两个节点,我们找这两个结点的 lca,求出这两个节点到 lca 的各自的最大值,再对这两个最大值取一个 \(\max\),就是我们要找的最大值
但是!这里要注意,如果新加的边与最大边的权值相等,增值为 \(0\),那就无法达到严格次小这个条件,这时该怎么办?
我们可以再找次大边,不能让增值为 \(0\),所以还要记录次大边,我们在找最大值时判断一下,如果与新加的边权值相等,那么就返回次大边。
好了,现在道理解释完了,回归最根本的问题,怎么找最大边和次大边?
主角登场——倍增
利用倍增,可以求出最大边和次大边,具体怎么求,看代码吧,有简洁的注释。
#include <iostream>
#include <cstdio>
#include <algorithm>
typedef long long ll;
using namespace std;
const ll M = 3e5 + 5;
const ll N = 1e5 + 5;
const ll inf = 1e15 + 10;
ll n, m, sum, cnt;
ll lg[N], deep[N], st[N][21];
ll f[N], mx[N][21], me[N][21], h[N];
// mx 记录最大边 me 记录次大边 h 遍历最小生成树时要用
bool vis[M];//记录第i条边是否用过
struct edge//存边
{
ll u, v, w, nxt;
bool operator < (const edge &b) const//重载运算符
{
return w < b.w;
}
} e[M], g[M];
//e 记录所有读入的边 g 记录构成最小生成树的边
inline ll read()//快读
{
ll x = 0;
bool flag = false;
char ch = getchar();
while(ch < '0' || ch > '9')
{
if(ch == '-') flag = true;
ch = getchar();
}
while(ch >= '0' && ch <= '9')
{
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return flag ? ~x + 1 : x;
}
void add(ll u, ll v, ll w)//加边,建最小生成树
{
g[++cnt].u = u;
g[cnt].v = v;
g[cnt].w = w;
g[cnt].nxt = h[u];
h[u] = cnt;
}
ll find(ll x)//并查集查找
{
return f[x] == x ? f[x] : f[x] = find(f[x]);
}
void kruskal()//kruskal最小生成树算法
{
sort(e + 1, e + m + 1);
ll tot = 0;
for(ll i = 1; i <= m; ++i)
{
ll x = e[i].u, y = e[i].v, w = e[i].w, fx = find(x), fy = find(y);
if(fx != fy)
{
f[fy] = fx;
tot++;
sum += w;
vis[i] = true;
add(x, y, w);
add(y, x, w);
}
if(tot == n-1) break;
}
}
void dfs(ll u, ll fat)//深搜,找最大边和次大边
{
deep[u] = deep[fat]+1;
st[u][0] = fat;
for(ll i = 1; i <= lg[deep[u]]; ++i)
{
st[u][i] = st[st[u][i-1]][i-1];//更新st表
ll a = mx[u][i-1], b = mx[st[u][i-1]][i-1];//最大边
ll c = me[u][i-1], d = me[st[u][i-1]][i-1];//次大边
mx[u][i] = max(a, b);//找最大边
if(a == b) me[u][i] = max(c,d);//找次大边
if(a > b) me[u][i] = max(b,c);
if(a < b) me[u][i] = max(a,d);
}
for(ll i = h[u]; i; i = g[i].nxt)
{
ll v = g[i].v, w = g[i].w;
if(v != fat)
{
mx[v][0] = w;//记录最大边,该节点到父节点的最大边就是这条连边
dfs(v, u);
}
}
}
ll LCA(ll x, ll y)//倍增求LCA
{
if(deep[x] < deep[y])
{
x = x ^ y;
y = x ^ y;
x = x ^ y;
}
while(deep[x] > deep[y])
{
x = st[x][lg[deep[x]-deep[y]]];
}
if(x == y) return x;
for(ll i = lg[deep[x]]; i>=0; --i)
{
if(st[x][i] != st[y][i])
{
x = st[x][i];
y = st[y][i];
}
}
return st[x][0];
}
ll fid(ll x, ll lca, ll w)
{
ll answer = 0;
for(ll i = lg[deep[x]]; i >= 0; --i)
{
if(deep[st[x][i]] >= deep[lca])
{
if(mx[x][i] == w) answer = max(answer, me[x][i]);
//因为严格次小,所以最大值相等,找次大值
else answer = max(answer, mx[x][i]);//找最大值
x = st[x][i];
}
}
return answer;
}
void work()
{
ll ans = inf;
for(ll i = 1; i <= m; ++i)//枚举每一条边,找到替换后结果最优的边
{
ll x = e[i].u, y = e[i].v, w = e[i].w;
if(vis[i] || x == y) continue;
//前面求最小生成树已经用过的边不算,自环不算
ll lca = LCA(x, y);//求lca
ll lmx = fid(x, lca, w),rmx = fid(y, lca, w);
//找x到lca的最大值,找y到lca的最大值
if(max(lmx, rmx) != w) ans = min(ans, sum + w - max(lmx, rmx));
//只要最大值不等于这条边,计算次小生成树
//为什么不能等于?
//因为是严格次小,这一加一减一个相等的数有什么变化?不符合严格次小
//因为严格次小,所以要找一个最小的值
}
printf("%lld\n",ans);//输出
}
int main()
{
n = read(), m = read();
for(ll i = 1; i <= n; ++i) f[i] = i;//并查集操作,更新每个点的父节点
for(ll i = 1; i <= m; ++i)
{
e[i].u = read(),e[i].v = read(),e[i].w = read();//读入边
}
lg[0] = -1;
for(ll i = 1; i <= n; ++i)
{
lg[i] = lg[i >> 1] + 1;//处理log
}
kruskal();//先构造最小生成树
dfs(1, 0);//深搜
work();//求次小生成树
return 0;
}
可以去看看这道模板题严格次小生成树
喜欢的话可以鼓励一下,写的不好,勿喷 QWQ!