专题·次小生成树【including 八中生成树, 洛谷·【模板】严格次小生成树[BJWC2010]

初见安~本篇前置知识::最小生成树,最近公共祖先

1.次小生成树

次小生成树——说白了就是选的边与最小生成树不同并且满足边权和最小。这里的不同是指只要不是最小生成树选的边刚好都是次小生成树的边就可以了。

具体题目描述大概是这样的:【题目选择某入门OJ:八中生成树

Description

八中有N个建筑物,某两点间有一条边存在。上一任Boss建立了个最小生成树出来,这样全校就可以电话联系了。现任Boss觉得万一出了什么紧急情况破坏这个最小生成树就不好了,于是他希望再建立一个最小生成树出来,其与开始那个方案不能一样,且总权值是小于等于上一个的。请问这两个最小生成树的代价。

Input

第一行两个数N(2<=N<=500),M,分别表示建筑物数和边数。 
接下来M行,每行三个正整数Ai,Bi,Ci,表示Ai和Bi之间有边,边长为Ci。 保证两点间只有一条边

Output

第一行:”Cost: “+一个整数,表示最小费用。(若不存在,输出-1) 
第二行:”Cost: “+一个整数,表示第二小费用。(若不存在,输出-1) 

Sample Input

4 6
1 2 2
2 3 2
3 4 2
4 1 2
1 3 1
2 4 1

Sample Output

Cost: 4
Cost: 4

Sol

*注意:这里的次小生成树并不严格,所以只要选的边和最小的不一样就可以了。【看样例就知道

看到数据这么小我就可以放心大胆地O(n*m)了【好像还不止】。所以当时做这个题就是直接暴力(算不上是真正的正解,毕竟是写给自己看的……)——枚举最小生成树的每条边,确认不选,然后暴力再生成一次最小生成树。相当于在最小生成树上删去一条边,用另一条边代替。所以为了操作方便,我们全程使用的最小生成树算法都是Kruskal。

上代码——

#include
#define maxn 505
using namespace std;
int n, m, fa[maxn], ans = 0, cnt = 0;
int flag[maxn], top = 0;
bool judge = 0;
struct edge
{
    int u, v, w;
    bool operator < (const edge &x) const {return w < x.w;}
}e[maxn * maxn];
 
int read()
{
    int x = 0, f = 1, ch = getchar();
    while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
    while(isdigit(ch)) x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
    return x * f;
}
 
int get(int x) 
{
    if(fa[x] == x) return x;
    return fa[x] = get(fa[x]);
}
 
int main()
{
    n = read(), m = read();
    if(m < n - 1) {puts("Cost: -1"); puts("Cost: -1"); return 0;}//毕竟可以无解
    for(int i = 1; i <= m; i++)
        e[i].u = read(), e[i].v = read(), e[i].w = read();
     
    sort(e + 1, e + 1 + m);
    for(int i = 1; i <= n; i++)
        fa[i] = i;
     
    register int u, v, ans2;
    for(int i = 1; i <= m && cnt < n - 1; i++)//cnt记选中的边数
    {
        u = get(e[i].u), v = get(e[i].v);
        if(u == v) continue;
        fa[u] = v; cnt++;
        ans += e[i].w;
        flag[++top] = i;//flag标记选中的边
    }
     
    printf("Cost: %d\n", ans);
    if(m == n - 1) {puts("Cost: -1"); return 0;}
    ans2 = 0; ans = 0x3f3f3f3f;
     
    for(int i = 1; i <= top; i++)//top = n - 1,这里是在枚举选中的边
    {
        for(int q = 1; q <= n; q++)//重新Kruskal n - 1次
            fa[q] =q;
        cnt = 0; ans2 = 0; judge = 0;
        for(int j = 1; j <= m; j++)
        {
            if(j == flag[i]) continue;
            u = get(e[j].u), v = get(e[j].v);
            if(u == v) continue;
            fa[u] = v; cnt++;
            ans2 += e[j].w;
            if(cnt == n - 1) {judge = 1; break;}
        }
        if(judge) ans = min(ans, ans2);//这个次小生成树合法,比较出答案
    }
     
    printf("Cost: %d\n", ans);
    return 0;
}

妥妥的暴力。

那如果是严格次小呢?所谓严格,就是一定次小的不等于最小的,在这个条件下上文的样例就应该是4和5。


2.严格次小生成树

很明显,就算是严格我们也可以遵循这个暴力的思路来把答案做出来。

