HUTACM2016 MST练习·解题报告

专题链接



A - 还是畅通工程

题解: n个村,m条路,要用最少的钱把所有村连接起来,MST的模板题,提供两种算法模板。

//使用Kruskal算法
#include<stdio.h>
#include<algorithm>
#include<string.h>
using namespace std;
const int N = 105;
int seed[N]; //构建并查集
int find_root(int x){
    return seed[x] < 0? x : seed[x] = find_root(seed[x]);
}
int join_seed(int a, int b){
    a = find_root(a), b = find_root(b);
    if(a == b) return 0;
    else seed[b] = a;
    return 1;
}
struct E{ // 定义边
    int u, v, cost;
}edg[N*N];
int ecnt;

void init(){ // 初始化
    memset(seed, -1, sizeof(seed));
    ecnt = 0;
}
void add(int u, int v, int w){
    edg[ecnt].u = u, edg[ecnt].v = v, edg[ecnt++].cost = w;
}
bool cmp(E a, E b){
    return a.cost < b.cost;
}
int main(){
    int n;
    while(scanf("%d", &n) != EOF && n != 0){
        init();
        int a, b, c;
        for(int i = n*(n-1)/2; i > 0; --i){
            scanf("%d%d%d", &a, &b, &c);
            add(a, b, c);
        }
        sort(edg, edg+ecnt, cmp);
        int ans = 0;
        for(int i = 0; i < ecnt; ++i){
            if(join_seed(edg[i].u, edg[i].v)) ans += edg[i].cost;
        }
        printf("%d\n", ans);
    }
}

//使用prim算法
#include<stdio.h>
#include<algorithm>
#include<string.h>
using namespace std;
const int INF = ~0u>>2;
const int N = 105;
int G[N][N];
int dis[N];
void init(int n){
    for(int i = 0; i <= n; ++i) dis[i] = INF;
    for(int i = 0; i <= n; ++i)
        for(int j = 0; j <= n; ++j)
            G[i][j] = INF;
}
int prim(int rt, int n){
    int vis[N] = {0};
    dis[rt] = 0;
    int res = 0;
    for(int i = 0; i < n; ++i){
        int min_u, min_dis = INF;
        for(int j = 1; j <= n; ++j){ //找最小花费的点
            if(vis[j] == 0 && dis[j] < min_dis){
                min_dis = dis[j];
                min_u = j;
            }
        }
        vis[min_u] = 1;
        res += min_dis;
        for(int j = 1; j <= n; ++j){ //用最小点去更新其他点
            if(vis[j] == 0 && dis[j] > G[min_u][j]){
                dis[j] = G[min_u][j];
            }
        }
    }
    return res;
}
int main(){
    int n;
    while(scanf("%d", &n) != EOF && n != 0){
        init(n);
        for(int i = n*(n-1)/2; i >= 1; --i){
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
            if(G[a][b] > c) G[a][b] = G[b][a] = c;
        }
        int ans = prim(1, n);
        printf("%d\n", ans);
    }
}

B - 畅通工程再续

题意: n个点,给出的是坐标,需要考虑建图了,按规则建图,然后求MST。

#include<stdio.h>
#include<algorithm>
#include<string.h>
#include<math.h>
using namespace std;
const int N = 105;
int seed[N];
int find_root(int x){
    return seed[x] < 0? x : seed[x] = find_root(seed[x]);
}
int join_seed(int a, int b){
    a = find_root(a), b = find_root(b);
    if(a == b) return 0;
    seed[b] = a;
    return 1;
}
struct E{
    int u, v, cost;
}edg[N*N];
int ecnt;
void init(){
    memset(seed, -1, sizeof(seed));
    ecnt = 0;
}
void add(int u, int v, int w){
    edg[ecnt].u = u, edg[ecnt].v = v, edg[ecnt++].cost = w;
}
bool cmp(E a, E b){
    return a.cost < b.cost;
}
int pnt[N][2];
int CalDist(int a, int b){ //两点距离的平方
    int x = pnt[a][0] - pnt[b][0];
    int y = pnt[a][1] - pnt[b][1];
    return x*x + y*y;
}
int main(){
    int n, T;
    scanf("%d", &T);
    while(T--){
        init();
        scanf("%d", &n);
        for(int i = 1; i <= n; ++i){
            scanf("%d%d", &pnt[i][0], &pnt[i][1]);
        }
        for(int i = 1; i <= n; ++i){
            for(int j = i+1; j <= n; ++j){
                int dist = CalDist(i, j);
                if(dist >= 10*10 && dist <= 1000*1000){
                    add(i, j, dist); //避免浮点数误差
                }
            }
        }
        sort(edg, edg+ecnt, cmp);
        double ans = 0;
        for(int i = 0; i < ecnt; ++i){
            if(join_seed(edg[i].u, edg[i].v)) ans += sqrt(edg[i].cost);
        }
        int flag = 0;
        for(int i = 1; i <= n; ++i) if(seed[i] < 0) flag += 1;
        if(flag >= 2) puts("oh!"); //构成MST的条件是并查集中最多存在一个根节点
        else printf("%.1f\n", 100*ans);
    }
}

