【题解】P3639 [APIO2013]道路费用

题目链接(UOJ)

题意

一个n个结点的无向图,有m条老边已经存在,给定起点、终点、权值,保证权值互不相同且此时图已经联通。还有k条新边是G老板的,给定起点、终点,权值由G老板指定。

在G老板指定完权值后,在图的最小生成树上,结点i上有pi个人要从结点i去结点1(只能走最小生成树上的边)。每个人如果路过新边就要给G老板交权值那么多钱。求G老板最多赚多少。

n105,m3×105,k20,时间限制:3

分析

暴力

发现k很小,可以2k枚举每条边取或不取。之后跑Kruskal,考虑如何求新边权值。对于不在MST上的权值为w的老边uvMSTuv的路径上所有边的边权都w。考虑暴力爬(u,v)这条链,权值对wmin

整体复杂度O(m×logm+2k×m2)

优化

先加入所有新边,然后加旧边直到图联通。这里添加的旧边是一定会出现在最后的MST里的。这些旧边被新边分成了k+1个连通块,可以缩成k+1个点。还有另外k条旧边可以让这些连通块联通,这些边可能会出现在最后的MST中。

于是我们每次2k枚举情况后只用再在k条边中跑Kruskal,树的大小只有2×k

整体复杂度O(m×logm+2k×k2)

实现

处理一定存在的边

1
2
3
4
5
6
7
8
9
10
11
12
13

void kruskal1() {
for (int i = 1; i <= k; i++) {
sAll.merge(edNew[i].u, edNew[i].v); // 先将所有新边加入
}
sort(edOld + 1, edOld + m + 1);
for (int i = 1; i <= m; i++) {
if (sAll.find(edOld[i].u) == sAll.find(edOld[i].v)) continue;
sAll.merge(edOld[i].u, edOld[i].v);
sOld.merge(edOld[i].u, edOld[i].v); // 专门记录旧边的并查集
}
}

缩点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void redu() { //缩点
for (int i = 1; i <= n; i++) { // 有多少个不同的并查集就有多少个连通块
if (sOld.find(i) == i) id[i] = ++cntCon;
}
rt = id[sOld.find(1)]; // 记录缩点后树的根
sAll = sOld;
for (int i = 1; i <= n; i++) pCon[id[sOld.find(i)]] += p[i]; // 每个联通块的总人数
for (int i = 1; i <= m; i++) {
if (sOld.find(edOld[i].u) == sOld.find(edOld[i].v)) continue;
sOld.merge(edOld[i].u, edOld[i].v);
edCot[++cntCot] = edOld[i]; // 记录用来联通联通块的边
}

for (int i = 1; i <= k; i++) { // 更新原来的编号为缩点后的编号
edNew[i].u = id[sAll.find(edNew[i].u)];
edNew[i].v = id[sAll.find(edNew[i].v)];
}
for (int i = 1; i <= k; i++) {
edCot[i].u = id[sAll.find(edCot[i].u)];
edCot[i].v = id[sAll.find(edCot[i].v)];
}
}

枚举所有情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void sol() { // 枚举
for (int sta = 0; sta < (1 << k); sta++) {
bool ex = false;
tmp = 0;
for (int i = 1; i <= cntCon; i++) { // 初始化
minn[i] = inf;
head[i] = 0;
sTmp.fa[i] = i;
}
for (int i = 1; i <= k; i++) {
if (!(sta & (1 << (i - 1)))) continue;
if (sTmp.find(edNew[i].u) == sTmp.find(edNew[i].v)) { // 加新边
ex = true;
break;
}
sTmp.merge(edNew[i].u, edNew[i].v);
add(edNew[i].u, edNew[i].v);
add(edNew[i].v, edNew[i].u);
}
if (ex) continue;
for (int i = 1; i <= cntCot; i++) { // 连可能的旧边
if (sTmp.find(edCot[i].u) == sTmp.find(edCot[i].v)) continue;
sTmp.merge(edCot[i].u, edCot[i].v);
add(edCot[i].u, edCot[i].v);
add(edCot[i].v, edCot[i].u);
}
ans = max(ans, getsum(sta)); // 统计当前树的收益
}
}

统计答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ll getsum(int sta) {
dfs(rt, 0); // 在MST上处理出人数和父亲
for (int i = 1; i <= cntCot; i++) { // 暴力爬链,计算权值
int x = edCot[i].u, y = edCot[i].v;
ll val = edCot[i].w;
while (x != y) {
if (dep[x] < dep[y]) swap(x, y);
minn[x] = min(minn[x], val); // minn记到深度较深的点上
x = fa[x];
}
}
ll sum = 0;
for (int i = 1; i <= k; i++) {
if (!(sta & (1 << (i - 1)))) continue;
int x = edNew[i].u, y = edNew[i].v;
if (dep[x] < dep[y]) swap(x, y);
sum += minn[x] * siz[x]; // 统计答案
}
return sum;
}

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
#include<bits/stdc++.h>

#define ll long long
using namespace std;
const ll N = 3000005, inf = 1e18;
int n, m, k, cntCot, cntCon, rt;
ll p[N], id[N], pCon[N], siz[N], minn[N], dep[N], fa[N], ans;

