JZOJ[5971]【北大2019冬令營模擬12.1】 party(1s,256MB)
題目
題目大意
給你一棵樹,在樹上的某一些節點上面有人,要用最小的步數和,使得這些人靠在一起。所謂靠在一起,即是任意兩個人之間的路徑上沒有空的節點(也就是連在一起)。
思考歷程
看了題目好久,沒有什麼思路。
想到DP,但不知道怎麼用DP做。
然後去翻翻題解,然後一臉懵逼,再去問問幾位大佬。
LYL、ZHJ、GMH這三個大爺都說這題很水,是聯賽難度。
不屑於給我講。
天哪,這就是人與人之間的差距!太恐怖了。
然後我只能依靠我自己硬是剛了四天,對,是四天。
終於搞了出來……
正解
首先這題是一個樹形DP。
看到這題的資料範圍這麼小,嗯,先估計一個時間複雜度。
就是
了
就這麼隨意
然後想想DP的狀態
設
表示在
這個節點的子樹中(除
以外),留下了
個人的最優解。(**在這個時候,其它的人被丟到了
處)
所謂的
個東西要和
接在一起。
注意,對於
這棵子樹,有可能會有其它的節點進去裡面,所以,這個
可以大於
(
表示
這個子樹中的人的數量)
然後考慮如何轉移。
我們分類討論:
- 如果沒有人在 這個節點上,那麼 這棵子樹裡面的人一定都會跑出來(不然就斷了),所以留下的為空。那麼此時
- 如果有人在
這個節點上,設留在
這棵子樹內的人數為
。顯然
注意一下我設的這個狀態,你會發現這個狀態不包括這個根節點。
如果有人在 這個節點上,那麼子樹剩餘的人數為 。
由於有 個人要出來,所以加上它們的貢獻。注意,有可能是進去的,所以要用絕對值。
DP搞完了,然後呢?每次換根重新操作?
當然不存在的,那樣就有可能T飛了。
我們列舉每一個節點,將其作為最後形成的塊的頂端。
在它子樹內的東西都已經搞好了,所以,只需要搞一搞子樹外的東西。
然後我們就可以發現只需要將它子樹外的人到它距離之和加起來就好了。
因為這個點是整塊的頂端,所以那些點必定會鑽到下面去。
鑽到下面之前肯定要先來到這個地方,剩下的就加上DP出來的結果就行了。
那麼用什麼來處理這個距離的和呢?
一開始,由於強迫症,我想用一些好的方法。
後來想想,反正範圍這麼小,那麼簡單粗暴一下就好了。
直接列舉子樹外面的節點,然後一個一個地將距離加上(Floyed預處理)。
判斷是否在子樹內用
序
時間複雜度
是不是和預想的一模一樣
其實這個題真的有點奇怪,的確不是很難,但是容易誤入歧途。
在一開始,我設的狀態為:
表示在
的子樹內,留下
個。
和最後的狀態有什麼區別呢?區別其實不大,就是包不包括
這個節點。
然後我發現這樣設繁瑣至極,自己在推理的過程中繞來繞去的,很暈。
因為在這個東西中,我需要考慮一下根節點有沒有人。
如果一開始沒有人,那就要拿下面的補上,然後我又想著要開多一維來記錄目前的這一塊中的深度最小的點,用它來補上去,然後特別麻煩,怎麼轉移都不知道。
後來想想,我的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\n",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]