C - Highways

题意: n个点,给出的是坐标,并且已经存在有m条边。现在要加入另外一些边,用最小的花费让n个点都连通,容易想到贪心的方法,每次都先加花费最小的边,就是Kruskal的过程。

#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 755;

int seed[N];
inline int find_root(int x){ return seed[x] < 0? x : seed[x] = find_root(seed[x]); }
inline int join_seed(int a, int b){
    a = find_root(a), b = find_root(b);
    if(a == b) return 0;
    seed[b] = a;
    return 1;
}

struct eg{
    int u, v, w;
}tmp;
vector<eg>edg;
inline bool cmp(eg a, eg b){
    return a.w < b.w;
}

int pnt[N][2];
inline int CalDist(int a, int b){
    int x = pnt[a][0] - pnt[b][0];
    int y = pnt[a][1] - pnt[b][1];
    return x*x+y*y;
}

int main(){
    int n, m;
    while(scanf("%d", &n) != EOF){
        memset(seed, -1, sizeof(seed));
        edg.clear();
        for(int i = 1; i <= n; ++i){
            scanf("%d%d", &pnt[i][0], &pnt[i][1]);
        }
        scanf("%d", &m);
        for(int i = 1; i <= m; ++i){
            int a, b; scanf("%d%d", &a, &b);
            join_seed(a, b); //ab已连通,直接join在一起
        }
        for(int i = 1; i <= n; ++i){
            for(int j = i+1; j <= n; ++j){
                if(find_root(i) == find_root(j)) continue; 
                //已有边连通就不用加边了
                tmp.u = i, tmp.v = j, tmp.w = CalDist(i, j);
                edg.push_back(tmp);
            }
        }
        sort(edg.begin(), edg.end(), cmp);
        for(int i = 0; i < edg.size(); ++i){
            if(join_seed(edg[i].u, edg[i].v)){
                printf("%d %d\n", edg[i].u, edg[i].v);
            }
        }
    }
}

D - QS Network

题意:英文题,题意比较难懂,看懂之后还是模板题。
给出每个点的花费,给出图的邻接矩阵,一条边的总花费就是两个点的花费+边花费,建完图求MST就行了。

#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 1005;
struct eg{
    int u, v, w;
}tmp;
inline bool cmp(eg a, eg b){
    return a.w < b.w;
}
vector<eg>edg;
int ada[N];
int seed[N];
int find_root(int x){
    return seed[x] < 0? x : seed[x] = find_root(seed[x]);
}
int join_seed(int a, int b){
    a = find_root(a), b = find_root(b);
    if(a == b) return 0;
    seed[b] = a;
    return 1;
}
void init(){
    edg.clear();
    memset(seed, -1, sizeof(seed));
}
int main(){
    int T;
    scanf("%d", &T);
    while(T--){
        init();
        int n;
        scanf("%d", &n);
        for(int i = 0; i < n; ++i) scanf("%d", &ada[i]);
        for(int i = 0; i < n; ++i){
            for(int j = 0; j < n; ++j){
                int cost;
                scanf("%d", &cost);
                if(i == j) continue;
                tmp.u = i, tmp.v = j, tmp.w = cost + ada[i] + ada[j];
                edg.push_back(tmp);
            }
        }
        sort(edg.begin(), edg.end(), cmp);
        int ans = 0;
        for(int i = 0; i < edg.size(); ++i){
            if(join_seed(edg[i].u, edg[i].v)){
                ans += edg[i].w;
            }
        }
        printf("%d\n", ans);
    }
}

E - 连接的管道

