【HNOI2014】米特运输

题目链接

米特运输

题目描述

米特是D星球上一种非常神秘的物质,蕴含着巨大的能量。在以米特为主要能源的D星上,这种米特能源的运输和储存一直是一个大问题。
D星上有\(N\)个城市,我们将其顺序编号为\(1\)\(N\)\(1\)号城市为首都。这\(N\)个城市由\(N-1\)条单向高速通道连接起来,构成一棵以\(1\)号城市(首部)为根的树,高速通道的方向由树中的儿子指向父亲。树按深度分层:根结点深度为\(0\),属于第\(1\)层;根结点的子节点深度为\(1\),属于第\(2\)层;依此类推,深度为\(i\)的结点属于第\(i+l\)层。
建好高速通道之后,D星人开始考虑如何具体地储存和传输米特资源。由于发展程度不同,每个城市储存米特的能力不尽相同,其中第\(i\)个城市建有一个容量为\(A[i]\)的米特储存器。这个米特储存器除了具有储存的功能,还具有自动收集米特的能力。
如果到了晚上六点,有某个储存器处于未满的状态,它就会自动收集大气中蕴含的米特能源,在早上六点之前就能收集满;但是,只有在储存器完全空的状态下启动自动收集程序才是安全的,未满而又非空时启动可能有安全隐患。
早上六点到七点间,根节点城市(\(1\)号城市)会将其储存器里的米特消耗殆尽。根节点不会自动搜集米特,它只接受子节点传输来的米特。
早上七点,城市之间启动米特传输过程,传输过程逐层递进:先是第\(2\)层节点城市向第\(1\)层(根节点城市,即\(1\)号城市)传输,直到第\(1\)层的储存器满或第\(2\)层的储存器全为空;然后是第\(3\)层向第\(2\)层传输,直到对于第\(2\)层的每个节点,其储存器满或其予节点(位于第\(3\)层)的储存器全为空;依此类推,直到最后一层传输完成。传输过程一定会在晚上六点前完成。
由于技术原因,运输方案需要满足以下条件:

  1. 不能让某个储存器到了晚上六点传输结束时还处于非空但又未满的状态,这个时候储存器仍然会启动自动收集米特的程序,而给已经储存有米特的储存器启动收集程序可能导致危险,也就是说要让储存器到了晚上六点时要么空要么满;
  2. 关于首都——即1号城市的特殊情况, 每天早上六点到七点间1号城市中的米特储存器里的米特会自动被消耗殆尽,即运输方案不需要考虑首都的米特怎么运走;
  3. 除了1号城市,每个节点必须在其子节点城市向它运输米特之前将这座城市的米特储存器中原本存有的米特全部运出去给父节点,不允许储存器中残存的米特与外来的米特发生混合;
  4. 运向某一个城市的若干个来源的米特数量必须完全相同,不然,这些来源不同的米特按不同比例混合之后可能发生危险。

现在D星人已经建立好高速通道,每个城市也有了一定储存容量的米特储存器。为了满足上面的限制条件,可能需要重建一些城市中的米特储存器。你可以,也只能,将某一座城市(包括首都)中原来存在的米特储存器摧毁,再新建一座任意容量的新的米特储存器,其容量可以是小数(在输入数据中,储存器原始容量是正整数,但重建后可以是小数),不能是负数或零,使得需要被重建的米特储存器的数目尽量少。

输入格式

第一行是一个正整数\(N\),表示城市的数目。
接下来\(N\)行,每行一个正整数,其中的第\(i\)行表示第\(i\)个城市原来存在的米特储存器的容量。
再接下来是\(N-I\)行,每行两个正整数\(a\)\(b\)表示城市\(b\)到城市\(a\)有一条高速通道\((a \not b)\)

输出格式

输出文件仅包含一行,一个整数,表示最少的被重建(即修改储存器容量)的米特储存器的数目。

样例输入

5
5
4
3
2
1
1 2
1 3
2 4
2 5

样例输出

3

样例解释

一个最优解是将\(A[1]\)改成\(8\)\(A[3]\)改成\(4\)\(A[5]\)改成\(2\)。这样,\(2\)\(3\)运给\(1\)的量相等,\(4\)\(5\)运给\(2\)的量相等,且每天晚上六点的时候,\(1\)\(2\)满,\(3\)\(4\)\(5\)空,满足所有限制条件。

数据范围

对于\(100\%\)的数据满足\(N \lt 500000\)\(A[j] \lt 10^8\)

