JZOJ[5971]【北大2019冬令营模拟12.1】 party(1s,256MB)

题目

在这里插入图片描述

题目大意

给你一棵树,在树上的某一些节点上面有人,要用最小的步数和,使得这些人靠在一起。所谓靠在一起,即是任意两个人之间的路径上没有空的节点(也就是连在一起)。
N200N leq 200


思考历程

看了题目好久,没有什么思路。
想到DP,但不知道怎么用DP做。
然后去翻翻题解,然后一脸懵逼,再去问问几位大佬。
LYL、ZHJ、GMH这三个大爷都说这题很水,是联赛难度。
不屑于给我讲。
天哪,这就是人与人之间的差距!太恐怖了。
然后我只能依靠我自己硬是刚了四天,对,是四天。
终于搞了出来……


正解

首先这题是一个树形DP。
看到这题的数据范围这么小,嗯,先估计一个时间复杂度。
就是O(N3)O(N^3)
就这么随意
然后想想DP的状态
fi,jf_{i,j}表示在ii这个节点的子树中(ii以外),留下了jj个人的最优解。(**在这个时候,其它的人被丢到了ii
所谓的jj个东西要和ii接在一起。
注意,对于ii这棵子树,有可能会有其它的节点进去里面,所以,这个jj可以大于sumsonsum_{son}sumisum_i表示ii这个子树中的人的数量)
然后考虑如何转移。
我们分类讨论:

  1. 如果没有人在sonson这个节点上,那么sonson这棵子树里面的人一定都会跑出来(不然就断了),所以留下的为空。那么此时fx,i=fson,0+sumson+fx,if_{x,i}=f_{son,0}+sum_{son}+f_{x,i}
  2. 如果有人在sonson这个节点上,设留在sonson这棵子树内的人数为jj。显然j0j eq 0
    注意一下我设的这个状态,你会发现这个状态不包括这个根节点。
    如果有人在sonson这个节点上,那么子树剩余的人数为j1j-1
    由于有sumsonjsum_{son}-j个人要出来,所以加上它们的贡献。注意,有可能是进去的,所以要用绝对值。
    fx,i=fson,j1+sumsonj+fx,ijf_{x,i}=f_{son,j-1}+|sum_{son}-j|+f_{x,i-j}

DP搞完了,然后呢?每次换根重新操作?
当然不存在的,那样就有可能T飞了。
我们枚举每一个节点,将其作为最后形成的块的顶端。
在它子树内的东西都已经搞好了,所以,只需要搞一搞子树外的东西。
然后我们就可以发现只需要将它子树外的人到它距离之和加起来就好了。
因为这个点是整块的顶端,所以那些点必定会钻到下面去。
钻到下面之前肯定要先来到这个地方,剩下的就加上DP出来的结果就行了。
那么用什么来处理这个距离的和呢?
一开始,由于强迫症,我想用一些好的方法。
后来想想,反正范围这么小,那么简单粗暴一下就好了。
直接枚举子树外面的节点,然后一个一个地将距离加上(Floyed预处理)。
判断是否在子树内用dfndfn
时间复杂度O(N3)O(N^3)
是不是和预想的一模一样


其实这个题真的有点奇怪,的确不是很难,但是容易误入歧途。
在一开始,我设的状态为:fi,jf_{i,j}表示在ii的子树内,留下jj个。
和最后的状态有什么区别呢?区别其实不大,就是包不包括ii这个节点。
然后我发现这样设繁琐至极,自己在推理的过程中绕来绕去的,很晕。
因为在这个东西中,我需要考虑一下根节点有没有人。
如果一开始没有人,那就要拿下面的补上,然后我又想着要开多一维来记录目前的这一块中的深度最小的点,用它来补上去,然后特别麻烦,怎么转移都不知道。
后来想想,我的DP可以设再设一下,表示根节点是否有东西……
我们知道在最后根节点一定有东西,或者所有的东西都跑到外面去,不可能出现根节点为空,而它的儿子的子树中还有东西。
那么在转移时还是有点麻烦。
最后,经过GMH的一点指导,然后我就又想了好久,才想了出来。
状态的这一点改动,实际上是解法的一个大大的更新。