题解: 一个 nm n ∗ m 的矩阵,每个元素可以与上下左右的元素相连,要用最小花费连接所有元素,道理也是建好图然后求MST。
每个元素和上下左右元素建一条边,这样一个图是很稀疏的,使用prim会超时。边数极限在 106 10 6 以上,内存卡的也很紧,无用的边尽量不加,能省就省,因为无向图的缘故,每个点只对下方和右方的元素加边就够了,数字范围很小,用short省内存,还可以用数组代替vector。
题目来自去年的百度之星。

#include<stdio.h>
#include<vector>
#include<algorithm>
#include<string.h>
using namespace std;
const int MX = 1005;
inline short abs(short x){ return x < 0? -x:x; }
struct eg{
    int u, v;
    short w;
}tmp;
vector<eg>edg;
inline bool cmp(eg a, eg b){
    return a.w < b.w;
}
int seed[MX*MX];
int find_root(int x){
    return seed[x] < 0? x : seed[x] = find_root(seed[x]);
}
int join_seed(int a, int b){
    a = find_root(a), b = find_root(b);
    if(a == b) return 0;
    seed[b] = a;
    return 1;
}
void init(){
    edg.clear();
    memset(seed, -1, sizeof(seed));
}
void add(int a, int b, int c){
    tmp.u = a, tmp.v = b, tmp.w = c;
    edg.push_back(tmp);
}
short maz[MX][MX];
int main(){
    int T, ca = 1;
    scanf("%d", &T);
    while(T--){
        init();
        int n, m;
        scanf("%d%d", &n, &m);
        for(int i = 1; i <= n; ++i){
            for(int j = 1; j <= m; ++j){
                scanf("%d", &maz[i][j]);
            }
        }
        for(int i = 1; i <= n; ++i){
            for(int j = 1; j <= m; ++j){
                if(i+1 <= n){
                    add( (i-1)*m+j, i*m+j, abs(maz[i][j]-maz[i+1][j]) );
                }
                if(j+1 <= m){
                    add( (i-1)*m+j, (i-1)*m+j+1, abs(maz[i][j]-maz[i][j+1]));
                }
            }
        }
        sort(edg.begin(), edg.end(), cmp);
        int ans = 0;
        for(int i = 0; i < edg.size(); ++i){
            if(join_seed(edg[i].u, edg[i].v)){
                ans += edg[i].w;
            }
        }
        printf("Case #%d:\n%d\n", ca++, ans);
    }
}

F - Air Ports

题解: n个点,有m条无向路可以修,选择一些点修机场,或者修路,用最少的花费让所有点都能到机场。
如果一个点拥有机场或者可以间接到机场,就说这个点能到机场。
如果两个点距离很远,那么更好的方法就是各修一个机场,否则就修路,这样可以得到一个贪心的策略。
为了更优雅的写代码,一种做法是当需要修机场时,在这两个点里任选一个点修就可以了,因为并查集需要一个根,没有修机场的就作为并查集的根,修了机场的点就合并到根下面。
需要注意的是此时虽然合并了并查集,但只有一个机场,两个点不一定都能到机场,所以求出MST之后,需要对所有的并查集根补修一个机场,所有的边都用过了,已经没有别的办法让点能够到达机场,只能修机场了。
假如有一个点需要合并进来,如果它合并到有机场的部分,那它是连通的,如果它合并到没修机场的部分,最后对所有根修机场的时候就处理了这种情况,同时还处理了原图不连通的情况。

#include<bits/stdc++.h>
using namespace std;
struct eg{
    int u, v, w;
    bool operator < (eg a) const{ //定义eg的 < 符号
        return w < a.w;
    }
}edg[100005];
int seed[10005];
int find(int x) { 
    return seed[x] < 0? x : seed[x]=find(seed[x]);
}
int join(int a, int b){
    a = find(a), b = find(b);
    if(a == b) return 0;
    if(seed[a] > seed[b]) seed[a] = b;
    else seed[b] = a;
}
int main(){
    int T, ca = 1;
    scanf("%d", &T);
    while(T--){
        int n, m, air;
        memset(seed, -1, sizeof(seed));
        scanf("%d%d%d", &n, &m, &air);
        for(int i = 0; i < m; ++i){
            scanf("%d%d%d", &edg[i].u, &edg[i].v, &edg[i].w);
        }
        sort(edg, edg+m);
        int ans = 0, cot = 0;
        for(int i = 0; i < m; ++i){
            if(join(edg[i].u, edg[i].v)){
                if(edg[i].w >= air){ //修机场
                    ans += air, cot += 1;
                }
                else ans += edg[i].w; // 修路
            }
        }
        for(int i = 1; i <= n; ++i){ //补修机场
            if(seed[i] < 0) ans += air, cot += 1;
        }
        printf("Case %d: %d %d\n", ca++, ans, cot);
    }
}

