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 的矩陣,每個元素可以與上下左右的元素相連,要用最小花費連接所有元素,道理也是建好圖然後求MST。
每個元素和上下左右元素建一條邊,這樣一個圖是很稀疏的,使用prim會超時。邊數極限在106 以上,內存卡的也很緊,無用的邊儘量不加,能省就省,因爲無向圖的緣故,每個點只對下方和右方的元素加邊就夠了,數字範圍很小,用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 時,這兩個點可以連通,另外還可以最多選S 個點放置衛星通信,有衛星的點之間連通,現在要讓所有點直接或間接連通,求D 的最小值。
注意到題目S>=1 ,不會出現S=0 的陷阱,因爲安放一個衛星還不如一個都不安放。

一種容易想到的貪心策略是先求出MST,然後讓MST裏面距離最遠的S1 條邊,也就是S 個點使用衛星通信,答案就是遞增排序後第S-2條邊的長度。

另外還有一種二分的方法。
我們要二分答案,也就是二分D 值,二分的根據是什麼?
假如答案爲ans ,現在我們的D1<ans ,會出現這樣一種狀況,用長度length<=D1 的邊求出MST,還需要加一些衛星才能要讓所有點直接間接連通,並且我們需要的衛星的個數超過了S ,當D1>ans ,顯然需要的衛星個數就小於等於S
對於任意一個D1 ,我們都可以知道它在ans 左邊還是右邊,這樣得到了一個二分策略,只需要不斷逼近臨界的ans 就可以了。複雜度O(Elog(E)log(1018)) 比第一種慢一點,但是這種二分答案的思想是很重要的。
其實log(1018)<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 但不存在於Tree2 ,我們認爲這兩棵樹不同。
確定MST是否唯一,暴力的做法是先跑出一棵MST,然後枚舉去掉這棵MST的每一條邊,再次跑MST,跑完再還原,看權值是否變化,如果不存在權值沒變的情況,我們可以判定MST唯一,複雜度爲O(nelog(e)) ,極限爲(n3log(n2))
此題數據範圍暴力即可。

#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 之間都能建邊,代價是06 這7個位置裏a[i]!=b[i] 的位置的個數。
枚舉ab ,建完圖跑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(m2log(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)) ,解決。

#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 ,假設有兩個不同的點(a,b) ,如果只選擇花費不大於D的邊,可以從a走到b的話,就說(a,b) 合法。對於每個d,要你算出有多少對合法的(a,b) ,ab和ba是不同的答案。

對於多詢問的問題,一般有在線和離線兩種解決方式,通常來說如果解決一次詢問的複雜度很低,如O(1),O(log) ,甚至一些比較小的O(N) ,那麼選擇在線處理,通常更容易編碼。
對於這題,很難做到O(n) 的在線回答,即使做到了,O(nq) 也會超時,只能考慮離線處理。

離線的精髓就是一次性讀取所有詢問,再分別回答,並且按照某種關係保留信息,利用上一次回答得到的信息減少下一次回答的複雜度。

在這題中,讀取所有詢問後,將所有D 遞增排序。
此時最前面的D0 就是最小的詢問,我們先採用暴力來回答這個詢問。
做法是Kruskal,只選擇費用不大於D0 的邊,同時在並查集中,我們需要維護額外的信息,對於一個並查集根i ,我們要維護i 下面的元素有多少個。
i 有什麼含義呢?前面說了,我們只選擇了費用不大於D0 的邊,也就是說i 下面的任意兩個元素都是可以通過不大於D0 的邊互相到達的,對於一個根i ,設它的大小爲size[i] ,那麼這個根及其元素對答案的貢獻就是A(size[i],2) ,即任選兩個元素的排列。

需要留意這種算貢獻的思想也是十分重要的,例如,逆序對也可以考慮成每個數對答案的貢獻。

現在處理完了最小的詢問,要處理下一個詢問,如何利用上次的信息呢?
假設現在有兩個根rt1rt2 ,在上一次詢問裏這兩個根沒有合併,說明這兩個根只靠通過不大於D0 的邊無法互相到達,現在的限制升高到D1 ,在Kruskal過程中,如果要將rt1rt2 合併,那麼貢獻是 rt1 內的任意兩點排列 + rt2 內的任意兩點排列 + 一點在rt1 ,另外一點在rt2 的排列 ,可以看出前面兩項的累加就是前一次的答案,所以新產生的貢獻就是最後一項,通過排列可以算出是2size[rt1]size[rt2] ,那麼本次詢問的答案ans1=ans0+2size[rt1]size[rt2] ,只需要遍歷所有需要合併的並查集就可以算出答案,注意到在Kruskal 的過程中正好需要合併並查集,其實代碼比較容易寫了。
結論就是我們發現本題採用離線回答時,當前詢問的答案只跟前一次的答案,以及並查集裏新合併的根的大小有關係。
每條邊只需要加入一次,複雜度瓶頸是快排O(mlog(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]);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章