题解:P12007 【MX-X10-T3】[LSOT-4] 全国联赛?
修改:2025.4.12,经提醒,修正状态转移方程中的一处笔误。
题意简述
有一张有
分析题目
我们将每个成员抽象成点,每个关系抽象成一条带权边,那我们的目标就是确定一张使得其中任意两个成员的距离的和最小的图。
不难发现,最后得到的目标图一定是一棵树,为什么呢?
首先,初始已经确定了
那么初始的输入里也不可能存在环。也就是说,我们会得到一颗颗小树,然后通过
解题思路
状态转移方程
我们不妨逆向思维一下,假设一些东西已经知道了,那就可以愉快的开始动态规划了 awa。
我们现在先考虑有两棵树,用一条权为
我们设
设将要合并的两棵棵树的根节点分别为
-
F_{root} = F_u + F_v + f_u \times c_v + f_v \times c_u + c_u \times c_v \times w
应该不难理解(知道你很急,怎么不写
从此处我们可以看出,先不考虑
假定
-
f_{root} = f_u + f_v + c_v \times w$ 选择 $u -
f_{root} = f_u + f_v + c_u \times w$ 选择 $v
由于
用这样不断地转移,最后只剩下一棵树那就是目标答案了。
此时读者可能要掀桌子了,你还没写许多关键的东西呢,怎么初始化、怎么安排合并的顺序以及
初始化
首先是
至于初始的
-
ans = \frac{1}{2} \times \sum\limits_{i = 1} ^ n f_i
乘上
合并顺序及边的安排
那么你肯定发现了,答案的值也会与合并的顺序以及选择的边有关,不能随便乱合并,那么究竟如何最优化呢?
首先,让越小的边承担越多的“行走”,即出现在两点间最短路的次数最多。
那么就是说最后合并两个剩下的最大的树时用最小的边权吗?当然不是!很显然首先是越早供合并用的边的承担的越多,其次才是供越大的两个树合并的边承担的越多。
为什么呢?你想,我们之前说了,新合成的树的重心会是原来那个树中点较多的一棵树的重心
读到此处,如果你觉得大彻大悟了,遍可开始自己着手写代码了,如果不知道具体如何设计程序可继续往下看。
程序设计
初始化 & 小小树的存储
前文所提的初始化很容易想到一遍循环遍历
这个过程,其实说白了就是求每个小小树的重心,然后用结构体储存下来该树所有点到重心的距离的和,放进优先队列里,以该树的节点数为关键字,后文要做“合并石子”。这一段的时间复杂度
算初始
对于未确定的
“合并石子”
我们发现,接下来的过程就像极了合并石子,每次取两个节点数最多的“石子”合并,刚好合并
当然,可以不用优先队列,说白了就是一路从大合到小,比赛时脑子不清醒打了优先队列,而且好像确实蛮方便的 awa。
讲到这里,是不是可以自己实现了?如果实在还有不明白的地方就请观看代码吧!
虽然蒟蒻的码风并不喜人,但是还是添加了足够的注释以备神犇观看的 qwq。
代码如下
优先队列:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e6 + 5;
const ll p = 1e9 + 7, inv2 = 5e8 + 4;//模数与逆元
struct node {
ll w;
int nxt, to;
} edge[N << 1];
struct Node {
ll f, cnt;//分别表示该树所有点到重心的距离和与该树的点数
inline bool operator < (const Node &b) const {return cnt < b.cnt;}//重载运算符,以该树的点数为关键字
} a;//描述一棵树
bool vis[N];
priority_queue<Node> q;//默认大根堆
ll f[N], size[N], F[N], ans, w[N];
int n, m, head[N], tot, res, rt;
inline bool cmp (Node x, Node y) {return x.cnt > y.cnt;}
void add (int from, int to, ll w) {
edge[++tot].nxt = head[from];
edge[tot].to = to;
edge[tot].w = w;
head[from] = tot;
}//链式前向星应该都会吧?
void dfs1 (int u, int pa) {
size[u] = 1, res++;
for (int i = head[u], v; i; i = edge[i].nxt) {
v = edge[i].to;
if (v == pa) continue;
dfs1(v, u), size[u] += size[v];
f[u] += f[v] + size[v] * edge[i].w;
}
}//第一次 dfs
void dfs2 (int u, int pa) {
vis[u] = true;
for (int i = head[u], v; i; i = edge[i].nxt) {
v = edge[i].to;
if (v == pa) continue;
f[v] = f[u] + (res - 2 * size[v]) * edge[i].w;
if (f[v] < a.f) a.f = f[v];
dfs2(v, u);
}
}//第二次 dfs,换根求重心应该都会吧?
int main() {
scanf("%d%d", &n, &m);
for (int i = 1, u, v, w; i <= m; i++) scanf("%d%d%d", &u, &v, &w), add(u, v, w), add(v, u, w);//无向边建俩
for (int i = 1; i <= n - m - 1; i++) scanf("%lld", &w[i]);//输入
for (int i = 1; i <= n; i++) {
if (!vis[i]) {
res = 0, dfs1(i, 0), a.f = f[i], dfs2(i, 0), a.cnt = res;//记录小小树
q.push(a);//上文所述的具体实现
}
}
for (int i = 1; i <= n; i++) ans = (ans + f[i]) % p;
ans = (ans * inv2) % p;//初始答案
Node u, v;
sort(w, w + n - m);
for (int i = 1; i <= n - m - 1; i++) {
u = q.top(), q.pop(), v = q.top(), q.pop();/*取当前节点数最多的两个树出来
(其实说白了就是从大合到小,一直合,也可以不用优先队列来实现,看个人,用优先队列其实不好
可以按节点数从大到小排序以后直接和,比这个快很多,堆的操作也是有时间复杂度的)*/
ans = (ans + u.f * v.cnt % p + v.f * u.cnt % p + u.cnt * v.cnt * w[i] % p) % p;//状态转移方程的程序实现
u.f = ((u.f + v.cnt * w[i]) % p + v.f) % p, u.cnt = (u.cnt + v.cnt) % p;//新的树的各种信息更新
q.push(u);//合并好的树继续放入堆中
}//类似于合并石子
printf("%lld", ans);
return 0;
}
不使用优先队列:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e6 + 5;
const ll p = 1e9 + 7, inv2 = 5e8 + 4;//模数与逆元
struct node {
ll w;
int nxt, to;
} edge[N << 1];
struct Node {
ll f, cnt;//分别表示该树所有点到重心的距离和与该树的点数
inline bool operator < (const Node &b) const {return cnt > b.cnt;}//重载运算符,小于变大于,从大到小排序
} a[N];//描述一棵树
bool vis[N];
ll f[N], size[N], F[N], ans, w[N];
int n, m, head[N], tot, res, rt, cnt;
void add (int from, int to, ll w) {
edge[++tot].nxt = head[from];
edge[tot].to = to;
edge[tot].w = w;
head[from] = tot;
}//链式前向星应该都会吧?
void dfs1 (int u, int pa) {
size[u] = 1, res++;
for (int i = head[u], v; i; i = edge[i].nxt) {
v = edge[i].to;
if (v == pa) continue;
dfs1(v, u), size[u] += size[v];
f[u] += f[v] + size[v] * edge[i].w;
}
}//第一次 dfs
void dfs2 (int u, int pa) {
vis[u] = true;
for (int i = head[u], v; i; i = edge[i].nxt) {
v = edge[i].to;
if (v == pa) continue;
f[v] = f[u] + (res - 2 * size[v]) * edge[i].w;
if (f[v] < a[cnt].f) a[cnt].f = f[v];
dfs2(v, u);
}
}//第二次 dfs,换根求重心应该都会吧?
int main() {
scanf("%d%d", &n, &m);
for (int i = 1, u, v, w; i <= m; i++) scanf("%d%d%d", &u, &v, &w), add(u, v, w), add(v, u, w);//无向边建俩
for (int i = 1; i <= n - m - 1; i++) scanf("%lld", &w[i]);//输入
for (int i = 1; i <= n; i++) {
if (!vis[i]) res = 0, dfs1(i, 0), a[++cnt].f = f[i], dfs2(i, 0), a[cnt].cnt = res;//记录小小树
}
for (int i = 1; i <= n; i++) ans = (ans + f[i]) % p;
ans = (ans * inv2) % p;//初始答案
Node u, v;
sort(w, w + n - m), sort(a + 1, a + 1 + cnt);
u = a[1];
for (int i = 2; i <= cnt; i++) {
v = a[i], ans = (ans + u.f * v.cnt % p + v.f * u.cnt % p + u.cnt * v.cnt * w[i - 1] % p) % p;//状态转移方程的程序实现
u.f = ((u.f + v.cnt * w[i - 1]) % p + v.f) % p, u.cnt = (u.cnt + v.cnt) % p;
}//正经实现
printf("%lld", ans);
return 0;
}
那么全文到这里就结束了,感谢审核管理员耐心看完,神犇幸苦了,求过 qwq。
如有不足,欢迎指出 awa。