G - Arctic Network

题解: n个点,给出坐标,当两个点距离 dis<=D d i s <= D 时,这两个点可以连通,另外还可以最多选 S S 个点放置卫星通信,有卫星的点之间连通,现在要让所有点直接或间接连通,求D 的最小值。
注意到题目 S>=1 S >= 1 ,不会出现 S=0 S = 0 的陷阱,因为安放一个卫星还不如一个都不安放。

一种容易想到的贪心策略是先求出MST,然后让MST里面距离最远的 S1 S − 1 条边,也就是 S S 个点使用卫星通信,答案就是递增排序后第S-2条边的长度。

另外还有一种二分的方法。
我们要二分答案,也就是二分D 值,二分的根据是什么?
假如答案为 ans a n s ,现在我们的 D1<ans D 1 < a n s ,会出现这样一种状况,用长度 length<=D1 l e n g t h <= D 1 的边求出MST,还需要加一些卫星才能要让所有点直接间接连通,并且我们需要的卫星的个数超过了 S S ,当D1>ans ,显然需要的卫星个数就小于等于 S S
对于任意一个D1,我们都可以知道它在 ans a n s 左边还是右边,这样得到了一个二分策略,只需要不断逼近临界的 ans a n s 就可以了。复杂度 O(Elog(E)log(1018)) O ( E ∗ l o g ( E ) ∗ l o g ( 10 18 ) ) 比第一种慢一点,但是这种二分答案的思想是很重要的。
其实 log(1018)<60 l o g ( 10 18 ) < 60 ,仍旧是非常高效的。
下面是二分的代码。

#include<stdio.h>
#include<vector>
#include<algorithm>
#include<math.h>
#include<string.h>
using namespace std;
const int inf = ~0u>>2;
int sate, n;
struct pt{
    int x, y;
    pt(){}
    pt(int a, int b){ x = a, y = b; }
};
struct eg{
    int u, v;
    double w;
    eg(){}
    eg(int a, int b, double c){ u = a, v = b, w = c; }
    bool operator < (const eg &a) const{
        return w < a.w;
    }
};
vector<pt>pnt;
vector<eg>edg;
inline double CalDist(int i, int j){
    double x = pnt[i].x - pnt[j].x;
    double y = pnt[i].y - pnt[j].y;
    return sqrt(x*x + y*y);
}
int seed[1005];
int find(int x){ return seed[x] < 0? x : seed[x] = find(seed[x]); }
int join(int a, int b){
    a = find(a), b = find(b);
    if(a == b) return 0;
    seed[b] = a;
    return 1;
}
int ok(double val){ //跑MST,求出需要的卫星个数
    memset(seed, -1, sizeof(seed));
    for(int i = 0; i < edg.size(); ++i){
        if(edg[i].w <= val) join(edg[i].u, edg[i].v);
    }
    int res = 0;
    for(int i = 0; i < n; ++i) if(seed[i] < 0) res += 1;
    return res;
}
int main(){
    int T;
    scanf("%d", &T);
    while(T--){
        edg.clear();
        pnt.clear();
        scanf("%d%d", &sate, &n);
        for(int i = 0; i < n; ++i){
            int x, y; scanf("%d%d", &x, &y);
            pnt.push_back( pt(x, y) );
        }
        for(int i = 0; i < pnt.size(); ++i){
            for(int j = i+1; j < pnt.size(); ++j){
                edg.push_back( eg(i, j, CalDist(i,j)) );
            }
        }
        sort(edg.begin(), edg.end());
        double l = 0, r = inf, mid;
        while(fabs(r-l) > 1e-5){
            mid = (l+r)/2;
            if(ok(mid) <= sate) r = mid; //卫星足够,r往左移
            else l = mid; // 卫星不够,l往右移
        }
        printf("%.2f\n", l);
    }
}

H - The Unique MST

