POJ 1848 Tree

一道树状DP的题目 =.=

题目链接   (用的Vjudge上的)

题意为给予一颗树,求最少加多少条边可以使得每个节点都属于唯一一个环。

问题可以转换为将一颗树分割成N条链,(因为成环最少要求三个节点所以每条链都要求长度>=3)求这个N的最小值。

从树的叶子节点向上推,对于每个节点,存在两种情况,1.和父节点断开(要求该节点所处的链长度>=3)。2.与父节点相连。

每个节点保存两个状态,一个是该节点的子节点个数对应的最少链的条数(Dp),一个是对应之前的子节点个数,当前节点所处的链的长度(Hav)。

这里很明显,每一个节点顶多有两个子节点,超过的话就不是条链了,所以Dp和Hav开[MAXM][3]就够了。(而且在该节点拥有两个子节点时,必须 要从父亲节点上断开。这里的子节点指的是直接连接的子节点。)

对于状态转移,更新Dp的时候,我们优先选择子节点中包含链最少的,然后再根据长度选择,越长的越好(越容易满足链长度>=3)。

为什么两个状态有优先级差距呢,或者说可以直接用贪心呢?

原因在于环的限制:

当我们要更新Dp [ i ] [ 0 ](节点 i ,拥有子节点个数 0 时的最少链数)时,长度无法造成影响,因为没有子节点相连,所以长度肯定是1,不考虑该状态直接选择包含链个数最少。

当我们要更新Dp [ i ] [ 1 ](节点 i ,拥有子节点个数 1 时的最少链数)时,因为只存在一个子链与其相连,其他全部断开,无法造成后续的影响,所以子节点包含链最少的肯定压制子节点包含链较多的,若存在个数相等的,则取长度大的)。

当我们要更新Dp [ i ] [ 2 ](节点 i ,拥有子节点个数 2 时的最少链数)时,对于父亲节点来说,当前这个 i 节点必然要被断开,所以长度无所谓。
总结下来,就是个数这个状态需要用贪心进行选择。
另外,设置Dp [ i ] [ 0 ] 并不是毫无意义,它的存在不是仅代表该节点是个叶子节点,也可能代表该节点的子节点全部被断开了,若该种可能不存在,我们就为其赋值INF。在每次子节点向上更新时,父亲节点 i 的三种状态Dp [ i ] [ 0 ]、Dp [ i ] [ 1 ]、Dp [ i ] [ 2 ] 都要进行更新,若无法 保持该状态,则为其赋值INF。例如a节点拥有三个子节点,在更新第三个子节点时,我们会发现,对于Dp [ a ] [ 2 ]来说,无法保持该状态(第三个点长度不满足>=3,无法断开,Dp [ a ] [ 1 ]在更新第二个节点的时候,因为无法保持,也无法从Dp [ a ] [ 0 ]转移过来,所以是INF),那就要对其赋值INF。
从另一种方向上来讲,可以认为是Dp [ i ] [ j ] [ t ](i为节点编号,j为更新到第 j 个子节点,t 就是包含子节点个数了)。每一次的更新都是从Dp [ i ] [ j - 1 ] [ t1 ]传递到Dp [ i ] [ j ] [ t2 ],只不过把这个结构优化为二维数组形式,手段类似于01背包二维转一维。

#include <stdio.h>
#include <algorithm>
using namespace std;

const int Size = 3;
const int MAXM = 110;
const int INF = 0x3f3f3f3f;

int Dp[MAXM][Size];
int Hav[MAXM][Size];

int Num;

int Total;
int End[MAXM];
int Data[MAXM<<1];
int Last[MAXM<<1];

void Add(int u,int v)
{
	Data[Total]=u;
	Last[Total]=End[v];
	End[v]=Total++;
}

void init_ver(int pos)
{
	Dp[pos][0]=1;Dp[pos][1]=Dp[pos][2]=INF;
	Hav[pos][0]=1;Hav[pos][1]=0;
}

void Init_Build()
{
	Total=0;
	for(int i=1;i<=Num;i++)End[i]=-1;
	int u,v;
	for(int i=1;i<Num;i++)
	{
		scanf("%d %d",&u,&v);
		Add(u,v);
		Add(v,u);
	}
}

void Deal(int now,int G)
{
	int Zero=INF;
	if(Hav[G][1]>=3)Zero=Dp[G][1];
	Zero=min(Zero,Dp[G][2]);
	Dp[now][2]=min(Dp[now][2]+Zero,Dp[now][1]+min(Dp[G][0],Dp[G][1])-1);
	Dp[now][2]=min(INF,Dp[now][2]);
	Dp[now][1]+=Zero;
	if(Dp[now][0]+Dp[G][1]-1<Dp[now][1]||(Dp[now][0]+Dp[G][1]-1==Dp[now][1]&&Hav[G][1]>=Hav[now][1]))
	{
		Dp[now][1]=Dp[now][0]+Dp[G][1]-1;
		Hav[now][1]=Hav[G][1]+1;
	}
	if(Dp[now][0]+Dp[G][0]-1<Dp[now][1])
	{
		Dp[now][1]=Dp[now][0]+Dp[G][0]-1;
		Hav[now][1]=2;
	}
	Dp[now][0]=min(INF,Dp[now][0]+Zero);
}

void DFS(int now,int ago)
{
	int pos=End[now],G;
	init_ver(now);
	while(~pos)
	{
		G=Data[pos];
		if(G!=ago)
		{
			DFS(G,now);
			Deal(now,G);
		}
		pos=Last[pos];
	}
	Hav[now][2]=0;
}

int main()
{
	scanf("%d",&Num);
	Init_Build();
	DFS(1,0);
	int best=Dp[1][2];
	if(Hav[1][1]>=3)best=min(best,Dp[1][1]);
	if(best==INF)best=-1;
	printf("%d\n",best);
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值