CF_GYM_105930_H

对于树来说,创造一颗树,比优化一棵树要简单得多。

如下面这道题:CF_GYM_105930_H 最小生成树, 这题就是变量下,最优化最小生成树,一般来说,我们很容易想到两种思路,一种是先做一遍最小生成树,然后再利用这 \(k\) 条边优化,但是稍微一试,就容易发现,这并不简单,一方面是要优化哪一条边,其次要用哪条边去优化,而且这个最小生成树之外的边是否还需要使用?这些使得我们,通过 “优化” 的方式,很难求出这棵树,于是可以考虑构造这棵树。

(其实这部分有点优化的意思)。从集合方面来考虑,如果目前有且只有两个联通块(整体思想,特殊情况),此时只需要从两个联通块里面各取一个点连接即可,可以发现,对于联通来说,我们并不关心是哪两个点构成了联通,因此是任意的两个点,又因为是最优化,所以我们很自然的会选择尽量接近的两个点连接,那么这时候有个问题,这两个联通块里面是否一定存在两个点 \(a,b\),使得 \(|a - b| = 1\)?而这点是一定的,假如一个联通块里面有 1,那么 2,要么再这个联通块,要么在另一个,如果在另一个联通块,那么就有 \(|a - b| = 1\),如果在这个联通块,那么 3 的话要么在这个联通块,要么在另一个 ,你会发现是递推的 ,推到最后,因为是两个连通快,所以一定会有一个在另一个联通块,形成 \(|a - b| = 1\)。也就是说,对于两个联通块,我们一定可以找出来一条边权为 \(1\) 的边来联通这个块,那么如果有3个联通块 \(a,b,c\),那么我们可以把 \(b,c\),看成一个块 \(d\),那么就可以把 \(a\)\(d\),接下来同理联通 \(b,c\),最终就可以把三个给连接到一块。也就是说,我们一定可以用边权为 \(1\) 的边把整个森林给联通,也就是说,我这 \(k\) 条边,如果能用到的话,一定都是 \(1\) 边权。那么知道这点就简单了。

我们考虑去构造这个棵树,kruscal 的原理是贪心,其本质是吧边排序后能加进去,就加进去,不能加就跳过。那么我们可以先给现有边排序,把边权小于等于 1 的边直接进行 kruscal,那么剩下来的原有边的边权一定大于 \(1\)

按照 kruscal 来说,我们接下里应该去枚举加入 \(k\)\(1\) 权边,但是这样搞的话,最后有可能无法形成一颗树,为了保障生成树,这 \(k\)\(1\) 权边需要最后加入,“补全” 联通块。

接下来的话我们给这 \(k\) 条可用边留出来位置(因为原有的边有 \(0\) 权和 \(1\) 权边,可能最后我们用不完这 k 条)。然后对剩下的边进行 kruscal。搞完后对 \(1\) 权的再做 kruscal 就可以了。看看代码应该会很清晰。

/*
对于树来说,创造树,远远比优化树要来的方便的多
*/

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

typedef long long LL;

const int N = 200010, M = N * 2;

int n, m, k;
int p[N];
vector<int> res;
LL sum;

struct Node 
{
    int a, b, w, id;
    bool operator<(const Node &W)const 
    {
        return w < W.w;
    }
}edges[N], eddge[N];

int find(int x)
{
    if (x != p[x]) p[x] = find(p[x]);
    return p[x];
}

void kruscal(int len)
{
    int cnt = 0;
    for (int i = 1; i <= m; i ++ )
    {
        if (cnt == len) break;
        auto e = edges[i];
        int pa = find(e.a), pb = find(e.b);
        if (pa == pb) continue;
        cnt ++ ;
        p[pb] = pa;
        res.push_back(e.id);
        sum += e.w;
    }

}

int main()
{
    int T;
    cin >> T;
    while (T -- )
    {
        
        res.clear();
        sum = 0;
        cin >> n >> m >> k;
        for (int i = 1; i <= n; i ++ ) p[i] = i;
        for (int i = 1; i <= m; i ++ )
        {
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
            edges[i] = {a, b, c, i};
        }
        for (int i = 1; i < n; i ++ )
        {
            eddge[i] = {i, i + 1, 1};
            
        } 
        
        sort(edges + 1, edges + 1 + m);
        // 处理小于等于1的
        for (int i = 1; i <= m; i ++ )
        {
            auto e = edges[i];
            if (e.w > 1) break;
            
            int pa = find(e.a), pb = find(e.b);
            if (pa == pb) continue;
            
            p[pb] = pa;
            res.push_back(e.id);
            sum += e.w;
        }
        //处理剩余边
        kruscal(max(n - 1 - k - (int)res.size(), 0));
        
        // 处理自选边
        int len = n - 1 - (int)res.size(), cnt = 0;
        cout << len << '\n';
        for (int i = 1; i < n; i ++ )
        {
            if (res.size() ==  n - 1) break;
            if (cnt == k) break;
            // cout << 'a';
            auto e = eddge[i];
            // if (e.w != 0) break;
            
            int pa = find(e.a), pb = find(e.b);
            if (pa == pb) continue;
            p[pb] = pa;
            cnt ++ ;
            res.push_back(m + cnt);
            sum += e.w;
            cout << e.a << ' ' << e.b << '\n';
        }
        cout << sum << '\n';
        for (int i : res) cout << i << ' ';
        puts("");
    }
    return 0;
}

启发

对于树来说,创造远远比调整优化要简单的多,kruscal 的贪心虽然简单,但其实包含了很大的智慧啊。而整体看待和特殊化的思想也非常重要,这是突破桎梏的关键。比赛中大模拟数据还是不太现实的,整体的话会发现小的零碎的数据所带不出来的惊喜。

posted @ 2026-05-18 20:43  blind5883  阅读(6)  评论(0)    收藏  举报