题解: n个点,m条边,现在可以容易地求出一棵MST,问是否存在另外一棵权值相同的MST。
如果一条边存在于树 Tree1 T r e e 1 但不存在于 Tree2 T r e e 2 ,我们认为这两棵树不同。
确定MST是否唯一,暴力的做法是先跑出一棵MST,然后枚举去掉这棵MST的每一条边,再次跑MST,跑完再还原,看权值是否变化,如果不存在权值没变的情况,我们可以判定MST唯一,复杂度为 O(nelog(e)) O ( n ∗ e ∗ l o g ( e ) ) ,极限为 (n3log(n2)) O ( n 3 ∗ l o g ( n 2 ) )
此题数据范围暴力即可。

#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 105;
const int INF = ~0u>>2;
struct eg{
    int u, v, w;
    bool operator < (const eg &a) const{
        return w < a.w;
    }
}tmp;
int n, m;
vector<eg>G;
vector<int>mst;
int seed[N];
int find(int x){ return seed[x] < 0? x : seed[x] = find(seed[x]); }
int join(int a, int b){
    a = find(a), b = find(b);
    if(a == b) return 0;
    seed[b] = a;
    return 1;
}
int kruskal(int no){
    memset(seed, -1, sizeof(seed));
    int res = 0, cnt = 0;
    for(int i = 0; i < G.size(); ++i){
        if(i == no) continue; //这条边已经去掉
        if(join(G[i].u, G[i].v)) res += G[i].w, ++cnt;
    }
    if(cnt != n-1) return INF;
    return res;
}
int main(){
    int T;
    scanf("%d", &T);
    while(T--){
        scanf("%d%d", &n, &m);
        G.clear();
        mst.clear();
        for(int i = 0; i < m; ++i){
            scanf("%d%d%d", &tmp.u, &tmp.v, &tmp.w);
            G.push_back(tmp);
        }
        memset(seed, -1, sizeof(seed));
        sort(G.begin(), G.end());
        int MST = 0;
        for(int i = 0; i < G.size(); ++i){
            if(join(G[i].u, G[i].v)){
                MST += G[i].w;
                mst.push_back(i); // 记录MST的边
            }
        }
        int ans = INF;
        for(int i = 0; i < mst.size(); ++i){ //枚举MST的边
            ans = min(ans, kruskal(mst[i]));
        }
        if(ans == MST) puts("Not Unique!");
        else printf("%d\n", MST); // MST唯一
    }
}

I - Truck History

题解: 卡题意及建图。字符串都能建图,就是这么酷。
给出n个字符串,每个串7个字符。
任意两个字符串 ab a b 之间都能建边,代价是 06 0 ~ 6 这7个位置里 a[i]!=b[i] a [ i ] ! = b [ i ] 的位置的个数。
枚举 ab a b ,建完图跑MST,然后按照格式输出就可以了。

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int N = 2005;
struct eg{
    int u, v, w;
    eg(){}
    eg(int a, int b, int c){ u = a, v = b, w = c; }
    bool operator < (const eg &a) const{
        return w < a.w;
    }
}edg[N*N];
int ecnt;
char str[N][10];
int cal(int a, int b){
    int res = 0;
    for(int i = 0; i < 7; ++i) if(str[a][i] != str[b][i]) res += 1;
    return res;
}
int seed[N];
int find(int x){ return seed[x] < 0? x : seed[x] = find(seed[x]); }
int join(int a, int b){
    a = find(a), b = find(b);
    if(a == b) return 0;
    seed[b] = a;
    return 1;
}
int main(){
    int n;
    while(scanf("%d", &n), n){
        ecnt = 0;
        memset(seed, -1, sizeof(seed));
        for(int i = 0; i < n; ++i) scanf("%s", str[i]);
        for(int i = 0; i < n; ++i){
            for(int j = i+1; j < n; ++j){
                edg[ecnt++] = eg(i, j, cal(i,j));
            }
        }
        sort(edg, edg+ecnt);
        int ans = 0;
        for(int i = 0; i < ecnt; ++i){
            if(join(edg[i].u, edg[i].v)) ans += edg[i].w;
        }
        printf("The highest possible quality is 1/%d.\n", ans);
    }
}

J - Trail Maintenance

