题解: 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);
}
}
题意: 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);
}
}
题意: 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);
}
}
}
}
题意:英文题,题意比较难懂,看懂之后还是模板题。
给出每个点的花费,给出图的邻接矩阵,一条边的总花费就是两个点的花费+边花费,建完图求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);
}
}
题解: 一个 n∗m 的矩阵,每个元素可以与上下左右的元素相连,要用最小花费连接所有元素,道理也是建好图然后求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);
}
}
题解: 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);
}
}
题解: n个点,给出坐标,当两个点距离 dis<=D 时,这两个点可以连通,另外还可以最多选 S 个点放置卫星通信,有卫星的点之间连通,现在要让所有点直接或间接连通,求 D 的最小值。
注意到题目 S>=1 ,不会出现 S=0 的陷阱,因为安放一个卫星还不如一个都不安放。
一种容易想到的贪心策略是先求出MST,然后让MST里面距离最远的 S−1 条边,也就是 S 个点使用卫星通信,答案就是递增排序后第S-2条边的长度。
另外还有一种二分的方法。
我们要二分答案,也就是二分 D 值,二分的根据是什么?
假如答案为 ans ,现在我们的 D1<ans ,会出现这样一种状况,用长度 length<=D1 的边求出MST,还需要加一些卫星才能要让所有点直接间接连通,并且我们需要的卫星的个数超过了 S ,当 D1>ans ,显然需要的卫星个数就小于等于 S 。
对于任意一个 D1 ,我们都可以知道它在 ans 左边还是右边,这样得到了一个二分策略,只需要不断逼近临界的 ans 就可以了。复杂度 O(E∗log(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);
}
}
题解: n个点,m条边,现在可以容易地求出一棵MST,问是否存在另外一棵权值相同的MST。
如果一条边存在于树 Tree1 但不存在于 Tree2 ,我们认为这两棵树不同。
确定MST是否唯一,暴力的做法是先跑出一棵MST,然后枚举去掉这棵MST的每一条边,再次跑MST,跑完再还原,看权值是否变化,如果不存在权值没变的情况,我们可以判定MST唯一,复杂度为 O(n∗e∗log(e)) ,极限为 O(n3∗log(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唯一
}
}
题解: 卡题意及建图。字符串都能建图,就是这么酷。
给出n个字符串,每个串7个字符。
任意两个字符串 ab 之间都能建边,代价是 0~6 这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);
}
}
题解: n个点, 一开始没有边,在m天里每天给出1条边,问已经给出了的边的MST值是多少,如果n个点无法连通就输出-1。
容易想到暴力的做法,每天都跑一次MST,最后一天的复杂度是熟悉的 O(m∗log(m)) ,然而每一天加起来,总复杂度接近 O(m2∗log(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(m∗n∗log(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
}
}
}
题解: 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(n∗q) 也会超时,只能考虑离线处理。
离线的精髓就是一次性读取所有询问,再分别回答,并且按照某种关系保留信息,利用上一次回答得到的信息减少下一次回答的复杂度。
在这题中,读取所有询问后,将所有 D 递增排序。
此时最前面的 D0 就是最小的询问,我们先采用暴力来回答这个询问。
做法是Kruskal,只选择费用不大于 D0 的边,同时在并查集中,我们需要维护额外的信息,对于一个并查集根 i ,我们要维护 i 下面的元素有多少个。
根 i 有什么含义呢?前面说了,我们只选择了费用不大于 D0 的边,也就是说 i 下面的任意两个元素都是可以通过不大于 D0 的边互相到达的,对于一个根 i ,设它的大小为 size[i] ,那么这个根及其元素对答案的贡献就是 A(size[i],2) ,即任选两个元素的排列。
需要留意这种算贡献的思想也是十分重要的,例如,逆序对也可以考虑成每个数对答案的贡献。
现在处理完了最小的询问,要处理下一个询问,如何利用上次的信息呢?
假设现在有两个根 rt1 和 rt2 ,在上一次询问里这两个根没有合并,说明这两个根只靠通过不大于 D0 的边无法互相到达,现在的限制升高到 D1 ,在Kruskal过程中,如果要将 rt1 和 rt2 合并,那么贡献是 rt1 内的任意两点排列 + rt2 内的任意两点排列 + 一点在 rt1 ,另外一点在 rt2 的排列 ,可以看出前面两项的累加就是前一次的答案,所以新产生的贡献就是最后一项,通过排列可以算出是 2∗size[rt1]∗size[rt2] ,那么本次询问的答案 ans1=ans0+∑2∗size[rt1]∗size[rt2] ,只需要遍历所有需要合并的并查集就可以算出答案,注意到在 Kruskal 的过程中正好需要合并并查集,其实代码比较容易写了。
结论就是我们发现本题采用离线回答时,当前询问的答案只跟前一次的答案,以及并查集里新合并的根的大小有关系。
每条边只需要加入一次,复杂度瓶颈是快排 O(m∗log(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]);
}
}