题目
题目背景
景区的石桥,下面有通道,于是有标牌 “小心碰头” 。
下面还有一行英语,写着 c a r e f u l l y m e e t \tt carefully\;\;meet carefullymeet 。
题目描述
沐目女未 有
n
n
n 个房子,有
n
−
1
n-1
n−1 条小路连接着它们,并保证两个房子之间互相可达。
现在有 m m m 个人去找 沐目女未。但是他们具体在哪里我们并不知道,沐目女未 的所在,我们也不知道。
于是这
m
m
m 个人准备确定一个集合点,使得每个人延最短路走到集合点的总距离之和最小。这个集合点只能是某个房子。每条小路都是
1
km
1\text{km}
1km 。不要在乎有的人可能要走五万公里。
对于每一个可能的 m m m 个人的所在地,你都要计算出这个总距离,并输出它们的和。提示:一共有 ( n m ) {n\choose m} (mn) 个方案。
数据范围与提示
1
≤
m
≤
n
≤
1
0
6
1\le m\le n\le 10^6
1≤m≤n≤106 。
思路壹
把人存在的地方叫做特殊点。
首先你要看出,集合点是加权重心。说白了,以它为根,所有子树的特殊点数量不超过 ⌊ m 2 ⌋ \lfloor\frac{m}{2}\rfloor ⌊2m⌋ 。
接下来,我们可以知道 每条边 被多少次经过,因为一定是特殊点较少的一边走到特殊点较多的一边。假设这条边断开后,形成的两个连通块的大小为 x x x 与 n − x n-x n−x ,那么贡献为
∑ i = 1 m − 1 ( x i ) ( n − x m − i ) × min ( i , m − i ) \sum_{i=1}^{m-1}{x\choose i}{n-x\choose m-i}\times\min(i,m-i) i=1∑m−1(ix)(m−in−x)×min(i,m−i)
由于 min \min min 不好操作,把它分成两部分考虑,以 m m m 为奇数且 i ≤ m − 1 2 i\le\frac{m-1}{2} i≤2m−1 为例。因为两边的形式是必然相同的——从现实意义上,我们只是讨论了 x x x 和 n − x n-x n−x 谁的特殊点更多。
这里我们要计算 ∑ i = 1 m − 1 2 ( x i ) ( n − x m − i ) × i \sum_{i=1}^{\frac{m-1}{2}}{x\choose i}{n-x\choose m-i}\times i i=1∑2m−1(ix)(m−in−x)×i
按照惯例,组合数得用递推搞定。然后我就在这里死掉了。
如果你愿意再推一步,就会把式子变形成
x × ∑ i = 1 m − 1 2 ( x − 1 i − 1 ) ( n − x m − i ) x\times\sum_{i=1}^{\frac{m-1}{2}}{x-1\choose i-1}{n-x\choose m-i} x×i=1∑2m−1(i−1x−1)(m−in−x)
(其实你直接得到这个式子也是很有道理的:在 x x x 里面选一个来计算贡献,然后剩下的乱放。)
我做到这一步了,然后放弃了。然后回头去想化简前的式子的递推,然后死球了。
接下来,右边的组合数可以递推。它的组合意义是 ( n − 1 m − 1 ) {n-1\choose m-1} (m−1n−1) 的这些方案中,前 x − 1 x-1 x−1 个位置最多拿 m − 1 2 − 1 \frac{m-1}{2}-1 2m−1−1 个。然后你发现可以直接用 f ( x − 1 ) f(x-1) f(x−1) 递推到 f ( x ) f(x) f(x) ,只需要减去极少的不合法情况。
复杂度 O ( n ) \mathcal O(n) O(n) ,代码也非常简单。
代码
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
typedef long long int_;
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
(c == '-' ? f = -f : 0);
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
const int Mod = 1e9+7;
const int MaxN = 1000005;
const int_ inv2 = (Mod+1)>>1;
struct Edge{
int to, nxt;
};
Edge e[MaxN];
int head[MaxN], cntEdge;
void addEdge(int a,int b){
e[cntEdge].to = b;
e[cntEdge].nxt = head[a];
head[a] = cntEdge ++;
}
int siz[MaxN];
int dfs(int x){
siz[x] = 1;
for(int i=head[x]; ~i; i=e[i].nxt)
siz[x] += dfs(e[i].to);
return siz[x];
}
int_ jc[MaxN], inv[MaxN];
void prepare(int n){
jc[1] = inv[1] = 1;
for(int i=2; i<=n; ++i){
inv[i] = (Mod-Mod/i)*inv[Mod%i]%Mod;
jc[i] = i*jc[i-1]%Mod;
}
for(int i=2; i<=n; ++i)
inv[i] = inv[i-1]*inv[i]%Mod;
inv[0] = jc[0] = 1;
}
int_ C(int n,int m){
if(m < 0 || n < m) return 0;
return jc[n]*inv[m]%Mod*inv[n-m]%Mod;
}
int f[MaxN]; // f[x]: 前 x 拿最多 xez 个
int main(){
// freopen("meeting.in","r",stdin);
// freopen("meeting.out","w",stdout);
int n = readint(), m = readint();
for(int i=1; i<=n; ++i)
head[i] = -1;
for(int i=2; i<=n; ++i)
addEdge(readint(),i);
dfs(1), prepare(n);
int xez = (m-1)>>1; // 最多选 xez-1 个
f[0] = C(n-1,m-1); // 随便选
for(int i=1; i<n; ++i){
f[i] = f[i-1]-C(i-1,xez-1)
*C(n-1-i,m-1-xez)%Mod;
f[i] = (f[i]+Mod)%Mod; // 减去恰好超了
// printf("f[%d] = %d\n",i,f[i]);
}
if(m <= 2) // 特殊情况:什么也不选
for(int i=0; i<n; ++i)
f[i] = 0;
int ans = 0;
for(int i=2; i<=n; ++i){
int_ x = siz[i];
ans += (x*f[x-1]+(n-x)*f[n-x-1])%Mod;
ans %= Mod; // 我这都能忘……我佛了
if((m&1) == 0) // 两边相等,少算了一次
ans= (ans+C(x,m>>1)*C(n-x,m>>1)
%Mod*(m>>1))%Mod; // 会算两次
// printf("ans = %d\n",ans);
}
printf("%d\n",ans);
return 0;
}
思路贰
知名博主 沐目女未 给了我一个想法。她枚举一个集合点,将其作为根的话,某个子树内的所有节点的出现次数都是相同的。怎么算呢?利用 ⌈ m + 1 2 ⌉ \lceil\frac{m+1}{2}\rceil ⌈2m+1⌉ 最多出现一次,减去不合法即可。(这个技巧在去年 C S P \tt CSP CSP 中有出现过。)
然后我推了很久,发现很难继续。感兴趣的可以自己推一推式子。这里给一个提示,
( s i z M ) ( n − M m − M ) {siz\choose M}{n-M\choose m-M} (Msiz)(m−Mn−M)
是 不能 计算出大小为 s i z siz siz 的子树中至少有 M M M 个特殊点的方案数的。因为不能分布进行。