神奇脑洞题解——树的最大匹配
(这是道CEOI2007的原题,洛谷上也有哦)
COGS 489
至于为啥没有洛谷链接,实验人怎么能用别人的评测机
其实只是洛谷数据过强,要写高精度的
一句话题面:给定一颗父子关系指明的树,记树上某个点和他的父亲可以形成一对匹配,求这棵树最多可以形成多少匹配,形成这么多种匹配的方案有几种?
【输入格式】
第一行一个数 N ,表示有多少个结点。
接下来 N 行,每行第一个数,表示要描述的那个结点的编号。然后一个数 m ,表示这个结点有 m 个儿子,接下来 m 个数,表示它的 m 个儿子的编号。
【输出格式】
输出两行,第一行为最大匹配数,第二行输出最大匹配方案数。
【输入样例】
7
1 3 2 4 7
2 1 3
4 1 6
3 0
7 1 5
5 0
6 0
【输出样例】
3
4
【数据规模】
N<=1000, 其中 40% 的数据答案不超过 10^7
看数据就知道要写高精,但COGS可以用long long int 水过去哈
首先考虑第一个问题:最大匹配是多少?
这一看就知道是个树形DP,而且每个点上的取值和这个点的状态有关
按照树形DP的套路,状态可以设为在x的子树内,能匹配的最大匹配对数(因为每个点只能和自己的父亲与儿子匹配,因此,各个子树间互不影响)
先大概脑补一下没有上司的舞会这道题,如果没写过的话直接肝这题有点难度哈
按照没有上司的舞会的套路,每个点状态分两种:dp[x][0]和dp[x][1]分别代表当点x不与自己儿子匹配时x内子树匹配最大值和当x参与时的最大值。
那么现在问题就明朗许多了。
先大概思考一下,对于dp[x][0]的计算,因为x点是不用的,因此x的儿子y是否被使用都无所谓,也就是说dp[x][0]=∑max(dp[y][0],dp[y][1]);
这里还可以再加强一波:可以得知dp[y][1]>=dp[y][0];
这个东西比较神奇,但是也可以感性理解一下。假设我们现在有一棵小树,这棵树有两个儿子,两个儿子的子树各是一条链,(实际上这个时候模拟的就是树的最底部)链有多长咱们不管,但是至少我们可以知道,匹配肯定是先选一条边两端的两个节点,但是与其相连的两条边都不能选,贪心可知,叶子节点一定要选,然后这样的话,假设一条长度为3的链,选上树根就可以多得到一对,偶数长度的选上树根也不行,所以说dp[y][1]>=dp[y][0]当且仅当y的子树的链长度均为偶数时取等号。
所以说dp[x][0]=∑dp[y][1];
考虑完dp[x][0],再考虑一下dp[x][1],众所周知,因为x是与自己的某个儿子匹配的,因此可以认为dp[x][1]可以认为是由∑dp[y][1]然后减去min(dp[y][1]-dp[y][0])再+1得到的
这里可以将求dp[x][0]和求dp[x][1]合在一起(因为dp[x][0]就是dp[y][1]之和)
提一句,维护顺序是先维护dp[x][1]再维护dp[x][0]这样可以有效利用之前计算的信息
下面的代码中,dp[x][0]是代表前i-1个子树dp[y][1]之和,而dp[x][1]已经开始维护第i个子树的影响了。
if(dp[x][1])
{ dp[x][1]+=dp[to][1]; } if(dp[x][0]+dp[to][0]+1>dp[x][1]) { dp[x][1]=dp[x][0]+dp[to][0]+1; }//因为开始时dp[x][1]初始值是0,那么直接进入下一步,否则先将当前y不和x相连的贡献算上,然后再考虑是否能更换那个与x相连的儿子(相当于假定第一个儿子与x相连,然后依次尝试替换)
好了,第一步,维护最大值完成了,下一问,计算方案数!
这道题说实话难就难到计算方案数上了。
仿照上面设计状态
g[x][0]代表,以x为根的子树,在不选x的情况下,达到最大值的方案数。
g[x][1]则代表选x的情况下的方案数。
我们先考虑g[x][0]的做法,
子树y对g[x][0]的贡献无非也就以下几类
- 当dp[y][0]==dp[y][1]时(上面有介绍这种情况)
- 当dp[y][0]!=dp[y][1]时
相信各位大佬的数学还是不错的,知道方案数计算应该用乘法吧,不知道的出门右拐小学奥数班哈
对于第一种情况,g[x][0]=g[x][0]*(g[y][0]+g[y][1])选哪个都一样,那我都要,小孩子才做选择。
第二种我木的选择 g[x][0]=g[x][0]*g[y][1]只能这样选了。
OK,对g[x][0]的维护完成了,下面开始考虑g[x][1]
首先声明维护顺序:一定要先维护dp[x][1]和g[x][1]
if(dp[x][1]) { dp[x][1]+=dp[to][1]; if(dp[to][0]!=dp[to][1]) { g[x][1]*=g[to][1]; } else { g[x][1]*=g[to][0]+g[to][1]; } } if(dp[x][0]+dp[to][0]+1>dp[x][1]) { dp[x][1]=dp[x][0]+dp[to][0]+1; g[x][1]=g[x][0]*g[to][0]; } else if(dp[x][0]+dp[to][0]+1==dp[x][1]) g[x][1]+=g[x][0]*g[to][0];
大家仔细看看,这份代码和上面是一个框架,但是加入了对g[x][1]的维护(比如最下面的else)
维护大概分以下几个步骤
- 先看看是不是取dp[y][0]和dp[y][1]对dp[x][1]的贡献一样(这个时候那个和自己父亲链接的点已经有了,现在的y先假定不和父亲链接)
- 如果现在的点取代之前的点和x相连更加合适,那么就更换,这时候就体现出先维护dp[x][1]的优点了,此时的g[x][0]是y之前所有子树取dp[子树][1]的选法(可能有dp[子树][0]==dp[子树][1]),那么这个时候注意是直接赋值,将g[x][1]=g[x][0]*g[y][0];
- 如果当前的点和之前选的那个一样优,这就相当于又贡献了一堆选法,注意这个时候是加法。
- 完了
这样的话,这道题就算完事了。
#include<iostream> #include<cstdio> #include<vector> #define int long long int using namespace std; int dp[1001][2]; int g[1001][2]; int n,m,a1,root; bool rd[1001]; vector<int> b[1001]; void DFS(int x,int fa) { dp[x][0]=0; dp[x][1]=0; g[x][0]=1; g[x][1]=1; int mxf=0,sum=0; for(int i=0;i<b[x].size();i++) { int to=b[x][i]; DFS(to,x); //dp[x][1]=max(dp[x][1],dp[to][0]-max(dp[to][0],dp[to][1])+1+dp[x][0]); if(dp[x][1]) { dp[x][1]+=dp[to][1]; if(dp[to][0]!=dp[to][1]) { g[x][1]*=g[to][1]; } else { g[x][1]*=g[to][0]+g[to][1]; } } if(dp[x][0]+dp[to][0]+1>dp[x][1]) { dp[x][1]=dp[x][0]+dp[to][0]+1; g[x][1]=g[x][0]*g[to][0]; } else if(dp[x][0]+dp[to][0]+1==dp[x][1]) g[x][1]+=g[x][0]*g[to][0]; dp[x][0]+=dp[to][1]; //dp[x][0]=max(dp[x][0],dp[x][0]+max(dp[to][0],dp[to][1])); if(dp[to][0]!=dp[to][1]) g[x][0]*=g[to][1]; else g[x][0]*=g[to][0]+g[to][1]; } if(!dp[x][1]) g[x][1]=0; } signed main() { freopen("treeb.in","r",stdin); freopen("treeb.out","w",stdout); scanf("%d",&n); for(int i=1;i<=n;i++) { int ke; scanf("%d",&ke); scanf("%d",&m); for(int j=1;j<=m;j++) { scanf("%d",&a1); rd[a1]++; b[ke].push_back(a1); } } for(int i=1;i<=n;i++) { if(rd[i]==0) { root=i; break; } } DFS(root,0); cout<<dp[root][1]<<endl; if(dp[root][0]!=dp[root][1]) { cout<<g[root][1]; } else cout<<g[root][1]+g[root][0]; return 0; }
完结撒花!