那么如果数据范围再恶毒一点呢?

【题目传送门:P4180 【模板】严格次小生成树[BJWC2010]】

 

题目描述

小C最近学了很多最小生成树的算法,Prim算法、Kurskal算法、消圈算法等等。正当小C洋洋得意之时,小P又来泼小C冷水了。小P说,让小C求出一个无向图的次小生成树,而且这个次小生成树还得是严格次小的,也就是说:如果最小生成树选择的边集是EM,严格次小生成树选择的边集是ES,那么需要满足:(value(e)表示边e的权值)\sum_{e \in E_M}value(e)<\sum_{e \in E_S}value(e)∑e∈EM​​value(e)<∑e∈ES​​value(e)

这下小 C 蒙了,他找到了你,希望你帮他解决这个问题

输入格式:

第一行包含两个整数N 和M,表示无向图的点数与边数。 接下来 M行,每行 3个数x y z 表示,点 x 和点y之间有一条边,边的权值为z。

输出格式:

包含一行,仅一个数,表示严格次小生成树的边权和。(数据保证必定存在严格次小生成树)

输入样例:

5 6
1 2 1 
1 3 2 
2 4 3 
3 5 4 
3 4 3 
4 5 6 

输出样例:

11

说明

数据中无向图无自环; 50% 的数据N≤2 000 M≤3 000; 80% 的数据N≤50 000 M≤100 000; 100% 的数据N≤100 000 M≤300 000 ,边权值非负且不超过 10^9

题解:

看说明!!!这么大的范围,求出来的最小和次小的大小是绝对可以爆int的,所以要开long long。

下面就是千篇一律的正解做法解析:

首先Kruskal求出最小生成树,这是肯定没问题的。

我们分析一下我们暴力中的操作的含义——删去一条边,用另一条没有选中的边来替换。如果不替换,很明显加上一条边这就会形成环,也就是说我们删去这个环里非加上的边的一条最长边即可。可能不太清晰,画个图来看看吧——

假设如图就是一个最小生成树了:

专题·次小生成树【including 八中生成树, 洛谷·【模板】严格次小生成树[BJWC2010]_第1张图片

由我们求最小生成树的贪心操作可以得出:我们可以枚举的所有没有选中的边,加上后一定是该环内的最长边。大概就是:

专题·次小生成树【including 八中生成树, 洛谷·【模板】严格次小生成树[BJWC2010]_第2张图片

如果我们要用这条长10的边来替换,那么我们在求次小的操作中保证最小,就一定是选择用非新边的最长边9来替换10。

那么如果这条边也是9呢?不好意思,要保证严格次小,我们就只有用6来替换了。6是个什么角色?环内除新加进来的边的次大边。所以我们不仅要预处理出任意两点之间的最长边,还要为了避免最长边和加进来的边等长,处理次长边

那么重点来了——怎么处理?明显n^2枚举是不可能的。但是我们知道:一条链上求出两点之间的最长边和次长边是很简单的。而形成的环无非是两条链。想到什么了?——LCA啊!LCA将新加进来的边连着的两点之间的路径分为两条链,然后分别求其最大,与新边相等则取其次大。这是后期求的操作。也就是说——我们的预处理和LCA的方法有关系。

这里就用倍增来解决了——(应该也是可以用树剖来维护的,但是想到开线段树和初始化的代码量之大我就放弃了,毕竟本来就不短……)最大值和次大值的存储方式和存祖祖辈辈的方式一样,在预处理父节点的同时将最大值更新为这条边的长度,次大值暂时不管【边权非负,不必处理为极小值】。而后倍增地刷新最大值和次大值即可。这里有一个陷阱,代码中解释。

下面上代码——

#include
#define maxn 100005
#define int long long//用这个修改可真是方便极了。
using namespace std;
int n, m;
bool vis[maxn << 2];//为了枚举没有选中的边 
int read()
{
    int x = 0, f = 1, ch = getchar();
    while(!isdigit(ch)) {if(ch =='-') f = -1; ch = getchar();}
    while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
    return x * f;
}

struct edge1//edge1是求最小生成树用的
{
    int u, v, w;
    edge1(){}
    edge1(int uu, int vv, int ww) {u = uu, v = vv, w = ww;}
    bool operator < (const edge1 &x) const {return w < x.w;}
}e1[maxn << 2];