struct edge {
int u, v;
ll w;

bool operator<(const edge &x) const {
return w < x.w;
}
} edNew[N], edOld[N], edCot[N];

struct uSet {
int fa[N];

void init() {
for (int i = 1; i <= n; i++) fa[i] = i;
}

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

void merge(int x, int y) {
x = find(x);
y = find(y);
if (x != y) fa[x] = y;
}
} sAll, sOld, sTmp;

struct node {
int to, nxt;
} e[N << 1];

int head[N], tmp;

void add(int x, int y) {
e[++tmp].to = y;
e[tmp].nxt = head[x];
head[x] = tmp;
}

void dfs(int x, int fath) {
fa[x] = fath;
dep[x] = dep[fath] + 1;
siz[x] = pCon[x];
for (int i = head[x]; i; i = e[i].nxt) {
int v = e[i].to;
if (v == fath) continue;
dfs(v, x);
siz[x] += siz[v];
}
}

void readIn() {
ios::sync_with_stdio(false);
cin >> n >> m >> k;
sAll.init();
sOld.init();
sTmp.init();
for (int i = 1; i <= m; i++) {
cin >> edOld[i].u >> edOld[i].v >> edOld[i].w;
}
for (int i = 1; i <= k; i++) {
cin >> edNew[i].u >> edNew[i].v;
}
for (int i = 1; i <= n; i++) {
cin >> p[i];
}
}

void kruskal1() {
for (int i = 1; i <= k; i++) {
sAll.merge(edNew[i].u, edNew[i].v); // 先将所有新边加入
}
sort(edOld + 1, edOld + m + 1);
for (int i = 1; i <= m; i++) {
if (sAll.find(edOld[i].u) == sAll.find(edOld[i].v)) continue;
sAll.merge(edOld[i].u, edOld[i].v);
sOld.merge(edOld[i].u, edOld[i].v); // 专门记录旧边的并查集
}
}

void redu() { //缩点
for (int i = 1; i <= n; i++) { // 有多少个不同的并查集就有多少个连通块
if (sOld.find(i) == i) id[i] = ++cntCon;
}
rt = id[sOld.find(1)]; // 记录缩点后树的根
sAll = sOld;
for (int i = 1; i <= n; i++) pCon[id[sOld.find(i)]] += p[i]; // 每个联通块的总人数
for (int i = 1; i <= m; i++) {
if (sOld.find(edOld[i].u) == sOld.find(edOld[i].v)) continue;
sOld.merge(edOld[i].u, edOld[i].v);
edCot[++cntCot] = edOld[i]; // 记录用来联通联通块的边
}

for (int i = 1; i <= k; i++) { // 更新原来的编号为缩点后的编号
edNew[i].u = id[sAll.find(edNew[i].u)];
edNew[i].v = id[sAll.find(edNew[i].v)];
}
for (int i = 1; i <= k; i++) {
edCot[i].u = id[sAll.find(edCot[i].u)];
edCot[i].v = id[sAll.find(edCot[i].v)];
}
}

ll getsum(int sta) {
dfs(rt, 0); // 在MST上处理出人数和父亲
for (int i = 1; i <= cntCot; i++) { // 暴力爬链,计算权值
int x = edCot[i].u, y = edCot[i].v;
ll val = edCot[i].w;
while (x != y) {
if (dep[x] < dep[y]) swap(x, y);
minn[x] = min(minn[x], val); // minn记到深度较深的点上
x = fa[x];
}
}
ll sum = 0;
for (int i = 1; i <= k; i++) {
if (!(sta & (1 << (i - 1)))) continue;
int x = edNew[i].u, y = edNew[i].v;
if (dep[x] < dep[y]) swap(x, y);
sum += minn[x] * siz[x]; // 统计答案
}
return sum;
}

void sol() { // 枚举
for (int sta = 0; sta < (1 << k); sta++) {
bool ex = false;
tmp = 0;
for (int i = 1; i <= cntCon; i++) { // 初始化
minn[i] = inf;
head[i] = 0;
sTmp.fa[i] = i;
}
for (int i = 1; i <= k; i++) {
if (!(sta & (1 << (i - 1)))) continue;
if (sTmp.find(edNew[i].u) == sTmp.find(edNew[i].v)) { // 加新边
ex = true;
break;
}
sTmp.merge(edNew[i].u, edNew[i].v);
add(edNew[i].u, edNew[i].v);
add(edNew[i].v, edNew[i].u);
}
if (ex) continue;
for (int i = 1; i <= cntCot; i++) { // 连可能的旧边
if (sTmp.find(edCot[i].u) == sTmp.find(edCot[i].v)) continue;
sTmp.merge(edCot[i].u, edCot[i].v);
add(edCot[i].u, edCot[i].v);
add(edCot[i].v, edCot[i].u);
}
ans = max(ans, getsum(sta)); // 统计当前树的收益
}
}

int main() {
readIn();
kruskal1();
redu();
sol();
cout << ans << endl;
return 0;
}