题解: n个点, 一开始没有边,在m天里每天给出1条边,问已经给出了的边的MST值是多少,如果n个点无法连通就输出-1。
容易想到暴力的做法,每天都跑一次MST,最后一天的复杂度是熟悉的 O(mlog(m)) O ( m ∗ l o g ( m ) ) ,然而每一天加起来,总复杂度接近 O(m2log(m)) O ( m 2 ∗ l o g ( m ) ) ,并且是多组数据,暴力是无法通过的。
那么如何优化呢?首先注意到在n-1天之前,答案必定是-1,因为n个点第一次连通必然出现再n-1天或之后,然而这是个常数优化,复杂度瓶颈并没有改变,但是可以给我们一些启示。
假设在第k天时,n个点第一次连通,那么此时按照我们暴力的做法,需要求一次MST。然而在第k+1天时,我们似乎没有必要去关注所有的边,复杂度很高的原因是因为我们在处理k+1天时,完全没有利用第k天留下的信息。
假如我们在第k天保存了MST信息,那么在k+1天时,只需要用第k天的MST和新加的边就可以求出第k+1天的MST,对于第k天时不在MST里的边,在k+1天也不可能出现在MST里,因为极端情况下可以不使用新加的边,完全使用第k天的MST,这样每次只需要对n条边跑MST,复杂度是 O(mnlog(n)) O ( m ∗ n ∗ l o g ( n ) ) ,解决。

#include<bits/stdc++.h>
using namespace std;
struct eg{
    int u, v, w, id;
    bool operator < (eg a) const{
        return w < a.w;
    }
}edg[6006], MST[300];
int seed[205];
int find(int x){ return seed[x] < 0? x : seed[x] = find(seed[x]); }
int join(int a, int b){
    a = find(a), b = find(b);
    if(a == b) return 0;
    if(seed[a] > seed[b]) seed[b] += seed[a], seed[a] = b;
    else seed[a] += seed[b], seed[b] = a;
    return 1;
}
int n, m;
int getMST(int e){ // 跑出MST,并保存边的信息
    memset(seed, -1, sizeof(seed));
    sort(edg, edg+e);
    int mcnt = 0, tmp = 0;
    for(int i = 0; i < e; ++i){
        if(join(edg[i].u, edg[i].v)) MST[mcnt++] = edg[i], tmp += edg[i].w;
    }
    printf("%d\n", tmp);
    return mcnt;
}
void kruskal(int x){
    int ans = 0, mcnt = 0;
    memset(seed, -1, sizeof(seed));
    sort(MST, MST+x);
    for(int i = 0; i < x; ++i){
        if(join(MST[i].u, MST[i].v)) ans += MST[i].w, MST[mcnt++] = MST[i];
    }
    printf("%d\n", ans);
}
int main(){
    int T, ca = 1;
    scanf("%d", &T);
    while(T--){
        scanf("%d%d", &n, &m);
        printf("Case %d:\n", ca++);
        memset(seed, -1, sizeof(seed));
        int cnt = n, i, flag = 0, mcnt = 0;
        for(i = 0; i < m; ++i){
            scanf("%d%d%d", &edg[i].u, &edg[i].v, &edg[i].w);
            if(join(edg[i].u, edg[i].v)) cnt -= 1;
            if(cnt != 1) printf("-1\n"); //n个点没连通
            else { mcnt = getMST(++i); break; } //第一次连通
        }
        for(; i < m; ++i){
            scanf("%d%d%d", &MST[mcnt].u, &MST[mcnt].v, &MST[mcnt].w);
            kruskal(mcnt+1); //用前一次的MST和新边跑MST
        }
    }
}

K - Travel

题解: n个点,m条无向边,q个询问。
每次询问给出一个费用限制 D D ,假设有两个不同的点(a,b) ,如果只选择花费不大于D的边,可以从a走到b的话,就说 (a,b) ( a , b ) 合法。对于每个d,要你算出有多少对合法的 (a,b) ( a , b ) ,ab和ba是不同的答案。

对于多询问的问题,一般有在线和离线两种解决方式,通常来说如果解决一次询问的复杂度很低,如 O(1),O(log) O ( 1 ) , O ( l o g ) ,甚至一些比较小的 O(N) O ( N ) ,那么选择在线处理,通常更容易编码。
对于这题,很难做到 O(n) O ( n ) 的在线回答,即使做到了, O(nq) O ( n ∗ q ) 也会超时,只能考虑离线处理。