struct edge//求次小生成树用的
{
    int to, w, nxt;
    edge(){}
    edge(int tt, int ww, int nn) {to = tt, w = ww, nxt = nn;}
}e[maxn << 1];

int k = 0, head[maxn];
void add(int u, int v, int w)
{
    e[k] = edge(v, w, head[u]);
    head[u] = k++;
}

int fa[maxn];
int get(int x) {return fa[x] == x? x : fa[x] = get(fa[x]);}

int bz[maxn][20], lg[maxn], dep[maxn];//倍增求LCA的基础操作
int maxx[maxn][20], minn[maxn][20];//最大,次大
void dfs1(int now, int father)
{
    dep[now] = dep[father] + 1;
    bz[now][0] = father;
    for(int i = 1; (1 << i) <= dep[now]; i++)
        bz[now][i] = bz[bz[now][i - 1]][i - 1];
        
    for(int i = head[now]; ~i; i = e[i].nxt)
    {
        register int v = e[i].to;
        if(v == father) continue;
        maxx[v][0] = e[i].w;//初始化最大
        dfs1(v, now);
    }
}

void dfs2()
{
    for(int j = 1; (1 << j) <= n; j++)//注意!!!这里一定要反过来,因为某祖先节点的maxx和minn不一定已经更新为正确值了,maxx和minn的更新会出错【WA了我30分
        for(int i = 1; i <= n; i++)
        {
            maxx[i][j] = max(maxx[i][j - 1], maxx[bz[i][j - 1]][j - 1]);
            minn[i][j] = max(minn[i][j - 1], minn[bz[i][j - 1]][j - 1]);
            if(maxx[i][j - 1] > maxx[bz[i][j - 1]][j - 1])
                minn[i][j] = max(minn[i][j], maxx[bz[i][j - 1]][j - 1]);
            else if(maxx[i][j - 1] < maxx[bz[i][j - 1]][j - 1])
                minn[i][j] = max(minn[i][j], maxx[i][j - 1]);
        }
}

int LCA(int x, int y)//这里直接板子操作
{
    if(dep[x] < dep[y]) swap(x, y);
    while(dep[x] > dep[y]) x = bz[x][lg[dep[x] - dep[y]] - 1];
    if(x == y) return x;
    for(int i = lg[dep[x]]; i >= 0; i--)
        if(bz[x][i] != bz[y][i]) x = bz[x][i], y = bz[y][i];
    return bz[x][0];
}

int qmax(int u, int v, int cmp)
{
    int ans = 0;
    for(int i = lg[n]; i >= 0; i--)
    {
        if(dep[bz[u][i]] >= dep[v])
        {
            if(cmp != maxx[u][i]) 
			ans = max(ans, maxx[u][i]);
            else ans = max(ans, minn[u][i]);
            u = bz[u][i];
        }
    }
    return ans;
}

signed main()//和int main()一样的
{
    memset(head, -1, sizeof head);
    n = read(), m = read();
    register int u, v, w;
    for(int i = 1; i <= m; i++)
    {
        u = read(), v = read(), w = read();
        e1[i] = edge1(u, v, w);
        if(i <= n) fa[i] = i;
    }
    
    sort(e1 + 1, e1 + 1 + m);
    
    register int cnt = 0, ans1 = 0, ans2 = 214748364700000;//ans1 is mst
    for(int i = 1; i <= m && cnt < n - 1; i++)
    {
        u = e1[i].u, v = e1[i].v, w = e1[i].w;
        if(get(e1[i].u) != get(e1[i].v))
        {
            ans1 += w, cnt++;
            fa[get(e1[i].u)] = get(e1[i].v); vis[i] = 1; 
            add(u, v, w), add(v, u, w);
        }
    }
    
    dfs1(1, 0);
    dfs2();
    
    for(int i = 1; i <= n; i++)
        lg[i] = lg[i - 1] + ((1 << lg[i - 1]) == i);
    for(int i = 1; i <= m; i++)
    {
        if(vis[i]) continue;
        u = e1[i].u, v = e1[i].v, w = e1[i].w;
        register int lca = LCA(u, v);
        ans2 = min(ans2, ans1 - max(qmax(u, lca, w), qmax(v, lca, w)) + w);//max两条链的最大值
    }
    printf("%lld\n", ans2);
    return 0;
}
//看起来是不是并没有开long long?注意上头的某一个define~

迎评:)
——End——

你可能感兴趣的:(树型结构,最近公共祖先LCA)