题解

题意:注意,理解题意是做出此题的关键
首先,我们有一颗树,每个节点都有一个容量,使每个节点的\(value\)值不能超过这个容量,我们可以把所有根节点除外的节点内的\(value\)转移到它的父亲节点上,并满足如下条件:

  1. 刚开始时根节点\(value\)\(0\),其他节点\(value\)为容量。
  2. 所有转移完成后要满足所有节点的\(value\)要么等于容量,要么为\(0\)
  3. 一个节点的所有儿子给这个点的\(value\)必须都相同。
  4. 当给一个节点转移时这个节点的\(value\)要为\(0\)
  5. 转移的顺序是从上往下的,也就是说当节点给父亲\(value\)时,可以认为父亲当前的\(value\)\(0\),只需要满足父亲的所有子节点给父亲的\(value\)之和不会超过父亲的容量就行了。

接下来我们考虑如下性质:
一个节点的所有儿子的容量都必须是(当前节点的容量)/(儿子的个数)。
因为\(value\)转移的顺序是从上往下的,所以对于当前节点来说,儿子给完前\(value\)\(0\),儿子给完后是满容量。
而且所有儿子给的值必须相等且所有节点必须没有残留。
所以儿子给的值就是容量的大小,而给的值可以通过当前节点来确定,那么如果确定了一个节点,以这个节点为根的子树就确定了。

同理,当前节点的父亲的容量也必须是(当前节点的容量)*(父亲的儿子数量),所以也可以通过一个儿子确定父亲的容量。

综上,只要确定树中一个点的容量,我们就能确定整棵树。

那么我们可以枚举每个点,如果这个点容量不变,那么整棵树是怎样的,如果枚举的\(k\)个点所生成的树是一样的,那么把原树修改成这棵树所需要修改的点就是\(n-k\)
因为一棵树可以通过一个点来确定,所以我们记录树的时候就可以用根节点的值来表示这棵树的信息(当然,如果你闲得蛋疼,也可以用其他点来表示信息)。
根据上面的结论,我们可以知道对于当前点确定时,父亲点的容量就是(当前点的容量)*(((当前点所有的祖先)的儿子数量)的乘积)。
那么我们只要跑一遍dfs就能求出所有点所对应的树了。
但是我们会发现这样一个问题,当你的点比较深,你的祖先的儿子有点多,那么会爆\(long\ long\)
那么我们把这个值缩小一点就可以了。
相信大家初中的时候都学过这个公式:\(log(a \times b)=loga+logb\)
那么我们用\(log\)存储,往下推的时候把乘换成加,就不会爆空间了。
但是要注意用浮点数的时候有精度问题,所以在最后比较是否相同的时候需要用\(eps\)来减小精度误差,具体实现看代码。
当然你也可以自己用一些奇技淫巧哈希一下,比如找个质数取模。
上代码:

#include<bits/stdc++.h>
using namespace std;
int n;
int a[500009];
struct aa{
	int to,nxt;
}p[1000009];
double f[500009];
int h[500009],len;
int u,v;
void add(int u,int v){
	p[++len].to=v;
	p[len].nxt=h[u];
	h[u]=len;
}
int cd[500009];
bool cmp(double x,double y){return x<y;}
void dfs(int u,int fa){
	if(u!=1) f[u]=log(cd[fa])+f[fa];//根节点是自己的1倍,所以f[1]=log(1)=0;
	for(int j=h[u];j;j=p[j].nxt)
		if(p[j].to!=fa) cd[u]++;//计算儿子数量
	for(int j=h[u];j;j=p[j].nxt){
		if(p[j].to==fa) continue;
		dfs(p[j].to,u);
	}
}
int ans,s;
int main(){
	scanf("%d",&n);
	for(int j=1;j<=n;j++)
		scanf("%d",&a[j]);
	for(int j=1;j<n;j++){
		scanf("%d%d",&u,&v);
		add(u,v);
		add(v,u);
	}
	dfs(1,0);
	for(int j=1;j<=n;j++){
		f[j]+=log(a[j]);
	}
	sort(f+1,f+n+1,cmp);
	for(int j=1;j<=n;j++){
		if(f[j]-f[j-1]>1e-6){//减小精度的误差
			ans=max(ans,s);
			s=1;
		}else s++;
	}
	ans=max(ans,s);
	printf("%d",n-ans);
	return 0;
}
posted @ 2020-05-04 15:32  oblivionl  阅读(152)  评论(0编辑  收藏  举报