离线的精髓就是一次性读取所有询问,再分别回答,并且按照某种关系保留信息,利用上一次回答得到的信息减少下一次回答的复杂度。

在这题中,读取所有询问后,将所有 D D 递增排序。
此时最前面的D0 就是最小的询问,我们先采用暴力来回答这个询问。
做法是Kruskal,只选择费用不大于 D0 D 0 的边,同时在并查集中,我们需要维护额外的信息,对于一个并查集根 i i ,我们要维护i 下面的元素有多少个。
i i 有什么含义呢?前面说了,我们只选择了费用不大于D0 的边,也就是说 i i 下面的任意两个元素都是可以通过不大于D0 的边互相到达的,对于一个根 i i ,设它的大小为size[i] ,那么这个根及其元素对答案的贡献就是 A(size[i],2) A ( s i z e [ i ] , 2 ) ,即任选两个元素的排列。

需要留意这种算贡献的思想也是十分重要的,例如,逆序对也可以考虑成每个数对答案的贡献。

现在处理完了最小的询问,要处理下一个询问,如何利用上次的信息呢?
假设现在有两个根 rt1 r t 1 rt2 r t 2 ,在上一次询问里这两个根没有合并,说明这两个根只靠通过不大于 D0 D 0 的边无法互相到达,现在的限制升高到 D1 D 1 ,在Kruskal过程中,如果要将 rt1 r t 1 rt2 r t 2 合并,那么贡献是 rt1 r t 1 内的任意两点排列 + + rt2 内的任意两点排列 + + 一点在rt1,另外一点在 rt2 r t 2 的排列 ,可以看出前面两项的累加就是前一次的答案,所以新产生的贡献就是最后一项,通过排列可以算出是 2size[rt1]size[rt2] 2 ∗ s i z e [ r t 1 ] ∗ s i z e [ r t 2 ] ,那么本次询问的答案 ans1=ans0+2size[rt1]size[rt2] a n s 1 = a n s 0 + ∑ 2 ∗ s i z e [ r t 1 ] ∗ s i z e [ r t 2 ] ,只需要遍历所有需要合并的并查集就可以算出答案,注意到在 Kruskal K r u s k a l 的过程中正好需要合并并查集,其实代码比较容易写了。
结论就是我们发现本题采用离线回答时,当前询问的答案只跟前一次的答案,以及并查集里新合并的根的大小有关系。
每条边只需要加入一次,复杂度瓶颈是快排 O(mlog(m)) O ( m ∗ l o g ( m ) ) ,解决。
本题是去年区域赛网赛水题。

#include<stdio.h>
#include<algorithm>
#include<string.h>
using namespace std;
const int M = 100005;
const int N = 20005;
struct eg{
    int u, v, w;
    bool operator < (const eg &a) const{
        return w < a.w;
    }
}edg[M];
pair<int,int> qry[5005];
int seed[N];
//seed值为负的表示是根,负数的大小表示元素个数
int find(int x){ return seed[x] < 0? x : seed[x] = find(seed[x]); }
int join(int a, int b, int &sum){
    a = find(a), b = find(b);
    if(a == b) return 0;
    sum += 2*seed[a]*seed[b]; //加入新贡献
    if(seed[a] > seed[b]) seed[b] += seed[a], seed[a] = b;
    else seed[a] += seed[b], seed[b] = a;
    return 1;
}
int ans[5005];
int main(){
    int T;
    scanf("%d", &T);
    while(T--){
        int n, m, q;
        scanf("%d%d%d", &n, &m, &q);
        memset(seed, -1, sizeof(seed));
        for(int i = 0; i < m; ++i) scanf("%d%d%d", &edg[i].u, &edg[i].v, &edg[i].w);
        sort(edg, edg+m);
        for(int i = 0; i < q; ++i) scanf("%d", &qry[i].first), qry[i].second = i; // 读取全部询问
        sort(qry, qry+q); // 排序询问
        int ecnt = 0, sum = 0;
        for(int i = 0; i < q; ++i){
            while(ecnt < m && edg[ecnt].w <= qry[i].first){ 
                //只加入当前需要的边
                join(edg[ecnt].u, edg[ecnt].v, sum);
                ecnt += 1;
            }
            ans[qry[i].second] = sum; //分别回答
        }
        for(int i = 0; i < q; ++i) printf("%d\n", ans[i]);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值