代码

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cstdlib>
#define N 200
#define INF 400000
int n;
char a[N+3];
struct EDGE{
	int to;
	EDGE *las;
} e[N*2+1];
int ne;
EDGE *last[N+1];
inline void link(int u,int v){
	++ne;
	e[ne].to=v,e[ne].las=last[u];
	last[u]=e+ne;
}
int dfn[N+1],nowdfn;
int siz[N+1],sum[N+1];
int f[N+1][N+1];
void dp(int,int);
int dis[N+1][N+1];
void dfs(int,int);
int ans;
int main(){
	freopen("party.in","r",stdin);
	freopen("party.out","w",stdout);
	scanf("%d%s",&n,a+1);
	for (int i=1;i<n;++i){
		int u,v;
		scanf("%d%d",&u,&v);
		link(u,v),link(v,u);
	}
	for (int i=1;i<=n;++i)
		fill(f[i],f[i]+n+1,INF);
	dp(1,0);
	for (int i=1;i<=n;++i)
		fill(dis[i],dis[i]+n+1,INF);
	for (int i=1;i<=n;++i)
		dis[i][i]=0;
	for (int i=1;i<=n;++i)
		for (EDGE *ei=last[i];ei;ei=ei->las)
			dis[i][ei->to]=dis[ei->to][i]=1;
	for (int k=1;k<=n;++k)
		for (int i=1;i<=n;++i)
			for (int j=1;j<=n;++j)
				dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
	ans=INF;
	dfs(1,0);
	printf("%d
",ans);
	return 0;
}
void dp(int x,int fa){
	dfn[x]=++nowdfn;
	siz[x]=1;
	if (a[x]=='0')
		sum[x]=0;
	else
		sum[x]=1;
	f[x][0]=0;
	for (EDGE *ei=last[x];ei;ei=ei->las)
		if (ei->to!=fa){
			dp(ei->to,x);
			siz[x]+=siz[ei->to],sum[x]+=sum[ei->to];
			for (int i=siz[x]-1;i>=0;--i){//i倒着枚举,就像是背包问题一样。另外,由于目前的节点总数为siz[x],而x不算,所以最多为siz[x]-1
				//转移,具体理由见上
				f[x][i]=f[ei->to][0]+sum[ei->to]+f[x][i];
				for (int j=1;j<=i;++j)
					f[x][i]=min(f[x][i],f[ei->to][j-1]+abs(sum[ei->to]-j)+f[x][i-j]);
			}
		}
} 
void dfs(int x,int fa){
	int s=0;
	for (int i=1;i<=n;++i)
		if (a[i]=='1' && !(dfn[x]<=dfn[i] && dfn[i]<dfn[x]+siz[x]))//后面的这个东西判断它是否在子树里面
			s+=dis[i][x];
	ans=min(ans,s+f[x][sum[1]-1]);//因为总共的人数是sum[1],而有一个在x上,所以为sum[1]-1
	for (EDGE *ei=last[x];ei;ei=ei->las)
		if (ei->to!=fa)
			dfs(ei->to,x);
}

总结

首先,在膜拜一下三个大佬。
还有后来随随便便AC的ZSH、PZM大佬。
对于在一棵树上又看起来不好用数据结构什么的东西来做的题目的时候,就想想能不能树形DP。
对于这题,树形DP的最大的妙处在于根节点不包括在内。
根节点有时候会处于一种比较尴尬的状况,所以,我们就要对它特殊地对待。
然后在状态中就把根节点排除在外,暂时不考虑。

其它的事

我翻了翻几位大佬的代码,都是枚举根节点然后搞一遍DP。
你们怎么不会TLE?而且,为什么,为什么时间还比我短!!!
有种被欺骗了的感觉。
还有这题的数据是真的水。
我的代码之前有一个很严重的错误,然而,带着这个错误,我居然有90分!
So strange!
这个错误是,Floyed中初值没有赋值好……
所以我不保证我现在这个代码一定是对的,但这个思想还是值得借鉴。

原文地址:https://www.cnblogs.com/jz-597/p/11145256.html